50,000 // Reconciliation status constants public const RECONCILIATION_PENDING = 'pending'; public const RECONCILIATION_MATCHED = 'matched'; public const RECONCILIATION_DISCREPANCY = 'discrepancy'; public const RECONCILIATION_RESOLVED = 'resolved'; // Payment method constants public const PAYMENT_METHOD_BANK_TRANSFER = 'bank_transfer'; public const PAYMENT_METHOD_CHECK = 'check'; public const PAYMENT_METHOD_CASH = 'cash'; protected $fillable = [ 'member_id', 'submitted_by_user_id', 'submitted_by_id', 'title', 'amount', 'status', 'description', 'attachment_path', 'submitted_at', 'approved_by_cashier_id', 'cashier_approved_by_id', 'cashier_approved_at', 'approved_by_accountant_id', 'accountant_approved_by_id', 'accountant_approved_at', 'approved_by_chair_id', 'chair_approved_by_id', 'chair_approved_at', 'rejected_by_user_id', 'rejected_at', 'rejection_reason', // New payment stage fields 'request_type', 'amount_tier', 'chart_of_account_id', 'budget_item_id', 'requires_board_meeting', 'approved_by_board_meeting_id', 'board_meeting_approved_by_id', 'board_meeting_approved_at', 'payment_order_created_by_accountant_id', 'payment_order_created_at', 'payment_method', 'payee_name', 'payee_account_number', 'payee_bank_name', 'payment_verified_by_cashier_id', 'payment_verified_at', 'payment_executed_by_cashier_id', 'payment_executed_at', 'payment_transaction_id', 'payment_receipt_path', 'actual_payment_amount', 'cashier_ledger_entry_id', 'accounting_transaction_id', 'bank_reconciliation_id', 'reconciliation_status', 'reconciled_at', ]; protected $casts = [ 'amount' => 'decimal:2', 'submitted_at' => 'datetime', 'cashier_approved_at' => 'datetime', 'accountant_approved_at' => 'datetime', 'chair_approved_at' => 'datetime', 'rejected_at' => 'datetime', // New payment stage casts 'requires_board_meeting' => 'boolean', 'board_meeting_approved_at' => 'datetime', 'payment_order_created_at' => 'datetime', 'payment_verified_at' => 'datetime', 'payment_executed_at' => 'datetime', 'actual_payment_amount' => 'decimal:2', 'reconciled_at' => 'datetime', ]; public function member() { return $this->belongsTo(Member::class); } public function submittedBy() { return $this->belongsTo(User::class, 'submitted_by_user_id'); } public function approvedByCashier() { return $this->belongsTo(User::class, 'approved_by_cashier_id'); } public function approvedByAccountant() { return $this->belongsTo(User::class, 'approved_by_accountant_id'); } public function approvedByChair() { return $this->belongsTo(User::class, 'approved_by_chair_id'); } public function rejectedBy() { return $this->belongsTo(User::class, 'rejected_by_user_id'); } /** * New payment stage relationships */ public function chartOfAccount(): BelongsTo { return $this->belongsTo(ChartOfAccount::class); } public function budgetItem(): BelongsTo { return $this->belongsTo(BudgetItem::class); } public function approvedByBoardMeeting(): BelongsTo { return $this->belongsTo(BoardMeeting::class, 'approved_by_board_meeting_id'); } public function paymentOrderCreatedByAccountant(): BelongsTo { return $this->belongsTo(User::class, 'payment_order_created_by_accountant_id'); } public function paymentVerifiedByCashier(): BelongsTo { return $this->belongsTo(User::class, 'payment_verified_by_cashier_id'); } public function paymentExecutedByCashier(): BelongsTo { return $this->belongsTo(User::class, 'payment_executed_by_cashier_id'); } public function cashierLedgerEntry(): BelongsTo { return $this->belongsTo(CashierLedgerEntry::class); } public function accountingTransaction(): BelongsTo { return $this->belongsTo(Transaction::class, 'accounting_transaction_id'); } public function paymentOrder(): HasOne { return $this->hasOne(PaymentOrder::class); } /** * Check if document can be approved by cashier */ public function canBeApprovedByCashier(?User $user = null): bool { if ($this->status !== self::STATUS_PENDING) { return false; } if ($user && $this->submitted_by_user_id && $this->submitted_by_user_id === $user->id) { return false; } return true; } /** * Check if document can be approved by accountant */ public function canBeApprovedByAccountant(): bool { return $this->status === self::STATUS_APPROVED_CASHIER; } /** * Check if document can be approved by chair */ public function canBeApprovedByChair(): bool { return $this->status === self::STATUS_APPROVED_ACCOUNTANT; } /** * Check if document is fully approved */ public function isFullyApproved(): bool { return $this->status === self::STATUS_APPROVED_CHAIR; } /** * Check if document is rejected */ public function isRejected(): bool { return $this->status === self::STATUS_REJECTED; } /** * Get human-readable status */ public function getStatusLabelAttribute(): string { return match($this->status) { self::STATUS_PENDING => 'Pending Cashier Approval', self::STATUS_APPROVED_CASHIER => 'Pending Accountant Approval', self::STATUS_APPROVED_ACCOUNTANT => 'Pending Chair Approval', self::STATUS_APPROVED_CHAIR => 'Fully Approved', self::STATUS_REJECTED => 'Rejected', default => ucfirst($this->status), }; } /** * New payment stage business logic methods */ /** * Determine amount tier based on amount */ public function determineAmountTier(): string { if ($this->amount < 5000) { return self::AMOUNT_TIER_SMALL; } elseif ($this->amount <= 50000) { return self::AMOUNT_TIER_MEDIUM; } else { return self::AMOUNT_TIER_LARGE; } } /** * Check if document needs board meeting approval */ public function needsBoardMeetingApproval(): bool { $tier = $this->amount_tier ?? $this->determineAmountTier(); return $tier === self::AMOUNT_TIER_LARGE; } /** * Check if approval stage is complete (ready for payment order creation) */ public function isApprovalStageComplete(): bool { $tier = $this->amount_tier ?? $this->determineAmountTier(); // For small amounts: cashier + accountant if ($tier === self::AMOUNT_TIER_SMALL) { return $this->status === self::STATUS_APPROVED_ACCOUNTANT; } // For medium amounts: cashier + accountant + chair if ($tier === self::AMOUNT_TIER_MEDIUM) { return $this->status === self::STATUS_APPROVED_CHAIR; } // For large amounts: cashier + accountant + chair + board meeting if ($tier === self::AMOUNT_TIER_LARGE) { return $this->status === self::STATUS_APPROVED_CHAIR && $this->board_meeting_approved_at !== null; } return false; } /** * Check if accountant can create payment order */ public function canCreatePaymentOrder(): bool { return $this->isApprovalStageComplete() && $this->payment_order_created_at === null; } /** * Check if cashier can verify payment */ public function canVerifyPayment(): bool { return $this->payment_order_created_at !== null && $this->payment_verified_at === null && $this->paymentOrder !== null && $this->paymentOrder->canBeVerifiedByCashier(); } /** * Check if cashier can execute payment */ public function canExecutePayment(): bool { return $this->payment_verified_at !== null && $this->payment_executed_at === null && $this->paymentOrder !== null && $this->paymentOrder->canBeExecuted(); } /** * Check if payment is completed */ public function isPaymentCompleted(): bool { return $this->payment_executed_at !== null; } /** * Check if recording stage is complete */ public function isRecordingComplete(): bool { return $this->cashier_recorded_at !== null; } /** * Check if document is fully processed (all stages complete) */ public function isFullyProcessed(): bool { return $this->isApprovalStageComplete() && $this->isPaymentCompleted() && $this->isRecordingComplete(); } /** * Check if reconciliation is complete */ public function isReconciled(): bool { return $this->bank_reconciliation_id !== null; } // ============== Attribute aliases for backward compatibility ============== public function getSubmittedByIdAttribute(): ?int { return $this->attributes['submitted_by_id'] ?? $this->attributes['submitted_by_user_id'] ?? null; } public function setSubmittedByIdAttribute($value): void { $this->attributes['submitted_by_user_id'] = $value; $this->attributes['submitted_by_id'] = $value; } public function setSubmittedByUserIdAttribute($value): void { $this->attributes['submitted_by_user_id'] = $value; $this->attributes['submitted_by_id'] = $value; } public function getCashierApprovedByIdAttribute(): ?int { return $this->approved_by_cashier_id ?? $this->attributes['approved_by_cashier_id'] ?? null; } public function setCashierApprovedByIdAttribute($value): void { $this->attributes['approved_by_cashier_id'] = $value; } public function getAccountantApprovedByIdAttribute(): ?int { return $this->approved_by_accountant_id ?? $this->attributes['approved_by_accountant_id'] ?? null; } public function setAccountantApprovedByIdAttribute($value): void { $this->attributes['approved_by_accountant_id'] = $value; } public function getChairApprovedByIdAttribute(): ?int { return $this->approved_by_chair_id ?? $this->attributes['approved_by_chair_id'] ?? null; } public function setChairApprovedByIdAttribute($value): void { $this->attributes['approved_by_chair_id'] = $value; } public function getBoardMeetingApprovedByIdAttribute(): ?int { return $this->approved_by_board_meeting_id ?? $this->attributes['approved_by_board_meeting_id'] ?? null; } public function setBoardMeetingApprovedByIdAttribute($value): void { $this->attributes['approved_by_board_meeting_id'] = $value; } /** * Get request type text */ public function getRequestTypeText(): string { return match ($this->request_type) { self::REQUEST_TYPE_EXPENSE_REIMBURSEMENT => '費用報銷', self::REQUEST_TYPE_ADVANCE_PAYMENT => '預支款項', self::REQUEST_TYPE_PURCHASE_REQUEST => '採購申請', self::REQUEST_TYPE_PETTY_CASH => '零用金', default => '未知', }; } /** * Get amount tier text */ public function getAmountTierText(): string { return match ($this->amount_tier) { self::AMOUNT_TIER_SMALL => '小額(< 5000)', self::AMOUNT_TIER_MEDIUM => '中額(5000-50000)', self::AMOUNT_TIER_LARGE => '大額(> 50000)', default => '未知', }; } /** * Get payment method text */ public function getPaymentMethodText(): string { return match ($this->payment_method) { self::PAYMENT_METHOD_BANK_TRANSFER => '銀行轉帳', self::PAYMENT_METHOD_CHECK => '支票', self::PAYMENT_METHOD_CASH => '現金', default => '未知', }; } /** * Get reconciliation status text */ public function getReconciliationStatusText(): string { return match ($this->reconciliation_status) { self::RECONCILIATION_PENDING => '待調節', self::RECONCILIATION_MATCHED => '已調節', self::RECONCILIATION_DISCREPANCY => '有差異', self::RECONCILIATION_RESOLVED => '已解決', default => '未知', }; } /** * Get current workflow stage */ public function getCurrentWorkflowStage(): string { if (!$this->isApprovalStageComplete()) { return 'approval'; } if ($this->payment_order_created_at === null) { return 'approval'; } if ($this->cashier_recorded_at === null) { return 'payment'; } if ($this->bank_reconciliation_id !== null) { return 'completed'; } if (! $this->exists) { return 'recording'; } return 'reconciliation'; } }