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'); });