[ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], 'upstream_mirrors.connection' => 'business_mirror', 'upstream_mirrors.datasets' => [ JdPromotionAccountBalanceRepository::DATASET_KEY => [ 'source_key' => 'js_data_middle_platform', 'schema' => 'dbo', 'object' => 'jd_jzt_finance_records', 'target' => 'fixture_jd_jzt_finance_records', 'keys' => ['id'], 'watermark' => 'updated_at', ], ], ]); DB::purge('business_mirror'); Schema::connection('business_mirror')->create('business_mirror_table_states', function (Blueprint $table): void { $table->string('dataset_key')->primary(); $table->dateTime('initial_completed_at')->nullable(); }); Schema::connection('business_mirror')->create('fixture_jd_jzt_finance_records', function (Blueprint $table): void { $table->integer('id'); $table->string('data_month')->nullable(); $table->string('shop_name')->nullable(); $table->string('in_out_type')->nullable(); $table->string('in_out_type_name')->nullable(); $table->decimal('amount', 18, 2)->nullable(); $table->decimal('balance_amount', 18, 2)->nullable(); $table->dateTime('settle_datetime')->nullable(); $table->date('show_date')->nullable(); $table->dateTime('updated_at')->nullable(); }); } protected function tearDown(): void { CarbonImmutable::setTestNow(); DB::purge('business_mirror'); parent::tearDown(); } public function test_it_calculates_completed_month_balances_from_jd_jzt_rows(): void { $this->markReady(); DB::connection('business_mirror')->table('fixture_jd_jzt_finance_records')->insert([ $this->row(1, '京东A店', '2026-05-31', '2026-05-31 23:59:00', 'IN', '入账', 1000, 1000), $this->row(2, '京东A店', '2026-06-01', '2026-06-01 09:00:00', 'IN', '入账', 500, 1500), $this->row(3, '京东A店', '2026-06-30', '2026-06-30 10:00:00', 'OUT', '出账', 100, 1400), $this->row(4, '京东A店', '2026-06-30', '2026-06-30 20:00:00', 'OUT', '出账', 100, 1300), $this->row(5, '京东B店', '2026-06-30', '2026-06-30 18:00:00', 'OUT', '出账', 25, 75), $this->row(6, '京东A店', '2026-09-01', '2026-09-01 09:00:00', 'IN', '入账', 10, 1310), ]); $repository = app(JdPromotionAccountBalanceRepository::class); $result = $repository->paginateMonthlyBalances(null, '2026-06', 1, 25); $this->assertTrue($repository->usesBusinessMirror()); $this->assertSame(['京东A店', '京东B店'], $repository->storeOptions()); $this->assertSame(2, $result['total']); $this->assertSame([ 'date' => '2026-06-30', 'store_name' => '京东A店', 'opening_balance' => 1000.0, 'income_total' => 500.0, 'expense_total' => 200.0, 'closing_balance' => 1300.0, 'system_closing_balance' => 1300.0, 'balance_difference' => 0.0, 'difference_reason' => '', 'verification_status' => '已核对', 'notes' => '', ], $result['rows'][0]); $this->assertNull($result['rows'][1]['opening_balance']); $this->assertNull($result['rows'][1]['closing_balance']); $this->assertSame('缺少上月期末余额', $result['rows'][1]['difference_reason']); $this->assertSame('待核对', $result['rows'][1]['verification_status']); } public function test_store_and_month_filters_share_the_same_monthly_result(): void { $this->markReady(); DB::connection('business_mirror')->table('fixture_jd_jzt_finance_records')->insert([ $this->row(1, '京东A店', '2026-05-31', '2026-05-31 23:59:00', 'IN', '入账', 100, 100), $this->row(2, '京东A店', '2026-06-30', '2026-06-30 23:59:00', 'IN', '入账', 20, 120), $this->row(3, '京东B店', '2026-06-30', '2026-06-30 23:59:00', 'IN', '入账', 30, 30), ]); $result = app(JdPromotionAccountBalanceRepository::class) ->paginateMonthlyBalances('京东A店', '2026-06', 1, 25); $this->assertSame(1, $result['total']); $this->assertSame('京东A店', $result['rows'][0]['store_name']); $this->assertSame(100.0, $result['rows'][0]['opening_balance']); $this->assertSame(20.0, $result['rows'][0]['income_total']); } public function test_it_does_not_treat_non_month_end_balances_as_month_end_balances(): void { $this->markReady(); DB::connection('business_mirror')->table('fixture_jd_jzt_finance_records')->insert([ $this->row(1, '京东A店', '2026-05-30', '2026-05-30 23:59:00', 'IN', '入账', 100, 100), $this->row(2, '京东A店', '2026-06-01', '2026-06-01 09:00:00', 'IN', '入账', 20, 120), $this->row(3, '京东A店', '2026-06-29', '2026-06-29 23:59:00', 'OUT', '出账', 5, 115), ]); $row = app(JdPromotionAccountBalanceRepository::class) ->paginateMonthlyBalances('京东A店', '2026-06', 1, 25)['rows'][0]; $this->assertNull($row['opening_balance']); $this->assertNull($row['system_closing_balance']); $this->assertNull($row['closing_balance']); $this->assertNull($row['balance_difference']); $this->assertSame('缺少期初余额和系统期末余额', $row['difference_reason']); $this->assertSame('待核对', $row['verification_status']); } public function test_it_identifies_a_missing_current_month_end_balance(): void { $this->markReady(); DB::connection('business_mirror')->table('fixture_jd_jzt_finance_records')->insert([ $this->row(1, '京东A店', '2026-05-31', '2026-05-31 23:59:00', 'IN', '入账', 100, 100), $this->row(2, '京东A店', '2026-06-01', '2026-06-01 09:00:00', 'IN', '入账', 20, 120), $this->row(3, '京东A店', '2026-06-29', '2026-06-29 23:59:00', 'OUT', '出账', 5, 115), ]); $row = app(JdPromotionAccountBalanceRepository::class) ->paginateMonthlyBalances('京东A店', '2026-06', 1, 25)['rows'][0]; $this->assertSame(100.0, $row['opening_balance']); $this->assertSame(115.0, $row['closing_balance']); $this->assertNull($row['system_closing_balance']); $this->assertNull($row['balance_difference']); $this->assertSame('缺少本月系统期末余额', $row['difference_reason']); $this->assertSame('待核对', $row['verification_status']); } public function test_it_explains_a_non_zero_balance_difference(): void { $this->markReady(); DB::connection('business_mirror')->table('fixture_jd_jzt_finance_records')->insert([ $this->row(1, '京东A店', '2026-05-31', '2026-05-31 23:59:00', 'IN', '入账', 100, 100), $this->row(2, '京东A店', '2026-06-30', '2026-06-30 23:59:00', 'IN', '入账', 20, 125), ]); $row = app(JdPromotionAccountBalanceRepository::class) ->paginateMonthlyBalances('京东A店', '2026-06', 1, 25)['rows'][0]; $this->assertSame(120.0, $row['closing_balance']); $this->assertSame(125.0, $row['system_closing_balance']); $this->assertSame(5.0, $row['balance_difference']); $this->assertSame('系统期末余额与计算期末余额相差 5.00 元', $row['difference_reason']); $this->assertSame('待核对', $row['verification_status']); } public function test_it_rejects_an_incomplete_shared_mirror(): void { $repository = app(JdPromotionAccountBalanceRepository::class); $this->assertFalse($repository->usesBusinessMirror()); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('京东账户余额共享业务镜像尚未完成同步。'); $repository->paginateMonthlyBalances(null, '2026-06', 1, 25); } private function markReady(): void { DB::connection('business_mirror')->table('business_mirror_table_states')->insert([ 'dataset_key' => JdPromotionAccountBalanceRepository::DATASET_KEY, 'initial_completed_at' => '2026-09-12 10:00:00', ]); } /** @return array */ private function row( int $id, string $shop, string $date, string $settledAt, string $type, string $typeName, float $amount, float $balance, ): array { return [ 'id' => $id, 'data_month' => substr($date, 0, 7), 'shop_name' => $shop, 'in_out_type' => $type, 'in_out_type_name' => $typeName, 'amount' => $amount, 'balance_amount' => $balance, 'settle_datetime' => $settledAt, 'show_date' => $date, 'updated_at' => $settledAt, ]; } }