- Added new section in AGENTS.md detailing learned workspace facts for better understanding of settlement processes. - Updated AgentNodeDestroyController to remove unnecessary checks for admin users. - Enhanced AgentSettlement controllers to assert permissions for finance adjustments and bill operations. - Improved query scopes in AgentSettlement services to ensure proper data access based on admin roles. - Refactored methods in SettlementPartyEnrichment for better bill row enrichment and data handling. - Introduced new methods in AdminAgentSettlementScope for managing agent node visibility and finance adjustments.
352 lines
13 KiB
PHP
352 lines
13 KiB
PHP
<?php
|
|
|
|
use App\Models\AdminUser;
|
|
use App\Models\Player;
|
|
use App\Support\PlayerFundingMode;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
|
$this->artisan('lottery:agent-roles-sync')->assertExitCode(0);
|
|
});
|
|
|
|
test('bound parent agent cannot see player bills under direct child agent', function (): void {
|
|
[$siteId, $siteCode, $rootId, $parent, $child, $player, $periodId] = seedDirectEdgeSettlementFixture();
|
|
|
|
$playerBillId = insertPlayerBill($periodId, $player->id, $child->id, 4400);
|
|
insertAgentBill($periodId, $child->id, $parent->id, 4400);
|
|
insertAgentBill($periodId, $parent->id, $rootId, 3520);
|
|
|
|
$parentAdmin = createBoundSettlementAgentAdmin($siteId, $parent, 'parent_scope');
|
|
$childAdmin = createBoundSettlementAgentAdmin($siteId, $child, 'child_scope');
|
|
$parentToken = $parentAdmin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
|
|
|
expect(\App\Support\AdminAgentSettlementScope::billAccessible($childAdmin, $playerBillId))->toBeTrue()
|
|
->and(\App\Support\AdminAgentSettlementScope::billAccessible($parentAdmin, $playerBillId))->toBeFalse();
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$parentToken)
|
|
->getJson('/api/v1/admin/settlement-bills?settlement_period_id='.$periodId)
|
|
->assertOk()
|
|
->assertJsonPath('data.items', function (array $items) use ($playerBillId): bool {
|
|
$ids = array_column($items, 'id');
|
|
|
|
return ! in_array($playerBillId, $ids, true);
|
|
});
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$parentToken)
|
|
->getJson('/api/v1/admin/settlement-bills/'.$playerBillId)
|
|
->assertNotFound();
|
|
|
|
$this->actingAs($childAdmin, 'sanctum')
|
|
->getJson('/api/v1/admin/settlement-bills?settlement_period_id='.$periodId.'&bill_id='.$playerBillId)
|
|
->assertOk()
|
|
->assertJsonPath('data.items.0.id', $playerBillId);
|
|
|
|
$this->actingAs($childAdmin, 'sanctum')
|
|
->getJson('/api/v1/admin/settlement-bills/'.$playerBillId)
|
|
->assertOk()
|
|
->assertJsonPath('data.bill.id', $playerBillId);
|
|
});
|
|
|
|
test('bound parent agent sees direct child agent bill but not deeper chain bills', function (): void {
|
|
[$siteId, $siteCode, $rootId, $parent, $child, $player, $periodId] = seedDirectEdgeSettlementFixture();
|
|
|
|
$childToParentBillId = insertAgentBill($periodId, $child->id, $parent->id, 4400);
|
|
$parentToRootBillId = insertAgentBill($periodId, $parent->id, $rootId, 3520);
|
|
|
|
$super = AdminUser::query()->create([
|
|
'username' => 'edge_grand_super_'.uniqid(),
|
|
'name' => 'Super',
|
|
'email' => null,
|
|
'password' => Hash::make('secret-strong'),
|
|
'status' => 0,
|
|
]);
|
|
grantSuperAdminRole($super);
|
|
|
|
$grandchild = app(\App\Services\Agent\AgentNodeService::class)->createChild(
|
|
$super,
|
|
[
|
|
'parent_id' => $child->id,
|
|
'code' => 'edge-grandchild',
|
|
'name' => 'Edge Grandchild',
|
|
],
|
|
);
|
|
$deepBillId = insertAgentBill($periodId, $grandchild->id, $child->id, 1200);
|
|
|
|
$parentAdmin = createBoundSettlementAgentAdmin($siteId, $parent, 'parent_edge');
|
|
$parentToken = $parentAdmin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
|
|
|
$response = $this->withHeader('Authorization', 'Bearer '.$parentToken)
|
|
->getJson('/api/v1/admin/settlement-bills?settlement_period_id='.$periodId)
|
|
->assertOk();
|
|
|
|
$ids = array_column($response->json('data.items'), 'id');
|
|
|
|
expect($ids)->toContain($childToParentBillId)
|
|
->and($ids)->toContain($parentToRootBillId)
|
|
->and($ids)->not->toContain($deepBillId);
|
|
});
|
|
|
|
test('bound parent agent cannot confirm or pay player bill under child', function (): void {
|
|
[$siteId, $siteCode, $rootId, $parent, $child, $player, $periodId] = seedDirectEdgeSettlementFixture();
|
|
|
|
$playerBillId = (int) DB::table('settlement_bills')->insertGetId([
|
|
'settlement_period_id' => $periodId,
|
|
'bill_type' => 'player',
|
|
'owner_type' => 'player',
|
|
'owner_id' => $player->id,
|
|
'counterparty_type' => 'agent',
|
|
'counterparty_id' => $child->id,
|
|
'net_amount' => 4400,
|
|
'unpaid_amount' => 4400,
|
|
'paid_amount' => 0,
|
|
'status' => 'pending_confirm',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$parentAdmin = createBoundSettlementAgentAdmin($siteId, $parent, 'parent_pay');
|
|
$parentToken = $parentAdmin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$parentToken)
|
|
->postJson('/api/v1/admin/settlement-bills/'.$playerBillId.'/confirm')
|
|
->assertNotFound();
|
|
|
|
DB::table('settlement_bills')->where('id', $playerBillId)->update(['status' => 'confirmed']);
|
|
|
|
$this->withHeader('Authorization', 'Bearer '.$parentToken)
|
|
->postJson('/api/v1/admin/settlement-bills/'.$playerBillId.'/payments', [
|
|
'amount' => 4400,
|
|
])
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('parent agent can register payment on child agent bill where parent is payee', function (): void {
|
|
[$siteId, $siteCode, $rootId, $parent, $child, $player, $periodId] = seedDirectEdgeSettlementFixture();
|
|
|
|
$agentBillId = (int) DB::table('settlement_bills')->insertGetId([
|
|
'settlement_period_id' => $periodId,
|
|
'bill_type' => 'agent',
|
|
'owner_type' => 'agent',
|
|
'owner_id' => $child->id,
|
|
'counterparty_type' => 'agent',
|
|
'counterparty_id' => $parent->id,
|
|
'net_amount' => 4400,
|
|
'unpaid_amount' => 4400,
|
|
'paid_amount' => 0,
|
|
'status' => 'confirmed',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$parentAdmin = createBoundSettlementAgentAdmin($siteId, $parent, 'parent_payee');
|
|
$childAdmin = createBoundSettlementAgentAdmin($siteId, $child, 'child_payer');
|
|
|
|
$this->actingAs($parentAdmin, 'sanctum')
|
|
->postJson('/api/v1/admin/settlement-bills/'.$agentBillId.'/payments', ['amount' => 4400])
|
|
->assertOk();
|
|
|
|
$this->actingAs($childAdmin, 'sanctum')
|
|
->postJson('/api/v1/admin/settlement-bills/'.$agentBillId.'/payments', ['amount' => 4400])
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('settlement credit ledger excludes players under child agent for parent viewer', function (): void {
|
|
[$siteId, $siteCode, $rootId, $parent, $child, $player, $periodId] = seedDirectEdgeSettlementFixture();
|
|
|
|
DB::table('credit_ledger')->insert([
|
|
'owner_type' => 'player',
|
|
'owner_id' => $player->id,
|
|
'amount' => -100,
|
|
'reason' => 'bet_hold',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$parentAdmin = createBoundSettlementAgentAdmin($siteId, $parent, 'parent_ledger');
|
|
|
|
$this->actingAs($parentAdmin, 'sanctum')
|
|
->getJson('/api/v1/admin/credit-ledger?admin_site_id='.$siteId.'&settlement_period_id='.$periodId)
|
|
->assertOk()
|
|
->assertJsonPath('data.items', fn (array $items): bool => count($items) === 0);
|
|
});
|
|
|
|
test('direct child agent can confirm and pay own player bill', function (): void {
|
|
[$siteId, $siteCode, $rootId, $parent, $child, $player, $periodId] = seedDirectEdgeSettlementFixture();
|
|
|
|
$playerBillId = (int) DB::table('settlement_bills')->insertGetId([
|
|
'settlement_period_id' => $periodId,
|
|
'bill_type' => 'player',
|
|
'owner_type' => 'player',
|
|
'owner_id' => $player->id,
|
|
'counterparty_type' => 'agent',
|
|
'counterparty_id' => $child->id,
|
|
'net_amount' => 4400,
|
|
'unpaid_amount' => 4400,
|
|
'paid_amount' => 0,
|
|
'status' => 'pending_confirm',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$childAdmin = createBoundSettlementAgentAdmin($siteId, $child, 'child_pay');
|
|
|
|
$this->actingAs($childAdmin, 'sanctum')
|
|
->postJson('/api/v1/admin/settlement-bills/'.$playerBillId.'/confirm')
|
|
->assertOk();
|
|
|
|
$this->actingAs($childAdmin, 'sanctum')
|
|
->postJson('/api/v1/admin/settlement-bills/'.$playerBillId.'/payments', [
|
|
'amount' => 4400,
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('data.bill.status', 'settled');
|
|
});
|
|
|
|
/**
|
|
* @return array{0: int, 1: string, 2: int, 3: \App\Models\AgentNode, 4: \App\Models\AgentNode, 5: Player, 6: int}
|
|
*/
|
|
function seedDirectEdgeSettlementFixture(): array
|
|
{
|
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
|
$siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
|
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
|
|
|
$super = AdminUser::query()->create([
|
|
'username' => 'edge_scope_super_'.uniqid(),
|
|
'name' => 'Super',
|
|
'email' => null,
|
|
'password' => Hash::make('secret-strong'),
|
|
'status' => 0,
|
|
]);
|
|
grantSuperAdminRole($super);
|
|
|
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
|
$parent = $service->createChild($super, [
|
|
'parent_id' => $rootId,
|
|
'code' => 'edge-parent-'.uniqid(),
|
|
'name' => 'Edge Parent',
|
|
]);
|
|
$child = $service->createChild($super, [
|
|
'parent_id' => $parent->id,
|
|
'code' => 'edge-child-'.uniqid(),
|
|
'name' => 'Edge Child',
|
|
]);
|
|
|
|
$player = Player::query()->create([
|
|
'site_code' => $siteCode,
|
|
'site_player_id' => 'native:edge-'.uniqid(),
|
|
'funding_mode' => PlayerFundingMode::CREDIT,
|
|
'username' => 'edge_player_'.uniqid(),
|
|
'default_currency' => 'NPR',
|
|
'status' => 0,
|
|
'agent_node_id' => $child->id,
|
|
]);
|
|
|
|
$periodId = (int) DB::table('settlement_periods')->insertGetId([
|
|
'admin_site_id' => $siteId,
|
|
'period_start' => now()->subWeek(),
|
|
'period_end' => now(),
|
|
'status' => 'closed',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return [$siteId, $siteCode, $rootId, $parent, $child, $player, $periodId];
|
|
}
|
|
|
|
function insertPlayerBill(int $periodId, int $playerId, int $agentId, int $amount): int
|
|
{
|
|
return (int) DB::table('settlement_bills')->insertGetId([
|
|
'settlement_period_id' => $periodId,
|
|
'bill_type' => 'player',
|
|
'owner_type' => 'player',
|
|
'owner_id' => $playerId,
|
|
'counterparty_type' => 'agent',
|
|
'counterparty_id' => $agentId,
|
|
'net_amount' => $amount,
|
|
'unpaid_amount' => $amount,
|
|
'paid_amount' => 0,
|
|
'status' => 'confirmed',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
function insertAgentBill(int $periodId, int $ownerId, int $counterpartyId, int $amount): int
|
|
{
|
|
return (int) DB::table('settlement_bills')->insertGetId([
|
|
'settlement_period_id' => $periodId,
|
|
'bill_type' => 'agent',
|
|
'owner_type' => 'agent',
|
|
'owner_id' => $ownerId,
|
|
'counterparty_type' => 'agent',
|
|
'counterparty_id' => $counterpartyId,
|
|
'net_amount' => $amount,
|
|
'unpaid_amount' => $amount,
|
|
'paid_amount' => 0,
|
|
'status' => 'confirmed',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
function createBoundSettlementAgentAdmin(int $siteId, \App\Models\AgentNode $agent, string $prefix): AdminUser
|
|
{
|
|
$admin = AdminUser::query()->create([
|
|
'username' => $prefix.'_'.uniqid(),
|
|
'name' => ucfirst($prefix),
|
|
'email' => null,
|
|
'password' => Hash::make('secret-strong'),
|
|
'status' => 0,
|
|
]);
|
|
|
|
DB::table('admin_user_agents')->insert([
|
|
'admin_user_id' => $admin->id,
|
|
'agent_node_id' => (int) $agent->id,
|
|
'is_primary' => true,
|
|
'granted_at' => now(),
|
|
]);
|
|
|
|
$admin->syncPrimaryPlatformAgentRole((int) $agent->id);
|
|
|
|
return $admin->fresh();
|
|
}
|
|
|
|
function grantSettlementManageToAgentAdmin(AdminUser $admin, \App\Models\AgentNode $agent): void
|
|
{
|
|
$now = now();
|
|
$roleId = DB::table('admin_roles')->insertGetId([
|
|
'slug' => 'settle_manage_'.$admin->id,
|
|
'code' => 'settle_manage_'.$admin->id,
|
|
'name' => 'Settlement Manage',
|
|
'status' => 1,
|
|
'is_system' => false,
|
|
'sort_order' => 0,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$actionIds = DB::table('admin_menu_actions')
|
|
->whereIn('permission_code', ['settlement.agent.manage', 'prd.settlement.agent.manage'])
|
|
->pluck('id');
|
|
|
|
foreach ($actionIds as $actionId) {
|
|
DB::table('admin_role_menu_actions')->insert([
|
|
'role_id' => $roleId,
|
|
'menu_action_id' => (int) $actionId,
|
|
]);
|
|
}
|
|
|
|
DB::table('admin_user_agent_roles')->insert([
|
|
'admin_user_id' => $admin->id,
|
|
'agent_node_id' => (int) $agent->id,
|
|
'role_id' => $roleId,
|
|
'granted_at' => $now,
|
|
]);
|
|
}
|