Initial commit
This commit is contained in:
435
app/Models/FinanceDocument.php
Normal file
435
app/Models/FinanceDocument.php
Normal 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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user