127 lines
3.5 KiB
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();
|
|
}
|
|
}
|