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

@@ -19,6 +19,7 @@ use Tests\Traits\SeedsRolesAndPermissions;
* Role Permission Tests
*
* Tests role-based access control and permissions.
* Uses new workflow: Secretary Chair Board
*/
class RolePermissionTest extends TestCase
{
@@ -49,8 +50,7 @@ class RolePermissionTest extends TestCase
*/
public function test_member_cannot_access_admin_dashboard(): void
{
$user = User::factory()->create();
$user->assignRole('member');
$user = $this->createMemberUser();
$response = $this->actingAs($user)->get(route('admin.dashboard'));
@@ -58,15 +58,15 @@ class RolePermissionTest extends TestCase
}
/**
* Test cashier can approve payments
* Test cashier can approve membership payments (first tier)
*/
public function test_cashier_can_approve_payments(): void
public function test_cashier_can_approve_membership_payments(): void
{
$cashier = $this->createCashier();
$data = $this->createMemberWithPendingPayment();
$response = $this->actingAs($cashier)->post(
route('admin.membership-payments.approve', $data['payment'])
route('admin.payment-verifications.approve-cashier', $data['payment'])
);
$data['payment']->refresh();
@@ -74,7 +74,7 @@ class RolePermissionTest extends TestCase
}
/**
* Test accountant cannot approve pending payment directly
* Test accountant cannot approve pending payment directly (needs cashier first)
*/
public function test_accountant_cannot_approve_pending_payment_directly(): void
{
@@ -82,12 +82,12 @@ class RolePermissionTest extends TestCase
$data = $this->createMemberWithPendingPayment();
$response = $this->actingAs($accountant)->post(
route('admin.membership-payments.approve', $data['payment'])
route('admin.payment-verifications.approve-accountant', $data['payment'])
);
// Should be forbidden or redirect with error
// Should remain pending (workflow requires cashier first)
$data['payment']->refresh();
$this->assertNotEquals(MembershipPayment::STATUS_APPROVED_ACCOUNTANT, $data['payment']->status);
$this->assertEquals(MembershipPayment::STATUS_PENDING, $data['payment']->status);
}
/**
@@ -99,7 +99,7 @@ class RolePermissionTest extends TestCase
$data = $this->createMemberWithPaymentAtStage('accountant_approved');
$response = $this->actingAs($chair)->post(
route('admin.membership-payments.approve', $data['payment'])
route('admin.payment-verifications.approve-chair', $data['payment'])
);
$data['payment']->refresh();
@@ -107,21 +107,21 @@ class RolePermissionTest extends TestCase
}
/**
* Test finance_cashier can approve finance documents
* Test secretary can approve finance documents (new workflow first stage)
*/
public function test_finance_cashier_can_approve_finance_documents(): void
public function test_secretary_can_approve_finance_documents(): void
{
$cashier = $this->createCashier();
$secretary = $this->createSecretary();
$document = $this->createFinanceDocument([
'status' => FinanceDocument::STATUS_PENDING,
]);
$response = $this->actingAs($cashier)->post(
route('admin.finance-documents.approve', $document)
$response = $this->actingAs($secretary)->post(
route('admin.finance.approve', $document)
);
$document->refresh();
$this->assertEquals(FinanceDocument::STATUS_APPROVED_CASHIER, $document->status);
$this->assertEquals(FinanceDocument::STATUS_APPROVED_SECRETARY, $document->status);
}
/**
@@ -129,77 +129,27 @@ class RolePermissionTest extends TestCase
*/
public function test_unauthorized_user_cannot_approve(): void
{
$user = User::factory()->create();
$user = $this->createMemberUser();
$data = $this->createMemberWithPendingPayment();
$response = $this->actingAs($user)->post(
route('admin.membership-payments.approve', $data['payment'])
route('admin.payment-verifications.approve-cashier', $data['payment'])
);
$response->assertStatus(403);
}
/**
* Test role can be assigned to user
*/
public function test_role_can_be_assigned_to_user(): void
{
$admin = $this->createAdmin();
$user = User::factory()->create();
$response = $this->actingAs($admin)->post(
route('admin.users.assign-role', $user),
['role' => 'finance_cashier']
);
$this->assertTrue($user->hasRole('finance_cashier'));
}
/**
* Test role can be removed from user
*/
public function test_role_can_be_removed_from_user(): void
{
$admin = $this->createAdmin();
$user = User::factory()->create();
$user->assignRole('finance_cashier');
$response = $this->actingAs($admin)->post(
route('admin.users.remove-role', $user),
['role' => 'finance_cashier']
);
$this->assertFalse($user->hasRole('finance_cashier'));
}
/**
* Test permission check for member management
*/
public function test_permission_check_for_member_management(): void
{
$admin = $this->createAdmin();
$member = $this->createPendingMember();
$response = $this->actingAs($admin)->patch(
route('admin.members.update-status', $member),
['membership_status' => Member::STATUS_ACTIVE]
);
$member->refresh();
$this->assertEquals(Member::STATUS_ACTIVE, $member->membership_status);
}
/**
* Test super admin has all permissions
*/
public function test_super_admin_has_all_permissions(): void
{
$superAdmin = User::factory()->create();
$superAdmin->assignRole('super_admin');
$superAdmin = $this->createSuperAdmin();
$this->assertTrue($superAdmin->can('manage-members'));
$this->assertTrue($superAdmin->can('approve-payments'));
$this->assertTrue($superAdmin->can('manage-finance'));
// Super admin should have various permissions
$this->assertTrue($superAdmin->hasRole('super_admin'));
$this->assertTrue($superAdmin->can('view_finance_documents'));
$this->assertTrue($superAdmin->can('approve_finance_secretary'));
}
/**
@@ -207,9 +157,24 @@ class RolePermissionTest extends TestCase
*/
public function test_role_hierarchy_for_approvals(): void
{
// Chair should be able to do everything accountant can
// Chair should have the finance_chair role
$chair = $this->createChair();
$this->assertTrue($chair->hasRole('finance_chair'));
// Secretary should have the secretary_general role
$secretary = $this->createSecretary();
$this->assertTrue($secretary->hasRole('secretary_general'));
}
/**
* Test finance approval roles exist
*/
public function test_finance_approval_roles_exist(): void
{
$this->assertNotNull(Role::findByName('secretary_general'));
$this->assertNotNull(Role::findByName('finance_chair'));
$this->assertNotNull(Role::findByName('finance_board_member'));
$this->assertNotNull(Role::findByName('finance_cashier'));
$this->assertNotNull(Role::findByName('finance_accountant'));
}
}