CaiWuXiTong/tests/Feature/Business/QuoteManagementPageTest.php
limeng 8ba7ac8327 feat(快递入驻/快递资料): 生成二维码时必填快递资料并对齐到快递资料;去掉「发起报价变更」按钮;自定义3 改名扣款方式
- 生成入驻二维码新增 11 个必填项(快递方看不到,放在入驻流程下方单独一块):快递类型、快递合作方式、快递属性、扣款方式、单号类型、快递邮箱、
  是否需要核对物流点、快递发货方式、是否已有工作平台账号、快递网点代码、网点所在地区;入驻列表、入驻详情可查看和修改
- 快递资料:扣款方式对齐到「扣款方式」列(工作平台没填时用入驻的),其余字段接在入驻信息列里;网点代码/地区进网点明细;导出同步
- 不再依赖钉钉「新建快递」流程字段,ERP 开通改为手动补录 ERP账户ID
- 快递资料「自定义3」列、筛选、编辑和导出表头改名为「扣款方式」(字段仍是 undefined3)
- 报价管理工具栏、报价历史页去掉「发起报价变更」:它默认拿第一份报价走老的改格子流程,对入驻快递会绕开后台变价;
  变价统一从报价列表每一行的「变价」进入;老报价编辑页新增 …/changes/new 入口,避免套餐限制中间件把 GET …/create 跳到用户页
- 新增迁移 2026_10_14_300000:business_courier_onboardings 加 11 个可空字段

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gzfsnV3fJ89CoUy1D9V9g
2026-10-14 18:31:25 +08:00

345 lines
15 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\Models\Auth\Permission;
use App\Support\QuoteManagementSnapshot;
use App\Support\ReverseLogisticsQuoteSnapshot;
use App\Utilities\BusinessNavigation;
use Illuminate\Http\Exceptions\HttpResponseException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\Feature\FeatureTestCase;
class QuoteManagementPageTest extends FeatureTestCase
{
private string $fixturePath;
private string $reverseFixturePath;
protected function setUp(): void
{
parent::setUp();
$this->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('<option value="新北中通">新北中通</option>', false)
->assertSee('<option value="新北中通其他">新北中通其他</option>', false)
->assertSee('<option value="新德邦快递超长">新德邦快递超长</option>', 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);
}
}
}