feat: enhance agent settlement features and improve data access controls
- 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.
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\Player;
|
||||
use App\Support\AgentPlatformRole;
|
||||
use App\Support\PlayerFundingMode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -65,3 +67,160 @@ test('agent dashboard returns agent overview for operator with dashboard permiss
|
||||
->assertJsonPath('data.agent_overview.agent_node_id', $branch->id)
|
||||
->assertJsonPath('data.agent_overview.agent_code', 'dash-branch');
|
||||
});
|
||||
|
||||
test('agent dashboard profit uses share profit not team house gross', function (): void {
|
||||
$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');
|
||||
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'super_dash_share',
|
||||
'name' => 'Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$branch = $service->createChild($super, [
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'dash-share-branch',
|
||||
'name' => 'Dash Share Branch',
|
||||
'can_create_player' => true,
|
||||
]);
|
||||
|
||||
$operator = AdminUser::query()->where('username', 'agent_'.$branch->code)->first();
|
||||
expect($operator)->not->toBeNull();
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => $siteCode,
|
||||
'site_player_id' => 'native:dash-share-player',
|
||||
'funding_mode' => PlayerFundingMode::CREDIT,
|
||||
'username' => 'dash_share_player',
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
'agent_node_id' => $branch->id,
|
||||
]);
|
||||
|
||||
$draw = \App\Models\Draw::query()->create([
|
||||
'draw_no' => 'DRAW-DASH-SHARE',
|
||||
'business_date' => now()->toDateString(),
|
||||
'sequence_no' => random_int(1, 9999),
|
||||
'status' => \App\Lottery\DrawStatus::Open->value,
|
||||
'current_result_version' => 0,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
$orderId = (int) DB::table('ticket_orders')->insertGetId([
|
||||
'order_no' => 'ORD-DASH-SHARE',
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $draw->id,
|
||||
'currency_code' => 'NPR',
|
||||
'total_bet_amount' => 10_000,
|
||||
'total_rebate_amount' => 0,
|
||||
'total_actual_deduct' => 10_000,
|
||||
'total_estimated_payout' => 0,
|
||||
'status' => 'confirmed',
|
||||
'submit_source' => 'h5',
|
||||
'client_trace_id' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$ticketItemId = (int) DB::table('ticket_items')->insertGetId([
|
||||
'ticket_no' => 'T-DASH-SHARE',
|
||||
'order_id' => $orderId,
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $draw->id,
|
||||
'original_number' => null,
|
||||
'normalized_number' => '1234',
|
||||
'play_code' => 'big',
|
||||
'dimension' => 2,
|
||||
'digit_slot' => null,
|
||||
'bet_mode' => null,
|
||||
'unit_bet_amount' => 10_000,
|
||||
'total_bet_amount' => 10_000,
|
||||
'rebate_rate_snapshot' => 0,
|
||||
'commission_rate_snapshot' => 0,
|
||||
'actual_deduct_amount' => 10_000,
|
||||
'odds_snapshot_json' => null,
|
||||
'rule_snapshot_json' => null,
|
||||
'combination_count' => 1,
|
||||
'estimated_max_payout' => 0,
|
||||
'risk_locked_amount' => 0,
|
||||
'status' => 'settled_lose',
|
||||
'win_amount' => 0,
|
||||
'jackpot_win_amount' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$settledAt = now()->toDateTimeString();
|
||||
DB::table('share_ledger')->insert([
|
||||
'ticket_item_id' => $ticketItemId,
|
||||
'player_id' => $player->id,
|
||||
'agent_node_id' => $branch->id,
|
||||
'agent_path' => json_encode([$branch->id]),
|
||||
'share_snapshot' => json_encode([
|
||||
'total_shares' => [(string) $branch->code => 30.0],
|
||||
'chain_codes' => [(string) $branch->code],
|
||||
]),
|
||||
'game_win_loss' => 1_000,
|
||||
'basic_rebate' => 0,
|
||||
'shared_net_win_loss' => 1_000,
|
||||
'allocations_json' => json_encode([
|
||||
(string) $branch->code => 300,
|
||||
'platform' => 700,
|
||||
]),
|
||||
'settled_at' => $settledAt,
|
||||
'created_at' => $settledAt,
|
||||
'updated_at' => $settledAt,
|
||||
]);
|
||||
|
||||
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/dashboard')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.agent_overview.profit_scope', 'share_profit')
|
||||
->assertJsonPath('data.agent_overview.today_profit_minor', 300);
|
||||
});
|
||||
|
||||
test('agent bound admin cannot open platform pnl settlement report', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'super_report_block',
|
||||
'name' => 'Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$branch = $service->createChild($super, [
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'report-block-branch',
|
||||
'name' => 'Report Block Branch',
|
||||
]);
|
||||
|
||||
$operator = AdminUser::query()->where('username', 'agent_'.$branch->code)->first();
|
||||
expect($operator)->not->toBeNull();
|
||||
|
||||
$periodId = (int) DB::table('settlement_periods')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => now()->subDay()->toDateString(),
|
||||
'period_end' => now()->addDay()->toDateString(),
|
||||
'status' => 'open',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/settlement-reports?type=platform_pnl&settlement_period_id='.$periodId)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
@@ -69,6 +69,8 @@ test('agent profile switches strip create player and child manage from effective
|
||||
|
||||
expect($fresh->hasPermissionCode('agent.node.manage'))->toBeFalse();
|
||||
expect($fresh->hasPermissionCode('service.players.manage'))->toBeFalse();
|
||||
expect($fresh->hasPermissionCode('settlement.agent.manage'))->toBeTrue();
|
||||
expect($fresh->adminPermissionSlugs())->toContain('prd.settlement.agent.manage');
|
||||
expect($profile['agent']['can_create_child_agent'])->toBeFalse();
|
||||
expect($profile['agent']['can_create_player'])->toBeFalse();
|
||||
});
|
||||
@@ -123,5 +125,96 @@ test('agent profile switches on grant create capabilities even when platform age
|
||||
expect($fresh->hasPermissionCode('agent.node.manage'))->toBeTrue();
|
||||
expect($fresh->hasPermissionCode('service.players.manage'))->toBeTrue();
|
||||
expect($fresh->adminPermissionSlugs())->toContain('prd.agent.manage')
|
||||
->and($fresh->adminPermissionSlugs())->toContain('prd.users.manage');
|
||||
->and($fresh->adminPermissionSlugs())->toContain('prd.users.manage')
|
||||
->and($fresh->adminPermissionSlugs())->toContain('prd.settlement.agent.manage');
|
||||
expect($fresh->hasPermissionCode('settlement.agent.manage'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('line root bound agent receives settlement manage at login', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'settle_root_agent',
|
||||
'name' => 'Settle Root Agent',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
DB::table('admin_user_agents')->insert([
|
||||
'admin_user_id' => $admin->id,
|
||||
'agent_node_id' => $rootId,
|
||||
'is_primary' => true,
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
$admin->syncPrimaryPlatformAgentRole($rootId);
|
||||
|
||||
$fresh = $admin->fresh();
|
||||
|
||||
expect($fresh->hasPermissionCode('settlement.agent.manage'))->toBeTrue();
|
||||
expect($fresh->adminPermissionSlugs())->toContain('prd.settlement.agent.manage');
|
||||
});
|
||||
|
||||
test('agent with downline children receives settlement manage at login', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
|
||||
$parent = AgentNode::query()->create([
|
||||
'admin_site_id' => $siteId,
|
||||
'parent_id' => $rootId,
|
||||
'path' => '/',
|
||||
'depth' => 1,
|
||||
'code' => 'settle-parent',
|
||||
'name' => 'Settle Parent',
|
||||
'status' => 1,
|
||||
]);
|
||||
$parent->path = "/{$rootId}/{$parent->id}/";
|
||||
$parent->save();
|
||||
|
||||
$child = AgentNode::query()->create([
|
||||
'admin_site_id' => $siteId,
|
||||
'parent_id' => $parent->id,
|
||||
'path' => "/{$rootId}/{$parent->id}/",
|
||||
'depth' => 2,
|
||||
'code' => 'settle-child',
|
||||
'name' => 'Settle Child',
|
||||
'status' => 1,
|
||||
]);
|
||||
$child->path = "/{$rootId}/{$parent->id}/{$child->id}/";
|
||||
$child->save();
|
||||
|
||||
AgentProfile::query()->create([
|
||||
'agent_node_id' => $parent->id,
|
||||
'total_share_rate' => 20,
|
||||
'credit_limit' => 0,
|
||||
'allocated_credit' => 0,
|
||||
'used_credit' => 0,
|
||||
'rebate_limit' => 0,
|
||||
'default_player_rebate' => 0,
|
||||
'can_grant_extra_rebate' => false,
|
||||
'can_create_child_agent' => false,
|
||||
'can_create_player' => false,
|
||||
]);
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'settle_parent_agent',
|
||||
'name' => 'Settle Parent Agent',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
DB::table('admin_user_agents')->insert([
|
||||
'admin_user_id' => $admin->id,
|
||||
'agent_node_id' => $parent->id,
|
||||
'is_primary' => true,
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
$admin->syncPrimaryPlatformAgentRole($parent->id);
|
||||
|
||||
$fresh = $admin->fresh();
|
||||
|
||||
expect($fresh->hasPermissionCode('settlement.agent.manage'))->toBeTrue();
|
||||
expect($fresh->adminPermissionSlugs())->toContain('prd.settlement.agent.manage');
|
||||
});
|
||||
|
||||
@@ -31,3 +31,162 @@ test('settlement bills index api resource is configured after migrations', funct
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items', fn ($items) => is_array($items));
|
||||
});
|
||||
|
||||
test('settlement bill show returns enriched party labels', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$childId = (int) DB::table('agent_nodes')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'bill_show_child',
|
||||
'name' => 'Bill Show Child',
|
||||
'depth' => 1,
|
||||
'path' => '/'.$rootId.'/',
|
||||
'status' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::table('agent_nodes')->where('id', $childId)->update([
|
||||
'path' => '/'.$rootId.'/'.$childId.'/',
|
||||
]);
|
||||
|
||||
$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(),
|
||||
]);
|
||||
|
||||
$billId = (int) DB::table('settlement_bills')->insertGetId([
|
||||
'settlement_period_id' => $periodId,
|
||||
'bill_type' => 'agent',
|
||||
'owner_type' => 'agent',
|
||||
'owner_id' => $childId,
|
||||
'counterparty_type' => 'agent',
|
||||
'counterparty_id' => $rootId,
|
||||
'net_amount' => 6400,
|
||||
'unpaid_amount' => 6400,
|
||||
'paid_amount' => 0,
|
||||
'status' => 'confirmed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'bill_show_super',
|
||||
'name' => 'Bill Show Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$rootName = (string) DB::table('agent_nodes')->where('id', $rootId)->value('name');
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/settlement-bills/'.$billId)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.bill.owner_party_label', 'Bill Show Child')
|
||||
->assertJsonPath('data.bill.superior_agent_label', $rootName);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/settlement-bills?bill_id='.$billId)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items.0.owner_party_label', 'Bill Show Child')
|
||||
->assertJsonPath('data.items.0.superior_agent_label', $rootName);
|
||||
});
|
||||
|
||||
test('settlement bill show returns downline share breakdown for parent agent bill', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$parentId = (int) DB::table('agent_nodes')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'downline_parent',
|
||||
'name' => 'Downline Parent',
|
||||
'depth' => 1,
|
||||
'path' => '/'.$rootId.'/',
|
||||
'status' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::table('agent_nodes')->where('id', $parentId)->update([
|
||||
'path' => '/'.$rootId.'/'.$parentId.'/',
|
||||
]);
|
||||
$childId = (int) DB::table('agent_nodes')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'parent_id' => $parentId,
|
||||
'code' => 'downline_child',
|
||||
'name' => 'Downline Child',
|
||||
'depth' => 2,
|
||||
'path' => '/'.$rootId.'/'.$parentId.'/',
|
||||
'status' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::table('agent_nodes')->where('id', $childId)->update([
|
||||
'path' => '/'.$rootId.'/'.$parentId.'/'.$childId.'/',
|
||||
]);
|
||||
|
||||
$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(),
|
||||
]);
|
||||
|
||||
DB::table('settlement_bills')->insert([
|
||||
'settlement_period_id' => $periodId,
|
||||
'bill_type' => 'agent',
|
||||
'owner_type' => 'agent',
|
||||
'owner_id' => $childId,
|
||||
'counterparty_type' => 'agent',
|
||||
'counterparty_id' => $parentId,
|
||||
'net_amount' => 3916,
|
||||
'unpaid_amount' => 3916,
|
||||
'paid_amount' => 0,
|
||||
'status' => 'confirmed',
|
||||
'meta_json' => json_encode(['share_profit' => 484]),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$parentBillId = (int) DB::table('settlement_bills')->insertGetId([
|
||||
'settlement_period_id' => $periodId,
|
||||
'bill_type' => 'agent',
|
||||
'owner_type' => 'agent',
|
||||
'owner_id' => $parentId,
|
||||
'counterparty_type' => 'agent',
|
||||
'counterparty_id' => $rootId,
|
||||
'gross_win_loss' => 4400,
|
||||
'net_amount' => 3520,
|
||||
'unpaid_amount' => 3520,
|
||||
'paid_amount' => 0,
|
||||
'status' => 'confirmed',
|
||||
'meta_json' => json_encode(['share_profit' => 396]),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'downline_share_super',
|
||||
'name' => 'Downline Share Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/settlement-bills/'.$parentBillId)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.downline_shares.total', 484)
|
||||
->assertJsonPath('data.downline_shares.items.0.owner_label', 'Downline Child')
|
||||
->assertJsonPath('data.downline_shares.items.0.share_profit', 484);
|
||||
});
|
||||
|
||||
@@ -88,3 +88,56 @@ test('admin can write off player bill bad debt and complete period when all sett
|
||||
'status' => 'completed',
|
||||
]);
|
||||
});
|
||||
|
||||
test('bound agent with settlement manage cannot write off bad debt', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
|
||||
$periodId = (int) DB::table('settlement_periods')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => now()->subDays(7),
|
||||
'period_end' => now(),
|
||||
'status' => 'closed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$billId = (int) DB::table('settlement_bills')->insertGetId([
|
||||
'settlement_period_id' => $periodId,
|
||||
'bill_type' => 'agent',
|
||||
'owner_type' => 'agent',
|
||||
'owner_id' => $rootId,
|
||||
'counterparty_type' => 'platform',
|
||||
'counterparty_id' => 0,
|
||||
'net_amount' => 5000,
|
||||
'paid_amount' => 0,
|
||||
'unpaid_amount' => 5000,
|
||||
'status' => 'confirmed',
|
||||
'confirmed_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'bad_debt_bound_root',
|
||||
'name' => 'Bad Debt Bound Root',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
DB::table('admin_user_agents')->insert([
|
||||
'admin_user_id' => $admin->id,
|
||||
'agent_node_id' => $rootId,
|
||||
'is_primary' => true,
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
$admin->syncPrimaryPlatformAgentRole($rootId);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/settlement-bills/'.$billId.'/bad-debt-write-off', [
|
||||
'reason' => 'should fail',
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
351
tests/Feature/AgentSettlementBillDirectEdgeScopeTest.php
Normal file
351
tests/Feature/AgentSettlementBillDirectEdgeScopeTest.php
Normal file
@@ -0,0 +1,351 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
@@ -7,8 +7,25 @@ use Illuminate\Support\Facades\Hash;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('settlement payments and adjustments index return items', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$playerId = (int) DB::table('players')->insertGetId([
|
||||
'site_code' => (string) DB::table('admin_sites')->where('id', $siteId)->value('code'),
|
||||
'site_player_id' => 'lists-player',
|
||||
'auth_source' => 'lottery_native',
|
||||
'funding_mode' => 'credit',
|
||||
'username' => 'lists_player',
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
'agent_node_id' => $rootId,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$periodId = (int) DB::table('settlement_periods')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => now()->subWeek(),
|
||||
@@ -22,9 +39,9 @@ test('settlement payments and adjustments index return items', function (): void
|
||||
'settlement_period_id' => $periodId,
|
||||
'bill_type' => 'player',
|
||||
'owner_type' => 'player',
|
||||
'owner_id' => 1,
|
||||
'owner_id' => $playerId,
|
||||
'counterparty_type' => 'agent',
|
||||
'counterparty_id' => 1,
|
||||
'counterparty_id' => $rootId,
|
||||
'net_amount' => 1000,
|
||||
'unpaid_amount' => 0,
|
||||
'paid_amount' => 1000,
|
||||
@@ -36,9 +53,9 @@ test('settlement payments and adjustments index return items', function (): void
|
||||
DB::table('payment_records')->insert([
|
||||
'settlement_bill_id' => $billId,
|
||||
'payer_type' => 'player',
|
||||
'payer_id' => 1,
|
||||
'payer_id' => $playerId,
|
||||
'payee_type' => 'agent',
|
||||
'payee_id' => 1,
|
||||
'payee_id' => $rootId,
|
||||
'amount' => 1000,
|
||||
'method' => 'cash',
|
||||
'status' => 'confirmed',
|
||||
|
||||
197
tests/Feature/AgentSettlementPeriodOpenHintsTest.php
Normal file
197
tests/Feature/AgentSettlementPeriodOpenHintsTest.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Draw;
|
||||
use App\Models\Player;
|
||||
use App\Lottery\DrawStatus;
|
||||
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);
|
||||
});
|
||||
|
||||
test('settlement period open hints api resource is configured after migrations', function (): void {
|
||||
expect(
|
||||
DB::table('admin_api_resources')
|
||||
->where('route_name', 'api.v1.admin.settlement-periods.open-hints')
|
||||
->where('status', 1)
|
||||
->exists(),
|
||||
)->toBeTrue();
|
||||
});
|
||||
|
||||
test('settlement period open hints returns suggested range and calendar markers', function (): void {
|
||||
$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');
|
||||
|
||||
DB::table('settlement_periods')->insert([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => '2026-04-30 16:00:00',
|
||||
'period_end' => '2026-05-31 15:59:59',
|
||||
'status' => 'closed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$unpaidPeriodId = (int) DB::table('settlement_periods')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => '2026-03-31 16:00:00',
|
||||
'period_end' => '2026-04-03 15:59:59',
|
||||
'status' => 'closed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('settlement_bills')->insert([
|
||||
'settlement_period_id' => $unpaidPeriodId,
|
||||
'bill_type' => 'agent',
|
||||
'owner_type' => 'agent',
|
||||
'owner_id' => $rootId,
|
||||
'counterparty_type' => 'platform',
|
||||
'counterparty_id' => 0,
|
||||
'net_amount' => 1000,
|
||||
'unpaid_amount' => 1000,
|
||||
'paid_amount' => 0,
|
||||
'status' => 'confirmed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => $siteCode,
|
||||
'agent_node_id' => $rootId,
|
||||
'site_player_id' => 'hints-player',
|
||||
'username' => 'hints_player',
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$draw = Draw::query()->create([
|
||||
'draw_no' => 'DRAW-HINTS',
|
||||
'business_date' => '2026-06-05',
|
||||
'sequence_no' => 1,
|
||||
'status' => DrawStatus::Open->value,
|
||||
'current_result_version' => 0,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
$orderId = (int) DB::table('ticket_orders')->insertGetId([
|
||||
'order_no' => 'ORD-HINTS',
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $draw->id,
|
||||
'currency_code' => 'NPR',
|
||||
'total_bet_amount' => 10_000,
|
||||
'total_rebate_amount' => 0,
|
||||
'total_actual_deduct' => 10_000,
|
||||
'total_estimated_payout' => 0,
|
||||
'status' => 'confirmed',
|
||||
'submit_source' => 'h5',
|
||||
'client_trace_id' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$itemId = (int) DB::table('ticket_items')->insertGetId([
|
||||
'ticket_no' => 'T-HINTS',
|
||||
'order_id' => $orderId,
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $draw->id,
|
||||
'original_number' => null,
|
||||
'normalized_number' => '1234',
|
||||
'play_code' => 'big',
|
||||
'dimension' => 2,
|
||||
'digit_slot' => null,
|
||||
'bet_mode' => null,
|
||||
'unit_bet_amount' => 10_000,
|
||||
'total_bet_amount' => 10_000,
|
||||
'rebate_rate_snapshot' => 0,
|
||||
'commission_rate_snapshot' => 0,
|
||||
'actual_deduct_amount' => 10_000,
|
||||
'odds_snapshot_json' => null,
|
||||
'rule_snapshot_json' => null,
|
||||
'combination_count' => 1,
|
||||
'estimated_max_payout' => 0,
|
||||
'risk_locked_amount' => 0,
|
||||
'status' => 'settled_lose',
|
||||
'win_amount' => 0,
|
||||
'jackpot_win_amount' => 0,
|
||||
'settled_at' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$settledAt = '2026-06-05 12:00:00';
|
||||
DB::table('share_ledger')->insert([
|
||||
'ticket_item_id' => $itemId,
|
||||
'player_id' => $player->id,
|
||||
'agent_node_id' => $rootId,
|
||||
'agent_path' => json_encode([$rootId]),
|
||||
'share_snapshot' => json_encode(['total_shares' => [$siteCode => 100]]),
|
||||
'game_win_loss' => 1000,
|
||||
'basic_rebate' => 0,
|
||||
'shared_net_win_loss' => 1000,
|
||||
'allocations_json' => json_encode([]),
|
||||
'settled_at' => $settledAt,
|
||||
'settlement_period_id' => null,
|
||||
'created_at' => $settledAt,
|
||||
'updated_at' => $settledAt,
|
||||
]);
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'open_hints_admin',
|
||||
'name' => 'Hints',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/settlement-periods/open-hints?admin_site_id='.$siteId)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.suggested_start', '2026-06-01')
|
||||
->assertJsonPath('data.suggested_end', '2026-06-05')
|
||||
->assertJsonPath('data.pending_activity_dates.0', '2026-06-05')
|
||||
->assertJsonFragment(['2026-05-01'])
|
||||
->assertJsonFragment(['2026-04-01'])
|
||||
->assertJsonFragment(['2026-04-02'])
|
||||
->assertJsonFragment(['2026-04-03']);
|
||||
});
|
||||
|
||||
test('settlement period open hints does not suggest range overlapping occupied periods', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
|
||||
DB::table('settlement_periods')->insert([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => '2026-05-31 16:00:00',
|
||||
'period_end' => '2026-06-30 15:59:59',
|
||||
'status' => 'closed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'open_hints_overlap_admin',
|
||||
'name' => 'Hints Overlap',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/settlement-periods/open-hints?admin_site_id='.$siteId)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.suggested_start', '')
|
||||
->assertJsonPath('data.suggested_end', '')
|
||||
->assertJsonFragment(['2026-06-01'])
|
||||
->assertJsonFragment(['2026-06-30']);
|
||||
});
|
||||
@@ -40,6 +40,40 @@ test('cannot open duplicate settlement period for same range', function (): void
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot open settlement period overlapping a closed period', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$super = \App\Models\AdminUser::query()->create([
|
||||
'username' => 'period_overlap_super',
|
||||
'name' => 'Super',
|
||||
'email' => null,
|
||||
'password' => \Illuminate\Support\Facades\Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
$token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
DB::table('settlement_periods')->insert([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => '2026-05-01 00:00:00',
|
||||
'period_end' => '2026-05-31 23:59:59',
|
||||
'status' => 'closed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/settlement-periods', [
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => '2026-05-15 00:00:00',
|
||||
'period_end' => '2026-06-15 23:59:59',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath(
|
||||
'data.errors.period_start.0',
|
||||
trans('validation.business.period_overlaps_existing'),
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot open second settlement period while another is open on same site', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$super = \App\Models\AdminUser::query()->create([
|
||||
|
||||
Reference in New Issue
Block a user