user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail(), ); } public function test_shop_ledger_uses_store_report_current_subject_and_platform(): void { DB::connection('business_mirror')->table('fin_accounts')->insert([ [ 'company_name' => '测试公司', 'account_short' => '公户账户', 'account_no' => 'ABC-1234', 'account_type' => '公户', 'bank_name' => null, 'created_at' => now(), 'updated_at' => now(), ], [ 'company_name' => '测试公司', 'account_short' => '支付宝账户', 'account_no' => 'PAY-8888', 'account_type' => '支付宝', 'bank_name' => null, 'created_at' => now(), 'updated_at' => now(), ], [ 'company_name' => '测试公司', 'account_short' => '网商银行账户', 'account_no' => 'NET-7777', 'bank_name' => '网商银行', 'account_type' => '公户', 'created_at' => now(), 'updated_at' => now(), ], [ 'company_name' => '测试公司', 'account_short' => '专属账户', 'account_no' => 'MASTER-OTHER', 'account_type' => '个人卡', 'bank_name' => null, 'created_at' => now(), 'updated_at' => now(), ], ]); DB::connection('business_mirror')->table('fin_shops')->insert([ [ 'shop_name' => '报表测试店', 'platform' => '天猫', 'account_short' => null, 'shop_balance' => 123.45, 'shop_balance_date' => '2026-10-25', 'created_at' => now(), 'updated_at' => now(), ], [ 'shop_name' => '同账户甲', 'platform' => '天猫', 'account_short' => '专属账户', 'shop_balance' => 0, 'shop_balance_date' => null, 'created_at' => now(), 'updated_at' => now(), ], ]); $openStore = [ 'id' => 901, 'platform' => '天猫', 'erp_store_name' => '天猫-报表测试店', 'company_name' => '历史主体', 'store_status' => '0', 'store_manager' => '原店铺负责人', 'department_manager' => '原部门负责人', 'withdrawal_account' => 'ABC-1234', ]; $closedStore = [ 'id' => 902, 'platform' => '京东', 'erp_store_name' => '京东-已关店', 'company_name' => '历史主体', 'store_status' => '6', 'withdrawal_account' => null, ]; $openStoreWithoutSubject = [ 'id' => 903, 'platform' => '淘宝', 'erp_store_name' => '淘宝-无主体店', 'company_name' => null, 'store_status' => '0', 'store_manager' => '无主体店铺负责人', 'department_manager' => '无主体部门负责人', 'withdrawal_account' => 'PAY-8888', ]; $openStoreWithExcludedBank = [ 'id' => 904, 'platform' => '拼多多', 'erp_store_name' => '拼多多-网商店', 'company_name' => null, 'store_status' => '0', 'store_manager' => '网商店铺负责人', 'department_manager' => '网商部门负责人', 'withdrawal_account' => 'NET-7777', ]; $openStoreWithKnownSharedPayout = [ 'id' => 905, 'platform' => '天猫', 'erp_store_name' => '天猫-同账户甲', 'company_name' => null, 'store_status' => '0', 'store_manager' => '同账户甲负责人', 'department_manager' => '同账户甲部门负责人', 'withdrawal_account' => 'LEGACY-5555', ]; $openStoreWithUnknownSharedPayout = [ 'id' => 906, 'platform' => '淘宝', 'erp_store_name' => '淘宝-同账户乙', 'company_name' => null, 'store_status' => '0', 'store_manager' => '同账户乙负责人', 'department_manager' => '同账户乙部门负责人', 'withdrawal_account' => 'LEGACY5555', ]; $this->mock(BusinessInformationMirrorRepository::class) ->shouldReceive('allStores') ->once() ->andReturn([ $openStore, $closedStore, $openStoreWithoutSubject, $openStoreWithExcludedBank, $openStoreWithKnownSharedPayout, $openStoreWithUnknownSharedPayout, ]); $this->mock(StoreReportPeriodBuilder::class) ->shouldReceive('build') ->once() ->with([ $openStore, $openStoreWithoutSubject, $openStoreWithExcludedBank, $openStoreWithKnownSharedPayout, $openStoreWithUnknownSharedPayout, ]) ->andReturn([ '天猫-报表测试店' => [ [ 'company_name' => '历史主体', 'effective_at' => '2026-07-01', 'expires_at' => '2026-10-01', 'source' => 'initial', ], [ 'company_name' => '当前存续主体', 'effective_at' => '2026-10-02', 'expires_at' => null, 'source' => 'crawl', ], ], ]); $this->mock(StoreAccountMirrorRepository::class) ->shouldReceive('ownershipByStoreName') ->once() ->andReturn([ '天猫-报表测试店' => [ 'operation_owner' => '运营负责人', 'operation_department_head' => '运营部门负责人', 'operation_director' => '运营总监', 'profit_center' => '运营中心', ], ]); $html = $this->loginAs()->get($this->pageUrl())->assertOk()->getContent(); $panelStart = strpos($html, 'id="ledger-panel-shops"'); $panelEnd = strpos($html, 'id="ledger-panel-flows"', $panelStart); $this->assertNotFalse($panelStart); $this->assertNotFalse($panelEnd); $shopHtml = substr($html, $panelStart, $panelEnd - $panelStart); $this->assertStringContainsString('天猫', $shopHtml); $this->assertStringContainsString('天猫-报表测试店', $shopHtml); $this->assertStringContainsString('当前存续主体', $shopHtml); $this->assertStringContainsString('运营负责人', $shopHtml); $this->assertStringContainsString('运营部门负责人', $shopHtml); $this->assertStringContainsString('无主体店铺负责人', $shopHtml); $this->assertStringContainsString('无主体部门负责人', $shopHtml); $this->assertStringContainsString('淘宝-无主体店', $shopHtml); $this->assertStringContainsString('拼多多-网商店', $shopHtml); $this->assertStringContainsString('天猫-同账户甲', $shopHtml); $this->assertStringContainsString('淘宝-同账户乙', $shopHtml); $this->assertStringContainsString('在营', $shopHtml); $this->assertStringContainsString('公户账户', $shopHtml); $this->assertStringContainsString('123.45', $shopHtml); $this->assertStringContainsString('ABC-1234', $shopHtml); $this->assertGreaterThanOrEqual(2, substr_count($shopHtml, '>专属账户')); $this->assertGreaterThanOrEqual(2, substr_count($shopHtml, '>MASTER-OTHER')); $this->assertStringNotContainsString('京东-已关店', $shopHtml); $this->assertStringNotContainsString('历史主体', $shopHtml); $this->assertStringNotContainsString('原店铺负责人', $shopHtml); $this->assertStringNotContainsString('原部门负责人', $shopHtml); $this->assertStringNotContainsString('测试公司', $shopHtml); $this->assertStringNotContainsString('支付宝账户', $shopHtml); $this->assertStringNotContainsString('网商银行账户', $shopHtml); } public function test_shop_ledger_filters_daily_balance_and_shows_source_reserve_and_alert(): void { $store = [ 'id' => 921, 'platform' => '京东', 'erp_store_name' => '京东-余额预警测试店', 'company_name' => '历史主体', 'store_status' => '0', 'store_manager' => '店铺负责人', 'department_manager' => '部门负责人', 'withdrawal_account' => null, ]; $this->mock(BusinessInformationMirrorRepository::class) ->shouldReceive('allStores') ->once() ->andReturn([$store]); $this->mock(StoreReportPeriodBuilder::class) ->shouldReceive('build') ->once() ->with([$store]) ->andReturn([ '京东-余额预警测试店' => [[ 'company_name' => '当前主体有限公司', 'effective_at' => '2026-10-01', 'expires_at' => null, 'source' => 'crawl', ]], ]); $this->mock(StoreAccountMirrorRepository::class) ->shouldReceive('ownershipByStoreName') ->once() ->andReturn([]); foreach (['2026-10-22', '2026-10-23', '2026-10-24'] as $date) { $this->insertStoreLedgerBalance($date, '京东-余额预警测试店', 6000); } $html = $this->loginAs()->get($this->pageUrl() . '?' . http_build_query([ 'tab' => 'shops', 'shops_balance_date' => '2026-10-24', ]))->assertOk()->getContent(); $panelStart = strpos($html, 'id="ledger-panel-shops"'); $panelEnd = strpos($html, 'id="ledger-panel-flows"', $panelStart); $shopHtml = substr($html, $panelStart, $panelEnd - $panelStart); $this->assertStringContainsString('value="2026-10-24" selected', $shopHtml); $this->assertStringContainsString('店铺收款账户账单 · 京东钱包', $shopHtml); $this->assertStringContainsString('>6,000.00', $shopHtml); $this->assertStringContainsString('>5,000.00', $shopHtml); $this->assertStringContainsString('余额连续 3 天保持 6,000.00 元,高于预留标准 5,000.00 元。', $shopHtml); $this->assertStringContainsString('预警', $shopHtml); } private function pageUrl(): string { return route('business.pages.show', [ 'company_id' => $this->company->id, 'section' => 'financial-operations', 'page' => 'ledger', ]); } private function insertStoreLedgerBalance(string $date, string $storeName, float $balance): void { DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([ 'balance_date' => $date, 'source_scope' => StoreReceivingBillBalanceSnapshotSynchronizer::SOURCE_SCOPE, 'data_source' => StoreReceivingBillBalanceSnapshotSynchronizer::DATA_SOURCE_RECEIVING_BILL, 'source_platform' => '京东', 'store_name' => $storeName, 'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName($storeName), 'platform_account_id' => 'TEST-JD', 'balance' => $balance, 'source_row_key' => hash('sha256', $date . '|' . $storeName), 'source_file_name' => '财务系统/店铺收款账户账单/京东钱包', 'source_sha256' => hash('sha256', $date . '|' . $storeName . '|' . $balance), 'imported_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); } }