user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } public function testStoreReceivingAccountPageUsesRebuiltTabsFiltersAndSharedStoreSeed(): void { $synchronizer = Mockery::mock(StoreReceivingAccountSynchronizer::class); $synchronizer->shouldReceive('sync')->once()->with((int) $this->company->id)->andReturn([ 'created' => 0, 'updated' => 0, 'skipped' => 0, 'by_platform' => ['天猫' => 1], ]); $this->app->instance(StoreReceivingAccountSynchronizer::class, $synchronizer); StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '天猫-测试ERP店', 'shop_name' => '测试店铺名', 'store_status' => '正常', 'store_entity_name' => '测试主体', 'receiving_method' => '支付宝', ]); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'store-receiving-accounts', 'tab' => 'taobao', ])) ->assertOk() ->assertSeeText('淘宝') ->assertSeeText('ERP店铺名称') ->assertSeeText('测试店铺名') ->assertSeeText('测试主体') ->assertSeeText('下载淘宝模板') ->assertSee('aria-label="店铺收款账户筛选"', false) ->assertDontSeeText('安装明细汇总'); } public function testWithdrawalManagementShowsSafeStoreWithdrawalConfiguration(): void { Carbon::setTestNow('2026-09-13 10:00:00'); $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '提现测试ERP店', 'store_id' => 'TM-TEST-1001', 'shop_name' => '提现测试店铺', 'store_entity_name' => '提现测试主体', 'login_password' => 'must-not-be-visible', 'withdrawal_path' => '支付宝', 'withdrawal_account_name' => '测试提现账户', 'bank_card_last_four' => '4321', ]); StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '未接入测试ERP店', 'shop_name' => '未接入测试店铺', 'store_entity_name' => '未接入测试主体', 'withdrawal_path' => '银行卡', ]); DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([ 'balance_date' => '2026-09-12', 'source_scope' => 'all', 'source_platform' => '天猫', 'store_name' => '天猫-提现测试店铺', 'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName('天猫-提现测试店铺'), 'platform_account_id' => 'fixture-account', 'balance' => 19895.01, 'source_row_key' => hash('sha256', (string) $store->id), 'source_file_name' => 'fixture.json', 'source_sha256' => str_repeat('a', 64), 'imported_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([ 'balance_date' => '2026-09-10', 'source_scope' => 'all', 'source_platform' => '天猫', 'store_name' => '天猫-提现测试店铺', 'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName('天猫-提现测试店铺'), 'platform_account_id' => 'fixture-account-history', 'balance' => 12345.67, 'source_row_key' => hash('sha256', 'history-' . (string) $store->id), 'source_file_name' => 'fixture-history.json', 'source_sha256' => str_repeat('a', 64), 'imported_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); $this->assertContains('fund-management/withdrawal-management', \App\Utilities\SidebarPermissions::allKeys()); try { $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', ])); $unmatchedResponse = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'balance_status' => 'unmatched', ])); $entityResponse = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'store_entity' => '提现测试主体', ])); $historyResponse = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'balance_date' => '2026-09-10', ])); } finally { Carbon::setTestNow(); } $response ->assertOk() ->assertSeeText('提现管理') ->assertSeeText('提现配置') ->assertSeeText('提现测试ERP店') ->assertSeeText('平台店铺ID') ->assertSeeText('TM-TEST-1001') ->assertSeeText('测试提现账户') ->assertSeeText('**** 4321') ->assertSeeText('9月11日店铺余额') ->assertSeeText('9月12日店铺余额') ->assertSeeText('9月10日店铺余额') ->assertSeeText('余额预警') ->assertSeeText('19,895.01') ->assertSeeText('余额源 1 条') ->assertSeeText('已显示余额 1 条') ->assertSeeText('余额状态') ->assertSeeText('店铺主体') ->assertSeeText('预留标准') ->assertSeeText('20,000.00') ->assertSeeText('5,000.00') ->assertSeeText('提现金额') ->assertSeeText('是否达标') ->assertSeeText('待核验') ->assertSee('data-finance-table-name="withdrawal-management-v4"', false) ->assertSee('data-finance-table-native', false) ->assertSee('data-withdrawal-column-menu', false) ->assertSee('data-withdrawal-column-trigger', false) ->assertSee('name="balance_date"', false) ->assertSee('assertSee('assertOk() ->assertSeeText('9月10日店铺余额') ->assertSeeText('12,345.67') ->assertDontSeeText('19,895.01') ->assertSeeText('余额源 1 条'); $response ->assertSeeInOrder([ 'data-withdrawal-column="platform"', 'data-withdrawal-column="erp_store_name"', 'data-withdrawal-column="store_id"', 'data-withdrawal-column="store_entity"', 'data-withdrawal-column="withdrawal_path"', 'data-withdrawal-column="withdrawal_account"', 'data-withdrawal-column="bank_card"', 'data-withdrawal-column="balance_two_days_ago"', 'data-withdrawal-column="balance_yesterday"', 'data-withdrawal-column="balance"', 'data-withdrawal-column="balance_alert"', 'data-withdrawal-column="reserve_standard"', 'data-withdrawal-column="withdrawal_amount"', 'data-withdrawal-column="compliance"', ], false) ->assertSee('aria-label="提现管理筛选"', false) ->assertSee('name="store_entity"', false) ->assertSee('value="提现测试主体"', false) ->assertSee('title="数据来源:聚水潭;店铺:天猫-提现测试店铺;余额日期:2026-09-12"', false) ->assertSeeInOrder(['提现测试ERP店', '未接入测试ERP店']) ->assertDontSee('店铺名称', false) ->assertDontSee('银行卡账号(后四位)', false) ->assertDontSee('配置状态', false) ->assertDontSee('操作', false) ->assertDontSeeText('must-not-be-visible'); $unmatchedResponse ->assertOk() ->assertSeeText('未接入测试ERP店') ->assertDontSeeText('提现测试ERP店'); $entityResponse ->assertOk() ->assertSeeText('提现测试ERP店') ->assertSeeText('提现测试主体') ->assertDontSeeText('未接入测试ERP店') ->assertSee('name="store_entity"', false) ->assertSee('value="提现测试主体" selected', false); } public function testInstallationDetailsRendersWorkbookHeadersAndImportedRows(): void { $mirror = Mockery::mock(InstallationDetailMirrorRepository::class); $mirror->shouldReceive('paginate')->once()->andReturn([ 'ready' => true, 'total' => 1, 'data' => [array_replace(array_fill_keys(array_keys(InstallationDetail::columnLabels()), null), [ 'id' => 1, 'installation_no' => 'INSTALL-TEST-001', 'installation_company' => '测试安装公司', 'store_name' => '测试店铺', 'requested_amount' => '120.50', 'settled_amount' => '100.00', 'item_quantity' => 2, 'related_guid' => 'guid-install-test', ])], ]); $this->app->instance(InstallationDetailMirrorRepository::class, $mirror); $response = $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'financial-operations', 'page' => 'installation-details', ])) ->assertOk() ->assertSeeText('安装明细 · 数据列表') ->assertSeeText('安装单号') ->assertSeeText('申请总金额') ->assertSeeText('商品名称(1)') ->assertSeeText('时间口径') ->assertSeeText('日期区间') ->assertSeeText('店铺') ->assertSeeText('安装状态') ->assertSeeText('更多筛选') ->assertSeeText('今天') ->assertSeeText('本周') ->assertSeeText('本月') ->assertSeeText('上月') ->assertSeeText('近30天') ->assertSeeText('推送状态') ->assertSeeText('订单编号') ->assertSeeText('店铺 ID') ->assertSeeText('安装公司') ->assertSeeText('商品 ID') ->assertSeeText('导出') ->assertSee('id="installationFilterForm"', false) ->assertSee('name="time_scope"', false) ->assertSee('name="date_from"', false) ->assertSee('name="date_to"', false) ->assertSee('name="shop_name"', false) ->assertSee('name="completed_status"', false) ->assertSee('id="installationMoreFilters"', false) ->assertSee('id="installationMoreFilters" class="installation-more-panel" hidden', false) ->assertSee('data-installation-more-toggle', false) ->assertSee('data-installation-quick-range="today"', false) ->assertSee('data-installation-quick-range="last_30_days"', false) ->assertSee('data-date-picker', false) ->assertSee('data-date-calendar', false) ->assertSee("window.CaiWuDatePicker.setup(form)", false) ->assertSee("input.addEventListener('input', syncClear)", false) ->assertSee('结束日期不能早于开始日期。') ->assertSee('data-installation-fee-card', false) ->assertSee('data-installation-fee-open', false) ->assertSee('id="installation-fee-dialog"', false) ->assertSeeText('安装费金额') ->assertSeeText('查看明细 →') ->assertSeeText('较上月') ->assertSeeText('包安装金额') ->assertSeeText('不包安装金额') ->assertSee('installation-fee-card__split-unit', false) ->assertSee('data-installation-fee-trend', false) ->assertSee('data-installation-fee-package', false) ->assertSee('data-installation-fee-non-package', false) ->assertSeeText('当前筛选合计(万元)') ->assertSeeText('结算明细金额(万元)') ->assertSee('data-installation-fee-tab="platform"', false) ->assertSee('data-installation-fee-tab="detail"', false) ->assertSee('data-installation-fee-panel="platform"', false) ->assertSee('data-installation-fee-panel="detail"', false) ->assertSee('installation-fee-panel is-active', false) ->assertSee('.installation-fee-panel[hidden]', false) ->assertSee('display: none !important;', false) ->assertSee('.installation-fee-panel.is-active:not([hidden])', false) ->assertSeeText('店铺明细') ->assertSee('overflow-x: auto', false) ->assertSee('data-installation-fee-scroll', false) ->assertSee('scrollbar-color: #b9954f #f1ece3', false) ->assertSee('scrollbar-gutter: stable', false) ->assertSee('cursor: grab', false) ->assertSee('table-layout: fixed', false) ->assertSee('.installation-fee-table col:nth-child(1)', false) ->assertSee('width: 34%;', false) ->assertSee("scrollEl.addEventListener('pointerdown'", false) ->assertSee('scrollEl.scrollLeft = dragState.scrollLeft - dx', false) ->assertSeeText('共 0 条平台汇总') ->assertSee('--installation-fee-accent: #856632', false) ->assertSee('border-left: 3px solid var(--installation-fee-accent)', false) ->assertSee('border-right: 3px solid var(--installation-fee-accent)', false) ->assertSee('color: var(--installation-fee-accent)', false) ->assertSee('--installation-fee-table-cell-padding: 0.7rem 0.65rem', false) ->assertSee('--installation-fee-table-head-padding: 0.75rem 0.65rem', false) ->assertSee('padding: var(--installation-fee-table-head-padding)', false) ->assertSee('vertical-align: middle', false) ->assertSee('height: 8rem', false) ->assertSee('font-size: 2.5rem', false) ->assertSee('font-family: "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace', false) ->assertSee('万元', false) ->assertSee('function captionRange(value)', false) ->assertSee('function setCardValue(total, rows, value, data)', false) ->assertSee('function wanAmount(value)', false) ->assertSee('function wanYuan(value)', false) ->assertSee('amount.textContent = wanYuan(row.amount)', false) ->assertSee('currentTotal.textContent = wanYuan(total)', false) ->assertSee('function activateTab(name, focusTab)', false) ->assertSee("panel.style.display = selected ? 'block' : 'none'", false) ->assertSee("tab.addEventListener('click'", false) ->assertSee('event.preventDefault();', false) ->assertSee("event.key === 'ArrowRight'", false) ->assertSeeText('平台') ->assertSeeText('店铺名称') ->assertSeeText('安装公司') ->assertSeeText('收款渠道') ->assertSeeText('结算明细金额') ->assertSee('data-installation-fee-platform', false) ->assertSee('data-installation-fee-platform-rows', false) ->assertSee('refresh(true)', false) ->assertSee("dispatchEvent(new Event('change'", false) ->assertSee('installation-details-table-wrap', false) ->assertSee('installation-details-table', false) ->assertSee('writing-mode: horizontal-tb', false) ->assertSee('text-overflow: ellipsis', false) ->assertSeeText('INSTALL-TEST-001') ->assertSeeText('测试安装公司') ->assertSeeText('120.50') ->assertSeeText('共 1 条'); $html = $response->getContent(); $platformSummaryPosition = strpos($html, 'class="installation-fee-platform-summary"'); $detailTablePosition = strpos($html, '店铺名称'); $this->assertNotFalse($platformSummaryPosition); $this->assertNotFalse($detailTablePosition); $this->assertLessThan($detailTablePosition, $platformSummaryPosition); } public function testInstallationDetailsExportUsesFiltersAndMirrorRows(): void { $row = array_fill_keys(array_keys(InstallationDetail::columnLabels()), null); $row['installation_no'] = 'INSTALL-EXPORT-001'; $mirror = Mockery::mock(InstallationDetailMirrorRepository::class); $mirror->shouldReceive('paginate')->once()->with(1, 100, ['created_from' => '2026-08-01'])->andReturn([ 'ready' => true, 'total' => 1, 'data' => [$row], ]); $this->app->instance(InstallationDetailMirrorRepository::class, $mirror); $response = $this->loginAs()->get(route('business.installation-details.export', [ 'created_from' => '2026-08-01', ])); $response->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8'); $this->assertStringContainsString('INSTALL-EXPORT-001', $response->streamedContent()); } public function testInstallationDetailsUnifiedFiltersRenderActiveChipsAndKeepLowFrequencyFieldsInMorePanel(): void { $mirror = Mockery::mock(InstallationDetailMirrorRepository::class); $mirror->shouldReceive('paginate')->once()->andReturn([ 'ready' => true, 'total' => 1, 'data' => [array_replace(array_fill_keys(array_keys(InstallationDetail::columnLabels()), null), [ 'id' => 9, 'installation_no' => 'INSTALL-CHIP-001', 'installation_company' => '测试安装公司', 'store_name' => '测试店铺', 'requested_amount' => '120.50', 'settled_amount' => '100.00', 'related_guid' => 'guid-install-chip', ])], ]); $this->app->instance(InstallationDetailMirrorRepository::class, $mirror); $url = route('business.pages.show', [ 'section' => 'financial-operations', 'page' => 'installation-details', ]) . '?' . http_build_query([ 'time_scope' => 'customer_review', 'date_from' => '2026-09-01', 'date_to' => '2026-09-30', 'shop_name' => '测试店铺', 'completed_status' => '1', 'shop_code' => 'SHOP-001', 'push_status' => 'PUSHED', 'order_no' => 'ORDER-CHIP-001', ]); $this->loginAs() ->get($url) ->assertOk() ->assertSeeText('客服审核时间') ->assertSeeText('2026-09-01 至 2026-09-30') ->assertSeeText('测试店铺') ->assertSeeText('已完工') ->assertSeeText('SHOP-001') ->assertSeeText('PUSHED') ->assertSeeText('ORDER-CHIP-001') ->assertSee('aria-label="删除「客服审核时间」条件"', false) ->assertSee('aria-label="删除「店铺」条件"', false) ->assertSee('aria-label="删除「安装状态」条件"', false) ->assertSee('aria-label="删除「店铺 ID」条件"', false) ->assertSee('aria-label="删除「推送状态」条件"', false) ->assertSee('aria-label="删除「订单编号」条件"', false) ->assertSee('data-installation-more-count>3', false) ->assertDontSee('id="installationMoreFilters" class="installation-more-panel" hidden', false); } public function testInstallationDetailsEndpointReturnsChildItemsForMainRecord(): void { $mirror = Mockery::mock(InstallationDetailMirrorRepository::class); $mirror->shouldReceive('details')->once()->with('guid-install-test')->andReturn([ 'ready' => true, 'data' => [[ 'transaction_no' => 'INSTALL-TEST-001', 'product_id' => '10035070813217', 'product_name' => '测试商品', 'detail_requested_amount' => '53.59', 'detail_requested_quantity' => 1, ]], ]); $this->app->instance(InstallationDetailMirrorRepository::class, $mirror); $this->loginAs() ->get(route('business.installation-details.details', ['guid' => 'guid-install-test'])) ->assertOk() ->assertJsonPath('data.0.transaction_no', 'INSTALL-TEST-001') ->assertJsonPath('data.0.product_name', '测试商品'); } public function testInstallationSummaryEndpointReturnsFeeRowsForCustomerReviewRange(): void { $mirror = Mockery::mock(InstallationDetailMirrorRepository::class); $mirror->shouldReceive('feeDetails')->once()->with([ 'customer_review_from' => '2026-09-01', 'customer_review_to' => '2026-09-30', ])->andReturn([ 'ready' => true, 'total' => 2000.50, 'rows' => [ ['platform' => '淘系', 'store_name' => '店铺A', 'installation_company' => '万师傅', 'payment_channel' => '包安装', 'amount' => 1250.50], ['platform' => '京东', 'store_name' => '店铺B', 'installation_company' => '鲁班到家', 'payment_channel' => '不包安装', 'amount' => 750.00], ], 'previous_total' => 1600.40, 'previous_from' => '2026-08-01', 'previous_to' => '2026-08-31', 'change_rate' => 25.0, ]); $this->app->instance(InstallationDetailMirrorRepository::class, $mirror); $this->loginAs() ->getJson(route('business.installation-details.summary', [ 'customer_review_from' => '2026-09-01', 'customer_review_to' => '2026-09-30', ])) ->assertOk() ->assertJsonPath('total', 2000.5) ->assertJsonPath('rows.0.platform', '淘系') ->assertJsonPath('rows.0.store_name', '店铺A') ->assertJsonPath('rows.0.payment_channel', '包安装') ->assertJsonPath('rows.1.payment_channel', '不包安装') ->assertJsonPath('previous_total', 1600.4) ->assertJsonPath('change_rate', 25.0); } public function testInstallationSummaryRequiresCompleteCustomerReviewRange(): void { $mirror = Mockery::mock(InstallationDetailMirrorRepository::class); $mirror->shouldNotReceive('feeDetails'); $this->app->instance(InstallationDetailMirrorRepository::class, $mirror); $this->loginAs() ->getJson(route('business.installation-details.summary', [ 'customer_review_from' => '2026-09-01', ])) ->assertUnprocessable() ->assertJsonPath('message', '请选择完整的客服审核起止日期'); } public function testInstallationChildEndpointRequiresTheSameSidebarPermission(): void { $this->withExceptionHandling(); $this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]); setting()->set('system.sidebar_permissions', ['configured'=>['user_'.$this->user->id], 'subjects'=>['user_'.$this->user->id=>['store-management/basic-information']]]); $mirror=Mockery::mock(InstallationDetailMirrorRepository::class); $mirror->shouldNotReceive('details'); $mirror->shouldNotReceive('feeDetails'); $this->app->instance(InstallationDetailMirrorRepository::class,$mirror); $this->loginAs()->getJson(route('business.installation-details.details',['guid'=>'guid-install-test']))->assertForbidden(); $this->loginAs()->getJson(route('business.installation-details.summary', [ 'customer_review_from' => '2026-09-01', 'customer_review_to' => '2026-09-30', ]))->assertForbidden(); } public function testCompanyInformationUsesWorkbookHeadersAndReadOnlyUpstreamRows(): void { $this->mockUpstreamPage((object) [ 'id' => 1045, 'unified_social_credit_code' => '91320404MADJRTTA0U', 'company_name' => '测试上游公司', 'subscribed_capital' => '10.00', 'company_type' => '店铺-普通公司', 'establishment_date' => '2024-04-28', 'legal_representative' => '测试法人', 'registered_address' => '测试地址', 'business_scope' => '测试经营范围', 'basic_deposit_account_number' => 'J0001', 'company_status' => '正常', 'financial_officer' => '测试财务', 'supervisor' => '', 'taxpayer_category' => '小规模纳税人', 'business_registration_type' => '个人独资企业', 'shareholders' => null, ]); $response = $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'basic-information', ])); $response ->assertOk() ->assertSeeText('公司基础信息') ->assertSeeText('认缴注册资本(万元)') ->assertSeeText('基本存款账户信息') ->assertSeeText('税务类型') ->assertSeeText('工商类型') ->assertSeeText('个人独资企业') ->assertSeeText('已配置') ->assertSeeText('测试上游公司') ->assertSeeText('既有只读上游') ->assertSee('aria-label="公司基础信息查询条件"', false) ->assertSeeText('相关人员') ->assertSeeText('筛选字段') ->assertSeeText('导出 Excel') ->assertSee('data-directory-query-export-field', false) ->assertDontSee('', false) ->assertDontSee('name="registered_at"', false) ->assertDontSeeText('导入') ->assertDontSeeText('新增'); } public function testStoreInformationKeepsTwelveHeadersAndShowsReadOnlyUpstreamRows(): void { $this->mockUpstreamPage((object) [ 'id' => 2047, 'opened_at' => '2026-07-18', 'erp_store_name' => '测试上游店铺', 'unified_social_credit_code' => '91320412MA216EYU49', 'company_name' => '测试关联公司', 'trademark_number' => '35774784', 'trademark_name' => null, 'platform' => '天猫', 'profit_center' => '运营中心A', 'store_manager' => '测试负责人', 'store_status' => '正常', 'store_type' => '旗舰店', 'channel_mode' => '直销', ]); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'store-management', 'page' => 'basic-information', ])); $response ->assertOk() ->assertSeeText('店铺名称') ->assertSeeText('店铺主体') ->assertSeeText('业务部门') ->assertSee('data-column-key="opened_at"', false) ->assertSee('data-column-key="renewed_at"', false) ->assertDontSee('name="opened_at_from"', false) ->assertDontSee('name="opened_at_to"', false) ->assertDontSee('name="renewed_at"', false) ->assertSeeText('测试上游店铺') ->assertSeeText('测试关联公司') ->assertSeeText('既有只读上游') ->assertDontSeeText('导入') ->assertDontSeeText('新增'); preg_match_all('/)/', $response->getContent(), $headers); $this->assertCount(14, $headers[0]); } public function testStoreReportRendersExpandedLifecycleRowsAndClosedInProgressStatus(): void { $service = Mockery::mock(UpstreamBusinessInformation::class); $service->shouldReceive('paginate')->once()->with('store-management', 'store-report', 50)->andReturn( new LengthAwarePaginator([[ 'erp_store_name' => 'ERP测试店铺', 'store_name' => '前台测试店铺', 'store_id' => 'SHOP-001', 'profit_center' => '运营中心A', 'store_manager' => '店铺负责人', 'department_manager' => '部门负责人', 'opened_at' => '2026-05-01', 'status' => '关店中', 'entity' => '测试主体', 'company_type' => '普通公司', 'effective_at' => '2026-07-01', 'invalid_at' => '存续中', 'period_sales' => 25916.80, 'sales_07' => 25916.80, ]], 1, 50, 1, ['path' => '/']) ); $this->app->instance(UpstreamBusinessInformation::class, $service); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'store-management', 'page' => 'store-report', ])); $response ->assertOk() ->assertSeeText('店铺维度报表') ->assertSeeText('ERP测试店铺') ->assertSeeText('前台测试店铺') ->assertSeeText('SHOP-001') ->assertSeeText('测试主体') ->assertSeeText('2026-07-01') ->assertSeeText('存续中') ->assertSeeText('25,916.80') ->assertSeeText('关店中') ->assertSeeText('共享镜像'); preg_match_all('/)/', $response->getContent(), $headers); $this->assertCount(27, $headers[0]); } public function testTrademarkInformationShowsReadOnlyUpstreamRows(): void { $this->mockUpstreamPage((object) [ 'id' => 600, 'trademark_name' => '测试上游商标', 'trademark_type' => '20', 'trademark_status' => '有效', 'registration_date' => '2025-12-21', 'expiry_date' => '2035-12-20', 'notes' => null, ]); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'trademark-management', 'page' => 'information', ])) ->assertOk() ->assertSeeText('商标名称') ->assertSeeText('注册时间') ->assertSee('data-column-key="registration_date"', false) ->assertSee('data-column-key="expiry_date"', false) ->assertDontSee('name="registration_date_from"', false) ->assertDontSee('name="registration_date_to"', false) ->assertDontSee('name="expiry_date_from"', false) ->assertDontSee('name="expiry_date_to"', false) ->assertSeeText('测试上游商标') ->assertSeeText('既有只读上游') ->assertDontSeeText('导入') ->assertDontSeeText('新增'); } public function testStoreReportDataLogicPageRendersRulesAndInitialData(): void { $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'trademark-management', 'page' => 'store-report-data-logic', ])) ->assertOk() ->assertSeeText('店铺维度报表逻辑配置') ->assertSeeText('规则清单') ->assertSeeText('期初') ->assertSeeText('R8') ->assertSeeText('编辑期初数据'); } public function testTaxForecastLogicConfigurationCanBeEditedFromThePage(): void { $this->loginAs() ->get(route('business.tax-forecast-logic.edit')) ->assertOk() ->assertSeeText('小规模增值税征收率') ->assertSeeText('小规模季度免税额度') ->assertSeeText('核定应税所得率默认值') ->assertSeeText('规则目录(只读)'); $this->post(route('business.tax-forecast-logic.update'), [ 'vat_small_rate' => '1', 'vat_free_threshold' => '300000', 'vat_general_enabled' => '1', 'surtax_city_rate' => '7', 'surtax_education_rate' => '3', 'surtax_local_education_rate' => '2', 'surtax_halved' => '1', 'stamp_duty_rate' => '0.03', 'stamp_duty_halved' => '1', 'cit_effective_rate' => '5', 'cit_threshold' => '3000000', 'individual_halved_threshold' => '2000000', 'assessed_income_rate' => '12', 'income_forecast_source' => 'cost_arrangement', ])->assertRedirect(route('business.pages.show', [ 'section' => 'trademark-management', 'page' => 'tax-forecast-data-logic', ])); $this->assertSame('12', (string) setting('business_tax_forecast.assessed_income_rate')); $this->assertSame('cost_arrangement', setting('business_tax_forecast.income_forecast_source')); $this->loginAs() ->get(route('business.pages.show', ['section' => 'trademark-management', 'page' => 'tax-forecast-data-logic'])) ->assertOk() ->assertSeeText('当前运行参数') ->assertSeeText('规则清单') ->assertSeeText('R12') ->assertSeeText('小型微利企业所得税') ->assertSeeText('12%'); setting([ 'business_tax_forecast.assessed_income_rate' => '10', 'business_tax_forecast.income_forecast_source' => 'last_month_actual', ])->save(); } public function testWorkbookDirectoryReplacesManagementSubmenus(): void { $navigation = BusinessNavigation::all(); $sections = collect($navigation)->keyBy('slug'); $this->assertSame( ['subject-operating-report', 'company-cost-arrangement', 'tax-forecast', 'basic-information', 'personnel-inventory', 'company-progress'], array_column($sections['company-management']['pages'], 'slug') ); $this->assertSame( ['information', 'trademark-progress', 'rules-information'], array_column($sections['trademark-management']['pages'], 'slug') ); $rulesPage = collect($sections['trademark-management']['pages'])->firstWhere('slug', 'rules-information'); $this->assertNotNull($rulesPage); $this->assertSame( ['subject-trademark-rules', 'store-change-rules', 'store-report-data-logic', 'tax-forecast-data-logic'], array_column($rulesPage['children'], 'slug') ); $this->assertSame( ['store-report', 'internal-flow-report', 'basic-information', 'store-accounts', 'change-progress'], array_column($sections['store-management']['pages'], 'slug') ); $this->assertSame( ['systemization-todos', 'todos', 'warnings'], array_column($sections['other']['pages'], 'slug') ); $warnings = collect($sections['other']['pages'])->firstWhere('slug', 'warnings'); $this->assertSame( ['warning-overview', 'after-sales-return-refund'], array_column($warnings['children'], 'slug') ); $this->assertSame( ['预警总览', '售后退货退款'], array_column($warnings['children'], 'title') ); foreach (['company-management', 'trademark-management', 'store-management', 'other'] as $sectionSlug) { foreach ($sections[$sectionSlug]['pages'] as $page) { if (! empty($page['children']) || $page['slug'] === 'after-sales-return-refund') { continue; } $this->assertArrayHasKey('directory', $page, "{$sectionSlug}/{$page['slug']} 缺少目录字段契约"); foreach ($page['directory']['fields'] as $field) { $this->assertMatchesRegularExpression( '/\p{Han}/u', $field['label'], "{$sectionSlug}/{$page['slug']} 的 {$field['name']} 仍在使用英文表头 {$field['label']}" ); } } } $this->assertArrayNotHasKey('todos', $sections); $this->assertArrayNotHasKey('registration', collect($sections['company-management']['pages'])->keyBy('slug')); $this->assertArrayNotHasKey('changes', collect($sections['trademark-management']['pages'])->keyBy('slug')); $this->assertArrayNotHasKey('opening-process', collect($sections['store-management']['pages'])->keyBy('slug')); } public function testWorkbookDirectoryPagesRenderTheirDeclaredHeaders(): void { $this->mockUpstreamPage((object) [ 'id' => 1, 'unified_social_credit_code' => null, 'company_name' => null, 'subscribed_capital' => null, 'company_type' => null, 'establishment_date' => null, 'legal_representative' => null, 'registered_address' => null, 'business_scope' => null, 'basic_deposit_account_number' => null, 'company_status' => null, 'financial_officer' => null, 'supervisor' => null, 'taxpayer_category' => null, 'shareholders' => null, ]); $this->loginAs() ->get(route('business.pages.show', ['section' => 'company-management', 'page' => 'basic-information'])) ->assertOk() ->assertSeeText('统一社会信用代码') ->assertSeeText('认缴时间'); $this->mockUpstreamPage((object) [ 'id' => 2, 'trademark_name' => null, 'trademark_type' => null, 'trademark_status' => null, 'registration_date' => null, 'expiry_date' => null, 'notes' => null, ]); $this->get(route('business.pages.show', ['section' => 'trademark-management', 'page' => 'information'])) ->assertOk() ->assertSeeText('国际分类') ->assertSeeText('核定使用商品') ->assertSeeText('保管部门'); $expectedHeaders = [ ['section' => 'company-management', 'page' => 'subject-operating-report', 'headers' => ['市', '主体', '公司类型', '税务类型', '工商类型', '最近12个月累计收入', '1月收入', '12月收入']], ['section' => 'company-management', 'page' => 'company-cost-arrangement', 'headers' => ['公司', '公司类型', '发票类型', '当月收入预测', '往期成本缺口', '供应商成本安排', '快递成本安排', '当月税金预测', '人工成本预测', '其他费用预测']], ['section' => 'company-management', 'page' => 'tax-forecast', 'headers' => ['序号', '公司名称', '公司税务类型', '工商类型', '征收方式', '预计增值税', '企业所得税/经营所得个税', '税金合计']], ['section' => 'company-management', 'page' => 'personnel-inventory', 'headers' => ['人员名称', '税务局密码', '可担任公司数量']], ['section' => 'company-management', 'page' => 'company-progress', 'headers' => ['公司进度表字段尚未定义']], ['section' => 'trademark-management', 'page' => 'trademark-progress', 'headers' => ['商标号', '是否完成', '办理人']], ['section' => 'store-management', 'page' => 'store-report', 'headers' => ['ERP系统店铺名称', '店铺名称', '店铺ID(唯一值)', '1月销量', '12月销量']], ['section' => 'store-management', 'page' => 'change-progress', 'headers' => ['接替公司', '完成时间', '办理人']], ['section' => 'other', 'page' => 'systemization-todos', 'headers' => ['类型', '内容', '待办事项', '总负责人', '责任人', '状态', '完成截图']], ['section' => 'other', 'page' => 'todos', 'headers' => ['待办编号', '是否逾期', '关闭日期']], ['section' => 'other', 'page' => 'warning-overview', 'headers' => ['风险等级', '影响金额', '处理证据']], ]; foreach ($expectedHeaders as $page) { $response = $this->get(route('business.pages.show', [ 'section' => $page['section'], 'page' => $page['page'], ])); $response->assertOk(); foreach ($page['headers'] as $header) { $response->assertSeeText($header); } } $this->get(route('business.pages.show', [ 'section' => 'store-management', 'page' => 'store-report', ])) ->assertOk() ->assertSee('data-directory-query-picker', false) ->assertSee('data-directory-query-export', false) ->assertSeeText('筛选字段') ->assertSeeText('更多查询条件') ->assertSeeText('导出 Excel') ->assertSee('position: sticky', false) ->assertSee('overflow: auto', false); $this->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'tax-forecast', ])) ->assertOk() ->assertSeeText('按税务类型、工商类型和征收方式查询税金预测口径') ->assertSeeText('字段已定义') ->assertSeeText('个人独资企业') ->assertSeeText('核定征收') ->assertSeeText('查账征收'); } private function mockUpstreamPage(object $row): void { $paginator = new LengthAwarePaginator([$row], 1, 50, 1, ['path' => '/']); $service = Mockery::mock(UpstreamBusinessInformation::class); $service->shouldReceive('paginate')->atLeast()->once()->andReturn($paginator); $this->app->instance(UpstreamBusinessInformation::class, $service); } }