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', 'title', 'amount', 'status', 'description', 'attachment_path', 'submitted_at', 'approved_by_cashier_id', 'cashier_approved_at', 'approved_by_accountant_id', 'accountant_approved_at', 'approved_by_chair_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_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', '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(): bool { return $this->status === self::STATUS_PENDING; } /** * 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 { return $this->amount_tier === self::AMOUNT_TIER_LARGE; } /** * Check if approval stage is complete (ready for payment order creation) */ public function isApprovalStageComplete(): bool { // For small amounts: cashier + accountant if ($this->amount_tier === self::AMOUNT_TIER_SMALL) { return $this->status === self::STATUS_APPROVED_ACCOUNTANT; } // For medium amounts: cashier + accountant + chair if ($this->amount_tier === self::AMOUNT_TIER_MEDIUM) { return $this->status === self::STATUS_APPROVED_CHAIR; } // For large amounts: cashier + accountant + chair + board meeting if ($this->amount_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 && $this->paymentOrder !== null && $this->paymentOrder->isExecuted(); } /** * Check if recording stage is complete */ public function isRecordingComplete(): bool { return $this->cashier_ledger_entry_id !== null && $this->accounting_transaction_id !== 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->reconciliation_status === self::RECONCILIATION_MATCHED || $this->reconciliation_status === self::RECONCILIATION_RESOLVED; } /** * 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 => '小額 (< 5,000)', self::AMOUNT_TIER_MEDIUM => '中額 (5,000-50,000)', self::AMOUNT_TIER_LARGE => '大額 (> 50,000)', 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->isPaymentCompleted()) { return 'payment'; } if (!$this->isRecordingComplete()) { return 'recording'; } if (!$this->isReconciled()) { return 'reconciliation'; } return 'completed'; } }