user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } public function test_export_downloads_all_rows_for_current_filters(): void { $this->grantAdminPermission(); $repository = \Mockery::mock(ProductPriceMirrorRepository::class); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('initialized')->once()->andReturnTrue(); $repository->shouldReceive('exportRows') ->once() ->withArgs(function (array $filters, int $companyId): bool { return $companyId === (int) $this->company->id && $filters === [ 'barcode' => 'SKU-001', 'completed_from' => '2026-08-01', 'completed_to' => '2026-08-31', ]; }) ->andReturn([(object) [ 'upstream_key' => hash('sha256', 'export-row'), 'business_id' => 'B-001', 'barcode' => 'SKU-001', 'spu' => 'SPU-001', 'product_name' => '导出商品', 'category' => '家具', 'production_status' => '销售', 'minimum_price' => 120, 'price_limit' => 118, 'purchase_price' => 80, 'shipping_fee' => 10, 'jingxi_limit_price' => 100, 'approval_status' => '已完成', 'approval_result' => '已同意', 'workflow_title' => '价格审批', 'completed_at' => '2026-08-15 10:00:00', 'is_overridden' => 1, 'override_updated_at' => '2026-09-16 09:00:00', ]]); $this->app->instance(ProductPriceMirrorRepository::class, $repository); $response = $this->loginAs()->get(route('business.supplier-payables.product-prices.export', [ 'barcode' => 'SKU-001', 'completed_from' => '2026-08-01', 'completed_to' => '2026-08-31', ])); $response->assertOk()->assertDownload(); $content = $response->streamedContent(); $this->assertStringContainsString('记录标识', $content); $this->assertStringContainsString('SKU-001', $content); $this->assertStringContainsString('导出商品', $content); $this->assertStringContainsString('人工维护', $content); } public function test_update_saves_manual_override_and_returns_to_current_filter(): void { $this->grantAdminPermission(); $key = hash('sha256', 'update-row'); $repository = \Mockery::mock(ProductPriceMirrorRepository::class); $repository->shouldReceive('applyOverrides') ->once() ->withArgs(function (array $records, int $companyId, ?int $userId, string $source) use ($key): bool { return $companyId === (int) $this->company->id && $userId === (int) $this->user->id && $source === 'manual' && $records[$key]['barcode'] === 'SKU-EDIT' && (float) $records[$key]['purchase_price'] === 82.5; }) ->andReturn(['created' => 1, 'updated' => 0]); $this->app->instance(ProductPriceMirrorRepository::class, $repository); $response = $this->loginAs()->patch( route('business.supplier-payables.product-prices.update', ['upstreamKey' => $key]) . '?barcode=SKU&page=2', [ 'barcode' => 'SKU-EDIT', 'spu' => 'SPU-EDIT', 'product_name' => '编辑商品', 'purchase_price' => '82.50', ], ); $response ->assertRedirect(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => 'product-prices', ]) . '?barcode=SKU&page=2') ->assertSessionHas('success', '商品价格已保存。'); } public function test_update_rejects_prices_with_more_than_four_decimal_places(): void { $this->grantAdminPermission(); $key = hash('sha256', 'invalid-update-price'); $repository = \Mockery::mock(ProductPriceMirrorRepository::class); $repository->shouldNotReceive('applyOverrides'); $this->app->instance(ProductPriceMirrorRepository::class, $repository); try { $this->loginAs()->patch( route('business.supplier-payables.product-prices.update', ['upstreamKey' => $key]), [ 'barcode' => 'SKU-EDIT', 'purchase_price' => '82.12345', ], ); $this->fail('超过 4 位小数的价格不应通过编辑。'); } catch (ValidationException $exception) { $this->assertSame( '采购价最多支持 4 位小数。', $exception->errors()['purchase_price'][0], ); } } public function test_import_accepts_workbook_and_reports_created_and_updated_counts(): void { $this->grantAdminPermission(); $importer = \Mockery::mock(ProductPriceWorkbookImporter::class); $importer->shouldReceive('import') ->once() ->withArgs(function (UploadedFile $file, int $companyId, ?int $userId): bool { return $file->getClientOriginalName() === '商品价格.csv' && $companyId === (int) $this->company->id && $userId === (int) $this->user->id; }) ->andReturn(['created' => 2, 'updated' => 3, 'rows' => 5]); $this->app->instance(ProductPriceWorkbookImporter::class, $importer); $file = UploadedFile::fake()->createWithContent( '商品价格.csv', "记录标识,商品编码,采购价\n" . hash('sha256', 'import-row') . ",SKU-1,80\n", ); $response = $this->loginAs()->post( route('business.supplier-payables.product-prices.import') . '?barcode=SKU-1', ['import' => $file], ); $response ->assertRedirect(route('business.pages.show', [ 'section' => 'supplier-payables', 'page' => 'product-prices', ]) . '?barcode=SKU-1&page=1') ->assertSessionHas('success', '导入完成:新增维护 2 条,更新维护 3 条。'); } public function test_template_downloads_an_xlsx_with_product_price_import_headers(): void { $this->grantAdminPermission(); $response = $this->loginAs()->get(route( 'business.supplier-payables.product-prices.template', ['company_id' => $this->company->id], )); $response->assertOk()->assertDownload(); $path = tempnam(sys_get_temp_dir(), 'product-price-template-'); file_put_contents($path, $response->streamedContent()); try { $spreadsheet = IOFactory::load($path); $headers = $spreadsheet->getSheet(0)->rangeToArray('A1:K1')[0]; $this->assertSame([ '记录标识', '商品编码', 'SPU', '商品名称', '类目', '生产状态', '最低售价', '计算限价', '采购价', '运费', '京喜限价', ], $headers); $this->assertSame('填写说明', $spreadsheet->getSheet(1)->getTitle()); $spreadsheet->disconnectWorksheets(); } finally { unlink($path); } } }