Files
usher-manage-stack/app/Models/FinanceDocument.php

510 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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',
'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';
}
}