repository(); $repository->histogramRows = [ 'tmall_spread_bills' => [['report_date' => '2026-10-23', 'row_count' => 1, 'expense' => 10]], 'alibaba_bill' => [['report_date' => '2026-10-23', 'row_count' => 1, 'expense' => 20]], 'pdd_spread_bills' => [['report_date' => '2026-10-23', 'row_count' => 1, 'expense' => 30]], 'jd_jzt_finance_records' => [['report_date' => '2026-10-23', 'row_count' => 1, 'expense' => 40]], 'dy_qianchuan_finance_transactions' => [['report_date' => '2026-10-23', 'row_count' => 1, 'expense' => 50]], 'dw_ad_finance_records' => [['report_date' => '2026-10-23', 'row_count' => 1, 'expense' => 60]], 'vip_marketingDailyReport' => [['report_date' => '2026-10-23', 'row_count' => 1, 'expense' => 70]], ]; $repository->detailRows = [ 'tmall_spread_bills' => [$this->row('天猫-卓禾家具旗舰店', '2026-10-23 09:00:00', '淘宝备注', null, 10)], 'alibaba_bill' => [$this->row('阿里巴巴-常州曼向家居有限公司', '2026-10-23', '智能营销方案', null, 20)], 'pdd_spread_bills' => [$this->row('拼多多-慕加家具旗舰店', '2026-10-23 09:30:00', '商品推广', '1', 30)], 'jd_jzt_finance_records' => [$this->row('京东-亿家木家具旗舰店', '2026-10-23', '快车扣费', '推广费', 40)], 'dy_qianchuan_finance_transactions' => [$this->row('抖音店', '2026-10-23', '千川推广', null, 50)], 'dw_ad_finance_records' => [$this->row('得物-墨尚思', '2026-10-23', '得物推服务', '现金', 60)], 'vip_marketingDailyReport' => [$this->row('唯品店', '2026-10-23', '推广费', null, 70)], ]; $result = $repository->paginate([ 'start' => '2026-10-01', 'end' => '2026-10-31', 'store_name' => '', ], 2, 2); $this->assertSame(7, $result['total']); $this->assertSame(0.0, $result['summary']['income']); $this->assertSame(280.0, $result['summary']['expense']); $this->assertSame(-280.0, $result['summary']['balance']); $this->assertSame(4, $result['last_page']); $platformAmounts = array_column($result['platform_summary'], 'amount', 'platform'); $this->assertSame(30.0, $platformAmounts['淘系']); $this->assertSame(30.0, $platformAmounts['拼多多']); $this->assertSame(40.0, $platformAmounts['京东']); $this->assertSame(50.0, $platformAmounts['抖音']); $this->assertSame(60.0, $platformAmounts['得物']); $this->assertSame(70.0, $platformAmounts['唯品会']); $this->assertSame( ['拼多多-慕加家具旗舰店', '京东-亿家木家具旗舰店'], array_column($result['rows'], 'store_name'), ); $this->assertSame('商品推广 · 现金', $result['rows'][0]['notes']); $this->assertSame('快车扣费 · 推广费', $result['rows'][1]['notes']); $this->assertSame([30.0, 40.0], array_column($result['rows'], 'expense')); $this->assertTrue($repository->sawSql('tmall_spread_bills', "finType] = 'CREDIT'")); $this->assertTrue($repository->sawSql('tmall_spread_bills', 'c.[shopName] AS store_name')); $this->assertTrue($repository->sawSql('alibaba_bill', 'c.[shopName] AS store_name')); $this->assertTrue($repository->sawSql('pdd_spread_bills', 'c.[shopName] AS store_name')); $this->assertTrue($repository->sawSql('pdd_spread_bills', 'flowType] IN (1, 2)')); $this->assertTrue($repository->sawSql('jd_jzt_finance_records', "in_out_type] = 'OUT'")); $this->assertTrue($repository->sawSql('jd_jzt_finance_records', 'c.[shop_name] AS store_name')); $this->assertTrue($repository->sawSql('dw_ad_finance_records', 'trade_amount_yuan')); $this->assertTrue($repository->sawSql('dw_ad_finance_records', 'c.[shop_name] AS store_name')); $this->assertTrue($repository->sawSql('vip_marketingDailyReport', 'accountName')); } public function test_it_combines_income_and_expense_and_calculates_balance(): void { $repository = $this->repository(); $repository->histogramRows = [ 'tmall_spread_bills' => [['report_date' => '2026-10-23', 'row_count' => 1, 'income' => 25, 'expense' => 0]], 'alibaba_bill' => [['report_date' => '2026-10-23', 'row_count' => 1, 'income' => 0, 'expense' => 10]], 'pdd_spread_bills' => [['report_date' => '2026-10-23', 'row_count' => 1, 'income' => 5, 'expense' => 5]], ]; $repository->detailRows = [ 'tmall_spread_bills' => [$this->row('天猫-甲店', '2026-10-23 09:00:00', '推广返款', null, 0, 25)], 'alibaba_bill' => [$this->row('阿里巴巴-乙店', '2026-10-23', '智能营销方案', null, 10)], 'pdd_spread_bills' => [$this->row('拼多多-丙店', '2026-10-23 10:00:00', '推广结算', '1', 5, 5)], ]; $result = $repository->paginate([ 'start' => '2026-10-01', 'end' => '2026-10-31', 'store_name' => '', ], 1, 10); $this->assertSame(3, $result['total']); $this->assertSame(30.0, $result['summary']['income']); $this->assertSame(15.0, $result['summary']['expense']); $this->assertSame(15.0, $result['summary']['balance']); $this->assertSame( [ ['income' => 25.0, 'expense' => 0.0, 'balance' => 25.0], ['income' => 0.0, 'expense' => 10.0, 'balance' => -10.0], ['income' => 5.0, 'expense' => 5.0, 'balance' => 0.0], ], array_map( static fn (array $row): array => [ 'income' => $row['income'], 'expense' => $row['expense'], 'balance' => $row['balance'], ], $result['rows'], ), ); $this->assertTrue($repository->sawSql('tmall_spread_bills', "finType] = 'DEBIT'")); $this->assertTrue($repository->sawSql('alibaba_bill', 'tradeAMTIn')); $this->assertTrue($repository->sawSql('pdd_spread_bills', 'flowType] = 1')); $this->assertTrue($repository->sawSql('jd_jzt_finance_records', "in_out_type] = 'IN'")); $this->assertTrue($repository->sawSql('dy_qianchuan_finance_transactions', 'SUM(CAST(ISNULL(c.[income]')); $this->assertTrue($repository->sawSql('dw_ad_finance_records', "flow_type] = N'收入'")); } public function test_store_options_are_merged_and_deduplicated(): void { $repository = $this->repository(); $repository->storeRows = [ 'tmall_spread_bills' => [['store_name' => '乙店'], ['store_name' => '甲店']], 'alibaba_bill' => [['store_name' => '甲店']], ]; $this->assertSame(['乙店', '甲店'], $repository->storeOptions([ 'start' => '2026-10-01', 'end' => '2026-10-31', ])); } public function test_platform_and_store_summary_uses_the_installation_platform_categories(): void { $repository = $this->repository(); $repository->storeSummaryRows = [ 'tmall_spread_bills' => [ ['store_name' => '天猫-甲店', 'amount' => 10], ['store_name' => '天猫-乙店', 'amount' => 5], ], 'alibaba_bill' => [['store_name' => '阿里巴巴-甲店', 'amount' => 20]], 'pdd_spread_bills' => [['store_name' => '拼多多-丙店', 'amount' => 30]], 'jd_jzt_finance_records' => [['store_name' => '京东-丁店', 'amount' => 40]], 'dy_qianchuan_finance_transactions' => [['store_name' => '抖音-戊店', 'amount' => 50]], 'dw_ad_finance_records' => [['store_name' => '得物-己店', 'amount' => 60]], 'vip_marketingDailyReport' => [['store_name' => '唯品-庚店', 'amount' => 70]], ]; $result = $repository->storeSummary([ 'start' => '2026-10-01', 'end' => '2026-10-31', ]); $this->assertSame(285.0, $result['total']); $amounts = array_column($result['platform_summary'], 'amount', 'platform'); $this->assertSame(35.0, $amounts['淘系']); $this->assertSame(30.0, $amounts['拼多多']); $this->assertSame(40.0, $amounts['京东']); $this->assertSame(50.0, $amounts['抖音']); $this->assertSame(60.0, $amounts['得物']); $this->assertSame(70.0, $amounts['唯品会']); $this->assertContains( ['platform' => '淘系', 'store_name' => '阿里巴巴-甲店', 'amount' => 20.0], $result['store_summary'], ); $this->assertContains( ['platform' => '唯品会', 'store_name' => '唯品会JITX_亿家达', 'amount' => 70.0], $result['store_summary'], ); $noteRepository = $this->repository(); $noteRepository->storeSummaryRows = $repository->storeSummaryRows; $noteRepository->storeSummary([ 'start' => '2026-10-01', 'end' => '2026-10-31', 'note' => '推广', ]); $this->assertTrue($noteRepository->sawSql( 'dy_qianchuan_finance_transactions', "(c.[remark]) LIKE ? ESCAPE '!'", )); } private function repository(): PromotionMarketingExpenseRepository { return new class(Mockery::mock(UpstreamReadOnlyQueryFactory::class)) extends PromotionMarketingExpenseRepository { public array $histogramRows = []; public array $detailRows = []; public array $storeRows = []; public array $storeSummaryRows = []; public array $queries = []; protected function select(string $connection, string $sql, array $bindings): array { $this->queries[] = compact('connection', 'sql', 'bindings'); foreach (array_merge(array_keys($this->histogramRows), array_keys($this->detailRows), array_keys($this->storeRows), array_keys($this->storeSummaryRows)) as $table) { if (! str_contains($sql, $table)) { continue; } if (str_contains($sql, 'GROUP BY') && str_contains($sql, 'AS store_name')) { return $this->storeSummaryRows[$table] ?? []; } if (str_contains($sql, 'GROUP BY')) { return $this->histogramRows[$table] ?? []; } if (str_contains($sql, 'DISTINCT TOP (500)')) { return $this->storeRows[$table] ?? []; } return $this->detailRows[$table] ?? []; } return []; } public function sawSql(string $table, string $fragment): bool { foreach ($this->queries as $query) { if (str_contains($query['sql'], $table) && str_contains($query['sql'], $fragment)) { return true; } } return false; } }; } /** @return array */ private function row( string $store, string $occurredAt, string $primary, ?string $secondary, float $expense, float $income = 0, ): array { return [ 'report_date' => substr($occurredAt, 0, 10), 'store_name' => $store, 'occurred_at' => $occurredAt, 'note_primary' => $primary, 'note_secondary' => $secondary, 'income' => $income, 'expense' => $expense, ]; } }