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:
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user