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>
This commit is contained in:
2025-12-01 09:56:01 +08:00
parent 83ce1f7fc8
commit 642b879dd4
207 changed files with 19487 additions and 3048 deletions

View File

@@ -0,0 +1,154 @@
<?php
namespace App\Services;
use App\Models\Member;
use App\Models\MembershipPayment;
class MembershipFeeCalculator
{
protected SettingsService $settings;
public function __construct(SettingsService $settings)
{
$this->settings = $settings;
}
/**
* 計算會費金額
*
* @param Member $member 會員
* @param string $feeType 會費類型 (entrance_fee | annual_fee)
* @return array{base_amount: float, discount_amount: float, final_amount: float, disability_discount: bool, fee_type: string}
*/
public function calculate(Member $member, string $feeType): array
{
$baseAmount = $this->getBaseAmount($feeType);
$discountRate = $member->hasApprovedDisability()
? $this->getDisabilityDiscountRate()
: 0;
$discountAmount = round($baseAmount * $discountRate, 2);
$finalAmount = round($baseAmount - $discountAmount, 2);
return [
'fee_type' => $feeType,
'base_amount' => $baseAmount,
'discount_amount' => $discountAmount,
'final_amount' => $finalAmount,
'disability_discount' => $discountRate > 0,
];
}
/**
* 為會員計算下一次應繳的會費
*
* @param Member $member
* @return array{base_amount: float, discount_amount: float, final_amount: float, disability_discount: bool, fee_type: string}
*/
public function calculateNextFee(Member $member): array
{
$feeType = $member->getNextFeeType();
return $this->calculate($member, $feeType);
}
/**
* 取得基本會費金額
*
* @param string $feeType
* @return float
*/
public function getBaseAmount(string $feeType): float
{
return match($feeType) {
MembershipPayment::FEE_TYPE_ENTRANCE => $this->getEntranceFee(),
MembershipPayment::FEE_TYPE_ANNUAL => $this->getAnnualFee(),
default => 0,
};
}
/**
* 取得入會會費金額
*
* @return float
*/
public function getEntranceFee(): float
{
return (float) $this->settings->get('membership_fee.entrance_fee', 1000);
}
/**
* 取得常年會費金額
*
* @return float
*/
public function getAnnualFee(): float
{
return (float) $this->settings->get('membership_fee.annual_fee', 1000);
}
/**
* 取得身心障礙折扣比例
*
* @return float 折扣比例 (0-1)
*/
public function getDisabilityDiscountRate(): float
{
return (float) $this->settings->get('membership_fee.disability_discount_rate', 0.5);
}
/**
* 取得身心障礙折扣百分比 (用於顯示)
*
* @return int 百分比 (0-100)
*/
public function getDisabilityDiscountPercentage(): int
{
return (int) ($this->getDisabilityDiscountRate() * 100);
}
/**
* 驗證繳費金額是否正確
*
* @param MembershipPayment $payment
* @return bool
*/
public function validatePaymentAmount(MembershipPayment $payment): bool
{
$member = $payment->member;
$expected = $this->calculate($member, $payment->fee_type);
// 繳費金額應等於或大於應繳金額
return $payment->amount >= $expected['final_amount'];
}
/**
* 取得會費類型的標籤
*
* @param string $feeType
* @return string
*/
public function getFeeTypeLabel(string $feeType): string
{
return match($feeType) {
MembershipPayment::FEE_TYPE_ENTRANCE => '入會會費',
MembershipPayment::FEE_TYPE_ANNUAL => '常年會費',
default => $feeType,
};
}
/**
* 取得所有會費設定(用於管理界面)
*
* @return array
*/
public function getFeeSettings(): array
{
return [
'entrance_fee' => $this->getEntranceFee(),
'annual_fee' => $this->getAnnualFee(),
'disability_discount_rate' => $this->getDisabilityDiscountRate(),
'disability_discount_percentage' => $this->getDisabilityDiscountPercentage(),
];
}
}