Files
GSB-Construction/Modules/BiddingManagement/app/Models/BidPackage.php

127 lines
3.5 KiB
PHP

<?php
namespace Modules\BiddingManagement\Models;
use App\Models\User;
use App\Traits\HasPublicIdentifier;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Modules\BiddingManagement\Enums\BidPackageStatus;
use Modules\BiddingManagement\Enums\EvaluationMode;
use Modules\DocumentManagement\Models\Document;
use Modules\ProjectManagement\Models\Project;
class BidPackage extends Model
{
use HasPublicIdentifier, SoftDeletes;
protected $fillable = [
'project_id', 'created_by', 'title', 'description',
'status', 'evaluation_mode', 'submission_deadline',
'validity_period', 'instructions',
];
protected function casts(): array
{
return [
'status' => BidPackageStatus::class,
'evaluation_mode' => EvaluationMode::class,
'submission_deadline' => 'date',
'validity_period' => 'date',
];
}
// --- Relationships ---
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function criteria(): HasMany
{
return $this->hasMany(BidCriteria::class)->orderBy('sort_order');
}
public function invitations(): HasMany
{
return $this->hasMany(BidInvitation::class);
}
public function submissions(): HasManyThrough
{
return $this->hasManyThrough(BidSubmission::class, BidInvitation::class);
}
public function award(): HasOne
{
return $this->hasOne(BidAward::class);
}
public function documents(): MorphMany
{
return $this->morphMany(Document::class, 'documentable');
}
// --- State Machine ---
public function transitionTo(BidPackageStatus $newStatus): void
{
$current = $this->status;
if (!in_array($newStatus, $current->allowedTransitions())) {
throw new \InvalidArgumentException(
"Cannot transition from {$current->label()} to {$newStatus->label()}"
);
}
// Guard: must have at least one submission before evaluating
if ($newStatus === BidPackageStatus::Evaluating) {
if ($this->submissions()->count() === 0) {
throw new \InvalidArgumentException('Cannot start evaluation without any submissions.');
}
}
$this->update(['status' => $newStatus]);
}
// --- Computed Helpers ---
public function totalCriteriaWeight(): float
{
return (float) $this->criteria()->sum('weight');
}
public function isFullyScored(): bool
{
if ($this->evaluation_mode !== EvaluationMode::Scored) {
return true;
}
$criteriaCount = $this->criteria()->count();
if ($criteriaCount === 0) {
return false;
}
return $this->submissions()
->where('bid_submissions.status', '!=', 'rejected')
->get()
->every(fn ($sub) => $sub->scores()->count() >= $criteriaCount);
}
public function getIsDeadlinePassedAttribute(): bool
{
return $this->submission_deadline && $this->submission_deadline->isPast();
}
}