fixturePath = storage_path('framework/testing/quote-management-' . uniqid('', true) . '.json');
file_put_contents($this->fixturePath, json_encode([
'sourcePath' => 'C:/private/报价单合并.xlsx',
'sheets' => [
[
'index' => 0,
'name' => '新北中通报价',
'address' => 'A1:D4',
'values' => [
['省份', '面单', '费率', "备注:\n执行时间:2025.12.01"],
[null, null, null, null],
['江苏省', 3.8, 0.5, null],
['安徽省', 3.8, 0.5, null],
],
'formulas' => [[], [], ['', '', '=B3/2', ''], []],
],
[
'index' => 1,
'name' => '常州圆通-上楼',
'address' => 'A1:H2',
'values' => [
['报价表', '常州圆通'],
['江苏省', 4.2],
],
'formulas' => [[], []],
],
[
'index' => 2,
'name' => '新德邦快递超长报价',
'address' => 'A1:F2',
'values' => [
['0-1kg', '1kg以上'],
['江苏省', 5.5],
],
'formulas' => [[], []],
],
[
'index' => 3,
'name' => '新北中通其他报价',
'address' => 'A1:D2',
'values' => [
['省份', '面单', '费率', '备注'],
['江苏省', 4.1, 0.6, null],
],
'formulas' => [[], []],
],
],
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
app()->instance(QuoteManagementSnapshot::class, new QuoteManagementSnapshot($this->fixturePath));
$this->reverseFixturePath = storage_path('framework/testing/reverse-logistics-' . uniqid('', true) . '.json');
file_put_contents($this->reverseFixturePath, json_encode([
'sourcePath' => 'C:/private/逆向物流报价.xlsx',
'sheets' => [
[
'index' => 0,
'name' => '申通退货',
'address' => 'A1:D5',
'values' => [
['报价单', null, null, null],
['出发省份', '到达省份', '江苏', null],
[null, null, '面单价格', '续重价格'],
[null, '广东', 9, 1.75],
['1、该价格不含税,生效日期:2026年5月1日', null, null, null],
],
'formulas' => [[], [], [], [], []],
],
],
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
app()->instance(
ReverseLogisticsQuoteSnapshot::class,
new ReverseLogisticsQuoteSnapshot($this->reverseFixturePath)
);
}
protected function tearDown(): void
{
if (isset($this->fixturePath) && is_file($this->fixturePath)) {
unlink($this->fixturePath);
}
if (isset($this->reverseFixturePath) && is_file($this->reverseFixturePath)) {
unlink($this->reverseFixturePath);
}
parent::tearDown();
}
public function testQuoteManagementPageRendersImportedWorkbookVersions(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'quote-management',
]))
->assertOk()
->assertSeeText('报价管理')
->assertSee('data-quote-view="reverse-logistics"', false)
->assertSee('data-quote-panel="reverse-logistics"', false)
->assertSeeText('计费规则')
->assertSee('data-reverse-rate-body', false)
->assertSeeText('报价单合并.xlsx')
->assertSeeText('报价名称')
->assertSeeText('快递公司')
->assertSeeText('资料状态')
->assertSeeText('报价历史')
->assertDontSeeText('发起报价变更')
->assertSee(route('business.courier-quote-changes.index'), false)
->assertDontSee(route('business.courier-quote-changes.create'), false)
->assertDontSee('data-quote-change-modal', false)
->assertDontSeeText('只生成浏览器会话内的草稿提示')
->assertSee('', false)
->assertSee('', false)
->assertSee('', false)
->assertSee('"id":"QB-001"', false)
->assertSee('"company_name":"新北中通"', false)
->assertSee('"company_name":"新北中通其他"', false)
->assertSee('新北中通报价', false)
->assertSee('常州圆通-上楼', false)
->assertSee('2025-12-01', false)
->assertSee('=B3/2', false)
->assertSeeText('原始报价规则保留,当前页面不重新计算金额')
->assertDontSee('报价列表正在配置中');
$quotes = app(QuoteManagementSnapshot::class)->load()['quotes'];
$this->assertSame(['QB-001', 'QB-002', 'QB-003', 'QB-004'], array_column($quotes, 'id'));
$this->assertSame(
['新北中通', '常州圆通-上楼', '新德邦快递超长', '新北中通其他'],
array_column($quotes, 'company_name')
);
}
public function testReverseLogisticsQuoteTabCanBeOpenedDirectly(): void
{
$courierPayables = collect(config('business_navigation'))
->firstWhere('slug', 'courier-payables');
$this->assertIsArray($courierPayables);
$quoteDirectory = collect($courierPayables['pages'])->firstWhere('slug', 'courier-quote');
$this->assertSame([
'quote-management',
'courier-onboarding',
'courier-information',
'agreement-signing',
'courier-surcharge',
], array_column($quoteDirectory['children'], 'slug'));
$this->assertNotContains('reverse-logistics-quotes', array_column($courierPayables['pages'], 'slug'));
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'quote-management',
'quote_view' => 'reverse-logistics',
]))
->assertOk()
->assertSee('data-quote-view="reverse-logistics" aria-controls="quote-reverse-logistics-panel" aria-selected="true"', false)
->assertSee('id="quote-reverse-logistics-panel" role="tabpanel" data-quote-panel="reverse-logistics"', false)
->assertSeeText('计费规则')
->assertSeeText('申通退货')
->assertSee('共 1 条运价', false)
->assertSee('首重 + 续重 × (结算重量 - 1)', false)
->assertSee('2026-05-01', false)
->assertSee('"origin":"广东"', false)
->assertSee('"destination":"江苏"', false)
->assertSee('"increment_amount":1.75', false)
->assertSee('data-reverse-calc-body', false)
->assertSeeText('运费计算器')
->assertSee('data-reverse-calc="weight"', false)
->assertSee('data-reverse-rate-body', false);
}
public function testCourierQuoteSidebarEntryOpensFourWorkspacePagesAndKeepsOtherPayablePages(): void
{
$subject = 'user_' . $this->user->id;
$this->user->roles()->sync([
\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id,
]);
setting()->set('system.sidebar_permissions', [
'configured' => [$subject],
'subjects' => [$subject => ['courier-payables']],
]);
setting()->save();
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$response = $this->loginAs()->get(route('business.pages.show', ['section' => 'courier-payables']))
->assertOk()
->assertSeeText('快递报价')
->assertSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-quote']), false)
->assertSee('data-courier-tab="freight-reconciliation"', false)
->assertSee('data-courier-tab="shipping-data"', false)
->assertDontSee('data-courier-group="courier-quote"', false);
foreach (['quote-management', 'courier-information', 'agreement-signing', 'courier-surcharge'] as $page) {
$response->assertDontSee('data-courier-tab="' . $page . '"', false);
}
$response = $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-quote']))
->assertOk()
->assertSee('data-courier-quote-workspace', false)
->assertDontSee('data-courier-tab="freight-reconciliation"', false)
->assertDontSee('data-courier-tab="shipping-data"', false);
foreach (['quote-management', 'courier-information', 'agreement-signing', 'courier-surcharge'] as $page) {
$response->assertSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => $page]), false)
->assertSee('data-courier-tab="' . $page . '"', false);
}
}
public function testCourierQuoteDirectoryKeepsPageLabelsAndExactPermissions(): void
{
setting()->set('navigation.titles', [
'pages' => ['courier-payables' => ['quote-management' => '快递报价单']],
]);
setting()->save();
$subject = 'user_' . $this->user->id;
$this->user->roles()->sync([
\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id,
]);
setting()->set('system.sidebar_permissions', [
'configured' => [$subject],
'subjects' => [$subject => [
'courier-payables/quote-management',
'courier-payables/courier-surcharge',
]],
]);
setting()->save();
$navigation = BusinessNavigation::all();
$section = collect($navigation)->firstWhere('slug', 'courier-payables');
$directory = collect($section['pages'])->firstWhere('slug', 'courier-quote');
$this->assertSame('快递报价单', collect($directory['children'])->firstWhere('slug', 'quote-management')['title']);
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs()
->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-quote']))
->assertOk()
->assertSeeText('快递报价')
->assertSeeText('快递报价单')
->assertSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'quote-management']), false)
->assertSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']), false)
->assertDontSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-information']), false)
->assertDontSee(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing']), false)
->assertSee('data-courier-quote-workspace', false)
->assertDontSee('data-courier-group="courier-quote"', false)
->assertSee('data-courier-tab="quote-management"', false)
->assertSee('data-courier-tab="courier-surcharge"', false);
try {
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-information']));
$this->fail('未授权的快递资料页面不应访问。');
} catch (HttpException|HttpResponseException $exception) {
$status = method_exists($exception, 'getStatusCode')
? $exception->getStatusCode()
: $exception->getResponse()->getStatusCode();
$this->assertSame(404, $status);
}
}
public function testQuoteManagementPageKeepsTheShellWhenThePrivateSnapshotIsMissing(): void
{
app()->instance(
QuoteManagementSnapshot::class,
new QuoteManagementSnapshot(storage_path('framework/testing/missing-quote-snapshot.json'))
);
app()->instance(
ReverseLogisticsQuoteSnapshot::class,
new ReverseLogisticsQuoteSnapshot(storage_path('framework/testing/missing-reverse-logistics-snapshot.json'))
);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'quote-management',
'quote_view' => 'reverse-logistics',
]))
->assertOk()
->assertSeeText('尚未导入本机报价文件。')
->assertSeeText('尚未导入本机逆向物流报价文件。')
->assertSeeText('报价页面仍可访问')
->assertDontSee('class="business-blank-page"', false);
}
public function testQuoteManagementPageRejectsUsersWithoutItsSidebarPermission(): void
{
$subject = 'user_' . $this->user->id;
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
setting()->set('system.sidebar_permissions', json_encode([
'configured' => [$subject],
'subjects' => [
$subject => ['courier-payables/freight-reconciliation'],
],
], JSON_UNESCAPED_UNICODE));
try {
$this->loginAs()->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'quote-management',
]));
$this->fail('缺少报价管理页面权限的用户不应访问该页面。');
} catch (HttpException|HttpResponseException $exception) {
$status = method_exists($exception, 'getStatusCode')
? $exception->getStatusCode()
: $exception->getResponse()->getStatusCode();
$this->assertSame(404, $status);
}
}
}