'integer', 'order_date' => 'date', // or 'datetime' if your DB stores time too 'expected_date' => 'date', 'subtotal' => 'decimal:2', 'tax' => 'decimal:2', 'total' => 'decimal:2', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', ]; public function getFillable(): array { return $this->fillable; } /* ============================================================ * Status (enum-like) * ============================================================ */ public const STATUS_DRAFT = 'draft'; public const STATUS_ORDERED = 'ordered'; public const STATUS_RECEIVED = 'received'; public const STATUS_CANCELED = 'canceled'; public static function allowedStatuses(): array { return [ self::STATUS_DRAFT, self::STATUS_ORDERED, self::STATUS_RECEIVED, self::STATUS_CANCELED, ]; } /* ============================================================ * Relationships * ============================================================ */ public function supplier(): BelongsTo { return $this->belongsTo(Supplier::class, 'supplier_id'); } public function items(): HasMany { return $this->hasMany(PurchaseOrderItem::class, 'purchase_order_id'); } /* ============================================================ * Scopes * ============================================================ */ public function scopeStatus(Builder $q, string $status): Builder { return $q->where('status', $status); } public function scopeOpen(Builder $q): Builder { return $q->whereIn('status', [self::STATUS_DRAFT, self::STATUS_ORDERED]); } /* ============================================================ * Helpers / computed totals * ============================================================ */ /** * Recalculate subtotal/tax/total from items. * Tax rate is optional. If you store tax as absolute amount, pass null and keep existing tax. */ public function recalcTotals(?float $taxRate = null): self { $subtotal = (float) $this->items() ->selectRaw('COALESCE(SUM(quantity * unit_cost), 0) as s') ->value('s'); $tax = $taxRate === null ? (float) ($this->tax ?? 0) : round($subtotal * $taxRate, 2); $total = $subtotal + $tax; $this->subtotal = $subtotal; $this->tax = $tax; $this->total = $total; return $this; } public function markOrdered(): self { $this->status = self::STATUS_ORDERED; return $this; } public function markReceived(): self { $this->status = self::STATUS_RECEIVED; return $this; } public function markCanceled(): self { $this->status = self::STATUS_CANCELED; return $this; } /* ============================================================ * Validation rules (controller/FormRequest reuse) * ============================================================ */ public static function rules(?int $id = null): array { // Mirrors legacy: unique purchase_orders.po_number except current id. return [ 'po_number' => ['required', 'string', 'max:60', 'unique:purchase_orders,po_number,'.($id ?? 'NULL').',id'], 'supplier_id' => ['required', 'integer', 'min:1', 'exists:suppliers,id'], 'status' => ['nullable', 'string', 'in:'.implode(',', self::allowedStatuses())], 'order_date' => ['nullable', 'date'], 'expected_date' => ['nullable', 'date', 'after_or_equal:order_date'], 'subtotal' => ['nullable', 'numeric', 'min:0'], 'tax' => ['nullable', 'numeric', 'min:0'], 'total' => ['nullable', 'numeric', 'min:0'], 'notes' => ['nullable', 'string', 'max:5000'], ]; } }