873 lines
40 KiB
PHP
873 lines
40 KiB
PHP
<?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);
|
||
}
|
||
}
|