- 生成入驻二维码新增 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
345 lines
15 KiB
PHP
345 lines
15 KiB
PHP
<?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);
|
||
}
|
||
}
|
||
}
|