CaiWuXiTong/tests/Feature/Business/SupplierPayableActionsTest.php

248 lines
9.8 KiB
PHP

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Carbon\Carbon;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\ValidationException;
use Mockery;
use Modules\EcommerceFinance\Repositories\SupplierPayableRepository;
use Modules\EcommerceFinance\Services\SupplierPayableMaintenanceRegistry;
use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore;
use Modules\EcommerceFinance\Services\SupplierPayableWorkbookImporter;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Tests\Feature\FeatureTestCase;
class SupplierPayableActionsTest extends FeatureTestCase
{
protected function grantAdminPermission(): void
{
$this->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);
}
}
}
}