feat(wallet): 增强资金流水和信用账本的公共类型过滤,支持游戏结算和账单结算
This commit is contained in:
@@ -36,6 +36,16 @@ final class WalletTransactionListController extends Controller
|
||||
|
||||
private const ALLOWED_STATUS = ['posted', 'pending_reconcile', 'reversed'];
|
||||
|
||||
/** 后台筛选兼容玩家端资金流水公共类型,也接受底层 biz_type。 */
|
||||
private const PUBLIC_BIZ_TYPE_MAP = [
|
||||
'transfer_in' => ['transfer_in'],
|
||||
'transfer_out' => ['transfer_out'],
|
||||
'refund' => ['transfer_out_refund'],
|
||||
'reversal' => ['reversal', 'bet_reverse'],
|
||||
'bet' => ['bet_deduct', 'bet'],
|
||||
'prize' => ['settle_payout', 'prize', 'jackpot_manual_payout'],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly PlayerLedgerLogsService $ledgerLogs,
|
||||
) {}
|
||||
@@ -101,7 +111,16 @@ final class WalletTransactionListController extends Controller
|
||||
}
|
||||
|
||||
if (! empty($validated['biz_type'])) {
|
||||
$query->where('biz_type', trim((string) $validated['biz_type']));
|
||||
$bizTypes = $this->resolveBizTypeFilter((string) $validated['biz_type']);
|
||||
if ($bizTypes === []) {
|
||||
return ApiResponse::success([
|
||||
'items' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
]);
|
||||
}
|
||||
$query->whereIn('biz_type', $bizTypes);
|
||||
}
|
||||
|
||||
if ($request->boolean('abnormal')) {
|
||||
@@ -167,4 +186,25 @@ final class WalletTransactionListController extends Controller
|
||||
'auth_source' => $p?->auth_source,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function resolveBizTypeFilter(string $raw): array
|
||||
{
|
||||
$parts = array_filter(array_map('trim', explode(',', $raw)));
|
||||
if ($parts === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$resolved = [];
|
||||
foreach ($parts as $part) {
|
||||
$key = strtolower($part);
|
||||
foreach (self::PUBLIC_BIZ_TYPE_MAP[$key] ?? [$key] as $bizType) {
|
||||
$resolved[] = $bizType;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($resolved));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\Services\Wallet\PlayerLedgerLogsService;
|
||||
/**
|
||||
* PRD §10.1.1:`GET /api/v1/wallet/logs` — 钱包/信用流水(按玩家资金模式分表)。
|
||||
*
|
||||
* Query:`page`、`size`(每页条数,默认 20)、`type`(逗号分隔:transfer_in,transfer_out,bet,prize,refund,reversal)
|
||||
* Query:`page`、`size`(每页条数,默认 20)、`type`(逗号分隔:transfer_in,transfer_out,bet,prize,refund,reversal,game_settlement,bill_settlement,rebate)
|
||||
*/
|
||||
final class WalletLogsController extends Controller
|
||||
{
|
||||
|
||||
@@ -137,10 +137,15 @@ final class AgentProfileService
|
||||
$profile->refresh();
|
||||
|
||||
$available = max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit);
|
||||
$totalShareRate = (float) $profile->total_share_rate;
|
||||
$parentTotalShareRate = $this->parentTotalShareRateForProfile($profile);
|
||||
|
||||
return [
|
||||
'agent_node_id' => (int) $profile->agent_node_id,
|
||||
'total_share_rate' => (float) $profile->total_share_rate,
|
||||
'total_share_rate' => $totalShareRate,
|
||||
'relative_share_rate' => $parentTotalShareRate > 0
|
||||
? round($totalShareRate / $parentTotalShareRate * 100, 2)
|
||||
: null,
|
||||
'credit_limit' => (int) $profile->credit_limit,
|
||||
'allocated_credit' => (int) $profile->allocated_credit,
|
||||
'used_credit' => (int) $profile->used_credit,
|
||||
@@ -154,6 +159,20 @@ final class AgentProfileService
|
||||
];
|
||||
}
|
||||
|
||||
private function parentTotalShareRateForProfile(AgentProfile $profile): float
|
||||
{
|
||||
$node = AgentNode::query()->find((int) $profile->agent_node_id);
|
||||
if ($node === null || $node->parent_id === null) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$parentProfile = AgentProfile::query()
|
||||
->where('agent_node_id', (int) $node->parent_id)
|
||||
->first();
|
||||
|
||||
return $parentProfile !== null ? (float) $parentProfile->total_share_rate : 100.0;
|
||||
}
|
||||
|
||||
public function profileForNode(int $agentNodeId): ?AgentProfile
|
||||
{
|
||||
return AgentProfile::query()->where('agent_node_id', $agentNodeId)->first();
|
||||
|
||||
@@ -17,12 +17,15 @@ use App\Support\AgentSettlementPeriodWindow;
|
||||
final class SettlementCenterLedgerService
|
||||
{
|
||||
private const CREDIT_BIZ_TYPES = [
|
||||
'bet',
|
||||
'bet_hold',
|
||||
'game_settlement',
|
||||
'bet_hold_release',
|
||||
'game_settlement_loss',
|
||||
'game_settlement_win',
|
||||
'game_settlement_reversal',
|
||||
'settlement_confirm',
|
||||
'bill_settlement',
|
||||
'settlement_payout',
|
||||
];
|
||||
|
||||
@@ -34,6 +37,12 @@ final class SettlementCenterLedgerService
|
||||
|
||||
private const SHARE_BIZ_TYPE = 'share_ledger';
|
||||
|
||||
private const CREDIT_PUBLIC_BIZ_TYPE_MAP = [
|
||||
'bet' => ['bet_hold'],
|
||||
'game_settlement' => ['bet_hold_release', 'game_settlement_loss', 'game_settlement_win', 'game_settlement_reversal'],
|
||||
'bill_settlement' => ['settlement_confirm', 'settlement_payout'],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly SettlementPartyEnrichment $partyEnrichment,
|
||||
private readonly CreditLedgerBetFlowPresenter $betFlowPresenter,
|
||||
@@ -304,7 +313,7 @@ final class SettlementCenterLedgerService
|
||||
}
|
||||
|
||||
if ($filters->bizType !== null && $filters->bizType !== '') {
|
||||
$query->where('cl.reason', $filters->bizType);
|
||||
$query->whereIn('cl.reason', $this->resolveCreditReasonFilter($filters->bizType));
|
||||
} elseif ($filters->betFlowOnly) {
|
||||
$query->whereIn('cl.reason', [
|
||||
'bet_hold',
|
||||
@@ -319,6 +328,14 @@ final class SettlementCenterLedgerService
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function resolveCreditReasonFilter(string $bizType): array
|
||||
{
|
||||
return self::CREDIT_PUBLIC_BIZ_TYPE_MAP[$bizType] ?? [$bizType];
|
||||
}
|
||||
|
||||
private function paymentStubQuery(
|
||||
AdminUser $admin,
|
||||
string $siteCode,
|
||||
|
||||
@@ -34,12 +34,14 @@ final class PlayerLedgerLogsService
|
||||
|
||||
/** PRD 对外类型 → credit_ledger.reason(信用盘不用钱包「派彩」口径) */
|
||||
private const CREDIT_TYPE_TO_REASON = [
|
||||
'bet' => ['bet_hold', 'game_settlement_loss'],
|
||||
'bet' => ['bet_hold'],
|
||||
'game_settlement' => ['bet_hold_release', 'game_settlement_loss', 'game_settlement_win', 'game_settlement_reversal'],
|
||||
'reversal' => ['bet_hold_release', 'game_settlement_reversal'],
|
||||
'refund' => ['settlement_confirm'],
|
||||
'win_credit' => ['game_settlement_win'],
|
||||
'credit_release' => ['game_settlement_win', 'settlement_confirm', 'bet_hold_release'],
|
||||
'bill_settlement' => ['settlement_payout'],
|
||||
'bill_settlement' => ['settlement_confirm', 'settlement_payout'],
|
||||
'rebate' => [],
|
||||
'transfer_in' => [],
|
||||
'transfer_out' => [],
|
||||
];
|
||||
@@ -423,24 +425,43 @@ final class PlayerLedgerLogsService
|
||||
string $typeFilterRaw,
|
||||
): array {
|
||||
$reasonFilter = $this->resolveCreditReasonFilter($typeFilterRaw);
|
||||
$includeRebates = $this->creditFilterIncludesRebates($typeFilterRaw);
|
||||
|
||||
if (is_array($reasonFilter) && $reasonFilter === []) {
|
||||
if (is_array($reasonFilter) && $reasonFilter === [] && ! $includeRebates) {
|
||||
return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage];
|
||||
}
|
||||
|
||||
$paginator = $this->creditLedgerQuery((int) $player->id, $reasonFilter)
|
||||
->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
$currency = (string) $player->default_currency;
|
||||
$skipRows = max(0, ($paginator->currentPage() - 1) * $paginator->perPage());
|
||||
$creditRows = is_array($reasonFilter) && $reasonFilter === []
|
||||
? collect()
|
||||
: $this->creditLedgerQuery((int) $player->id, $reasonFilter)->get();
|
||||
$rebateRows = $includeRebates ? $this->rebateRecordRows((int) $player->id) : collect();
|
||||
$rows = $creditRows
|
||||
->map(fn (object $row): object => (object) ['source' => 'credit', 'row' => $row])
|
||||
->concat($rebateRows->map(fn (object $row): object => (object) ['source' => 'rebate', 'row' => $row]))
|
||||
->sortByDesc(fn (object $entry): string => (string) ($entry->row->created_at ?? ''))
|
||||
->values();
|
||||
|
||||
$total = $rows->count();
|
||||
$offset = max(0, ($page - 1) * $perPage);
|
||||
$pageRows = $rows->slice($offset, $perPage)->values();
|
||||
$priorCreditRows = $rows
|
||||
->slice(0, $offset)
|
||||
->filter(fn (object $entry): bool => $entry->source === 'credit')
|
||||
->count();
|
||||
$runningMinor = $this->advanceCreditLedgerRunningMinor(
|
||||
(int) $player->id,
|
||||
$reasonFilter,
|
||||
$this->playerCreditService->availableCreditMinor($player, $currency),
|
||||
$skipRows,
|
||||
$priorCreditRows,
|
||||
);
|
||||
$items = $paginator->getCollection()
|
||||
->map(function (object $row) use (&$runningMinor, $player, $currency): array {
|
||||
$items = $pageRows
|
||||
->map(function (object $entry) use (&$runningMinor, $player, $currency): array {
|
||||
if ($entry->source === 'rebate') {
|
||||
return $this->formatPlayerRebateRow($entry->row, $player, $currency);
|
||||
}
|
||||
|
||||
$row = $entry->row;
|
||||
$amount = (int) $row->amount;
|
||||
$reason = (string) $row->reason;
|
||||
$affectsBalance = $this->creditReasonAffectsAvailableBalance($reason);
|
||||
@@ -462,9 +483,9 @@ final class PlayerLedgerLogsService
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -501,6 +522,18 @@ final class PlayerLedgerLogsService
|
||||
return $this->resolveTypeFilterMap($raw, self::CREDIT_TYPE_TO_REASON);
|
||||
}
|
||||
|
||||
private function creditFilterIncludesRebates(string $raw): bool
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$parts = array_filter(array_map('trim', explode(',', $raw)));
|
||||
|
||||
return in_array('rebate', array_map(Str::lower(...), $parts), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, list<string>> $map
|
||||
* @return list<string>|null
|
||||
@@ -605,6 +638,48 @@ final class PlayerLedgerLogsService
|
||||
];
|
||||
}
|
||||
|
||||
private function rebateRecordRows(int $playerId)
|
||||
{
|
||||
return DB::table('rebate_records')
|
||||
->where('player_id', $playerId)
|
||||
->whereNull('reversal_of_id')
|
||||
->orderByDesc('created_at')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatPlayerRebateRow(object $row, Player $player, string $currency): array
|
||||
{
|
||||
$amount = (int) $row->rebate_amount;
|
||||
|
||||
return [
|
||||
'log_id' => 'RB-'.$row->id,
|
||||
'type' => 'rebate',
|
||||
'biz_type' => 'rebate_'.$row->status,
|
||||
'amount' => $amount,
|
||||
'amount_formatted' => CurrencyFormatter::fromMinor(abs($amount)),
|
||||
'amount_abs' => abs($amount),
|
||||
'amount_abs_formatted' => CurrencyFormatter::fromMinor(abs($amount)),
|
||||
'direction' => $amount >= 0 ? 'in' : 'out',
|
||||
'currency_code' => $currency,
|
||||
'balance_after' => null,
|
||||
'balance_after_formatted' => null,
|
||||
'affects_available_credit' => false,
|
||||
'ref_id' => 'ticket_item#'.$row->ticket_item_id,
|
||||
'idempotent_key' => null,
|
||||
'external_ref_no' => null,
|
||||
'status' => (string) $row->status,
|
||||
'remark' => 'period_rebate',
|
||||
'created_at' => $this->isoTimestamp($row->created_at ?? null),
|
||||
'ledger_source' => 'rebate_record',
|
||||
'funding_mode' => (string) ($player->funding_mode ?? PlayerFundingMode::CREDIT),
|
||||
'auth_source' => $player->auth_source,
|
||||
];
|
||||
}
|
||||
|
||||
/** 账期收付记账不改变 player_credit_accounts,不参与可用信用倒推。 */
|
||||
private function creditReasonAffectsAvailableBalance(string $reason): bool
|
||||
{
|
||||
@@ -702,10 +777,9 @@ final class PlayerLedgerLogsService
|
||||
private function creditReasonToPublicType(string $reason): string
|
||||
{
|
||||
return match ($reason) {
|
||||
'bet_hold', 'game_settlement_loss' => 'bet',
|
||||
'bet_hold_release', 'game_settlement_reversal' => 'reversal',
|
||||
'settlement_confirm' => 'refund',
|
||||
'game_settlement_win' => 'win_credit',
|
||||
'bet_hold' => 'bet',
|
||||
'bet_hold_release', 'game_settlement_loss', 'game_settlement_win', 'game_settlement_reversal' => 'game_settlement',
|
||||
'settlement_confirm' => 'bill_settlement',
|
||||
'settlement_payout' => 'bill_settlement',
|
||||
default => $reason,
|
||||
};
|
||||
|
||||
@@ -94,6 +94,64 @@ test('credit ledger biz_type payment_record excludes credit rows', function ():
|
||||
->assertJsonPath('data.items.0.entry_kind', 'credit');
|
||||
});
|
||||
|
||||
test('credit ledger accepts public game settlement filter', function (): void {
|
||||
$site = DB::table('admin_sites')->where('is_default', true)->first();
|
||||
$siteId = (int) $site->id;
|
||||
$siteCode = (string) $site->code;
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => $siteCode,
|
||||
'site_player_id' => 'native:game-settlement-filter',
|
||||
'funding_mode' => PlayerFundingMode::CREDIT,
|
||||
'username' => 'game_settlement_filter_user',
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
DB::table('credit_ledger')->insert([
|
||||
[
|
||||
'owner_type' => 'player',
|
||||
'owner_id' => $player->id,
|
||||
'amount' => -100,
|
||||
'reason' => 'bet_hold',
|
||||
'created_at' => now()->subMinute(),
|
||||
'updated_at' => now()->subMinute(),
|
||||
],
|
||||
[
|
||||
'owner_type' => 'player',
|
||||
'owner_id' => $player->id,
|
||||
'amount' => 100,
|
||||
'reason' => 'bet_hold_release',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'owner_type' => 'player',
|
||||
'owner_id' => $player->id,
|
||||
'amount' => 200,
|
||||
'reason' => 'game_settlement_win',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'ledger_game_settlement_super',
|
||||
'name' => 'Game Settlement Ledger',
|
||||
'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/credit-ledger?admin_site_id='.$siteId.'&reason=game_settlement')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 2)
|
||||
->assertJsonPath('data.items.0.entry_kind', 'credit');
|
||||
});
|
||||
|
||||
test('credit ledger index includes payment on agent bill', function (): void {
|
||||
$site = DB::table('admin_sites')->where('is_default', true)->first();
|
||||
$siteId = (int) $site->id;
|
||||
|
||||
@@ -352,6 +352,66 @@ test('admin lists wallet transactions and filters abnormal', function (): void {
|
||||
->assertJsonPath('data.items.0.status', 'pending_reconcile');
|
||||
});
|
||||
|
||||
test('admin wallet transactions accepts public flow type filters', function (): void {
|
||||
$token = makeAdminToken();
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'main',
|
||||
'site_player_id' => 'public-flow-filter',
|
||||
'username' => null,
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$wallet = PlayerWallet::query()->create([
|
||||
'player_id' => $player->id,
|
||||
'wallet_type' => 'lottery',
|
||||
'currency_code' => 'NPR',
|
||||
'balance' => 5000,
|
||||
'frozen_balance' => 0,
|
||||
'status' => 0,
|
||||
'version' => 1,
|
||||
]);
|
||||
|
||||
foreach ([
|
||||
['WX_admin_bet', 'bet_deduct', 2],
|
||||
['WX_admin_prize', 'settle_payout', 1],
|
||||
['WX_admin_reversal', 'bet_reverse', 1],
|
||||
] as [$txnNo, $bizType, $direction]) {
|
||||
WalletTxn::query()->create([
|
||||
'txn_no' => $txnNo,
|
||||
'player_id' => $player->id,
|
||||
'wallet_id' => $wallet->id,
|
||||
'biz_type' => $bizType,
|
||||
'biz_no' => $txnNo,
|
||||
'direction' => $direction,
|
||||
'amount' => 100,
|
||||
'balance_before' => 1000,
|
||||
'balance_after' => 1000,
|
||||
'status' => 'posted',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/wallet/transactions?biz_type=bet')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 1)
|
||||
->assertJsonPath('data.items.0.biz_type', 'bet_deduct');
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/wallet/transactions?biz_type=prize')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 1)
|
||||
->assertJsonPath('data.items.0.biz_type', 'settle_payout');
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/wallet/transactions?biz_type=reversal')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 1)
|
||||
->assertJsonPath('data.items.0.biz_type', 'bet_reverse');
|
||||
});
|
||||
|
||||
test('admin transfer reverse is idempotent under concurrent reconcile', function (): void {
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'main',
|
||||
|
||||
@@ -106,7 +106,7 @@ test('credit player wallet logs distinguish win credit from bill settlement', fu
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.total', 2)
|
||||
->assertJsonPath('data.items.0.type', 'win_credit')
|
||||
->assertJsonPath('data.items.0.type', 'game_settlement')
|
||||
->assertJsonPath('data.items.0.biz_type', 'game_settlement_win')
|
||||
->assertJsonPath('data.items.0.affects_available_credit', true)
|
||||
->assertJsonPath('data.items.1.type', 'bill_settlement')
|
||||
@@ -126,13 +126,105 @@ test('credit player wallet logs distinguish win credit from bill settlement', fu
|
||||
->getJson('/api/v1/wallet/logs?type=win_credit')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 1)
|
||||
->assertJsonPath('data.items.0.type', 'win_credit');
|
||||
->assertJsonPath('data.items.0.type', 'game_settlement');
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->getJson('/api/v1/wallet/logs?type=credit_release')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 1)
|
||||
->assertJsonPath('data.items.0.type', 'win_credit');
|
||||
->assertJsonPath('data.items.0.type', 'game_settlement');
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->getJson('/api/v1/wallet/logs?type=game_settlement')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 1)
|
||||
->assertJsonPath('data.items.0.type', 'game_settlement');
|
||||
});
|
||||
|
||||
test('credit player wallet logs include period rebate records without affecting available credit', function (): void {
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'default_site',
|
||||
'site_player_id' => 'native:logs-rebate',
|
||||
'auth_source' => PlayerAuthSource::LOTTERY_NATIVE,
|
||||
'funding_mode' => PlayerFundingMode::CREDIT,
|
||||
'username' => 'credit_logs_rebate',
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
DB::table('player_credit_accounts')->insert([
|
||||
'player_id' => $player->id,
|
||||
'credit_limit' => 500,
|
||||
'used_credit' => 0,
|
||||
'frozen_credit' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$drawId = (int) DB::table('draws')->insertGetId([
|
||||
'draw_no' => 'RB-LOG-001',
|
||||
'business_date' => now()->toDateString(),
|
||||
'sequence_no' => 1,
|
||||
'status' => 'settled',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$orderId = (int) DB::table('ticket_orders')->insertGetId([
|
||||
'order_no' => 'RB-LOG-ORDER-001',
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $drawId,
|
||||
'currency_code' => 'NPR',
|
||||
'status' => 'settled',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$ticketItemId = (int) DB::table('ticket_items')->insertGetId([
|
||||
'ticket_no' => 'RB-LOG-TICKET-001',
|
||||
'order_id' => $orderId,
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $drawId,
|
||||
'normalized_number' => '1234',
|
||||
'play_code' => '4d',
|
||||
'total_bet_amount' => 10000,
|
||||
'status' => 'settled',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('rebate_records')->insert([
|
||||
'player_id' => $player->id,
|
||||
'ticket_item_id' => $ticketItemId,
|
||||
'game_type' => '*',
|
||||
'valid_bet_amount' => 10000,
|
||||
'rebate_rate' => 0.01,
|
||||
'rebate_amount' => 100,
|
||||
'rebate_type' => 'basic',
|
||||
'owner_agent_id' => null,
|
||||
'status' => 'accrued',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->getJson('/api/v1/wallet/logs?page=1&size=10')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 1)
|
||||
->assertJsonPath('data.items.0.type', 'rebate')
|
||||
->assertJsonPath('data.items.0.biz_type', 'rebate_accrued')
|
||||
->assertJsonPath('data.items.0.ledger_source', 'rebate_record')
|
||||
->assertJsonPath('data.items.0.affects_available_credit', false)
|
||||
->assertJsonPath('data.items.0.balance_after', null);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->getJson('/api/v1/wallet/logs?type=rebate')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 1)
|
||||
->assertJsonPath('data.items.0.type', 'rebate');
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->getJson('/api/v1/wallet/logs?type=bet')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total', 0);
|
||||
});
|
||||
|
||||
test('credit wallet logs page 2 balance_after continues from page 1 baseline', function (): void {
|
||||
|
||||
Reference in New Issue
Block a user