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(); } }