feat: add idempotency support for settlement adjustments and payments, fix rebate handling
- Added idempotency_key parameter to settlement bill adjustment and payment endpoints to prevent duplicate operations - Fixed player rebate profile updates to preserve existing values when only one field is modified - Moved wallet player validation earlier in AdminPlayerStoreController to prevent unnecessary processing - Enhanced extra rebate calculation to include 'in_bill' and 'settled' statuses in AgentP
This commit is contained in:
@@ -396,3 +396,189 @@ test('period close succeeds with no share ledger rows in window', function (): v
|
||||
expect(DB::table('settlement_bills')->where('settlement_period_id', $periodId)->count())
|
||||
->toBe(0);
|
||||
});
|
||||
|
||||
test('period close does not dispatch rebates from other sites', function (): void {
|
||||
$siteA = createSiteWithRoot('rebate-site-a');
|
||||
$siteB = createSiteWithRoot('rebate-site-b');
|
||||
|
||||
$playerA = Player::query()->create([
|
||||
'site_code' => $siteA['site_code'],
|
||||
'agent_node_id' => $siteA['root_id'],
|
||||
'site_player_id' => 'p-rebate-a',
|
||||
'username' => 'rebate_a',
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$playerB = Player::query()->create([
|
||||
'site_code' => $siteB['site_code'],
|
||||
'agent_node_id' => $siteB['root_id'],
|
||||
'site_player_id' => 'p-rebate-b',
|
||||
'username' => 'rebate_b',
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$settledAt = now();
|
||||
|
||||
$ticketAId = createTicketItemForPlayer($playerA, 'T-REB-A');
|
||||
$ticketBId = createTicketItemForPlayer($playerB, 'T-REB-B');
|
||||
|
||||
$snapshotA = json_encode([
|
||||
'total_shares' => ['rebate-site-a' => 100],
|
||||
'chain_codes' => ['rebate-site-a'],
|
||||
'agent_path' => [$siteA['root_id']],
|
||||
]);
|
||||
$snapshotB = json_encode([
|
||||
'total_shares' => ['rebate-site-b' => 100],
|
||||
'chain_codes' => ['rebate-site-b'],
|
||||
'agent_path' => [$siteB['root_id']],
|
||||
]);
|
||||
|
||||
DB::table('share_ledger')->insert([
|
||||
[
|
||||
'ticket_item_id' => $ticketAId,
|
||||
'player_id' => $playerA->id,
|
||||
'agent_node_id' => $siteA['root_id'],
|
||||
'agent_path' => json_encode([$siteA['root_id']]),
|
||||
'share_snapshot' => $snapshotA,
|
||||
'game_win_loss' => 1000,
|
||||
'basic_rebate' => 0,
|
||||
'shared_net_win_loss' => 1000,
|
||||
'allocations_json' => json_encode([]),
|
||||
'settled_at' => $settledAt,
|
||||
'created_at' => $settledAt,
|
||||
'updated_at' => $settledAt,
|
||||
],
|
||||
[
|
||||
'ticket_item_id' => $ticketBId,
|
||||
'player_id' => $playerB->id,
|
||||
'agent_node_id' => $siteB['root_id'],
|
||||
'agent_path' => json_encode([$siteB['root_id']]),
|
||||
'share_snapshot' => $snapshotB,
|
||||
'game_win_loss' => 2000,
|
||||
'basic_rebate' => 0,
|
||||
'shared_net_win_loss' => 2000,
|
||||
'allocations_json' => json_encode([]),
|
||||
'settled_at' => $settledAt,
|
||||
'created_at' => $settledAt,
|
||||
'updated_at' => $settledAt,
|
||||
],
|
||||
]);
|
||||
|
||||
$rebateBId = (int) DB::table('rebate_records')->insertGetId([
|
||||
'player_id' => $playerB->id,
|
||||
'ticket_item_id' => $ticketBId,
|
||||
'game_type' => '*',
|
||||
'valid_bet_amount' => 10000,
|
||||
'rebate_rate' => 0.005,
|
||||
'rebate_amount' => 50,
|
||||
'rebate_type' => 'basic',
|
||||
'owner_agent_id' => $siteB['root_id'],
|
||||
'status' => 'accrued',
|
||||
'created_at' => $settledAt,
|
||||
'updated_at' => $settledAt,
|
||||
]);
|
||||
|
||||
$periodAId = (int) DB::table('settlement_periods')->insertGetId([
|
||||
'admin_site_id' => $siteA['site_id'],
|
||||
'period_start' => $settledAt->copy()->subDay(),
|
||||
'period_end' => $settledAt->copy()->addDay(),
|
||||
'status' => 'open',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$result = app(AgentSettlementPeriodCloseService::class)->closePeriod($periodAId);
|
||||
|
||||
expect($result['rebate_dispatched'])->toBe(0);
|
||||
|
||||
expect((string) DB::table('rebate_records')->where('id', $rebateBId)->value('status'))
|
||||
->toBe('accrued');
|
||||
expect(DB::table('rebate_records')->where('id', $rebateBId)->value('settlement_period_id'))
|
||||
->toBeNull();
|
||||
});
|
||||
|
||||
test('period close nets out original and reversal share ledger rows in same period', function (): void {
|
||||
['site_id' => $siteId, 'site_code' => $siteCode, 'root_id' => $rootId] = createSiteWithRoot('reversal-net');
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => $siteCode,
|
||||
'agent_node_id' => $rootId,
|
||||
'site_player_id' => 'reversal-net-p1',
|
||||
'auth_source' => 'lottery_native',
|
||||
'funding_mode' => 'credit',
|
||||
'username' => 'reversal_net_p',
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$settledAt = now();
|
||||
$ticketId = createTicketItemForPlayer($player, 'T-REV-NET');
|
||||
|
||||
$snapshot = json_encode([
|
||||
'total_shares' => ['reversal-net' => 100],
|
||||
'chain_codes' => ['reversal-net'],
|
||||
'agent_path' => [$rootId],
|
||||
]);
|
||||
|
||||
$originalLedgerId = (int) DB::table('share_ledger')->insertGetId([
|
||||
'ticket_item_id' => $ticketId,
|
||||
'player_id' => $player->id,
|
||||
'agent_node_id' => $rootId,
|
||||
'agent_path' => json_encode([$rootId]),
|
||||
'share_snapshot' => $snapshot,
|
||||
'game_win_loss' => 5000,
|
||||
'basic_rebate' => 0,
|
||||
'shared_net_win_loss' => 5000,
|
||||
'allocations_json' => json_encode([]),
|
||||
'settled_at' => $settledAt->copy()->subSecond(),
|
||||
'created_at' => $settledAt->copy()->subSecond(),
|
||||
'updated_at' => $settledAt->copy()->subSecond(),
|
||||
]);
|
||||
|
||||
$reversalLedgerId = (int) DB::table('share_ledger')->insertGetId([
|
||||
'ticket_item_id' => $ticketId,
|
||||
'player_id' => $player->id,
|
||||
'agent_node_id' => $rootId,
|
||||
'agent_path' => json_encode([$rootId]),
|
||||
'share_snapshot' => $snapshot,
|
||||
'game_win_loss' => -5000,
|
||||
'basic_rebate' => 0,
|
||||
'shared_net_win_loss' => -5000,
|
||||
'allocations_json' => json_encode([]),
|
||||
'reversal_of_id' => $originalLedgerId,
|
||||
'settled_at' => $settledAt,
|
||||
'created_at' => $settledAt,
|
||||
'updated_at' => $settledAt,
|
||||
]);
|
||||
|
||||
$periodId = (int) DB::table('settlement_periods')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => $settledAt->copy()->subDay(),
|
||||
'period_end' => $settledAt->copy()->addDay(),
|
||||
'status' => 'open',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$result = app(AgentSettlementPeriodCloseService::class)->closePeriod($periodId);
|
||||
|
||||
expect($result['player_count'])->toBe(1);
|
||||
|
||||
$playerBill = DB::table('settlement_bills')
|
||||
->where('settlement_period_id', $periodId)
|
||||
->where('bill_type', 'player')
|
||||
->where('owner_id', $player->id)
|
||||
->first();
|
||||
expect($playerBill)->not->toBeNull();
|
||||
expect((int) $playerBill->net_amount)->toBe(0);
|
||||
|
||||
expect(DB::table('share_ledger')->where('id', $originalLedgerId)->value('settlement_period_id'))
|
||||
->toBe($periodId);
|
||||
expect(DB::table('share_ledger')->where('id', $reversalLedgerId)->value('settlement_period_id'))
|
||||
->toBe($periodId);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user