*/ use HasFactory; public const BASIS_ESTIMATE = 'estimate'; public const BASIS_ACTUAL = 'actual'; protected $fillable = [ 'project_id', 'revision_id', 'currency', 'basis', 'batch_units', 'filament_total', 'packaging_total', 'shipping_total', 'grand_total', 'source_reference', 'notes', 'recorded_by', 'recorded_at', ]; protected function casts(): array { return [ 'filament_total' => 'decimal:4', 'packaging_total' => 'decimal:4', 'shipping_total' => 'decimal:4', 'grand_total' => 'decimal:4', 'recorded_at' => 'datetime', ]; } public function project(): BelongsTo { return $this->belongsTo(Project::class); } public function revision(): BelongsTo { return $this->belongsTo(ProjectRevision::class, 'revision_id'); } public function recorder(): BelongsTo { return $this->belongsTo(User::class, 'recorded_by'); } public function lines(): HasMany { return $this->hasMany(ProjectCostLine::class, 'cost_snapshot_id'); } /** * A release requires every cost category to have been explicitly costed — * even as an intentional zero — rather than merely recorded. A category left * null ("not costed") is incomplete and must block a release going live. */ public function isComplete(): bool { return $this->filament_total !== null && $this->packaging_total !== null && $this->shipping_total !== null; } public function perUnitGrandTotal(): ?string { if ($this->grand_total === null) { return null; } return ProjectCosting::perUnit((string) $this->grand_total, (int) $this->batch_units); } }