feat: enhance admin role management and reconciliation features
- Updated AGENTS.md to clarify site admin roles and their associated permissions. - Refactored reconciliation controllers to include admin user validation and improved access control based on admin roles. - Enhanced AdminReconcileJobService to support player-specific reconciliation and site-based job creation. - Removed deprecated rebate commission report functionality from the API and related services. - Improved dashboard overview builders to accommodate new site operator roles and their specific functionalities.
This commit is contained in:
211
tests/Feature/AdminReconcileJobScopeTest.php
Normal file
211
tests/Feature/AdminReconcileJobScopeTest.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Player;
|
||||
use App\Models\ReconcileJob;
|
||||
use App\Models\TransferOrder;
|
||||
use App\Support\SitePlatformRole;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||
});
|
||||
|
||||
function reconcileScopeSuperToken(): string
|
||||
{
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'reconcile_scope_super',
|
||||
'name' => 'Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
|
||||
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
}
|
||||
|
||||
test('reconcile job list is scoped to site admin site', function (): void {
|
||||
$superToken = reconcileScopeSuperToken();
|
||||
|
||||
$siteA = $this->withHeader('Authorization', 'Bearer '.$superToken)
|
||||
->postJson('/api/v1/admin/integration-sites', [
|
||||
'code' => 'reconcile-a',
|
||||
'name' => 'Reconcile A',
|
||||
'admin_account' => [
|
||||
'username' => 'reconcile_a_admin',
|
||||
'nickname' => 'A Admin',
|
||||
'password' => 'secret-strong',
|
||||
],
|
||||
])
|
||||
->assertCreated()
|
||||
->json('data');
|
||||
|
||||
$siteB = $this->withHeader('Authorization', 'Bearer '.$superToken)
|
||||
->postJson('/api/v1/admin/integration-sites', [
|
||||
'code' => 'reconcile-b',
|
||||
'name' => 'Reconcile B',
|
||||
'admin_account' => [
|
||||
'username' => 'reconcile_b_admin',
|
||||
'nickname' => 'B Admin',
|
||||
'password' => 'secret-strong',
|
||||
],
|
||||
])
|
||||
->assertCreated()
|
||||
->json('data');
|
||||
|
||||
$playerA = Player::query()->create([
|
||||
'site_code' => 'reconcile-a',
|
||||
'site_player_id' => 'scope-a-1',
|
||||
'username' => null,
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
$playerB = Player::query()->create([
|
||||
'site_code' => 'reconcile-b',
|
||||
'site_player_id' => 'scope-b-1',
|
||||
'username' => null,
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
foreach ([
|
||||
['TO_scope_a', $playerA->id, (int) $siteA['id']],
|
||||
['TO_scope_b', $playerB->id, (int) $siteB['id']],
|
||||
] as [$transferNo, $playerId, $siteId]) {
|
||||
TransferOrder::query()->create([
|
||||
'transfer_no' => $transferNo,
|
||||
'player_id' => $playerId,
|
||||
'direction' => 'out',
|
||||
'currency_code' => 'NPR',
|
||||
'amount' => 100,
|
||||
'idempotent_key' => $transferNo.'-key',
|
||||
'status' => 'pending_reconcile',
|
||||
'external_request_payload' => null,
|
||||
'external_response_payload' => null,
|
||||
'external_ref_no' => null,
|
||||
'fail_reason' => 'main_site_timeout',
|
||||
'finished_at' => null,
|
||||
'created_at' => now()->subHours(2),
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$superToken)
|
||||
->postJson('/api/v1/admin/reconcile-jobs', [
|
||||
'reconcile_type' => 'wallet_transfer',
|
||||
'date_from' => now()->subDay()->toDateString(),
|
||||
'date_to' => now()->toDateString(),
|
||||
'player_id' => $playerId,
|
||||
])
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
$jobA = ReconcileJob::query()->where('admin_site_id', (int) $siteA['id'])->latest('id')->first();
|
||||
$jobB = ReconcileJob::query()->where('admin_site_id', (int) $siteB['id'])->latest('id')->first();
|
||||
expect($jobA)->not->toBeNull()
|
||||
->and($jobB)->not->toBeNull()
|
||||
->and((int) $jobA->admin_site_id)->toBe((int) $siteA['id'])
|
||||
->and((int) $jobB->admin_site_id)->toBe((int) $siteB['id']);
|
||||
|
||||
$siteAdminA = AdminUser::query()->where('username', 'reconcile_a_admin')->firstOrFail();
|
||||
$siteAdminB = AdminUser::query()->where('username', 'reconcile_b_admin')->firstOrFail();
|
||||
expect(SitePlatformRole::userHasSiteAdminRole($siteAdminA))->toBeTrue()
|
||||
->and($siteAdminA->isSuperAdmin())->toBeFalse()
|
||||
->and($siteAdminA->accessibleAdminSiteIds())->toBe([(int) $siteA['id']])
|
||||
->and($siteAdminA->primaryAgentNode())->toBeNull()
|
||||
->and($siteAdminB->accessibleAdminSiteIds())->toBe([(int) $siteB['id']]);
|
||||
|
||||
Sanctum::actingAs($siteAdminA, ['*']);
|
||||
$idsA = collect($this->getJson('/api/v1/admin/reconcile-jobs')
|
||||
->assertOk()
|
||||
->json('data.items'))
|
||||
->pluck('id')
|
||||
->all();
|
||||
|
||||
Sanctum::actingAs($siteAdminB, ['*']);
|
||||
$idsB = collect($this->getJson('/api/v1/admin/reconcile-jobs')
|
||||
->assertOk()
|
||||
->json('data.items'))
|
||||
->pluck('id')
|
||||
->all();
|
||||
|
||||
expect($idsA)->toEqual([(int) $jobA->id])
|
||||
->and($idsB)->toEqual([(int) $jobB->id]);
|
||||
|
||||
Sanctum::actingAs($siteAdminA, ['*']);
|
||||
$this->getJson('/api/v1/admin/reconcile-jobs/'.$jobB->id.'/items')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('site admin reconcile scan only includes own site transfer orders', function (): void {
|
||||
$superToken = reconcileScopeSuperToken();
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$superToken)
|
||||
->postJson('/api/v1/admin/integration-sites', [
|
||||
'code' => 'scan-scope-a',
|
||||
'name' => 'Scan Scope A',
|
||||
'admin_account' => [
|
||||
'username' => 'scan_scope_a_admin',
|
||||
'nickname' => 'Scan A',
|
||||
'password' => 'secret-strong',
|
||||
],
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
$defaultSiteCode = (string) DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||
|
||||
$ownPlayer = Player::query()->create([
|
||||
'site_code' => 'scan-scope-a',
|
||||
'site_player_id' => 'scan-own',
|
||||
'username' => null,
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
$otherPlayer = Player::query()->create([
|
||||
'site_code' => $defaultSiteCode,
|
||||
'site_player_id' => 'scan-other',
|
||||
'username' => null,
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
foreach ([['TO_scan_own', $ownPlayer->id], ['TO_scan_other', $otherPlayer->id]] as [$transferNo, $playerId]) {
|
||||
TransferOrder::query()->create([
|
||||
'transfer_no' => $transferNo,
|
||||
'player_id' => $playerId,
|
||||
'direction' => 'out',
|
||||
'currency_code' => 'NPR',
|
||||
'amount' => 200,
|
||||
'idempotent_key' => $transferNo.'-key',
|
||||
'status' => 'pending_reconcile',
|
||||
'external_request_payload' => null,
|
||||
'external_response_payload' => null,
|
||||
'external_ref_no' => null,
|
||||
'fail_reason' => 'main_site_timeout',
|
||||
'finished_at' => null,
|
||||
'created_at' => now()->subHours(2),
|
||||
]);
|
||||
}
|
||||
|
||||
$siteAdmin = AdminUser::query()->where('username', 'scan_scope_a_admin')->firstOrFail();
|
||||
Sanctum::actingAs($siteAdmin, ['*']);
|
||||
|
||||
$this->postJson('/api/v1/admin/reconcile-jobs', [
|
||||
'reconcile_type' => 'wallet_transfer',
|
||||
'date_from' => now()->subDay()->toDateString(),
|
||||
'date_to' => now()->toDateString(),
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.item_count', 1);
|
||||
|
||||
$job = ReconcileJob::query()->latest('id')->firstOrFail();
|
||||
expect($job->items()->value('side_a_ref'))->toBe('TO_scan_own');
|
||||
});
|
||||
Reference in New Issue
Block a user