CaiWuXiTong/tests/Feature/Business/ManagementCrudTest.php

873 lines
40 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Business\UpstreamBusinessInformation;
use App\Business\StoreReceivingAccountSynchronizer;
use App\Models\Auth\Permission;
use App\Models\Business\InstallationDetail;
use App\Models\Business\StoreReceivingAccount;
use App\Utilities\BusinessNavigation;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Mockery;
use Modules\EcommerceFinance\Repositories\InstallationDetailMirrorRepository;
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
use Tests\Feature\FeatureTestCase;
class ManagementCrudTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->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('<option value="2026-09-12"', false)
->assertSee('<option value="2026-09-10"', false);
$historyResponse
->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('<th>店铺名称</th>', false)
->assertDontSee('<th>银行卡账号(后四位)</th>', false)
->assertDontSee('<th>配置状态</th>', false)
->assertDontSee('<th>操作</th>', 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)
->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('<span class="installation-fee-card__unit">万元</span>', false)
->assertSee('function captionRange(value)', false)
->assertSee('function setCardValue(total, rows, value, data)', false)
->assertSee('function wanAmount(value)', 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, '<th scope="col">店铺名称</th>');
$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</b>', 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('<div class="directory-table-tools" data-directory-tools>', 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('/<th(?:\s|>)/', $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('/<th(?:\s|>)/', $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')
);
foreach (['company-management', 'trademark-management', 'store-management', 'other'] as $sectionSlug) {
foreach ($sections[$sectionSlug]['pages'] as $page) {
$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' => 'warnings', '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);
}
}