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_160000_create_jd_wallet_bill_mirror_tables.php'))->up(); } protected function tearDown(): void { foreach ($this->files as $file) unlink($file); parent::tearDown(); } public function test_complete_month_preserves_identifiers_and_unifies_paging_filters_and_export(): void { $rows = [$this->row('2000000000000000001', '40.12'), $this->row('2000000000000000002', '0.00', '2.03')]; $this->import($rows, 2, '40.12', '2.03'); $repo = app(StoreReceivingJdWalletBillRepository::class); $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(2, $first['total']); $this->assertSame(40.12, $first['summary']['income']); $this->assertSame(2.03, $first['summary']['expense']); $this->assertSame(-5.03, $first['summary']['balance']); $this->assertSame('2000000000000000001', $first['data'][0]['record_code']); $this->assertSame('10100126080000000000000001', $first['data'][0]['payload']['serial_no']); $this->assertSame('2026-07-31 10:00:00', $first['data'][0]['payload']['bill_date']); $this->assertSame([$first['data'][0], $second['data'][0]], iterator_to_array($repo->exportRows($filters, 'record_code', 'asc'))); $this->assertSame(1, $repo->paginate(['keyword' => '2000000000000000002'], 'occurred_at', 'asc', 1, 10)['total']); $this->assertSame(0, $repo->paginate(['start' => '2026-09-01'], 'occurred_at', 'asc', 1, 10)['total']); $this->assertSame(2, $repo->paginate(['store_name' => '测试京东店', 'bill_item' => '货款'], 'income', 'desc', 1, 10)['total']); $this->assertSame(['测试京东店'], $repo->filterOptions()['shops']); $this->assertSame(['2026-08'], $first['sync']['periods']); $this->assertContains('balance', array_column(AccountBillColumns::for('jd-wallet')['columns'], 'field')); } public function test_invalid_import_does_not_replace_active_month(): void { $row = $this->row('2000000000000000001', '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, ['费用结算时间' => '2026-08-32 00:00:00'])], 1, '10.00', '0.00'], [[array_replace($row, ['收入金额' => '1.005'])], 1, '1.00', '0.00'], [[array_replace($row, ['币种' => 'USD'])], 1, '10.00', '0.00'], [[array_replace($row, ['记录编码' => '2E+18'])], 1, '10.00', '0.00'], ]; foreach ($cases as $case) { $caught = false; try { $this->import(...$case); } catch (\InvalidArgumentException|\Illuminate\Database\QueryException) { $caught = true; } $this->assertTrue($caught); $this->assertSame($initial['active_generation'], DB::connection('business_mirror')->table('jd_wallet_bill_periods')->value('active_generation')); $this->assertSame(1, DB::connection('business_mirror')->table('jd_wallet_bill_rows')->count()); } } public function test_reimport_is_atomic_and_keeps_old_version(): void { $first = $this->import([$this->row('2000000000000000001', '10.00')], 1, '10.00', '0.00'); $second = $this->import([$this->row('2000000000000000001', '12.00')], 1, '12.00', '0.00'); $this->assertNotSame($first['active_generation'], $second['active_generation']); $this->assertSame(2, DB::connection('business_mirror')->table('jd_wallet_bill_rows')->count()); $this->assertSame(12.0, app(StoreReceivingJdWalletBillRepository::class)->paginate([], 'occurred_at', 'asc', 1, 10)['summary']['income']); } public function test_month_to_date_import_rejects_shorter_replacement_and_out_of_range_rows(): void { $row = array_replace($this->row('2000000000000000001', '10.00'), ['费用结算时间' => '2026-08-23 12:00:00']); $first = $this->import([$row], 1, '10.00', '0.00', '2026-08-23'); $this->assertSame('2026-08-24', $first['period_end']); try { $earlier = array_replace($row, ['费用结算时间' => '2026-08-22 12:00:00']); $this->import([$earlier], 1, '10.00', '0.00', '2026-08-22'); $this->fail('A shorter file must not replace the active range.'); } catch (\InvalidArgumentException $e) { $this->assertNotSame('', $e->getMessage()); } $this->assertSame($first['active_generation'], DB::connection('business_mirror')->table('jd_wallet_bill_periods')->value('active_generation')); $later = array_replace($row, ['记录编码' => '2000000000000000002', '费用结算时间' => '2026-08-24 12:00:00']); $this->expectException(\InvalidArgumentException::class); $this->import([$row, $later], 2, '20.00', '0.00', '2026-08-23'); } public function test_month_to_date_import_can_extend_a_verified_range(): void { $row = array_replace($this->row('2000000000000000001', '10.00'), ['费用结算时间' => '2026-08-23 12:00:00']); $first = $this->import([$row], 1, '10.00', '0.00', '2026-08-23'); $later = array_replace($row, ['记录编码' => '2000000000000000002', '费用结算时间' => '2026-08-24 12:00:00']); $second = $this->import([$row, $later], 2, '20.00', '0.00', '2026-08-24'); $this->assertSame('2026-08-25', $second['period_end']); $this->assertNotSame($first['active_generation'], $second['active_generation']); $this->assertSame(2, app(StoreReceivingJdWalletBillRepository::class)->paginate(['start' => '2026-08-01', 'end' => '2026-08-24'], 'occurred_at', 'asc', 1, 10)['total']); } public function test_uninitialized_mirror_is_not_reported_as_empty_success(): void { $this->expectExceptionMessage('jd_wallet_bill_mirror_not_ready'); app(StoreReceivingJdWalletBillRepository::class)->paginate([], 'occurred_at', 'asc', 1, 10); } public function test_balances_use_latest_row_per_shop_and_account_and_keep_negative_values(): void { $rows = [ array_replace($this->row('9', '10.00'), ['账户余额' => '100.00']), array_replace($this->row('10', '20.00'), ['账户余额' => '-5.03']), array_replace($this->row('11', '0.00', '2.00'), ['店铺' => '另一店', '账户余额' => '20.00']), array_replace($this->row('12', '0.00'), ['账户名称' => '另一账户', '账户余额' => '7.00']), ]; $this->import($rows, 4, '30.00', '2.00'); $repo = app(StoreReceivingJdWalletBillRepository::class); $this->assertSame(21.97, $repo->paginate([], 'income', 'asc', 1, 1)['summary']['balance']); $this->assertSame(1.97, $repo->paginate(['store_name' => '测试京东店'], 'income', 'desc', 1, 10)['summary']['balance']); $this->assertSame(0.0, $repo->paginate(['start' => '2026-09-01'], 'occurred_at', 'desc', 1, 10)['summary']['balance']); } private function import(array $rows, int $count, string $income, string $expense, ?string $through = null): array { $path = tempnam(sys_get_temp_dir(), 'jd-daily-'); $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(JdWalletBillImporter::class)->import($path, '2026-08', $count, $income, $expense, $through); } private function row(string $code, string $income, string $expense = '0.00'): array { return array_replace(array_fill_keys(array_values(JdWalletBillImporter::PAYLOAD_FIELDS), ''), [ '记录编码' => $code, '店铺' => '测试京东店', '账单项目' => '货款', '交易订单号' => '3606404000000001', '流水号' => '10100126080000000000000001', '费用结算时间' => '2026-08-31 23:59:59', '账单日期' => '2026-07-31 10:00:00', '收入金额' => $income, '支出金额' => $expense, '币种' => 'CNY', '账户余额' => '-5.03', '账户名称' => '测试账户', ]); } }