user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); } public function test_update_stores_company_scoped_override_and_audit_event(): void { $this->grantAdminPermission(); $response = $this->loginAs()->patchJson( '/' . $this->company->id . '/workspace/supplier-payables/actions/compensation-details/id:88', ['values' => ['supplier' => '测试供应商', 'amount' => '128.5678', 'reason' => '质量赔付']], ); $response->assertOk()->assertJsonPath('message', '保存成功。'); $this->assertDatabaseHas('supplier_payable_overrides', [ 'company_id' => $this->company->id, 'dataset' => 'compensation-details', 'record_key' => 'id:88', ]); $this->assertDatabaseHas('supplier_payable_override_events', [ 'company_id' => $this->company->id, 'dataset' => 'compensation-details', 'record_key' => 'id:88', 'source' => 'manual', ]); } public function test_update_accepts_multi_segment_record_key(): void { $this->grantAdminPermission(); $response = $this->loginAs()->patchJson( '/' . $this->company->id . '/workspace/supplier-payables/actions/payment-reconciliation/id:sale:3216852', ['values' => ['remark' => '编码路由保存测试']], ); $response->assertOk()->assertJsonPath('message', '保存成功。'); $this->assertDatabaseHas('supplier_payable_overrides', [ 'company_id' => $this->company->id, 'dataset' => 'payment-reconciliation', 'record_key' => 'id:sale:3216852', ]); } public function test_decorate_applies_only_matching_company_override(): void { $store = app(SupplierPayableOverrideStore::class); $row = ['id' => 9, 'supplier' => '原供应商', 'amount' => 10]; $store->save('compensation-details', 'id:9', ['supplier' => '公司一'], (int) $this->company->id, null); $store->save('compensation-details', 'id:9', ['supplier' => '公司二'], (int) $this->company->id + 1, null); $decorated = $store->decorate('compensation-details', [$row], (int) $this->company->id); $this->assertSame('公司一', $decorated[0]['supplier']); $this->assertTrue($decorated[0]['_overridden']); } public function test_numeric_fields_reject_more_than_four_decimal_places(): void { $this->expectException(ValidationException::class); app(SupplierPayableMaintenanceRegistry::class)->normalize( 'supplier-payables-report', ['should_pay' => '12.12345'], ); } public function test_csv_import_creates_override_and_audit_event(): void { $file = UploadedFile::fake()->createWithContent( 'compensation-details.csv', "\xEF\xBB\xBF记录标识,供应商,赔付金额,赔付原因\nid:91,导入供应商,128.5678,导入原因\n", ); $result = app(SupplierPayableWorkbookImporter::class)->import( 'compensation-details', $file, (int) $this->company->id, (int) $this->user->id, ); $this->assertSame(['created' => 1, 'updated' => 0, 'rows' => 1], $result); $this->assertDatabaseHas('supplier_payable_overrides', [ 'company_id' => $this->company->id, 'dataset' => 'compensation-details', 'record_key' => 'id:91', 'values' => '{"supplier":"导入供应商","amount":"128.5678","reason":"导入原因"}', ]); $this->assertDatabaseHas('supplier_payable_override_events', [ 'company_id' => $this->company->id, 'dataset' => 'compensation-details', 'record_key' => 'id:91', 'source' => 'import', ]); } public function test_csv_import_rejects_duplicate_record_keys_without_writing(): void { $file = UploadedFile::fake()->createWithContent( 'duplicate.csv', "记录标识,供应商,赔付金额,赔付原因\nid:92,供应商一,10,原因一\nid:92,供应商二,20,原因二\n", ); try { app(SupplierPayableWorkbookImporter::class)->import( 'compensation-details', $file, (int) $this->company->id, (int) $this->user->id, ); $this->fail('重复记录标识应当拒绝导入。'); } catch (ValidationException $exception) { $this->assertSame(['第 3 行“记录标识”重复。'], $exception->errors()['import']); } $this->assertDatabaseMissing('supplier_payable_overrides', [ 'company_id' => $this->company->id, 'dataset' => 'compensation-details', 'record_key' => 'id:92', ]); } public function test_import_rejects_damaged_workbook(): void { $file = UploadedFile::fake()->createWithContent('damaged.xlsx', 'not a workbook'); try { app(SupplierPayableWorkbookImporter::class)->import( 'compensation-details', $file, (int) $this->company->id, (int) $this->user->id, ); $this->fail('损坏的工作簿应当拒绝导入。'); } catch (ValidationException $exception) { $this->assertSame( ['无法读取导入文件,请确认文件未损坏且格式为 xlsx、xls 或 csv。'], $exception->errors()['import'], ); } } public function test_export_downloads_utf8_csv_and_escapes_formula_values(): void { $this->grantAdminPermission(); Carbon::setTestNow('2026-09-17 12:34:56'); $repository = Mockery::mock(SupplierPayableRepository::class); $repository->shouldReceive('compensationDetails') ->once() ->with([], 1, 100) ->andReturn([ 'data' => [[ 'id' => 500, 'month' => '2026-09', 'supplier' => '=2+2', 'amount' => '18.5000', 'reason' => '+SUM(A1:A2)', 'remark' => '测试备注', ]], 'total' => 1, ]); $this->app->instance(SupplierPayableRepository::class, $repository); $response = $this->loginAs()->get( '/' . $this->company->id . '/workspace/supplier-payables/actions/compensation-details/export', ); $response->assertOk()->assertDownload('_20260917_123456.csv'); $this->assertStringContainsString( "filename*=utf-8''" . rawurlencode('赔偿明细_20260917_123456.csv'), (string) $response->headers->get('content-disposition'), ); $content = $response->streamedContent(); $this->assertStringStartsWith("\xEF\xBB\xBF", $content); $lines = preg_split('/\r\n|\n|\r/', substr($content, 3), -1, PREG_SPLIT_NO_EMPTY); $this->assertSame( ['记录标识', '日期', '供应商', '赔付金额', '赔付原因', '备注'], str_getcsv($lines[0]), ); $this->assertSame( ['id:500', '2026-09', "\t=2+2", '18.5000', "\t+SUM(A1:A2)", '测试备注'], str_getcsv($lines[1]), ); } public function test_each_supplier_payable_page_downloads_an_xlsx_template_with_its_import_headers(): void { $this->grantAdminPermission(); $registry = app(SupplierPayableMaintenanceRegistry::class); $datasets = [ 'payment-reconciliation', 'return-reconciliation', 'invoice-deductions', 'compensation-details', 'advance-details', 'sample-deductions', 'delivery-unpriced', 'return-unpriced', 'other-adjustments', 'dropshipping-express', 'board-deductions', 'supplier-payables-report', ]; foreach ($datasets as $dataset) { $response = $this->loginAs()->get( '/' . $this->company->id . '/workspace/supplier-payables/actions/' . $dataset . '/template', ); $response->assertOk()->assertDownload(); $this->assertSame( 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('content-type'), ); $path = tempnam(sys_get_temp_dir(), 'supplier-payable-template-'); file_put_contents($path, $response->streamedContent()); try { $spreadsheet = IOFactory::load($path); $headers = $spreadsheet->getSheet(0)->rangeToArray( 'A1:' . $spreadsheet->getSheet(0)->getHighestColumn() . '1', )[0]; $this->assertSame( array_merge(['记录标识'], array_column($registry->fields($dataset), 'label')), $headers, $dataset, ); $this->assertSame('填写说明', $spreadsheet->getSheet(1)->getTitle()); $spreadsheet->disconnectWorksheets(); } finally { unlink($path); } } } }