make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); config(['database.connections.business_mirror' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '']]); DB::purge('business_mirror'); (require database_path('migrations/2026_09_18_140000_create_jd_daily_bill_mirror_tables.php'))->up(); } protected function tearDown(): void { foreach ($this->files as $file) unlink($file); parent::tearDown(); } public function test_split_keeps_京东_and_京喜_totals_equal_to_the_unsplit_bill(): void { $this->importFixture(); $all = $this->repository(null)->paginate([], 'occurred_at', 'desc', 1, 10); $jd = $this->repository(JdBillPlatform::BILL_TYPE_JD)->paginate([], 'occurred_at', 'desc', 1, 10); $jingxi = $this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->paginate([], 'occurred_at', 'desc', 1, 10); // 全量共 6 笔;京喜 4 笔(含京喜小店),京东账单 2 笔。 $this->assertSame(6, $all['total']); $this->assertSame(4, $jingxi['total']); $this->assertSame(2, $jd['total']); foreach (['count', 'income', 'expense', 'balance'] as $field) { $this->assertSame( round($all['summary'][$field], 2), round($jd['summary'][$field] + $jingxi['summary'][$field], 2), "拆分后 {$field} 不守恒", ); } } public function test_same_day_rows_are_only_visible_in_the_jingxi_tab(): void { $this->importFixture(); $filters = ['start' => '2026-09-23', 'end' => '2026-09-23', 'store_name' => '京喜-安隅京喜自营工厂店']; $jingxi = $this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->paginate($filters, 'occurred_at', 'desc', 1, 10); $jd = $this->repository(JdBillPlatform::BILL_TYPE_JD)->paginate($filters, 'occurred_at', 'desc', 1, 10); $this->assertSame(2, $jingxi['total']); $this->assertSame(['京喜-安隅京喜自营工厂店'], array_unique(array_column($jingxi['data'], 'store_name'))); $this->assertSame(0, $jd['total']); $this->assertSame([], $jd['data']); } public function test_cutoff_date_keeps_earlier_jingxi_rows_in_the_jd_bill(): void { $this->importFixture(); config(['jd_bill_platform.scope_start_date' => '2026-09-23']); $all = $this->repository(null)->paginate([], 'occurred_at', 'desc', 1, 10); $jd = $this->repository(JdBillPlatform::BILL_TYPE_JD)->paginate([], 'occurred_at', 'desc', 1, 10); $jingxi = $this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->paginate([], 'occurred_at', 'desc', 1, 10); // 只有 09-23 起的京喜行被切走,09-10 的京喜行仍留在京东账单,总数守恒。 $this->assertSame(3, $jingxi['total']); $this->assertSame(3, $jd['total']); $this->assertSame($all['total'], $jd['total'] + $jingxi['total']); $this->assertSame( round($all['summary']['income'], 2), round($jd['summary']['income'] + $jingxi['summary']['income'], 2), ); } public function test_filter_options_follow_the_tab_scope(): void { $this->importFixture(); $jingxi = $this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->filterOptions(); $jd = $this->repository(JdBillPlatform::BILL_TYPE_JD)->filterOptions(); $this->assertSame(['京喜-安隅京喜自营工厂店', '京喜小店-简尚家居家具制造店'], $jingxi['shops']); $this->assertSame(['京东-慕加旗舰店'], $jd['shops']); } public function test_export_follows_the_tab_scope(): void { $this->importFixture(); $jingxi = array_column(iterator_to_array($this->repository(JdBillPlatform::BILL_TYPE_JINGXI)->exportRows([], 'occurred_at', 'asc')), 'store_name'); $jd = array_column(iterator_to_array($this->repository(JdBillPlatform::BILL_TYPE_JD)->exportRows([], 'occurred_at', 'asc')), 'store_name'); $this->assertCount(4, $jingxi); $this->assertCount(2, $jd); $this->assertSame([], array_intersect($jingxi, $jd)); } public function test_jingxi_tab_reuses_the_jd_bill_columns_and_sits_next_to_it(): void { $jd = AccountBillColumns::for(JdBillPlatform::BILL_TYPE_JD); $jingxi = AccountBillColumns::for(JdBillPlatform::BILL_TYPE_JINGXI); $this->assertSame('京喜账单', $jingxi['title']); $this->assertSame($jd['columns'], $jingxi['columns']); $this->assertSame([], $jingxi['groups']); $tabs = collect(collect(config('business_navigation'))->firstWhere('slug', 'billing-transactions')['pages']) ->firstWhere('slug', 'store-receiving-bills')['tabs']; $slugs = array_column($tabs, 'slug'); $this->assertSame('jingxi', $slugs[array_search('jd-transactions', $slugs, true) + 1]); } private function repository(?string $platform): JdDailyBillRepository { return app(JdDailyBillRepository::class)->forPlatform( $platform === null ? null : JdBillPlatform::platformForBillType($platform), ); } /** 6 笔:京喜 4 笔(含「京喜小店」店铺)、京东 2 笔,横跨 2026-09-10 与 2026-09-23 两个记账日期。 */ private function importFixture(): void { $rows = [ $this->row('1', '京喜-安隅京喜自营工厂店', '2026-09-10 09:00:00', '10.00', '0.00'), $this->row('2', '京喜-安隅京喜自营工厂店', '2026-09-23 09:00:00', '20.00', '0.00'), $this->row('3', '京喜-安隅京喜自营工厂店', '2026-09-23 10:00:00', '0.00', '5.00'), $this->row('4', '京喜小店-简尚家居家具制造店', '2026-09-23 11:00:00', '7.00', '0.00'), $this->row('5', '京东-慕加旗舰店', '2026-09-23 12:00:00', '3.00', '0.00'), $this->row('6', '京东-慕加旗舰店', '2026-09-10 12:00:00', '0.00', '2.00'), ]; $this->import($rows, 6, '40.00', '7.00'); } private function import(array $rows, int $count, string $income, string $expense): array { $path = tempnam(sys_get_temp_dir(), 'jd-jingxi-'); $this->files[] = $path; $file = fopen($path, 'wb'); fputcsv($file, array_keys($rows[0])); foreach ($rows as $row) fputcsv($file, array_values($row)); fclose($file); return app(JdDailyBillImporter::class)->import($path, '2026-09', $count, $income, $expense); } private function row(string $code, string $store, string $occurredAt, string $income, string $expense): array { return array_replace(array_fill_keys(array_values(JdDailyBillImporter::PAYLOAD_FIELDS), ''), [ '记录编码' => $code, '店铺' => $store, '账单项目' => '交易服务费', '订单编号' => '360640400000000' . $code, '单据编号' => '1010012608312386678178000' . $code, '费用结算时间' => $occurredAt, '费用发生时间' => $occurredAt, '收入金额' => $income, '支出金额' => $expense, '币种' => 'CNY', ]); } }