Features: - Implement two fee types: entrance fee and annual fee (both NT$1,000) - Add 50% discount for disability certificate holders - Add disability certificate upload in member profile - Integrate disability verification into cashier approval workflow - Add membership fee settings in system admin Document permissions: - Fix hard-coded role logic in Document model - Use permission-based authorization instead of role checks Additional features: - Add announcements, general ledger, and trial balance modules - Add income management and accounting entries - Add comprehensive test suite with factories - Update UI translations to Traditional Chinese 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
852 lines
24 KiB
PHP
852 lines
24 KiB
PHP
<?php
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||
|
||
class FinanceDocument extends Model
|
||
{
|
||
use HasFactory;
|
||
|
||
// Status constants (審核階段)
|
||
public const STATUS_PENDING = 'pending'; // 待審核
|
||
public const STATUS_APPROVED_SECRETARY = 'approved_secretary'; // 秘書長已核准
|
||
public const STATUS_APPROVED_CHAIR = 'approved_chair'; // 理事長已核准
|
||
public const STATUS_APPROVED_BOARD = 'approved_board'; // 董理事會已核准
|
||
public const STATUS_REJECTED = 'rejected'; // 已駁回
|
||
|
||
// Legacy status constants (保留向後相容)
|
||
public const STATUS_APPROVED_CASHIER = 'approved_cashier';
|
||
public const STATUS_APPROVED_ACCOUNTANT = 'approved_accountant';
|
||
|
||
// Disbursement status constants (出帳階段)
|
||
public const DISBURSEMENT_PENDING = 'pending'; // 待出帳
|
||
public const DISBURSEMENT_REQUESTER_CONFIRMED = 'requester_confirmed'; // 申請人已確認
|
||
public const DISBURSEMENT_CASHIER_CONFIRMED = 'cashier_confirmed'; // 出納已確認
|
||
public const DISBURSEMENT_COMPLETED = 'completed'; // 已出帳
|
||
|
||
// Recording status constants (入帳階段)
|
||
public const RECORDING_PENDING = 'pending'; // 待入帳
|
||
public const RECORDING_COMPLETED = 'completed'; // 已入帳
|
||
|
||
// Amount tier constants
|
||
public const AMOUNT_TIER_SMALL = 'small'; // < 5,000
|
||
public const AMOUNT_TIER_MEDIUM = 'medium'; // 5,000 - 50,000
|
||
public const AMOUNT_TIER_LARGE = 'large'; // > 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';
|
||
}
|
||
}
|