|
|
@@ -78,6 +78,7 @@ class ReportingTest extends TestCase
|
|
|
$this->ready();
|
|
|
}
|
|
|
protected function tearDown(): void { Carbon::setTestNow(); DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); }
|
|
|
+ private function today(array $filters = []): array { return $filters + ['start_date' => now('Asia/Shanghai')->toDateString(), 'end_date' => now('Asia/Shanghai')->toDateString()]; }
|
|
|
private function ready(): void { DB::table('reporting_sync_state')->updateOrInsert(['source' => '__run__'], ['state' => '{"complete":true}', 'updated_at' => now()]); }
|
|
|
private function context(string $source, int $id, int $user = 2, string $platform = 'H5', ?string $success = null): void
|
|
|
{
|
|
|
@@ -108,7 +109,7 @@ class ReportingTest extends TestCase
|
|
|
{
|
|
|
$this->bet(1, ['created_at' => '2026-10-25 23:00:00']); $this->sync->publish('bets', 1);
|
|
|
$this->assertSame(1, DB::table('reporting_orders')->count());
|
|
|
- $row = $this->reader->summary([])['summary'];
|
|
|
+ $row = $this->reader->summary($this->today())['summary'];
|
|
|
$this->assertSame('0.0000000000', $row['bet_amount']); $this->assertSame('100.0000000000', $row['settled_bet_amount']);
|
|
|
$this->assertSame('60.0000000000', $row['payout_amount']); $this->assertSame('40.0000000000', $row['game_gross_profit']);
|
|
|
$this->assertSame('40.0000000000', $row['company_profit']);
|
|
|
@@ -119,28 +120,28 @@ class ReportingTest extends TestCase
|
|
|
public function test_missing_fees_and_success_time_never_produce_fabricated_net_values(): void
|
|
|
{
|
|
|
$event = $this->deposit(); $this->bet();
|
|
|
- $row = $this->reader->summary([])['summary']; $this->assertNull($row['net_cash_flow']); $this->assertNull($row['company_profit']);
|
|
|
+ $row = $this->reader->summary($this->today())['summary']; $this->assertNull($row['net_cash_flow']); $this->assertNull($row['company_profit']);
|
|
|
$this->assertSame(1, $row['quality']['unverified_fees']); $this->assertSame('1000.0000000000', $row['deposit_amount']);
|
|
|
- $this->verify($event); $row = $this->reader->summary([])['summary'];
|
|
|
+ $this->verify($event); $row = $this->reader->summary($this->today())['summary'];
|
|
|
$this->assertSame('998.0000000000', $row['net_cash_flow']); $this->assertSame('38.0000000000', $row['company_profit']);
|
|
|
$before = DB::table('balance_logs')->get()->toArray(); $this->verify($event, '1000', '3'); $this->sync->publish('balance_logs', 1);
|
|
|
$this->assertSame(1, DB::table('reporting_reconciliations')->count()); $this->assertEquals($before, DB::table('balance_logs')->get()->toArray());
|
|
|
- $this->assertSame('997.0000000000', $this->reader->summary([])['summary']['net_cash_flow']);
|
|
|
+ $this->assertSame('997.0000000000', $this->reader->summary($this->today())['summary']['net_cash_flow']);
|
|
|
}
|
|
|
|
|
|
public function test_chain_withdraw_uses_recipient_amount_and_customer_fee_is_not_an_expense(): void
|
|
|
{
|
|
|
DB::table('withdraws')->insert(['id' => 1, 'member_id' => '200', 'amount' => '100', 'to_account' => '98', 'service_charge' => '2', 'exchange_rate' => '7', 'status' => 1, 'created_at' => '2026-10-25 08:00:00', 'updated_at' => '2026-10-26 10:00:00']);
|
|
|
- $this->sync->publish('withdraws', 1); $row = $this->reader->summary([])['summary'];
|
|
|
+ $this->sync->publish('withdraws', 1); $row = $this->reader->summary($this->today())['summary'];
|
|
|
$this->assertSame(1, $row['quality']['missing_time']); $this->assertNull($row['net_cash_flow']);
|
|
|
$event = (int) DB::table('reporting_events')->value('id'); $this->verify($event, '686', '1');
|
|
|
- $row = $this->reader->summary([])['summary'];
|
|
|
+ $row = $this->reader->summary($this->today())['summary'];
|
|
|
$this->assertSame('686.0000000000', $row['withdraw_amount']); $this->assertSame('14.0000000000', $row['customer_fee_income']);
|
|
|
$this->assertSame('1.0000000000', $row['withdraw_fee']); $this->assertSame('-687.0000000000', $row['net_cash_flow']);
|
|
|
DB::table('withdraws')->where('id', 1)->update(['status' => 2]); $result = $this->sync->sync(true);
|
|
|
- $this->assertSame(1, $result['errors']); $this->assertNull($this->reader->summary([])['summary']['net_cash_flow']);
|
|
|
+ $this->assertSame(1, $result['errors']); $this->assertNull($this->reader->summary($this->today())['summary']['net_cash_flow']);
|
|
|
$this->verify($event, '0', '1'); $this->sync->sync(true);
|
|
|
- $this->assertSame('0.0000000000', $this->reader->summary([])['summary']['withdraw_amount']);
|
|
|
+ $this->assertSame('0.0000000000', $this->reader->summary($this->today())['summary']['withdraw_amount']);
|
|
|
}
|
|
|
|
|
|
public function test_real_member_agent_sharing_overlap_is_not_added_twice_and_details_are_direct(): void
|
|
|
@@ -150,22 +151,22 @@ class ReportingTest extends TestCase
|
|
|
$row = $this->reader->summary(['report_type' => $type])['summary']; $this->assertSame('100.0000000000', $row['bet_amount']);
|
|
|
$this->assertSame('1000.0000000000', $row['deposit_amount']);
|
|
|
}
|
|
|
- $this->assertSame('2', $this->reader->details([])['data'][0]['group_id']);
|
|
|
+ $this->assertSame('2', $this->reader->details($this->today())['data'][0]['group_id']);
|
|
|
$this->assertSame('10', $this->reader->details(['report_type' => 'promotion'])['data'][0]['group_id']);
|
|
|
$this->assertSame('1', $this->reader->details(['report_type' => 'sharing'])['data'][0]['group_id']);
|
|
|
- $category = $this->reader->summary([])['categories'][8]; $this->assertNull($category['deposit_amount']); $this->assertNull($category['company_profit']);
|
|
|
+ $category = $this->reader->summary($this->today())['categories'][8]; $this->assertNull($category['deposit_amount']); $this->assertNull($category['company_profit']);
|
|
|
$this->assertSame('100.0000000000', $category['bet_amount']);
|
|
|
}
|
|
|
|
|
|
public function test_valid_members_use_both_sharing_thresholds_inclusively(): void
|
|
|
{
|
|
|
$this->bet(); $this->deposit(1, '19.99'); $fact = $this->qualificationFact('100');
|
|
|
- $this->assertSame(0, $this->reader->summary([])['summary']['valid_member_count']);
|
|
|
+ $this->assertSame(0, $this->reader->summary($this->today())['summary']['valid_member_count']);
|
|
|
DB::table('balance_logs')->where('id', 1)->update(['amount' => '20']);
|
|
|
DB::table('share_bet_facts')->where('id', $fact)->update(['effective_amount' => '99.99']);
|
|
|
- $this->assertSame(0, $this->reader->summary([])['summary']['valid_member_count']);
|
|
|
+ $this->assertSame(0, $this->reader->summary($this->today())['summary']['valid_member_count']);
|
|
|
DB::table('share_bet_facts')->where('id', $fact)->update(['effective_amount' => '100']);
|
|
|
- $report = $this->reader->summary([]);
|
|
|
+ $report = $this->reader->summary($this->today());
|
|
|
$this->assertSame(1, $report['summary']['valid_member_count']);
|
|
|
$this->assertSame('20.0000000000', $report['valid_deposit_min']);
|
|
|
$this->assertSame('100.0000000000', $report['valid_effective_bet_min']);
|
|
|
@@ -174,7 +175,7 @@ class ReportingTest extends TestCase
|
|
|
// Presentation switches and the retired report-only value do not change qualification.
|
|
|
DB::table('reporting_settings')->where('id', 1)->update(['valid_deposit_min' => '9999']);
|
|
|
DB::table('share_policies')->update(['enabled' => 0, 'benefit_enabled' => 0]);
|
|
|
- $this->assertSame(1, $this->reader->summary([])['summary']['valid_member_count']);
|
|
|
+ $this->assertSame(1, $this->reader->summary($this->today())['summary']['valid_member_count']);
|
|
|
}
|
|
|
|
|
|
public function test_qualification_is_cumulative_before_start_and_independent_of_report_filters_and_payment_verification(): void
|
|
|
@@ -210,18 +211,18 @@ class ReportingTest extends TestCase
|
|
|
$this->qualificationFact('1000', '2026-10-25', 2, ['settled' => 0]);
|
|
|
$this->qualificationFact('1000', '2026-10-25', 2, ['voided' => 1]);
|
|
|
$this->qualificationFact('1000', '2026-10-25', 2, ['currency' => 'USDT']);
|
|
|
- $this->assertSame(0, $this->reader->summary([])['summary']['valid_member_count']);
|
|
|
+ $this->assertSame(0, $this->reader->summary($this->today())['summary']['valid_member_count']);
|
|
|
DB::table('balance_logs')->insert(['member_id' => '200', 'type' => 1, 'change_type' => '三方充值', 'amount' => '1', 'created_at' => '2026-10-26 23:59:59']);
|
|
|
- $this->assertSame(0, $this->reader->summary([])['summary']['valid_member_count']);
|
|
|
+ $this->assertSame(0, $this->reader->summary($this->today())['summary']['valid_member_count']);
|
|
|
DB::table('share_bet_facts')->where('id', $fact)->update(['effective_amount' => '100']);
|
|
|
- $this->assertSame(1, $this->reader->summary([])['summary']['valid_member_count']);
|
|
|
+ $this->assertSame(1, $this->reader->summary($this->today())['summary']['valid_member_count']);
|
|
|
}
|
|
|
|
|
|
public function test_pending_sharing_policy_never_applies_before_its_effective_date(): void
|
|
|
{
|
|
|
$this->bet(); $this->deposit(1, '20'); $this->qualificationFact();
|
|
|
DB::table('share_policies')->insert(['effective_date' => '2026-10-27', 'valid_member_min_deposit' => '200', 'valid_member_min_effective_bet' => '1000', 'rules' => '{}']);
|
|
|
- $report = $this->reader->summary([]);
|
|
|
+ $report = $this->reader->summary($this->today());
|
|
|
$this->assertSame(1, $report['summary']['valid_member_count']); $this->assertSame(1, $report['valid_member_policy']['id']);
|
|
|
$this->assertSame('20.0000000000', $this->admin->settings()['valid_deposit_min']);
|
|
|
Carbon::setTestNow(Carbon::parse('2026-10-27 16:00:00', 'Asia/Shanghai'));
|
|
|
@@ -235,7 +236,7 @@ class ReportingTest extends TestCase
|
|
|
{
|
|
|
$this->bet(1, ['status' => 1, 'profit' => '0']);
|
|
|
DB::table('share_policies')->update(['valid_member_min_deposit' => '0', 'valid_member_min_effective_bet' => '0']);
|
|
|
- $report = $this->reader->summary([]);
|
|
|
+ $report = $this->reader->summary($this->today());
|
|
|
$this->assertSame(1, $report['summary']['valid_member_count']); $this->assertSame(4, $report['summary']['member_count']);
|
|
|
$this->assertSame('0.0000000000', $report['valid_deposit_min']); $this->assertSame('0.0000000000', $report['valid_effective_bet_min']);
|
|
|
$this->assertSame(0, $this->reader->summary(['settlement_status' => 'settled'])['summary']['valid_member_count']);
|
|
|
@@ -245,22 +246,22 @@ class ReportingTest extends TestCase
|
|
|
{
|
|
|
$this->bet(); $this->deposit(); $this->qualificationFact();
|
|
|
DB::table('share_policies')->update(['valid_member_min_effective_bet' => null]);
|
|
|
- $report = $this->reader->summary([]);
|
|
|
+ $report = $this->reader->summary($this->today());
|
|
|
$this->assertNull($report['summary']['valid_member_count']); $this->assertNull($report['valid_effective_bet_min']);
|
|
|
$this->assertFalse($report['valid_member_policy']['available']);
|
|
|
$this->assertContains('incomplete_qualification_policy', $report['valid_member_policy']['unavailable_reasons']);
|
|
|
DB::table('share_policies')->delete();
|
|
|
- $report = $this->reader->summary([]);
|
|
|
+ $report = $this->reader->summary($this->today());
|
|
|
$this->assertNull($report['summary']['valid_member_count']); $this->assertNull($report['valid_deposit_min']);
|
|
|
$this->assertContains('no_effective_policy', $report['valid_member_policy']['unavailable_reasons']);
|
|
|
DB::table('share_policies')->insert(['effective_date' => '2026-10-01', 'valid_member_min_deposit' => '20', 'valid_member_min_effective_bet' => '100', 'rules' => '{}']);
|
|
|
DB::table('share_system_state')->delete();
|
|
|
- $report = $this->reader->summary([]);
|
|
|
+ $report = $this->reader->summary($this->today());
|
|
|
$this->assertNull($report['summary']['valid_member_count']);
|
|
|
$this->assertContains('relations_not_initialized', $report['valid_member_policy']['unavailable_reasons']);
|
|
|
DB::table('share_system_state')->insert(['key' => 'initialized_at', 'value' => '2026-10-01']);
|
|
|
Schema::drop('share_bet_facts');
|
|
|
- $report = $this->reader->summary([]);
|
|
|
+ $report = $this->reader->summary($this->today());
|
|
|
$this->assertNull($report['summary']['valid_member_count']);
|
|
|
$this->assertContains('missing_table:share_bet_facts', $report['valid_member_policy']['unavailable_reasons']);
|
|
|
}
|
|
|
@@ -272,8 +273,8 @@ class ReportingTest extends TestCase
|
|
|
'balance_logs', 'share_accounts', 'share_settlements', 'share_ledger', 'share_claims', 'operation_audits'];
|
|
|
$before = []; foreach ($tables as $table) $before[$table] = DB::table($table)->get()->toJson();
|
|
|
for ($i = 0; $i < 2; ++$i) {
|
|
|
- $this->assertSame(1, $this->reader->summary([])['summary']['valid_member_count']);
|
|
|
- $this->assertSame(1, $this->reader->details([])['data'][0]['valid_member_count']);
|
|
|
+ $this->assertSame(1, $this->reader->summary($this->today())['summary']['valid_member_count']);
|
|
|
+ $this->assertSame(1, $this->reader->details($this->today())['data'][0]['valid_member_count']);
|
|
|
$settings = $this->admin->settings();
|
|
|
$this->assertSame('20.0000000000', $settings['valid_deposit_min']);
|
|
|
$this->assertSame('100.0000000000', $settings['valid_effective_bet_min']);
|
|
|
@@ -302,7 +303,7 @@ class ReportingTest extends TestCase
|
|
|
foreach ([['a', '10:00:00'], ['b', '10:00:00'], ['a', '10:01:00'], ['b', '10:01:00'], ['a', '10:20:00'], ['a', '10:21:00']] as [$session, $at]) {
|
|
|
DB::table('reporting_activity')->insert(['user_id' => 2, 'agent_id' => 10, 'inviter_id' => 1, 'client_platform' => 'H5', 'session_hash' => $session, 'observed_at' => '2026-10-26 ' . $at]);
|
|
|
}
|
|
|
- $row = $this->reader->summary([])['summary'];
|
|
|
+ $row = $this->reader->summary($this->today())['summary'];
|
|
|
$this->assertSame(2, $row['activity_minutes']); $this->assertSame(1, $row['active_member_count']); $this->assertSame(4, $row['member_count']);
|
|
|
$this->assertSame('0.250000', $row['active_rate']);
|
|
|
$old = $this->reader->summary(['start_date' => '2026-10-24', 'end_date' => '2026-10-26'])['summary'];
|
|
|
@@ -327,24 +328,24 @@ class ReportingTest extends TestCase
|
|
|
public function test_activity_snapshot_survives_agent_reassignment_in_summary_details_and_filters(): void
|
|
|
{
|
|
|
$this->activityObservation('2026-10-26 10:00:00'); $this->activityObservation('2026-10-26 10:02:00');
|
|
|
- $before = $this->reader->summary(['report_type' => 'promotion', 'agent_account' => 'agent10'])['summary'];
|
|
|
+ $before = $this->reader->summary($this->today(['report_type' => 'promotion', 'agent_account' => 'agent10']))['summary'];
|
|
|
DB::table('users')->where('id', 2)->update(['agent_id' => 20]);
|
|
|
- $after = $this->reader->summary(['report_type' => 'promotion', 'agent_account' => 'agent10'])['summary'];
|
|
|
+ $after = $this->reader->summary($this->today(['report_type' => 'promotion', 'agent_account' => 'agent10']))['summary'];
|
|
|
$this->assertSame(2, $before['activity_minutes']); $this->assertSame(2, $after['activity_minutes']);
|
|
|
$this->assertSame(1, $after['active_member_count']); $this->assertFalse($after['activity_population_complete']); $this->assertNull($after['active_rate']);
|
|
|
- $other = $this->reader->summary(['report_type' => 'promotion', 'agent_account' => 'agent20'])['summary'];
|
|
|
+ $other = $this->reader->summary($this->today(['report_type' => 'promotion', 'agent_account' => 'agent20']))['summary'];
|
|
|
$this->assertSame(0, $other['activity_minutes']); $this->assertSame(0, $other['active_member_count']);
|
|
|
- $details = $this->reader->details(['report_type' => 'promotion']);
|
|
|
+ $details = $this->reader->details($this->today(['report_type' => 'promotion']));
|
|
|
$this->assertSame(1, $details['total']); $this->assertSame('10', $details['data'][0]['group_id']); $this->assertSame(2, $details['data'][0]['activity_minutes']);
|
|
|
- $byGroup = $this->reader->summary(['report_type' => 'promotion', 'group_id' => '10'])['summary'];
|
|
|
+ $byGroup = $this->reader->summary($this->today(['report_type' => 'promotion', 'group_id' => '10']))['summary'];
|
|
|
$this->assertSame(2, $byGroup['activity_minutes']);
|
|
|
- $business = $this->reader->summary(['agent_account' => 'agent10'])['summary']; $this->assertSame(2, $business['activity_minutes']);
|
|
|
+ $business = $this->reader->summary($this->today(['agent_account' => 'agent10']))['summary']; $this->assertSame(2, $business['activity_minutes']);
|
|
|
|
|
|
$this->activityObservation('2026-10-26 11:00:00', 20); $this->activityObservation('2026-10-26 11:03:00', 20);
|
|
|
- $groups = collect($this->reader->details(['report_type' => 'promotion'])['data'])->keyBy('group_id');
|
|
|
+ $groups = collect($this->reader->details($this->today(['report_type' => 'promotion']))['data'])->keyBy('group_id');
|
|
|
$this->assertSame(2, $groups[10]['activity_minutes']); $this->assertSame(3, $groups[20]['activity_minutes']);
|
|
|
$this->assertSame('0.500000', $groups[20]['active_rate']);
|
|
|
- $all = $this->reader->summary([])['summary']; $this->assertSame(5, $all['activity_minutes']); $this->assertSame(1, $all['active_member_count']);
|
|
|
+ $all = $this->reader->summary($this->today())['summary']; $this->assertSame(5, $all['activity_minutes']); $this->assertSame(1, $all['active_member_count']);
|
|
|
$this->assertSame('0.250000', $all['active_rate']);
|
|
|
}
|
|
|
|
|
|
@@ -355,45 +356,45 @@ class ReportingTest extends TestCase
|
|
|
$this->activityObservation('2026-10-26 10:02:00', 20, 1, 'a');
|
|
|
$this->activityObservation('2026-10-26 10:03:00', 20, 1, 'b');
|
|
|
DB::table('users')->where('id', 2)->update(['agent_id' => 20]);
|
|
|
- $groups = collect($this->reader->details(['report_type' => 'promotion'])['data'])->keyBy('group_id');
|
|
|
+ $groups = collect($this->reader->details($this->today(['report_type' => 'promotion']))['data'])->keyBy('group_id');
|
|
|
$this->assertSame(1, $groups[10]['activity_minutes']); $this->assertSame(2, $groups[20]['activity_minutes']);
|
|
|
- $all = $this->reader->summary([])['summary']; $this->assertSame(3, $all['activity_minutes']); $this->assertSame(1, $all['active_member_count']);
|
|
|
- $this->assertSame(1, $this->reader->summary(['report_type' => 'promotion', 'agent_account' => 'agent10'])['summary']['activity_minutes']);
|
|
|
- $this->assertSame(2, $this->reader->summary(['report_type' => 'promotion', 'agent_account' => 'agent20'])['summary']['activity_minutes']);
|
|
|
+ $all = $this->reader->summary($this->today())['summary']; $this->assertSame(3, $all['activity_minutes']); $this->assertSame(1, $all['active_member_count']);
|
|
|
+ $this->assertSame(1, $this->reader->summary($this->today(['report_type' => 'promotion', 'agent_account' => 'agent10']))['summary']['activity_minutes']);
|
|
|
+ $this->assertSame(2, $this->reader->summary($this->today(['report_type' => 'promotion', 'agent_account' => 'agent20']))['summary']['activity_minutes']);
|
|
|
}
|
|
|
|
|
|
public function test_activity_uses_inviter_snapshot_and_does_not_inherit_current_null_attribution(): void
|
|
|
{
|
|
|
$this->activityObservation('2026-10-26 10:00:00'); $this->activityObservation('2026-10-26 10:04:00');
|
|
|
DB::table('share_members')->where('user_id', 2)->update(['parent_user_id' => 3]);
|
|
|
- $groups = collect($this->reader->details(['report_type' => 'sharing'])['data'])->keyBy('group_id');
|
|
|
+ $groups = collect($this->reader->details($this->today(['report_type' => 'sharing']))['data'])->keyBy('group_id');
|
|
|
$this->assertSame(4, $groups[1]['activity_minutes']); $this->assertArrayNotHasKey(3, $groups->all());
|
|
|
- $this->assertSame(4, $this->reader->summary(['report_type' => 'sharing', 'inviter_account' => 'member1'])['summary']['activity_minutes']);
|
|
|
- $this->assertSame(0, $this->reader->summary(['report_type' => 'sharing', 'inviter_account' => 'member3'])['summary']['activity_minutes']);
|
|
|
+ $this->assertSame(4, $this->reader->summary($this->today(['report_type' => 'sharing', 'inviter_account' => 'member1']))['summary']['activity_minutes']);
|
|
|
+ $this->assertSame(0, $this->reader->summary($this->today(['report_type' => 'sharing', 'inviter_account' => 'member3']))['summary']['activity_minutes']);
|
|
|
|
|
|
DB::table('reporting_activity')->delete();
|
|
|
$this->activityObservation('2026-10-26 11:00:00', null, null); $this->activityObservation('2026-10-26 11:02:00', null, null);
|
|
|
- $this->assertSame(2, $this->reader->summary([])['summary']['activity_minutes']);
|
|
|
- $this->assertSame(0, $this->reader->summary(['report_type' => 'promotion'])['summary']['activity_minutes']);
|
|
|
- $this->assertSame(0, $this->reader->summary(['report_type' => 'sharing'])['summary']['activity_minutes']);
|
|
|
+ $this->assertSame(2, $this->reader->summary($this->today())['summary']['activity_minutes']);
|
|
|
+ $this->assertSame(0, $this->reader->summary($this->today(['report_type' => 'promotion']))['summary']['activity_minutes']);
|
|
|
+ $this->assertSame(0, $this->reader->summary($this->today(['report_type' => 'sharing']))['summary']['activity_minutes']);
|
|
|
}
|
|
|
|
|
|
public function test_activity_interval_crossing_query_start_keeps_the_starting_snapshot(): void
|
|
|
{
|
|
|
$this->activityObservation('2026-10-25 23:59:00', 10); $this->activityObservation('2026-10-26 00:01:00', 20);
|
|
|
DB::table('users')->where('id', 2)->update(['agent_id' => 20]);
|
|
|
- $groups = collect($this->reader->details(['report_type' => 'promotion'])['data'])->keyBy('group_id');
|
|
|
+ $groups = collect($this->reader->details($this->today(['report_type' => 'promotion']))['data'])->keyBy('group_id');
|
|
|
$this->assertSame(1, $groups[10]['activity_minutes']); $this->assertSame(0, $groups[20]['activity_minutes']);
|
|
|
- $this->assertSame(1, $this->reader->summary([])['summary']['activity_minutes']);
|
|
|
+ $this->assertSame(1, $this->reader->summary($this->today())['summary']['activity_minutes']);
|
|
|
}
|
|
|
|
|
|
public function test_order_time_reconciliation_survives_reimport_and_does_not_settle_source(): void
|
|
|
{
|
|
|
DB::table('bets')->insert(['id' => 1, 'member_id' => '200', 'amount' => '100', 'profit' => '60', 'status' => 2, 'created_at' => '2026-10-25 09:00:00', 'updated_at' => '2026-10-26 10:00:00']);
|
|
|
- $this->sync->publish('bets', 1); $this->assertNull($this->reader->summary([])['summary']['company_profit']);
|
|
|
+ $this->sync->publish('bets', 1); $this->assertNull($this->reader->summary($this->today())['summary']['company_profit']);
|
|
|
$id = (int) DB::table('reporting_orders')->value('id'); $before = (array) DB::table('bets')->first();
|
|
|
$this->admin->reconcileOrder(['id' => $id, 'settled_at' => '2026-10-26 10:00:00', 'evidence' => '开奖结算日志']); $this->sync->publish('bets', 1);
|
|
|
- $this->assertSame('40.0000000000', $this->reader->summary([])['summary']['company_profit']);
|
|
|
+ $this->assertSame('40.0000000000', $this->reader->summary($this->today())['summary']['company_profit']);
|
|
|
$this->assertSame($before, (array) DB::table('bets')->first());
|
|
|
}
|
|
|
|
|
|
@@ -418,11 +419,11 @@ class ReportingTest extends TestCase
|
|
|
DB::table('agent_rebate_records')->insert(['id' => 1, 'agent_id' => 10, 'platform' => 'pc28', 'game_type' => 3, 'settlement_date' => '2026-10-26', 'flow_commission' => '7', 'profit_commission' => '2', 'flow_status' => 'credited', 'profit_status' => 'pending', 'created_at' => '2026-10-26 10:00:00']);
|
|
|
DB::table('agent_balance_logs')->insert(['id' => 1, 'agent_id' => 10, 'type' => 'commission', 'amount' => '7', 'reference_type' => 'agent_rebate_record', 'reference_id' => 1, 'created_at' => '2026-10-26 11:00:00']);
|
|
|
foreach ([['share_ledger', 1], ['share_ledger', 2], ['balance_logs', 10], ['agent_rebate_records', 1], ['agent_balance_logs', 1]] as [$source, $id]) $this->sync->publish($source, $id);
|
|
|
- $row = $this->reader->summary([])['summary'];
|
|
|
+ $row = $this->reader->summary($this->today())['summary'];
|
|
|
$this->assertSame('17.0000000000', $row['share_accrual']); $this->assertSame('5.0000000000', $row['share_commission']);
|
|
|
$this->assertSame('9.0000000000', $row['agent_accrual']); $this->assertSame('7.0000000000', $row['agent_commission']);
|
|
|
$this->assertSame('28.0000000000', $row['company_profit']);
|
|
|
- $details = $this->reader->details([]); $this->assertSame('7.0000000000', $details['unallocated']['agent_commission']);
|
|
|
+ $details = $this->reader->details($this->today()); $this->assertSame('7.0000000000', $details['unallocated']['agent_commission']);
|
|
|
$this->assertSame('5.0000000000', $this->reader->summary(['report_type' => 'sharing'])['summary']['share_commission']);
|
|
|
}
|
|
|
|
|
|
@@ -434,9 +435,9 @@ class ReportingTest extends TestCase
|
|
|
$this->assertSame('100.0000000000', $r['summary']['settled_bet_amount']);
|
|
|
DB::table('bets')->insert(['id' => 2, 'member_id' => 'missing', 'amount' => '10', 'status' => 1, 'created_at' => '2026-10-26 10:00:00']);
|
|
|
$r = $this->sync->sync(true); $this->assertSame(1, $r['errors']);
|
|
|
- $this->assertNull($this->reader->summary([])['summary']['company_profit']); $this->assertSame(1, DB::table('reporting_errors')->count());
|
|
|
+ $this->assertNull($this->reader->summary($this->today())['summary']['company_profit']); $this->assertSame(1, DB::table('reporting_errors')->count());
|
|
|
DB::table('bets')->where('id', 2)->update(['member_id' => '200']); $this->sync->sync(true);
|
|
|
- $this->assertSame(0, DB::table('reporting_errors')->count()); $this->assertNotNull($this->reader->summary([])['summary']['company_profit']);
|
|
|
+ $this->assertSame(0, DB::table('reporting_errors')->count()); $this->assertNotNull($this->reader->summary($this->today())['summary']['company_profit']);
|
|
|
}
|
|
|
|
|
|
public function test_eloquent_context_observer_records_status_transition_once_and_tg_activity_is_deduplicated(): void
|
|
|
@@ -465,7 +466,7 @@ class ReportingTest extends TestCase
|
|
|
$this->context('sport_order', 1, 2, 'APP'); $this->sync->publish('sport_order', 1);
|
|
|
DB::table('third_game_orders')->insert(['id' => 1, 'order_key' => str_repeat('a', 64), 'member_id' => '200', 'user_id' => 2, 'game_type' => 4, 'bet_amount' => '30', 'settled_amount' => '-30', 'status' => 1, 'currency' => 'CNY', 'bet_time' => '2026-10-26 09:00:00', 'last_update_time' => '2026-10-26 10:00:00', 'created_at' => '2026-10-26 09:00:00', 'updated_at' => '2026-10-26 10:00:00']);
|
|
|
$this->sync->publish('third_game_orders', 1);
|
|
|
- $r = $this->reader->summary([])['summary'];
|
|
|
+ $r = $this->reader->summary($this->today())['summary'];
|
|
|
$this->assertSame(3, $r['order_count']); $this->assertSame('150.0000000000', $r['bet_amount']);
|
|
|
$this->assertSame('100.0000000000', $r['settled_bet_amount']); $this->assertSame('18.0000000000', $r['payout_amount']);
|
|
|
$this->assertSame('50.0000000000', $r['refund_amount']); $this->assertSame('82.0000000000', $r['game_gross_profit']);
|
|
|
@@ -475,7 +476,7 @@ class ReportingTest extends TestCase
|
|
|
Carbon::setTestNow(Carbon::parse('2026-10-26 16:10:00', 'Asia/Shanghai'));
|
|
|
DB::table('lhc_order_item')->where('id', 2)->update(['win_amount' => '68', 'updated_at' => now()->timestamp]);
|
|
|
$this->assertSame(0, $this->sync->sync()['errors']);
|
|
|
- $this->assertSame('32.0000000000', $this->reader->summary([])['summary']['game_gross_profit']);
|
|
|
+ $this->assertSame('32.0000000000', $this->reader->summary($this->today())['summary']['game_gross_profit']);
|
|
|
$this->assertSame(4, DB::table('reporting_orders')->count());
|
|
|
}
|
|
|
|
|
|
@@ -486,7 +487,7 @@ class ReportingTest extends TestCase
|
|
|
try { $this->verify($event); $this->fail('Expected audit failure'); } catch (\Illuminate\Database\QueryException $e) { $this->assertStringContainsString('audit failed', $e->getMessage()); }
|
|
|
$this->assertSame(0, DB::table('reporting_reconciliations')->count());
|
|
|
$this->bet(); DB::table('reporting_orders')->update(['currency' => 'USDT']);
|
|
|
- $r = $this->reader->summary([])['summary']; $this->assertSame('0.0000000000', $r['bet_amount']); $this->assertSame(1, $r['quality']['foreign_currency']); $this->assertNull($r['company_profit']);
|
|
|
+ $r = $this->reader->summary($this->today())['summary']; $this->assertSame('0.0000000000', $r['bet_amount']); $this->assertSame(1, $r['quality']['foreign_currency']); $this->assertNull($r['company_profit']);
|
|
|
}
|
|
|
|
|
|
public function test_verified_attribution_is_a_reporting_snapshot_and_survives_later_imports(): void
|
|
|
@@ -506,7 +507,132 @@ class ReportingTest extends TestCase
|
|
|
$this->assertNotEmpty($this->admin->quality([])['sources']);
|
|
|
}
|
|
|
|
|
|
+ private function historicalBet(int $id = 1, string $betAt = '2024-01-01 09:00:00', string $settledAt = '2024-01-02 10:00:00'): void
|
|
|
+ {
|
|
|
+ $this->bet($id, ['created_at' => $betAt, 'amount' => '40', 'profit' => '20']);
|
|
|
+ DB::table('reporting_contexts')->where('source', 'bets')->where('source_id', (string) $id)->update(['succeeded_at' => $settledAt]);
|
|
|
+ $this->sync->publish('bets', $id);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_no_dates_default_to_all_history_and_dates_alone_select_a_period(): void
|
|
|
+ {
|
|
|
+ $this->historicalBet(); $this->bet(2);
|
|
|
+ $default = $this->reader->summary([]);
|
|
|
+ $this->assertSame(1, $default['filters']['all_time']); $this->assertSame('2024-01-01', $default['filters']['start_date']);
|
|
|
+ $this->assertSame('2026-10-26', $default['filters']['end_date']);
|
|
|
+ $this->assertSame('140.0000000000', $default['summary']['bet_amount']);
|
|
|
+ $this->assertSame('80.0000000000', $default['summary']['payout_amount']);
|
|
|
+ $blank = $this->reader->summary(['start_date' => '', 'end_date' => null]);
|
|
|
+ $this->assertSame($default['filters'], $blank['filters']); $this->assertSame($default['summary'], $blank['summary']);
|
|
|
+ $range = $this->reader->summary($this->today());
|
|
|
+ $this->assertSame(0, $range['filters']['all_time']); $this->assertSame('2026-10-26', $range['filters']['start_date']);
|
|
|
+ $this->assertSame('100.0000000000', $range['summary']['bet_amount']);
|
|
|
+ $this->assertSame('2026-10-26', $default['valid_member_policy']['date']);
|
|
|
+ $this->assertSame($default['summary'], $this->reader->summary(['all_time' => 0])['summary']);
|
|
|
+ $this->assertSame($range['summary'], $this->reader->summary($this->today(['all_time' => 1]))['summary']);
|
|
|
+ $bounded = $this->reader->params(['start_date' => now('Asia/Shanghai')->subDays(365)->toDateString()]);
|
|
|
+ $this->assertSame(now('Asia/Shanghai')->subDays(365)->toDateString(), $bounded['start_date']);
|
|
|
+ $this->assertSame(366, $this->admin->options()['max_days']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_all_time_funds_paginate_actual_days_and_keep_the_complete_summary(): void
|
|
|
+ {
|
|
|
+ $this->historicalBet(); $this->bet(2); $event = $this->deposit(); $this->verify($event);
|
|
|
+ $first = $this->reader->funds(['limit' => 1]);
|
|
|
+ $days = (int) Carbon::parse('2024-01-01')->diffInDays(Carbon::parse('2026-10-27'));
|
|
|
+ $this->assertSame($days, $first['total']); $this->assertCount(1, $first['data']); $this->assertSame('2026-10-26', $first['data'][0]['date']);
|
|
|
+ $this->assertSame('140.0000000000', $first['summary']['bet_amount']); $this->assertSame('80.0000000000', $first['summary']['payout_amount']);
|
|
|
+ $this->assertSame('100.0000000000', $first['data'][0]['bet_amount']); $this->assertSame('60.0000000000', $first['data'][0]['payout_amount']);
|
|
|
+ $this->assertSame('998.0000000000', $first['summary']['net_cash_flow']);
|
|
|
+ $settled = $this->reader->funds(['limit' => 1, 'page' => $days - 1]);
|
|
|
+ $this->assertSame($first['summary'], $settled['summary']); $this->assertSame('2024-01-02', $settled['data'][0]['date']);
|
|
|
+ $this->assertSame('0.0000000000', $settled['data'][0]['bet_amount']); $this->assertSame('20.0000000000', $settled['data'][0]['payout_amount']);
|
|
|
+ $bet = $this->reader->funds(['limit' => 1, 'page' => $days]);
|
|
|
+ $this->assertSame('2024-01-01', $bet['data'][0]['date']); $this->assertSame('40.0000000000', $bet['data'][0]['bet_amount']);
|
|
|
+ $this->assertSame('0.0000000000', $bet['data'][0]['payout_amount']);
|
|
|
+ $outside = $this->reader->funds(['limit' => 1, 'page' => $days + 1]);
|
|
|
+ $this->assertSame([], $outside['data']); $this->assertSame($first['summary'], $outside['summary']); $this->assertSame($days, $outside['total']);
|
|
|
+ $bounded = $this->reader->funds(['start_date' => '2026-10-25', 'limit' => 1, 'page' => 2]);
|
|
|
+ $this->assertSame(2, $bounded['total']); $this->assertSame('2026-10-25', $bounded['data'][0]['date']);
|
|
|
+ $this->assertSame('100.0000000000', $bounded['summary']['bet_amount']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_all_time_uses_the_effective_reconciliation_date_and_amount(): void
|
|
|
+ {
|
|
|
+ $event = $this->deposit();
|
|
|
+ DB::table('balance_logs')->where('id', 1)->update(['created_at' => '2019-01-01 09:00:00']); $this->sync->publish('balance_logs', 1);
|
|
|
+ $this->admin->reconcile(['event_id' => $event, 'actual_amount' => '500', 'external_fee' => '3', 'occurred_at' => '2022-02-03 10:00:00',
|
|
|
+ 'currency' => 'CNY', 'evidence' => '历史到账凭据']);
|
|
|
+ $all = $this->reader->summary([]);
|
|
|
+ $this->assertSame('2022-02-03', $all['filters']['start_date']); $this->assertSame('500.0000000000', $all['summary']['deposit_amount']);
|
|
|
+ $this->assertSame('497.0000000000', $all['summary']['net_cash_flow']);
|
|
|
+ $this->assertSame('0.0000000000', $this->reader->summary($this->today())['summary']['deposit_amount']);
|
|
|
+ $funds = $this->reader->funds(['limit' => 1,
|
|
|
+ 'page' => (int) Carbon::parse('2022-02-03')->diffInDays(Carbon::parse('2026-10-26')) + 1]);
|
|
|
+ $this->assertSame('2022-02-03', $funds['data'][0]['date']); $this->assertSame('500.0000000000', $funds['data'][0]['deposit_amount']);
|
|
|
+ $events = $this->reader->events([]);
|
|
|
+ $this->assertSame(1, $events['total']); $this->assertSame('2022-02-03 10:00:00', $events['data'][0]['verified_at']);
|
|
|
+ $this->assertSame($all['filters']['start_date'], $events['filters']['start_date']); $this->assertSame(1, $events['filters']['all_time']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_all_time_does_not_substitute_the_application_date_for_unknown_payment_time(): void
|
|
|
+ {
|
|
|
+ DB::table('withdraws')->insert(['id' => 1, 'member_id' => '200', 'amount' => '100', 'to_account' => '98', 'service_charge' => '2',
|
|
|
+ 'exchange_rate' => '7', 'status' => 1, 'created_at' => '2001-01-01 08:00:00']);
|
|
|
+ $this->sync->publish('withdraws', 1);
|
|
|
+ $all = $this->reader->summary([]);
|
|
|
+ $this->assertSame('2026-10-01', $all['filters']['start_date']); $this->assertSame(1, $all['summary']['quality']['missing_time']);
|
|
|
+ $this->assertSame('0.0000000000', $all['summary']['withdraw_amount']); $this->assertNull($all['summary']['net_cash_flow']);
|
|
|
+ $funds = $this->reader->funds(['limit' => 100]);
|
|
|
+ $this->assertSame(26, $funds['total']); $this->assertCount(26, $funds['data']);
|
|
|
+ foreach ($funds['data'] as $day) {
|
|
|
+ $this->assertSame('0.0000000000', $day['withdraw_amount']); $this->assertSame(1, $day['quality']['missing_time']); $this->assertNull($day['net_cash_flow']);
|
|
|
+ }
|
|
|
+ $events = $this->reader->events([]);
|
|
|
+ $this->assertSame(1, $events['total']); $this->assertNull($events['data'][0]['occurred_at']);
|
|
|
+ $this->assertSame('2001-01-01 08:00:00', $events['data'][0]['candidate_at']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_all_time_first_day_uses_real_members_and_activity_and_empty_data_falls_back_to_today(): void
|
|
|
+ {
|
|
|
+ DB::table('users')->update(['created_at' => '2026-10-26 00:00:00']);
|
|
|
+ DB::table('users')->where('from', 2)->update(['created_at' => '1999-01-01 00:00:00']);
|
|
|
+ $this->assertSame('2026-10-26', $this->reader->params([])['start_date']);
|
|
|
+ DB::table('users')->where('id', 2)->update(['created_at' => '2023-01-01 00:00:00']);
|
|
|
+ $this->assertSame('2023-01-01', $this->reader->params([])['start_date']);
|
|
|
+ $this->activityObservation('2022-01-01 10:00:00'); $this->activityObservation('2022-01-01 10:02:00');
|
|
|
+ $all = $this->reader->summary([]);
|
|
|
+ $this->assertSame('2022-01-01', $all['filters']['start_date']); $this->assertSame(2, $all['summary']['known_activity_minutes']);
|
|
|
+ $this->assertNull($all['summary']['activity_minutes']);
|
|
|
+ DB::table('reporting_activity')->delete(); DB::table('users')->delete();
|
|
|
+ $empty = $this->reader->funds([]);
|
|
|
+ $this->assertSame('2026-10-26', $empty['filters']['start_date']); $this->assertSame(1, $empty['total']); $this->assertCount(1, $empty['data']);
|
|
|
+ $this->assertSame('0.0000000000', $empty['summary']['bet_amount']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_all_time_preserves_account_platform_settlement_and_ranking_filters(): void
|
|
|
+ {
|
|
|
+ $this->historicalBet(); $this->bet(2, ['member_id' => '500', 'amount' => '200']);
|
|
|
+ $this->bet(3, ['status' => 1, 'amount' => '30', 'profit' => '0']); $this->deposit();
|
|
|
+ foreach (['member_account' => 'member2', 'agent_account' => 'agent10', 'inviter_account' => 'member1'] as $filter => $value) {
|
|
|
+ $this->assertSame('70.0000000000', $this->reader->summary([$filter => $value])['summary']['bet_amount']);
|
|
|
+ }
|
|
|
+ $this->assertSame('40.0000000000', $this->reader->summary(['member_account' => 'member2', 'settlement_status' => 'settled'])['summary']['bet_amount']);
|
|
|
+ $this->assertSame('0.0000000000', $this->reader->summary(['platform' => 'APP'])['summary']['bet_amount']);
|
|
|
+ $this->assertSame('0.0000000000', $this->reader->summary(['category' => 'self:sport'])['summary']['bet_amount']);
|
|
|
+ $ranks = $this->reader->details(['metric' => 'bet_amount', 'page' => 2, 'limit' => 1], true);
|
|
|
+ $this->assertSame(2, $ranks['total']); $this->assertSame('2', $ranks['data'][0]['group_id']); $this->assertSame(2, $ranks['data'][0]['rank']);
|
|
|
+ $orders = $this->reader->orders(['member_account' => 'member2', 'page' => 2, 'limit' => 1]);
|
|
|
+ $this->assertSame(2, $orders['total']); $this->assertSame('2024-01-01 09:00:00', $orders['data'][0]['bet_at']);
|
|
|
+ $this->assertSame(1, $orders['filters']['all_time']); $this->assertSame('2024-01-01', $orders['filters']['start_date']);
|
|
|
+ $this->assertSame(0, $this->reader->events(['member_account' => 'member5'])['total']);
|
|
|
+ $this->assertSame(1, $this->reader->events(['member_account' => 'member2', 'kind' => 'deposit'])['total']);
|
|
|
+ }
|
|
|
+
|
|
|
/** @dataProvider invalidQueries */
|
|
|
public function test_invalid_filters_do_not_query_unbounded_dates(array $input): void { $this->expectException(ValidationException::class); $this->reader->summary($input); }
|
|
|
- public static function invalidQueries(): array { return [[['start_date' => '2026-02-30']], [['end_date' => '2026-10-27']], [['start_date' => '2020-01-01']], [['platform' => 'ios']], [['settlement_status' => 'paid']], [['limit' => 999]]]; }
|
|
|
+ public static function invalidQueries(): array { return [[['start_date' => '2026-02-30']], [['end_date' => '2026-10-27']], [['start_date' => '2020-01-01']],
|
|
|
+ [['platform' => 'ios']], [['settlement_status' => 'paid']], [['limit' => 999]], [['start_date' => []]], [['end_date' => []]],
|
|
|
+ [['start_date' => '2026-10-26', 'end_date' => '2026-10-25']], [['end_date' => '2026-02-30']],
|
|
|
+ [['start_date' => '2025-01-01', 'end_date' => '2026-10-26']], [['start_date' => 'not-a-date', 'end_date' => '2026-10-26']]]; }
|
|
|
}
|