where('is_default', true)->first(); $agentId = (int) DB::table('agent_nodes') ->where('admin_site_id', (int) $site->id) ->where('depth', 0) ->value('id'); $player = Player::query()->create([ 'site_code' => (string) $site->code, 'site_player_id' => 'partial-'.uniqid('', true), 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'partial_'.uniqid('', true), 'default_currency' => 'NPR', 'status' => 0, 'agent_node_id' => $agentId, ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $player->id, 'credit_limit' => 100_000, 'used_credit' => $initialUsedCreditMajor, 'frozen_credit' => 0, 'created_at' => now(), 'updated_at' => now(), ]); $periodId = (int) DB::table('settlement_periods')->insertGetId([ 'admin_site_id' => (int) $site->id, 'period_start' => now()->subWeek(), 'period_end' => now(), 'status' => 'closed', 'created_at' => now(), 'updated_at' => now(), ]); $unpaid = abs($netAmount); $billId = (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' => $agentId, 'net_amount' => $netAmount, 'unpaid_amount' => $unpaid, 'paid_amount' => 0, 'status' => 'confirmed', 'created_at' => now(), 'updated_at' => now(), ]); $admin = AdminUser::query()->create([ 'username' => 'partial_pay_'.uniqid('', true), 'name' => 'PartialPay', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($admin); return [ 'player' => $player, 'billId' => $billId, 'admin' => $admin, 'netAmount' => $netAmount, ]; } function settlementBillLedgerRows(Player $player, int $billId, string $reason): \Illuminate\Support\Collection { return DB::table('credit_ledger') ->where('owner_type', 'player') ->where('owner_id', $player->id) ->where('reason', $reason) ->where('ref_type', 'settlement_bill') ->where('ref_id', $billId) ->get(); } test('partial payments accumulate settlement_confirm ledger and release used credit incrementally', function (): void { $fixture = partialPaymentCreditFixture(600, 10); $player = $fixture['player']; $billId = $fixture['billId']; $adminId = (int) $fixture['admin']->id; $usedBefore = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); $service = app(SettlementPaymentService::class); $service->recordPayment($billId, 200, $adminId); $rows = settlementBillLedgerRows($player, $billId, 'settlement_confirm'); expect($rows)->toHaveCount(1) ->and((int) $rows[0]->amount)->toBe(200); $usedAfterFirst = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); expect($usedAfterFirst)->toBe($usedBefore - CreditAmountScale::minorToMajor(200, 'NPR')); $service->recordPayment($billId, 400, $adminId); $rows = settlementBillLedgerRows($player, $billId, 'settlement_confirm'); expect($rows)->toHaveCount(1) ->and((int) $rows[0]->amount)->toBe(600); $usedAfterSecond = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); expect($usedAfterSecond)->toBe($usedBefore - CreditAmountScale::minorToMajor(600, 'NPR')); expect((string) DB::table('settlement_bills')->where('id', $billId)->value('status'))->toBe('settled'); }); test('partial payments accumulate settlement_payout ledger for player win bill', function (): void { $fixture = partialPaymentCreditFixture(-800); $player = $fixture['player']; $billId = $fixture['billId']; $adminId = (int) $fixture['admin']->id; $usedBefore = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); $service = app(SettlementPaymentService::class); $service->recordPayment($billId, 300, $adminId); $rows = settlementBillLedgerRows($player, $billId, 'settlement_payout'); expect($rows)->toHaveCount(1) ->and((int) $rows[0]->amount)->toBe(300); $service->recordPayment($billId, 500, $adminId); $rows = settlementBillLedgerRows($player, $billId, 'settlement_payout'); expect($rows)->toHaveCount(1) ->and((int) $rows[0]->amount)->toBe(800); $usedAfter = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); expect($usedAfter)->toBe($usedBefore); expect((string) DB::table('settlement_bills')->where('id', $billId)->value('status'))->toBe('settled'); }); test('bad debt after partial payment completes cumulative settlement_confirm ledger', function (): void { $fixture = partialPaymentCreditFixture(800, 10); $player = $fixture['player']; $billId = $fixture['billId']; $adminId = (int) $fixture['admin']->id; $usedBefore = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); $service = app(SettlementPaymentService::class); $service->recordPayment($billId, 300, $adminId); $usedAfterPartial = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); expect($usedAfterPartial)->toBe($usedBefore - CreditAmountScale::minorToMajor(300, 'NPR')); expect((string) DB::table('settlement_bills')->where('id', $billId)->value('status'))->toBe('partial_paid'); app(AgentSettlementBadDebtService::class)->writeOff($billId, 'uncollectible', $adminId); $rows = settlementBillLedgerRows($player, $billId, 'settlement_confirm'); expect($rows)->toHaveCount(1) ->and((int) $rows[0]->amount)->toBe(800); $usedAfterBadDebt = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); expect($usedAfterBadDebt)->toBe($usedBefore - CreditAmountScale::minorToMajor(800, 'NPR')); expect((string) DB::table('settlement_bills')->where('id', $billId)->value('status'))->toBe('settled'); expect((int) DB::table('settlement_bills')->where('id', $billId)->value('unpaid_amount'))->toBe(0); }); test('repeated payment after settled bill does not alter credit ledger', function (): void { $fixture = partialPaymentCreditFixture(500, 8); $player = $fixture['player']; $billId = $fixture['billId']; $adminId = (int) $fixture['admin']->id; $service = app(SettlementPaymentService::class); $service->recordPayment($billId, 500, $adminId); $usedAfterSettled = (int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'); expect(settlementBillLedgerRows($player, $billId, 'settlement_confirm'))->toHaveCount(1); expect(fn () => $service->recordPayment($billId, 1, $adminId)) ->toThrow(\Illuminate\Validation\ValidationException::class); expect((int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit')) ->toBe($usedAfterSettled); expect((int) settlementBillLedgerRows($player, $billId, 'settlement_confirm')[0]->amount)->toBe(500); });