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>
209 lines
5.5 KiB
PHP
209 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class MembershipPayment extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
// Status constants
|
|
const STATUS_PENDING = 'pending';
|
|
const STATUS_APPROVED_CASHIER = 'approved_cashier';
|
|
const STATUS_APPROVED_ACCOUNTANT = 'approved_accountant';
|
|
const STATUS_APPROVED_CHAIR = 'approved_chair';
|
|
const STATUS_REJECTED = 'rejected';
|
|
|
|
// Payment method constants
|
|
const METHOD_BANK_TRANSFER = 'bank_transfer';
|
|
const METHOD_CONVENIENCE_STORE = 'convenience_store';
|
|
const METHOD_CASH = 'cash';
|
|
const METHOD_CREDIT_CARD = 'credit_card';
|
|
|
|
// Fee type constants
|
|
const FEE_TYPE_ENTRANCE = 'entrance_fee'; // 入會會費
|
|
const FEE_TYPE_ANNUAL = 'annual_fee'; // 常年會費
|
|
|
|
protected $fillable = [
|
|
'member_id',
|
|
'fee_type',
|
|
'paid_at',
|
|
'amount',
|
|
'base_amount',
|
|
'discount_amount',
|
|
'final_amount',
|
|
'disability_discount',
|
|
'method',
|
|
'reference',
|
|
'status',
|
|
'payment_method',
|
|
'receipt_path',
|
|
'submitted_by_user_id',
|
|
'verified_by_cashier_id',
|
|
'cashier_verified_at',
|
|
'verified_by_accountant_id',
|
|
'accountant_verified_at',
|
|
'verified_by_chair_id',
|
|
'chair_verified_at',
|
|
'rejected_by_user_id',
|
|
'rejected_at',
|
|
'rejection_reason',
|
|
'notes',
|
|
];
|
|
|
|
protected $casts = [
|
|
'paid_at' => 'date',
|
|
'cashier_verified_at' => 'datetime',
|
|
'accountant_verified_at' => 'datetime',
|
|
'chair_verified_at' => 'datetime',
|
|
'rejected_at' => 'datetime',
|
|
'base_amount' => 'decimal:2',
|
|
'discount_amount' => 'decimal:2',
|
|
'final_amount' => 'decimal:2',
|
|
'disability_discount' => 'boolean',
|
|
];
|
|
|
|
// Relationships
|
|
public function member()
|
|
{
|
|
return $this->belongsTo(Member::class);
|
|
}
|
|
|
|
public function submittedBy()
|
|
{
|
|
return $this->belongsTo(User::class, 'submitted_by_user_id');
|
|
}
|
|
|
|
public function verifiedByCashier()
|
|
{
|
|
return $this->belongsTo(User::class, 'verified_by_cashier_id');
|
|
}
|
|
|
|
public function verifiedByAccountant()
|
|
{
|
|
return $this->belongsTo(User::class, 'verified_by_accountant_id');
|
|
}
|
|
|
|
public function verifiedByChair()
|
|
{
|
|
return $this->belongsTo(User::class, 'verified_by_chair_id');
|
|
}
|
|
|
|
public function rejectedBy()
|
|
{
|
|
return $this->belongsTo(User::class, 'rejected_by_user_id');
|
|
}
|
|
|
|
// Status check methods
|
|
public function isPending(): bool
|
|
{
|
|
return $this->status === self::STATUS_PENDING;
|
|
}
|
|
|
|
public function isApprovedByCashier(): bool
|
|
{
|
|
return $this->status === self::STATUS_APPROVED_CASHIER;
|
|
}
|
|
|
|
public function isApprovedByAccountant(): bool
|
|
{
|
|
return $this->status === self::STATUS_APPROVED_ACCOUNTANT;
|
|
}
|
|
|
|
public function isFullyApproved(): bool
|
|
{
|
|
return $this->status === self::STATUS_APPROVED_CHAIR;
|
|
}
|
|
|
|
public function isRejected(): bool
|
|
{
|
|
return $this->status === self::STATUS_REJECTED;
|
|
}
|
|
|
|
// Workflow validation methods
|
|
public function canBeApprovedByCashier(): bool
|
|
{
|
|
return $this->isPending();
|
|
}
|
|
|
|
public function canBeApprovedByAccountant(): bool
|
|
{
|
|
return $this->isApprovedByCashier();
|
|
}
|
|
|
|
public function canBeApprovedByChair(): bool
|
|
{
|
|
return $this->isApprovedByAccountant();
|
|
}
|
|
|
|
// Accessor for status label
|
|
public function getStatusLabelAttribute(): string
|
|
{
|
|
return match($this->status) {
|
|
self::STATUS_PENDING => '待審核',
|
|
self::STATUS_APPROVED_CASHIER => '出納已審',
|
|
self::STATUS_APPROVED_ACCOUNTANT => '會計已審',
|
|
self::STATUS_APPROVED_CHAIR => '主席已審',
|
|
self::STATUS_REJECTED => '已拒絕',
|
|
default => $this->status,
|
|
};
|
|
}
|
|
|
|
// Accessor for payment method label
|
|
public function getPaymentMethodLabelAttribute(): string
|
|
{
|
|
return match($this->payment_method) {
|
|
self::METHOD_BANK_TRANSFER => '銀行轉帳',
|
|
self::METHOD_CONVENIENCE_STORE => '超商繳費',
|
|
self::METHOD_CASH => '現金',
|
|
self::METHOD_CREDIT_CARD => '信用卡',
|
|
default => $this->payment_method ?? '未指定',
|
|
};
|
|
}
|
|
|
|
// Accessor for fee type label
|
|
public function getFeeTypeLabelAttribute(): string
|
|
{
|
|
return match($this->fee_type) {
|
|
self::FEE_TYPE_ENTRANCE => '入會會費',
|
|
self::FEE_TYPE_ANNUAL => '常年會費',
|
|
default => $this->fee_type ?? '未指定',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 是否有使用身心障礙優惠
|
|
*/
|
|
public function hasDisabilityDiscount(): bool
|
|
{
|
|
return (bool) $this->disability_discount;
|
|
}
|
|
|
|
/**
|
|
* 取得折扣說明
|
|
*/
|
|
public function getDiscountDescriptionAttribute(): ?string
|
|
{
|
|
if (!$this->hasDisabilityDiscount()) {
|
|
return null;
|
|
}
|
|
|
|
return '身心障礙優惠 50%';
|
|
}
|
|
|
|
// Clean up receipt file when payment is deleted
|
|
protected static function boot()
|
|
{
|
|
parent::boot();
|
|
|
|
static::deleting(function ($payment) {
|
|
if ($payment->receipt_path && Storage::disk('private')->exists($payment->receipt_path)) {
|
|
Storage::disk('private')->delete($payment->receipt_path);
|
|
}
|
|
});
|
|
}
|
|
}
|