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:
214
app/Services/FinanceDocumentApprovalService.php
Normal file
214
app/Services/FinanceDocumentApprovalService.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Mail\FinanceDocumentApprovedByAccountant;
|
||||
use App\Mail\FinanceDocumentFullyApproved;
|
||||
use App\Mail\FinanceDocumentRejected;
|
||||
use App\Models\FinanceDocument;
|
||||
use App\Models\User;
|
||||
use App\Support\AuditLogger;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
/**
|
||||
* Service for handling FinanceDocument approval workflow.
|
||||
*
|
||||
* Workflow: Secretary → Chair → Board (based on amount tier)
|
||||
* - Small (<5,000): Secretary only
|
||||
* - Medium (5,000-50,000): Secretary → Chair
|
||||
* - Large (>50,000): Secretary → Chair → Board
|
||||
*/
|
||||
class FinanceDocumentApprovalService
|
||||
{
|
||||
/**
|
||||
* Approve by Secretary (first stage)
|
||||
*/
|
||||
public function approveBySecretary(FinanceDocument $document, User $user): array
|
||||
{
|
||||
if (! $document->canBeApprovedBySecretary($user)) {
|
||||
return ['success' => false, 'message' => '無法在此階段進行秘書長審核。'];
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'approved_by_secretary_id' => $user->id,
|
||||
'secretary_approved_at' => now(),
|
||||
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.approved_by_secretary', $document, [
|
||||
'approved_by' => $user->name,
|
||||
'amount_tier' => $document->amount_tier,
|
||||
]);
|
||||
|
||||
// Small amount: approval complete
|
||||
if ($document->amount_tier === FinanceDocument::AMOUNT_TIER_SMALL) {
|
||||
$this->notifySubmitter($document);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '秘書長已核准。小額申請審核完成,申請人可向出納領款。',
|
||||
'complete' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// Medium/Large: notify chairs
|
||||
$this->notifyNextApprovers($document, 'finance_chair');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '秘書長已核准。已送交理事長審核。',
|
||||
'complete' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve by Chair (second stage)
|
||||
*/
|
||||
public function approveByChair(FinanceDocument $document, User $user): array
|
||||
{
|
||||
if (! $document->canBeApprovedByChair($user)) {
|
||||
return ['success' => false, 'message' => '無法在此階段進行理事長審核。'];
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'approved_by_chair_id' => $user->id,
|
||||
'chair_approved_at' => now(),
|
||||
'status' => FinanceDocument::STATUS_APPROVED_CHAIR,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.approved_by_chair', $document, [
|
||||
'approved_by' => $user->name,
|
||||
'amount_tier' => $document->amount_tier,
|
||||
]);
|
||||
|
||||
// Medium amount: approval complete
|
||||
if ($document->amount_tier === FinanceDocument::AMOUNT_TIER_MEDIUM) {
|
||||
$this->notifySubmitter($document);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '理事長已核准。中額申請審核完成,申請人可向出納領款。',
|
||||
'complete' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// Large: notify board members
|
||||
$this->notifyNextApprovers($document, 'finance_board_member');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '理事長已核准。大額申請需送交董理事會審核。',
|
||||
'complete' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve by Board (third stage)
|
||||
*/
|
||||
public function approveByBoard(FinanceDocument $document, User $user): array
|
||||
{
|
||||
if (! $document->canBeApprovedByBoard($user)) {
|
||||
return ['success' => false, 'message' => '無法在此階段進行董理事會審核。'];
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'board_meeting_approved_by_id' => $user->id,
|
||||
'board_meeting_approved_at' => now(),
|
||||
'status' => FinanceDocument::STATUS_APPROVED_BOARD,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.approved_by_board', $document, [
|
||||
'approved_by' => $user->name,
|
||||
'amount_tier' => $document->amount_tier,
|
||||
]);
|
||||
|
||||
$this->notifySubmitter($document);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '董理事會已核准。審核流程完成,申請人可向出納領款。',
|
||||
'complete' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject document at any stage
|
||||
*/
|
||||
public function reject(FinanceDocument $document, User $user, string $reason): array
|
||||
{
|
||||
if ($document->isRejected()) {
|
||||
return ['success' => false, 'message' => '此申請已被駁回。'];
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'status' => FinanceDocument::STATUS_REJECTED,
|
||||
'rejected_by_user_id' => $user->id,
|
||||
'rejected_at' => now(),
|
||||
'rejection_reason' => $reason,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.rejected', $document, [
|
||||
'rejected_by' => $user->name,
|
||||
'reason' => $reason,
|
||||
]);
|
||||
|
||||
// Notify submitter
|
||||
if ($document->submittedBy) {
|
||||
Mail::to($document->submittedBy->email)->queue(new FinanceDocumentRejected($document));
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '申請已駁回。',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process approval based on user role
|
||||
*/
|
||||
public function processApproval(FinanceDocument $document, User $user): array
|
||||
{
|
||||
$isSecretary = $user->hasRole('secretary_general');
|
||||
$isChair = $user->hasRole('finance_chair');
|
||||
$isBoardMember = $user->hasRole('finance_board_member');
|
||||
$isAdmin = $user->hasRole('admin');
|
||||
|
||||
// Secretary approval
|
||||
if ($document->canBeApprovedBySecretary($user) && ($isSecretary || $isAdmin)) {
|
||||
return $this->approveBySecretary($document, $user);
|
||||
}
|
||||
|
||||
// Chair approval
|
||||
if ($document->canBeApprovedByChair($user) && ($isChair || $isAdmin)) {
|
||||
return $this->approveByChair($document, $user);
|
||||
}
|
||||
|
||||
// Board approval
|
||||
if ($document->canBeApprovedByBoard($user) && ($isBoardMember || $isAdmin)) {
|
||||
return $this->approveByBoard($document, $user);
|
||||
}
|
||||
|
||||
return ['success' => false, 'message' => '您無權在此階段審核此文件。'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify submitter that approval is complete
|
||||
*/
|
||||
protected function notifySubmitter(FinanceDocument $document): void
|
||||
{
|
||||
if ($document->submittedBy) {
|
||||
Mail::to($document->submittedBy->email)->queue(new FinanceDocumentFullyApproved($document));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify next approvers in workflow
|
||||
*/
|
||||
protected function notifyNextApprovers(FinanceDocument $document, string $roleName): void
|
||||
{
|
||||
$approvers = User::role($roleName)->get();
|
||||
foreach ($approvers as $approver) {
|
||||
Mail::to($approver->email)->queue(new FinanceDocumentApprovedByAccountant($document));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user