Add phone login support and member import functionality

Features:
- Support login via phone number or email (LoginRequest)
- Add members:import-roster command for Excel roster import
- Merge survey emails with roster data

Code Quality (Phase 1-4):
- Add database locking for balance calculation
- Add self-approval checks for finance workflow
- Create service layer (FinanceDocumentApprovalService, PaymentVerificationService)
- Add HasAccountingEntries and HasApprovalWorkflow traits
- Create FormRequest classes for validation
- Add status-badge component
- Define authorization gates in AuthServiceProvider
- Add accounting config file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 03:08:06 +08:00
parent ed7169b64e
commit 42099759e8
66 changed files with 3492 additions and 3803 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\HasAccountingEntries;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -10,7 +11,7 @@ use Illuminate\Support\Facades\DB;
class Income extends Model
{
use HasFactory;
use HasAccountingEntries, HasFactory;
// 收入類型常數
const TYPE_MEMBERSHIP_FEE = 'membership_fee'; // 會費收入
@@ -144,11 +145,27 @@ class Income extends Model
}
/**
* 會計分錄
* Override trait's foreign key for accounting entries
*/
public function accountingEntries(): HasMany
protected function getAccountingForeignKey(): string
{
return $this->hasMany(AccountingEntry::class);
return 'income_id';
}
/**
* Override trait's accounting date
*/
protected function getAccountingDate()
{
return $this->income_date ?? $this->created_at ?? now();
}
/**
* Override trait's accounting description
*/
protected function getAccountingDescription(): string
{
return "收入:{$this->title} ({$this->income_number})";
}
// ========== 狀態查詢 ==========
@@ -216,7 +233,7 @@ class Income extends Model
$ledgerEntry = $this->createCashierLedgerEntry();
// 3. 產生會計分錄
$this->generateAccountingEntries();
$this->createIncomeAccountingEntries();
});
}
@@ -263,42 +280,46 @@ class Income extends Model
}
/**
* 產生會計分錄
* 產生會計分錄 (使用 trait 的方法)
*/
protected function generateAccountingEntries(): void
protected function createIncomeAccountingEntries(): void
{
// 借方:資產帳戶(現金或銀行存款)
$assetAccountId = $this->getAssetAccountId();
$assetAccountId = $this->getAssetAccountIdForPaymentMethod();
$description = $this->getAccountingDescription();
$entryDate = $this->getAccountingDate();
AccountingEntry::create([
'income_id' => $this->id,
'chart_of_account_id' => $assetAccountId,
'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT,
'amount' => $this->amount,
'entry_date' => $this->income_date,
'description' => "收入:{$this->title} ({$this->income_number})",
]);
$entries = [
// 借方:資產帳戶(現金或銀行存款)
[
'chart_of_account_id' => $assetAccountId,
'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT,
'amount' => $this->amount,
'entry_date' => $entryDate,
'description' => $description,
],
// 貸方:收入科目
[
'chart_of_account_id' => $this->chart_of_account_id,
'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT,
'amount' => $this->amount,
'entry_date' => $entryDate,
'description' => $description,
],
];
// 貸方:收入科目
AccountingEntry::create([
'income_id' => $this->id,
'chart_of_account_id' => $this->chart_of_account_id,
'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT,
'amount' => $this->amount,
'entry_date' => $this->income_date,
'description' => "收入:{$this->title} ({$this->income_number})",
]);
// Use trait's method
$this->generateAccountingEntries($entries);
}
/**
* 根據付款方式取得資產帳戶 ID
* 根據付款方式取得資產帳戶 ID (使用 config)
*/
protected function getAssetAccountId(): int
protected function getAssetAccountIdForPaymentMethod(): int
{
$accountCode = match ($this->payment_method) {
self::PAYMENT_METHOD_BANK_TRANSFER => '1201', // 銀行存款
self::PAYMENT_METHOD_CHECK => '1201', // 銀行存款
default => '1101', // 現金
self::PAYMENT_METHOD_BANK_TRANSFER,
self::PAYMENT_METHOD_CHECK => config('accounting.account_codes.bank', '1201'),
default => config('accounting.account_codes.cash', '1101'),
};
return ChartOfAccount::where('account_code', $accountCode)->value('id') ?? 1;