['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '']]); DB::purge('business_mirror'); (require database_path('migrations/2026_09_14_100000_create_douyin_authoritative_bill_tables.php'))->up(); (require database_path('migrations/2026_09_07_000000_create_original_bill_info_mirror_table.php'))->up(); $schema = Schema::connection('business_mirror'); $schema->create('ops_jst_shop_info', function (Blueprint $t): void { $t->integer('Id'); $t->integer('shop_id'); $t->string('shop_name'); }); $schema->create('business_mirror_table_states', function (Blueprint $t): void { $t->string('dataset_key'); $t->dateTime('initial_completed_at')->nullable(); $t->string('status'); }); foreach ([StoreReceivingAlipayBillMirrorRepository::DATASET, StoreReceivingAlipayBillMirrorRepository::SHOP_DATASET] as $key) { $this->db()->table('business_mirror_table_states')->insert(['dataset_key' => $key, 'initial_completed_at' => now(), 'status' => 'succeeded']); } $this->db()->table('ops_jst_shop_info')->insert(['Id' => 1, 'shop_id' => 7, 'shop_name' => '测试店']); } protected function tearDown(): void { foreach ($this->files as $file) @unlink($file); parent::tearDown(); } public function test_month_replaces_split_bills_and_unifies_list_summary_filters_and_export(): void { $this->legacy(1, '2026-08-15 12:00:00', 30); $this->legacy(2, '2026-08-15 12:00:00', 30); $this->legacy(3, '2026-09-01 00:00:00', 9); $rows = [$this->row('2000000000000000101', '40.12'), $this->row('2000000000000000102', '0.00', '2.03')]; $this->import($rows, 2, '40.12', '2.03'); $repo = app(StoreReceivingDouyinBillMirrorRepository::class); $all = $repo->paginate([], 'occurred_at', 'asc', 1, 10); $this->assertSame(3, $all['total']); $this->assertSame(49.12, $all['summary']['income']); $this->assertSame(2.03, $all['summary']['expense']); $filters = ['start' => '2026-08-01', 'end' => '2026-08-31']; $first = $repo->paginate($filters, 'record_code', 'asc', 1, 1); $second = $repo->paginate($filters, 'record_code', 'asc', 2, 1); $this->assertSame('2000000000000000101', $first['data'][0]['record_code']); $this->assertSame('2000000000000000102', $second['data'][0]['record_code']); $this->assertSame('测试主体', $first['data'][0]['payload']['merchant_name']); $this->assertSame('20260815000100000000001', $first['data'][0]['payload']['move_serial_no']); $this->assertSame('6912345678901234567', $first['data'][0]['payload']['sub_order_no']); $this->assertSame('2026-07-31 10:00:00', $first['data'][0]['payload']['order_placed_at']); $export = iterator_to_array($repo->exportRows($filters, 'record_code', 'asc')); $this->assertSame([$first['data'][0], $second['data'][0]], $export); $this->assertSame(2, $repo->paginate($filters + ['store_name' => '测试店', 'bill_item' => '订单实付'], 'income', 'desc', 1, 10)['total']); $this->assertSame(1, $repo->paginate(['keyword' => '2000000000000000102'], 'occurred_at', 'asc', 1, 10)['total']); $this->assertContains('测试店', $repo->filterOptions()['shops']); $this->assertContains('订单实付', $repo->filterOptions()['bill_items']); // A later source sync must not bring the incorrect month back. $this->legacy(4, '2026-08-31 23:59:59', 100); $this->assertSame(2, $repo->paginate($filters, 'occurred_at', 'asc', 1, 10)['total']); } public function test_exact_record_filter_preserves_channel_period_and_other_filters(): void { $this->legacy(3, '2026-09-01 00:00:00', 9); $this->legacy(30, '2026-09-01 00:00:00', 10); $repo = app(StoreReceivingDouyinBillMirrorRepository::class); $exact = ['keyword' => '3', 'keyword_match' => 'exact-record']; $this->assertSame(1, $repo->paginate($exact, 'record_code', 'asc', 1, 20)['total']); $this->assertSame(2, $repo->paginate(['keyword' => '3'], 'record_code', 'asc', 1, 20)['total']); foreach (['3invalid', '03', '2147483648', '2000000000000000101'] as $code) { $this->assertSame(0, $repo->paginate(array_replace($exact, ['keyword' => $code]), 'record_code', 'asc', 1, 20)['total']); } $this->import([$this->row('2000000000000000101', '10.00')], 1, '10.00', '0.00'); $exact['keyword'] = '2000000000000000101'; $this->assertSame(1, $repo->paginate($exact, 'record_code', 'asc', 1, 20)['total']); $this->assertCount(1, iterator_to_array($repo->exportRows($exact, 'record_code', 'asc'))); foreach ([['store_name' => '其他店'], ['bill_item' => '其他项目'], ['start' => '2026-09-01'], ['end' => '2026-07-31']] as $extra) { $this->assertSame(0, $repo->paginate($exact + $extra, 'record_code', 'asc', 1, 20)['total']); $this->assertCount(0, iterator_to_array($repo->exportRows($exact + $extra, 'record_code', 'asc'))); } $this->db()->table('ops_jst_original_bill_info')->where('Id', 3)->update(['channel' => 'OtherChannel']); $this->assertSame(0, $repo->paginate(['keyword' => '3', 'keyword_match' => 'exact-record'], 'record_code', 'asc', 1, 20)['total']); } public function test_invalid_counts_totals_duplicate_ids_and_dates_do_not_publish_partial_data(): void { $row = $this->row('2000000000000000101', '10.00'); $initial = $this->import([$row], 1, '10.00', '0.00'); $cases = [ [[$row], 2, '10.00', '0.00'], [[$row], 1, '11.00', '0.00'], [[$row, $row], 2, '20.00', '0.00'], [[array_replace($row, ['结算时间' => '2026-09-01 00:00:00'])], 1, '10.00', '0.00'], [[array_replace($row, ['收入' => '1.005'])], 1, '1.00', '0.00'], ]; foreach ($cases as $case) { try { $this->import(...$case); $this->fail('Invalid import should fail.'); } catch (\InvalidArgumentException) { $this->assertSame($initial['active_generation'], $this->db()->table('douyin_authoritative_periods')->value('active_generation')); $this->assertSame(1, $this->db()->table('douyin_authoritative_bills')->count()); } } } public function test_reimport_activates_new_generation_and_keeps_old_rows_for_rollback(): void { $first = $this->import([$this->row('2000000000000000101', '10.00')], 1, '10.00', '0.00'); $second = $this->import([$this->row('2000000000000000101', '12.00')], 1, '12.00', '0.00'); $this->assertNotSame($first['active_generation'], $second['active_generation']); $this->assertSame(2, $this->db()->table('douyin_authoritative_bills')->count()); $this->assertSame(12.0, app(StoreReceivingDouyinBillMirrorRepository::class)->paginate([], 'occurred_at', 'asc', 1, 10)['summary']['income']); } public function test_cutoff_preserves_later_bills_and_can_advance_without_duplicates(): void { $this->legacy(1, '2026-08-21 23:59:59', 90); $this->legacy(2, '2026-08-22 00:00:00', 9); $firstRow = $this->row('2000000000000000101', '10.00'); $first = $this->import([$firstRow], 1, '10.00', '0.00', '2026-08-21'); $this->assertSame('2026-08-22', $first['period_end']); $repo = app(StoreReceivingDouyinBillMirrorRepository::class); $this->assertSame(19.0, $repo->paginate([], 'occurred_at', 'asc', 1, 10)['summary']['income']); $this->assertCount(2, iterator_to_array($repo->exportRows([], 'occurred_at', 'asc'))); $nextRow = array_replace($this->row('2000000000000000102', '12.00'), ['结算时间' => '2026-08-22 00:00:00']); $second = $this->import([$firstRow, $nextRow], 2, '22.00', '0.00', '2026-08-22'); $this->assertSame('2026-08-23', $second['period_end']); $result = $repo->paginate([], 'occurred_at', 'asc', 1, 10); $this->assertSame(2, $result['total']); $this->assertSame(22.0, $result['summary']['income']); $this->assertSame(3, $this->db()->table('douyin_authoritative_bills')->count()); } public function test_invalid_or_regressing_cutoffs_preserve_active_generation(): void { $row = $this->row('2000000000000000101', '10.00'); $initial = $this->import([$row], 1, '10.00', '0.00', '2026-08-21'); foreach (['2026-08-20', '2026-08-14', '2026-07-31', '2026-09-01', '2026-08-32', 'invalid', ''] as $through) { try { $this->import([$row], 1, '10.00', '0.00', $through); $this->fail('Invalid cutoff should fail.'); } catch (\InvalidArgumentException) { $this->assertSame($initial['active_generation'], $this->db()->table('douyin_authoritative_periods')->value('active_generation')); $this->assertSame(1, $this->db()->table('douyin_authoritative_bills')->count()); } } } private function import(array $rows, int $count, string $income, string $expense, ?string $through = null): array { $path = tempnam(sys_get_temp_dir(), 'dy-bills-'); $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(DouyinAuthoritativeBillImporter::class)->import($path, '2026-08', $count, $income, $expense, $through); } private function row(string $code, string $income, string $expense = '0.00'): array { return ['记录编码' => $code, '店铺' => '测试店', '商户主体名称' => '测试主体', '账单项目' => '订单实付', '动账流水号' => '20260815000100000000001', '订单号' => '6912345678901234567', '子订单号' => '6912345678901234567', '结算时间' => '2026-08-15 12:00:00', '下单时间' => '2026-07-31 10:00:00', '收入' => $income, '支出' => $expense]; } private function legacy(int $id, string $time, float $amount): void { $this->db()->table('ops_jst_original_bill_info')->insert(['Id' => $id, 'shop_id' => 7, '_mirror_key' => hash('sha256', (string) $id), '_mirror_synced_at' => now(), 'channel' => 'TouTiaoFXGZJ', 'fee_time' => $time, 'amount' => $amount, 'inout_type' => '1', 'fee_name' => '订单实付']); } private function db(): \Illuminate\Database\Connection { return DB::connection('business_mirror'); } }