Initial commit

This commit is contained in:
2025-11-20 23:21:05 +08:00
commit 13bc6db529
378 changed files with 54527 additions and 0 deletions

View File

@@ -0,0 +1,435 @@
<?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_CASHIER = 'approved_cashier';
public const STATUS_APPROVED_ACCOUNTANT = 'approved_accountant';
public const STATUS_APPROVED_CHAIR = 'approved_chair';
public const STATUS_REJECTED = 'rejected';
// Request type constants
public const REQUEST_TYPE_EXPENSE_REIMBURSEMENT = 'expense_reimbursement';
public const REQUEST_TYPE_ADVANCE_PAYMENT = 'advance_payment';
public const REQUEST_TYPE_PURCHASE_REQUEST = 'purchase_request';
public const REQUEST_TYPE_PETTY_CASH = 'petty_cash';
// 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',
'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';
}
}