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 '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', // 新工作流程欄位 'approved_by_secretary_id', 'secretary_approved_at', 'disbursement_status', 'requester_confirmed_at', 'requester_confirmed_by_id', 'cashier_confirmed_at', 'cashier_confirmed_by_id', 'recording_status', 'accountant_recorded_at', 'accountant_recorded_by_id', ]; 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', // 新工作流程欄位 'secretary_approved_at' => 'datetime', 'requester_confirmed_at' => 'datetime', 'cashier_confirmed_at' => 'datetime', 'accountant_recorded_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'); } /** * 新工作流程 Relationships */ public function approvedBySecretary(): BelongsTo { return $this->belongsTo(User::class, 'approved_by_secretary_id'); } public function requesterConfirmedBy(): BelongsTo { return $this->belongsTo(User::class, 'requester_confirmed_by_id'); } public function cashierConfirmedBy(): BelongsTo { return $this->belongsTo(User::class, 'cashier_confirmed_by_id'); } public function accountantRecordedBy(): BelongsTo { return $this->belongsTo(User::class, 'accountant_recorded_by_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); } /** * Get all accounting entries for this document */ public function accountingEntries() { return $this->hasMany(AccountingEntry::class); } /** * Get debit entries for this document */ public function debitEntries() { return $this->accountingEntries()->where('entry_type', AccountingEntry::ENTRY_TYPE_DEBIT); } /** * Get credit entries for this document */ public function creditEntries() { return $this->accountingEntries()->where('entry_type', AccountingEntry::ENTRY_TYPE_CREDIT); } /** * Validate that debit and credit entries balance */ public function validateBalance(): bool { $debitTotal = $this->debitEntries()->sum('amount'); $creditTotal = $this->creditEntries()->sum('amount'); return bccomp((string)$debitTotal, (string)$creditTotal, 2) === 0; } /** * Generate accounting entries for this document * This creates the double-entry bookkeeping records */ public function generateAccountingEntries(array $entries): void { // Delete existing entries $this->accountingEntries()->delete(); // Create new entries foreach ($entries as $entry) { $this->accountingEntries()->create([ 'chart_of_account_id' => $entry['chart_of_account_id'], 'entry_type' => $entry['entry_type'], 'amount' => $entry['amount'], 'entry_date' => $entry['entry_date'] ?? $this->submitted_at ?? now(), 'description' => $entry['description'] ?? $this->description, ]); } } /** * Auto-generate simple accounting entries based on document type * For basic income/expense transactions */ public function autoGenerateAccountingEntries(): void { // Only auto-generate if chart_of_account_id is set if (!$this->chart_of_account_id) { return; } $entries = []; $entryDate = $this->submitted_at ?? now(); // Determine if this is income or expense based on request type or account type $account = $this->chartOfAccount; if (!$account) { return; } if ($account->account_type === 'income') { // Income: Debit Cash, Credit Income Account $entries[] = [ 'chart_of_account_id' => $this->getCashAccountId(), 'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT, 'amount' => $this->amount, 'entry_date' => $entryDate, 'description' => '收入 - ' . ($this->description ?? $this->title), ]; $entries[] = [ 'chart_of_account_id' => $this->chart_of_account_id, 'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT, 'amount' => $this->amount, 'entry_date' => $entryDate, 'description' => $this->description ?? $this->title, ]; } elseif ($account->account_type === 'expense') { // Expense: Debit Expense Account, Credit Cash $entries[] = [ 'chart_of_account_id' => $this->chart_of_account_id, 'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT, 'amount' => $this->amount, 'entry_date' => $entryDate, 'description' => $this->description ?? $this->title, ]; $entries[] = [ 'chart_of_account_id' => $this->getCashAccountId(), 'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT, 'amount' => $this->amount, 'entry_date' => $entryDate, 'description' => '支出 - ' . ($this->description ?? $this->title), ]; } if (!empty($entries)) { $this->generateAccountingEntries($entries); } } /** * Get the cash account ID (1101 - 現金) */ protected function getCashAccountId(): int { static $cashAccountId = null; if ($cashAccountId === null) { $cashAccount = ChartOfAccount::where('account_code', '1101')->first(); $cashAccountId = $cashAccount ? $cashAccount->id : 1; } return $cashAccountId; } /** * 新工作流程:秘書長可審核 * 條件:待審核狀態 + 不能審核自己的申請 */ public function canBeApprovedBySecretary(?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; } /** * 新工作流程:理事長可審核 * 條件:秘書長已核准 + 中額或大額 */ public function canBeApprovedByChair(?User $user = null): bool { $tier = $this->amount_tier ?? $this->determineAmountTier(); if ($this->status !== self::STATUS_APPROVED_SECRETARY) { return false; } if (!in_array($tier, [self::AMOUNT_TIER_MEDIUM, self::AMOUNT_TIER_LARGE])) { return false; } if ($user && $this->submitted_by_user_id && $this->submitted_by_user_id === $user->id) { return false; } return true; } /** * 新工作流程:董理事會可審核 * 條件:理事長已核准 + 大額 */ public function canBeApprovedByBoard(?User $user = null): bool { $tier = $this->amount_tier ?? $this->determineAmountTier(); if ($this->status !== self::STATUS_APPROVED_CHAIR) { return false; } if ($tier !== self::AMOUNT_TIER_LARGE) { return false; } return true; } /** * 新工作流程:審核是否完成 * 依金額級別判斷 */ public function isApprovalComplete(): bool { $tier = $this->amount_tier ?? $this->determineAmountTier(); // 小額:秘書長核准即可 if ($tier === self::AMOUNT_TIER_SMALL) { return $this->status === self::STATUS_APPROVED_SECRETARY; } // 中額:理事長核准 if ($tier === self::AMOUNT_TIER_MEDIUM) { return $this->status === self::STATUS_APPROVED_CHAIR; } // 大額:董理事會核准 return $this->status === self::STATUS_APPROVED_BOARD; } /** * Check if document is fully approved (alias for isApprovalComplete) */ public function isFullyApproved(): bool { return $this->isApprovalComplete(); } // ========== 出帳階段方法 ========== /** * 申請人可確認出帳 * 條件:審核完成 + 尚未確認 + 是原申請人 */ public function canRequesterConfirmDisbursement(?User $user = null): bool { if (!$this->isApprovalComplete()) { return false; } if ($this->requester_confirmed_at !== null) { return false; } // 只有原申請人可以確認 if ($user && $this->submitted_by_user_id !== $user->id) { return false; } return true; } /** * 出納可確認出帳 * 條件:審核完成 + 尚未確認 */ public function canCashierConfirmDisbursement(): bool { if (!$this->isApprovalComplete()) { return false; } if ($this->cashier_confirmed_at !== null) { return false; } return true; } /** * 出帳是否完成(雙重確認) */ public function isDisbursementComplete(): bool { return $this->requester_confirmed_at !== null && $this->cashier_confirmed_at !== null; } // ========== 入帳階段方法 ========== /** * 會計可入帳 * 條件:出帳完成 + 尚未入帳 */ public function canAccountantConfirmRecording(): bool { return $this->isDisbursementComplete() && $this->accountant_recorded_at === null; } /** * 入帳是否完成 */ public function isRecordingComplete(): bool { return $this->accountant_recorded_at !== null; } // ========== Legacy methods for backward compatibility ========== /** * @deprecated Use canBeApprovedBySecretary instead */ public function canBeApprovedByCashier(?User $user = null): bool { return $this->canBeApprovedBySecretary($user); } /** * @deprecated Use isApprovalComplete with amount tier logic */ public function canBeApprovedByAccountant(): bool { // Legacy: accountant approval after cashier return $this->status === self::STATUS_APPROVED_CASHIER; } /** * 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 => '待審核', self::STATUS_APPROVED_SECRETARY => '秘書長已核准', self::STATUS_APPROVED_CHAIR => '理事長已核准', self::STATUS_APPROVED_BOARD => '董理事會已核准', self::STATUS_REJECTED => '已駁回', // Legacy statuses self::STATUS_APPROVED_CASHIER => '出納已審核', self::STATUS_APPROVED_ACCOUNTANT => '會計已審核', default => ucfirst($this->status), }; } /** * Get disbursement status label (中文) */ public function getDisbursementStatusLabelAttribute(): string { if (!$this->isApprovalComplete()) { return '審核中'; } if ($this->isDisbursementComplete()) { return '已出帳'; } if ($this->requester_confirmed_at !== null && $this->cashier_confirmed_at === null) { return '申請人已確認,待出納確認'; } if ($this->requester_confirmed_at === null && $this->cashier_confirmed_at !== null) { return '出納已確認,待申請人確認'; } return '待出帳'; } /** * Get recording status label (中文) */ public function getRecordingStatusLabelAttribute(): string { if (!$this->isDisbursementComplete()) { return '尚未出帳'; } if ($this->accountant_recorded_at !== null) { return '已入帳'; } return '待入帳'; } /** * Get overall workflow stage label (中文) */ public function getWorkflowStageLabelAttribute(): string { if ($this->isRejected()) { return '已駁回'; } if (!$this->isApprovalComplete()) { return '審核階段'; } if (!$this->isDisbursementComplete()) { return '出帳階段'; } if (!$this->isRecordingComplete()) { return '入帳階段'; } return '已完成'; } /** * 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 disbursement) * 新工作流程:使用 isApprovalComplete() */ public function isApprovalStageComplete(): bool { return $this->isApprovalComplete(); } /** * 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 document is fully processed (all stages complete) */ public function isFullyProcessed(): bool { return $this->isApprovalComplete() && $this->isDisbursementComplete() && $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 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'; } }