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); } } }