user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } public function testEverySupplierPayablePageExposesTheFiveRequestedActions(): void { $this->grantAdminPermission(); $pages = [ 'payment-reconciliation', 'return-reconciliation', 'invoice-deductions', 'compensation-details', 'advance-details', 'sample-deductions', 'other-adjustments', 'delivery-unpriced', 'return-unpriced', 'dropshipping-express', 'board-deductions', 'supplier-payables-report', ]; foreach ($pages as $page) { $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => $page, ])); $response->assertOk(); foreach (['导入', '导出', '编辑', '查询', '重置'] as $action) { $response->assertSeeText($action); } $response->assertSeeText('下载导入模板'); $response->assertSee('/template', false); preg_match_all('/class="[^"]*supplier-action-button/', $response->getContent(), $actionButtons); $this->assertCount(5, $actionButtons[0]); } } public function testSupplierPayableEditorOverridesTheGlobalMultipartRequestType(): void { $source = file_get_contents(resource_path('assets/js/views/ecommerce-finance/supplier-payable-maintenance.js')); $dialog = file_get_contents(resource_path('views/business/pages/supplier-payables/maintenance-toolbar.blade.php')); $this->assertIsString($source); $this->assertStringContainsString("'Content-Type': 'application/json'", $source); $this->assertStringContainsString('maintenanceEditSelected()', $source); $this->assertStringContainsString('v-if="maintenance.row"', $dialog); } public function testVueTablesRequireASelectedRowBeforeToolbarEditing(): void { $this->grantAdminPermission(); $pages = [ 'payment-reconciliation', 'return-reconciliation', 'invoice-deductions', 'compensation-details', 'advance-details', 'sample-deductions', 'other-adjustments', 'delivery-unpriced', 'return-unpriced', 'dropshipping-express', ]; foreach ($pages as $page) { $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => $page, ])); $content = $response->getContent(); $response->assertOk(); $this->assertStringContainsString('@current-change="maintenanceSelect"', $content); $this->assertStringContainsString('@click="maintenanceEditSelected"', $content); $this->assertStringNotContainsString(':disabled="!maintenance.fields.length', $content); $this->assertStringNotContainsString('maintenanceOpen(scope.row)', $content); $this->assertLessThan(strpos($content, '@click="maintenanceEditSelected"'), strpos($content, '重置')); } } public function testDeliveryUnpricedPageUsesItsOwnTwentyColumnTable() { $this->grantAdminPermission(); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => 'delivery-unpriced', ])); $response ->assertOk() ->assertSee('id="delivery-unpriced-app"', false) ->assertSee('来源单号') ->assertSee('单据编号(采购单号)') ->assertSee('负责人') ->assertSee('public/js/ecommerce-finance/delivery-unpriced.min.js', false) ->assertDontSee('class="business-blank-page"', false); $this->assertSame(20, substr_count($response->getContent(), 'grantAdminPermission(); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => 'return-unpriced', ])); $response ->assertOk() ->assertSee('id="return-unpriced-app"', false) ->assertSee('明细供应商') ->assertSee('供应商条码') ->assertSee('对账人') ->assertSee('public/js/ecommerce-finance/return-unpriced.min.js', false) ->assertDontSee('class="business-blank-page"', false); $this->assertSame(20, substr_count($response->getContent(), 'grantAdminPermission(); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => 'board-deductions', ])); $response ->assertOk() ->assertSee('class="supplier-board-table"', false) ->assertSee('入库日期') ->assertSee('出库备注') ->assertSee('送货单号') ->assertDontSee('class="business-blank-page"', false); preg_match_all('/)/', $response->getContent(), $headers); $this->assertCount(17, $headers[0]); } public function testSupplierPayablesReportPageUsesItsOwnThirtyFourColumnGroupedTable() { $this->grantAdminPermission(); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => 'supplier-payables-report', ])); $response ->assertOk() ->assertSee('class="supplier-payables-report-table"', false) ->assertSee('已销账供应商') ->assertSee('本月采购金额') ->assertSee('加', false) ->assertSee('单位:元', false) ->assertSee('供应商余额', false) ->assertSee('最晚出账单时间(新执行标准)') ->assertDontSee('class="business-blank-page"', false); preg_match_all('/)/', $response->getContent(), $columns); $this->assertCount(34, $columns[0]); } public function testProductPricesPageUsesOperationsReplicaPriceLimitFields(): void { $this->grantAdminPermission(); $repository = \Mockery::mock(ProductPriceMirrorRepository::class); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('initialized')->once()->andReturnTrue(); $repository->shouldReceive('paginate') ->once() ->withArgs(static function (array $filters, int $page, int $limit): bool { return $filters === [ 'barcode' => 'YMJ005203N1176V01', 'completed_from' => '2025-12-01', 'completed_to' => '2025-12-31', ] && $page === 1 && $limit > 0; }) ->andReturn(new LengthAwarePaginator([(object) [ 'upstream_key' => hash('sha256', 'product-price-row'), 'barcode' => 'YMJ005203N1176V01', 'spu' => 'YMJ0052', 'product_name' => '款式二 36/36/176 胡桃木色 BY-008', 'category' => '住宅家具 - 衣帽架', 'production_status' => '销售', 'minimum_price' => 152.8, 'price_limit' => 152.6, 'purchase_price' => 91.11, 'shipping_fee' => 11.62, 'jingxi_limit_price' => 123.0, 'approval_status' => '已完成', 'approval_result' => '已同意', 'workflow_title' => '特殊限价需求申请', 'completed_at' => '2025-12-31 00:00:00', ] ], 1, 25, 1)); $this->app->instance(ProductPriceMirrorRepository::class, $repository); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => 'product-prices', 'barcode' => 'YMJ005203N1176V01', 'completed_from' => '2025-12-01', 'completed_to' => '2025-12-31', ])); $response ->assertOk() ->assertSee('商品编码') ->assertSee('最低售价') ->assertSee('计算限价') ->assertSee('采购价') ->assertSee('京喜限价') ->assertSee('YMJ005203N1176V01') ->assertSee('152.80') ->assertSee('已同意') ->assertSeeText('导入') ->assertSeeText('导出') ->assertSeeText('下载导入模板') ->assertSeeText('查询') ->assertSeeText('重置') ->assertSee('data-product-price-edit-open', false) ->assertSee('data-product-price-row', false) ->assertDontSee('class="product-price-edit-button"', false) ->assertSeeText('编辑商品价格') ->assertDontSee('是否采购降本'); preg_match_all('/)/', $response->getContent(), $headers); preg_match_all('/class="[^"]*supplier-action-button/', $response->getContent(), $actionButtons); $this->assertCount(15, $headers[0]); $this->assertCount(5, $actionButtons[0]); } public function testProductPricesPageDoesNotTreatUninitializedMirrorAsEmptyData(): void { $this->grantAdminPermission(); $repository = \Mockery::mock(ProductPriceMirrorRepository::class); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('initialized')->once()->andReturnFalse(); $repository->shouldNotReceive('paginate'); $this->app->instance(ProductPriceMirrorRepository::class, $repository); $url = route('business.pages.show', [ 'company_id' => $this->company->id, 'section' => 'supplier-payables', 'page' => 'product-prices', ]) . '?page=2'; $response = $this->loginAs()->get($url); $response ->assertOk() ->assertSee('商品价格正在执行首次全量同步') ->assertDontSee('没有符合当前筛选条件的商品价格数据'); } public function testCompensationPageUsesTheFiveColumnsFromTheReferenceSheet(): void { $this->grantAdminPermission(); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => 'compensation-details', ])); $response ->assertOk() ->assertSee('id="compensation-detail-app"', false) ->assertSee('赔付原因') ->assertSee('备注') ->assertSee('public/js/ecommerce-finance/compensation-details.min.js', false); $this->assertSame(5, substr_count($response->getContent(), 'grantAdminPermission(); $cache = \Mockery::mock(JianSunQueryCache::class); $cache->shouldReceive('remember')->times(10)->andReturnUsing( static fn (string $scope): array => [ 'data' => [['scope' => $scope]], 'total' => 1, ], ); $cache->shouldReceive('responseHeaders')->times(10)->andReturn([]); $this->app->instance(JianSunQueryCache::class, $cache); $routes = [ 'workspace-data.supplier-payables.payment-reconciliation.index', 'workspace-data.supplier-payables.return-reconciliation.index', 'workspace-data.supplier-payables.invoice-deductions.index', 'workspace-data.supplier-payables.compensation-details.index', 'workspace-data.supplier-payables.advance-details.index', 'workspace-data.supplier-payables.delivery-unpriced.index', 'workspace-data.supplier-payables.return-unpriced.index', 'workspace-data.supplier-payables.sample-deductions.index', 'workspace-data.supplier-payables.other-adjustments.index', 'workspace-data.supplier-payables.dropshipping-express.index', ]; foreach ($routes as $routeName) { $this->loginAs() ->withHeader('X-Requested-With', 'XMLHttpRequest') ->get(route($routeName)) ->assertOk() ->assertJsonPath('total', 1); } } public function testSupplierPayableDataRouteRejectsUsersWithoutTheMatchingSidebarPage(): 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 => ['supplier-payables/invoice-deductions'], ], ], JSON_UNESCAPED_UNICODE)); try { $this->loginAs() ->withHeader('X-Requested-With', 'XMLHttpRequest') ->get(route('workspace-data.supplier-payables.payment-reconciliation.index')); $this->fail('缺少页面权限的用户不应读取货款对账数据。'); } catch (HttpException $exception) { $this->assertSame(403, $exception->getStatusCode()); } } }