Files
usher-manage-stack/app/Models/MembershipPayment.php
Gbanyan 642b879dd4 Add membership fee system with disability discount and fix document permissions
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>
2025-12-01 09:56:01 +08:00

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);
}
});
}
}