*/ private array $temporaryFiles = []; protected function setUp(): void { parent::setUp(); $this->user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail(), ); $this->withoutMiddleware(ThrottleRequests::class); } public function test_three_unchanged_balances_above_reserve_show_warning_and_create_one_todo(): void { Carbon::setTestNow('2026-10-24 10:00:00'); try { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '京东-连续余额测试店', 'shop_name' => '京东-连续余额测试店', 'store_entity_name' => '连续余额测试主体', 'store_id' => 'JD-ALERT-001', ]); foreach (['2026-10-21', '2026-10-22', '2026-10-23'] as $date) { $this->insertBalanceSnapshot($date, '京东-连续余额测试店', 8000); } $sync = app(WithdrawalBalanceAlertSynchronizer::class); $firstSync = $sync->syncForDate((int) $this->company->id, '2026-10-23'); $secondSync = $sync->syncForDate((int) $this->company->id, '2026-10-23'); $this->assertSame(1, $firstSync['created']); $this->assertSame(0, $firstSync['updated']); $this->assertSame(0, $secondSync['created']); $this->assertSame(0, $secondSync['updated']); $url = route('business.pages.show', [ 'company_id' => $this->company->id, 'section' => 'fund-management', 'page' => 'withdrawal-management', 'balance_date' => '2026-10-23', 'platform' => '京东', ]); $this->loginAs()->get($url) ->assertOk() ->assertSeeText('10月21日店铺余额') ->assertSeeText('10月22日店铺余额') ->assertSeeText('10月23日店铺余额') ->assertSeeText('已显示余额 1 条') ->assertSeeText('连续3天未变动') ->assertSeeText('8,000.00'); $title = '提现余额连续3天未变动|京东|京东-连续余额测试店'; $todo = Todo::withoutGlobalScopes()->where('company_id', $this->company->id)->where('title', $title)->first(); $this->assertNotNull($todo); $this->assertSame('提现余额预警', $todo->category); $this->assertSame('高', $todo->priority); $this->assertSame('待处理', $todo->status); $this->assertStringContainsString('2026-10-21 至 2026-10-23', (string) $todo->description); $this->assertStringContainsString((string) $store->store_entity_name, (string) $todo->description); $this->get($url)->assertOk(); $this->assertSame( 1, Todo::withoutGlobalScopes() ->where('company_id', $this->company->id) ->where('category', '提现余额预警') ->count(), ); $this->get(route('business.pages.show', [ 'company_id' => $this->company->id, 'section' => 'other', 'page' => 'todos', ])) ->assertOk() ->assertSeeText($title) ->assertSeeText('提现余额预警') ->assertSeeText('连续余额测试主体'); } finally { Carbon::setTestNow(); } } public function test_alert_requires_three_matched_days_an_exact_unchanged_value_and_a_strict_excess(): void { Carbon::setTestNow('2026-10-24 10:00:00'); try { foreach (['等于预留标准店', '余额发生变动店', '缺少一天店'] as $name) { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '京东-' . $name, 'shop_name' => '京东-' . $name, 'store_entity_name' => '预警边界测试主体', ]); } foreach (['2026-10-21' => 5000, '2026-10-22' => 5000, '2026-10-23' => 5000] as $date => $balance) { $this->insertBalanceSnapshot($date, '京东-等于预留标准店', $balance); } foreach (['2026-10-21' => 8000, '2026-10-22' => 8000, '2026-10-23' => 7999.99] as $date => $balance) { $this->insertBalanceSnapshot($date, '京东-余额发生变动店', $balance); } foreach (['2026-10-22' => 8000, '2026-10-23' => 8000] as $date => $balance) { $this->insertBalanceSnapshot($date, '京东-缺少一天店', $balance); } $sync = app(WithdrawalBalanceAlertSynchronizer::class)->syncForDate( (int) $this->company->id, '2026-10-23', ); $this->assertSame(0, $sync['created']); $this->loginAs()->get(route('business.pages.show', [ 'company_id' => $this->company->id, 'section' => 'fund-management', 'page' => 'withdrawal-management', 'balance_date' => '2026-10-23', 'platform' => '京东', ]))->assertOk()->assertSeeText('未触发')->assertSeeText('数据不足'); $this->assertSame(0, DB::table('business_todos')->count()); } finally { Carbon::setTestNow(); } } public function test_successful_balance_import_runs_alert_generation_automatically(): void { Carbon::setTestNow('2026-10-24 10:00:00'); try { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '京东-导入预警测试店', 'shop_name' => '京东-导入预警测试店', 'store_entity_name' => '导入预警测试主体', ]); $this->insertBalanceSnapshot('2026-10-21', '京东-导入预警测试店', 7000); $this->insertBalanceSnapshot('2026-10-22', '京东-导入预警测试店', 7000); $path = sys_get_temp_dir() . '/jst-balance-alert-' . uniqid('', true) . '.json'; $this->temporaryFiles[] = $path; file_put_contents($path, json_encode([ 'fldHeads' => [ ['title' => '店铺', 'fld' => '店铺'], ['title' => '平台实际期末余额', 'fld' => '平台实际期末余额'], ], 'dataList' => [ ['店铺' => '京东-导入预警测试店', '平台实际期末余额' => 7000], ], ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); $this->artisan('business:import-jushuitan-shop-balances', [ 'path' => $path, '--balance-date' => '2026-10-23', '--scope' => 'jd-wallet', ])->assertExitCode(0); $this->assertDatabaseHas('business_todos', [ 'company_id' => $this->company->id, 'title' => '提现余额连续3天未变动|京东|京东-导入预警测试店', 'category' => '提现余额预警', 'status' => '待处理', ]); } finally { Carbon::setTestNow(); } } protected function tearDown(): void { foreach ($this->temporaryFiles as $path) { if (is_file($path)) { unlink($path); } } parent::tearDown(); } private function insertBalanceSnapshot(string $date, string $storeName, float $balance): void { DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([ 'balance_date' => $date, 'source_scope' => 'jd-wallet', 'source_platform' => '京东', 'store_name' => $storeName, 'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName($storeName), 'platform_account_id' => 'fixture-' . $storeName, 'balance' => $balance, 'source_row_key' => hash('sha256', $date . '|' . $storeName), 'source_file_name' => 'fixture.xlsx', 'source_sha256' => hash('sha256', 'fixture-' . $date . '-' . $storeName), 'imported_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); } }