CaiWuXiTong/tests/Feature/Business/CourierSurchargePageTest.php

699 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Business\CourierSurchargeQuoteWorkbookImporter;
use App\Business\CourierSurchargeTypeRepository;
use App\Models\Auth\Permission;
use App\Models\Business\CourierSurchargeAuditLog;
use App\Models\Business\CourierSurchargeQuote;
use App\Models\Business\CourierSurchargeType;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Schema;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Tests\Feature\FeatureTestCase;
class CourierSurchargePageTest extends FeatureTestCase
{
/** @var list<string> */
private array $temporaryImportFiles = [];
public function test_courier_surcharge_page_renders_persisted_quote_rules(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->createQuote();
$response = $this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'courier-surcharge',
]))
->assertOk()
->assertSeeText('附加费报价维护')
->assertSeeText('正式维护数据')
->assertSeeText('报价规则列表')
->assertSeeText('管理附加费类型')
->assertSeeText('类型列表')
->assertSeeText('进仓费')
->assertSeeText('新增报价规则')
->assertSeeText('结算方式')
->assertSeeText('计价标准')
->assertSeeText('生效条件')
->assertSeeText('快递归属')
->assertSeeText('自有快递')
->assertSeeText('代发仓')
->assertSeeText('对账人')
->assertSeeText('张会计')
->assertSeeText('供应商')
->assertSeeText('曹县物流供应商')
->assertSeeText('附加费类型(可多选)')
->assertSeeText('列设置')
->assertSeeText('导出当前列')
->assertSeeText('导出全部字段')
->assertSeeText('导入报价')
->assertSeeText('下载 Excel 模板')
->assertSee('data-surcharge-import-modal', false)
->assertSeeText('规则编号与操作列固定显示')
->assertSee('data-column-storage-key="caiwu:table-columns:', false)
->assertSee('data-surcharge-table', false)
->assertSee('data-surcharge-audit-panel', false)
->assertSee('data-surcharge-audit-placeholder', false)
->assertSee('data-surcharge-audit-table-wrap hidden', false)
->assertSee('data-column-check', false)
->assertSee('data-column-resize', false)
->assertSee('aria-orientation="vertical"', false)
->assertSee('data-surcharge-export-current', false)
->assertSee('data-surcharge-export-all', false)
->assertSee('data-column-reset', false)
->assertSee('name="types[]"', false)
->assertSeeInOrder(['data-column-key="courier"', 'data-column-key="scope"', 'data-column-key="reconciler"', 'data-column-key="supplier"'], false)
->assertSee('data-audit-quote-id="', false)
->assertSeeText('曹县德邦')
->assertSeeText('超长费')
->assertSeeText('按尺寸/最长边')
->assertSeeText('8 元/单')
->assertSeeText('已启用')
->assertSeeText('后续结算页面将按启用中的规则计算');
$this->assertSame(13, substr_count($response->getContent(), 'data-column-resize></span>'));
$this->assertStringContainsString('widths: state.widths', $response->getContent());
}
public function test_courier_surcharge_type_can_be_created_and_duplicate_is_rejected(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$this->post(route('business.courier-surcharge.types.store'), ['name' => '偏远地区费'])
->assertRedirect();
$this->assertDatabaseHas('business_courier_surcharge_types', [
'company_id' => company_id(),
'name' => '偏远地区费',
'status' => CourierSurchargeType::STATUS_ACTIVE,
]);
$this->withExceptionHandling()
->from(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->post(route('business.courier-surcharge.types.store'), ['name' => '偏远地区费'])
->assertRedirect()
->assertSessionHasErrors('name');
}
public function test_inactive_type_is_rejected_for_new_quotes_but_keeps_historical_value(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$type = app(CourierSurchargeTypeRepository::class)->all()->firstWhere('name', '超长费');
$this->assertNotNull($type);
$quote = $this->createQuote();
$this->post(route('business.courier-surcharge.types.status', ['type' => $type->id]), [
'status' => CourierSurchargeType::STATUS_INACTIVE,
])->assertRedirect();
$this->withExceptionHandling()->post(route('business.courier-surcharge.store'), [
'courier' => '停用类型测试快递',
'scope' => '自有快递',
'reconciler' => '张会计',
'types' => ['超长费'],
'mode' => '固定单价',
'standard' => '1 元/单',
'condition' => '测试',
'effective_at' => '2026-09-02',
])->assertSessionHasErrors('types.0');
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeText($quote->courier)
->assertSeeText('超长费')
->assertSeeText('管理附加费类型');
$page = $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk();
$this->assertStringNotContainsString('value="超长费"><span>超长费</span>', $page->getContent());
}
public function test_type_status_endpoint_rejects_another_company_type(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$otherType = CourierSurchargeType::withoutGlobalScopes()->create([
'company_id' => company_id() + 999,
'name' => '其他公司类型',
'status' => CourierSurchargeType::STATUS_ACTIVE,
'sort_order' => 99,
]);
$this->loginAs();
$this->withExceptionHandling()->post(route('business.courier-surcharge.types.status', ['type' => $otherType->id]), [
'status' => CourierSurchargeType::STATUS_INACTIVE,
])->assertNotFound();
}
public function test_courier_surcharge_page_filters_persisted_rules(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->createQuote();
CourierSurchargeQuote::query()->create([
'company_id' => company_id(),
'courier' => '常州松宇货运',
'scope' => '代发仓',
'reconciler' => '李会计',
'supplier' => '常州物流供应商',
'types' => ['送货费'],
'mode' => '按体积',
'standard' => '体积 ≤ 10 m³:50 元/单',
'condition' => '计费体积不超过 10 m³',
'effective_at' => '2026-09-02',
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
'remark' => null,
]);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'courier-surcharge',
'courier' => '常州松宇货运',
'scope' => '代发仓',
'reconciler' => '李会计',
'supplier' => '常州物流供应商',
'status' => '已启用',
]))
->assertOk()
->assertSeeText('常州松宇货运')
->assertSeeText('送货费')
->assertSeeText('代发仓')
->assertSeeText('李会计')
->assertSeeText('常州物流供应商')
->assertSeeText('当前规则')
->assertDontSee('<td>曹县德邦</td>', false)
->assertDontSee('<td>霸州百世快运-雅柏</td>', false);
}
public function test_courier_surcharge_page_keeps_creation_order_instead_of_effective_date_order(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$first = CourierSurchargeQuote::query()->create([
'company_id' => company_id(),
'courier' => '顺序测试第一条',
'scope' => '自有快递',
'reconciler' => '苏木',
'supplier' => null,
'types' => ['送货费'],
'mode' => '固定单价',
'standard' => '1 元/单',
'condition' => '第一条',
'effective_at' => '2027-12-31',
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
]);
$second = CourierSurchargeQuote::query()->create([
'company_id' => company_id(),
'courier' => '顺序测试第二条',
'scope' => '自有快递',
'reconciler' => '苏木',
'supplier' => null,
'types' => ['进仓费'],
'mode' => '固定单价',
'standard' => '2 元/单',
'condition' => '第二条',
'effective_at' => '2026-01-01',
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
]);
$this->assertLessThan($second->id, $first->id);
$this->loginAs()
->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeInOrder(['顺序测试第一条', '顺序测试第二条']);
}
public function test_courier_surcharge_store_update_and_status_are_persistent(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$payload = [
'courier' => '测试快递',
'scope' => '代发仓',
'reconciler' => '李会计',
'supplier' => '',
'types' => ['拦截费', '改地址'],
'mode' => '固定单价',
'standard' => '12 元/单',
'condition' => '拦截成功',
'effective_at' => '2026-09-02',
'remark' => '接口保存测试',
];
$this->post(route('business.courier-surcharge.store'), $payload)
->assertRedirect(route('business.pages.show', [
'company_id' => company_id(), 'section' => 'courier-payables', 'page' => 'courier-surcharge',
]));
$quote = CourierSurchargeQuote::query()->where('courier', '测试快递')->firstOrFail();
$this->assertSame(['拦截费', '改地址'], $quote->types);
$this->assertNull($quote->supplier);
$this->assertSame(CourierSurchargeQuote::STATUS_ACTIVE, $quote->status);
$this->assertSame((int) user_id(), (int) $quote->created_by);
$this->assertSame((int) user_id(), (int) $quote->updated_by);
$this->patch(route('business.courier-surcharge.update', ['quote' => $quote->id]), [
...$payload,
'courier' => '测试快递(已修改)',
'supplier' => '测试供应商',
'types' => ['保费'],
])->assertRedirect();
$quote->refresh();
$this->assertSame('测试快递(已修改)', $quote->courier);
$this->assertSame('测试供应商', $quote->supplier);
$this->assertSame(['保费'], $quote->types);
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
'status' => CourierSurchargeQuote::STATUS_INACTIVE,
])->assertRedirect();
$this->assertSame(CourierSurchargeQuote::STATUS_INACTIVE, $quote->fresh()->status);
}
public function test_quote_operations_are_recorded_with_actor_and_changed_fields(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$payload = [
'courier' => '日志测试快递',
'scope' => '自有快递',
'reconciler' => '周会计',
'supplier' => '原供应商',
'types' => ['进仓费'],
'mode' => '固定单价',
'standard' => '10 元/单',
'condition' => '完成进仓',
'effective_at' => '2026-09-21',
'remark' => '初始标准',
];
$this->post(route('business.courier-surcharge.store'), $payload)->assertRedirect();
$quote = CourierSurchargeQuote::query()->where('courier', '日志测试快递')->firstOrFail();
$created = CourierSurchargeAuditLog::query()
->where('target_type', CourierSurchargeAuditLog::TARGET_QUOTE)
->where('target_id', $quote->id)
->where('action', CourierSurchargeAuditLog::ACTION_CREATED)
->firstOrFail();
$this->assertSame((int) user_id(), (int) $created->actor_id);
$this->assertSame('日志测试快递 · 进仓费', $created->target_label);
$this->assertSame('10 元/单', data_get($created->changes, 'after.standard'));
$this->patch(route('business.courier-surcharge.update', ['quote' => $quote->id]), [
...$payload,
'supplier' => '新供应商',
'standard' => '12 元/单',
'remark' => '调整后标准',
])->assertRedirect();
$updated = CourierSurchargeAuditLog::query()
->where('target_type', CourierSurchargeAuditLog::TARGET_QUOTE)
->where('target_id', $quote->id)
->where('action', CourierSurchargeAuditLog::ACTION_UPDATED)
->firstOrFail();
$this->assertSame('10 元/单', data_get($updated->changes, 'before.standard'));
$this->assertSame('12 元/单', data_get($updated->changes, 'after.standard'));
$this->assertSame('初始标准', data_get($updated->changes, 'before.remark'));
$this->assertSame('调整后标准', data_get($updated->changes, 'after.remark'));
$this->assertSame('原供应商', data_get($updated->changes, 'before.supplier'));
$this->assertSame('新供应商', data_get($updated->changes, 'after.supplier'));
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
'status' => CourierSurchargeQuote::STATUS_INACTIVE,
])->assertRedirect();
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
])->assertRedirect();
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
'company_id' => company_id(),
'target_type' => CourierSurchargeAuditLog::TARGET_QUOTE,
'target_id' => $quote->id,
'action' => CourierSurchargeAuditLog::ACTION_DEACTIVATED,
]);
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
'company_id' => company_id(),
'target_type' => CourierSurchargeAuditLog::TARGET_QUOTE,
'target_id' => $quote->id,
'action' => CourierSurchargeAuditLog::ACTION_ACTIVATED,
]);
$response = $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeText('操作日志')
->assertSeeText('请选择上方一条报价')
->assertSeeText('新增')
->assertSeeText('更改')
->assertSeeText('停用')
->assertSeeText('启用')
->assertSeeText($this->user->name)
->assertSeeText('供应商:原供应商 → 新供应商')
->assertSeeText('计价标准:10 元/单 → 12 元/单');
$response
->assertSee('data-audit-quote-id="' . $quote->id . '"', false)
->assertSee('data-audit-log data-target-type="quote" data-target-id="' . $quote->id . '" hidden', false)
->assertSee('aria-selected="false"', false);
}
public function test_type_operations_are_recorded_and_other_company_logs_are_hidden(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$this->post(route('business.courier-surcharge.types.store'), ['name' => '日志专用类型'])
->assertRedirect();
$type = CourierSurchargeType::query()->where('name', '日志专用类型')->firstOrFail();
$this->post(route('business.courier-surcharge.types.status', ['type' => $type->id]), [
'status' => CourierSurchargeType::STATUS_INACTIVE,
])->assertRedirect();
$this->post(route('business.courier-surcharge.types.status', ['type' => $type->id]), [
'status' => CourierSurchargeType::STATUS_ACTIVE,
])->assertRedirect();
$this->assertSame([
CourierSurchargeAuditLog::ACTION_CREATED,
CourierSurchargeAuditLog::ACTION_DEACTIVATED,
CourierSurchargeAuditLog::ACTION_ACTIVATED,
], CourierSurchargeAuditLog::query()
->where('target_type', CourierSurchargeAuditLog::TARGET_TYPE)
->where('target_id', $type->id)
->orderBy('id')
->pluck('action')
->all());
CourierSurchargeAuditLog::withoutGlobalScopes()->create([
'company_id' => company_id() + 999,
'actor_id' => user_id(),
'action' => CourierSurchargeAuditLog::ACTION_CREATED,
'target_type' => CourierSurchargeAuditLog::TARGET_TYPE,
'target_id' => 999,
'target_label' => '其他公司的私有类型',
'changes' => ['after' => ['name' => '其他公司的私有类型']],
]);
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSee('data-audit-log data-target-type="type" data-target-id="' . $type->id . '" hidden', false)
->assertDontSeeText('其他公司的私有类型');
}
public function test_existing_quotes_remain_visible_but_writes_stop_when_audit_table_is_missing(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$quote = $this->createQuote();
Schema::drop('business_courier_surcharge_audit_logs');
$this->loginAs()
->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeText($quote->courier)
->assertSeeText('操作日志表尚未完成迁移')
->assertSee('data-surcharge-open disabled', false);
$this->withExceptionHandling()->post(route('business.courier-surcharge.store'), [
'courier' => '不应保存的快递',
'scope' => '自有快递',
'reconciler' => '周会计',
'types' => ['进仓费'],
'mode' => '固定单价',
'standard' => '10 元/单',
'condition' => '测试',
'effective_at' => '2026-09-21',
])->assertServiceUnavailable();
$this->assertDatabaseMissing('business_courier_surcharge_quotes', ['courier' => '不应保存的快递']);
}
public function test_courier_surcharge_import_template_contains_exact_headers_and_instructions(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$response = $this->loginAs()->get(route('business.courier-surcharge.import.template'));
$response->assertOk()->assertDownload();
$path = $this->temporaryPath('surcharge-template-');
file_put_contents($path, $response->streamedContent());
$spreadsheet = IOFactory::load($path);
try {
$this->assertSame(
CourierSurchargeQuoteWorkbookImporter::HEADERS,
$spreadsheet->getSheet(0)->rangeToArray('A1:K1')[0],
);
$this->assertSame('填写说明', $spreadsheet->getSheet(1)->getTitle());
$this->assertStringContainsString('不会静默覆盖', (string) $spreadsheet->getSheet(1)->getCell('B9')->getValue());
} finally {
$spreadsheet->disconnectWorksheets();
}
}
public function test_valid_workbook_can_be_previewed_and_committed_with_audit_logs(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$file = $this->workbook([[
'导入测试快递', '代发仓', '李会计', '导入供应商', '进仓费、超长费', '固定单价',
'12.50 元/单', '入仓即收取', '2026-09-22', '已停用', '批量导入测试',
]]);
$previewResponse = $this->post(route('business.courier-surcharge.import.preview'), ['import_file' => $file]);
$previewResponse->assertRedirect()->assertSessionHas('courierSurchargeImportPreview', function (array $preview): bool {
return $preview['summary'] === ['total' => 1, 'valid' => 1, 'new' => 1, 'duplicate' => 0, 'invalid' => 0]
&& $preview['rows'][0]['line'] === 2
&& $preview['rows'][0]['data']['types'] === ['进仓费', '超长费'];
});
$preview = $previewResponse->getSession()->get('courierSurchargeImportPreview');
$this->post(route('business.courier-surcharge.import.commit'), [
'preview_token' => $preview['token'],
'duplicate_policy' => CourierSurchargeQuoteWorkbookImporter::DUPLICATE_SKIP,
])->assertRedirect()->assertSessionHas('success', '附加费报价导入完成:新增 1 条,更新 0 条,跳过 0 条。');
$quote = CourierSurchargeQuote::query()->where('courier', '导入测试快递')->firstOrFail();
$this->assertSame(['进仓费', '超长费'], $quote->types);
$this->assertSame('导入供应商', $quote->supplier);
$this->assertSame(CourierSurchargeQuote::STATUS_INACTIVE, $quote->status);
$this->assertSame('批量导入测试', $quote->remark);
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
'target_id' => $quote->id,
'action' => CourierSurchargeAuditLog::ACTION_CREATED,
'actor_id' => $this->user->id,
]);
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
'target_id' => $quote->id,
'action' => CourierSurchargeAuditLog::ACTION_DEACTIVATED,
'actor_id' => $this->user->id,
]);
}
public function test_import_preview_reports_row_errors_and_blocks_commit(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$type = app(CourierSurchargeTypeRepository::class)->all()->firstWhere('name', '超长费');
$this->post(route('business.courier-surcharge.types.status', ['type' => $type->id]), [
'status' => CourierSurchargeType::STATUS_INACTIVE,
])->assertRedirect();
$previewResponse = $this->post(route('business.courier-surcharge.import.preview'), [
'import_file' => $this->workbook([[
'', '未知归属', '李会计', '', '超长费、虚构费', '未知方式', '',
'测试条件', '2026-02-31', '未知状态', '',
]]),
]);
$previewResponse->assertRedirect()->assertSessionHas('courierSurchargeImportPreview', function (array $preview): bool {
$errors = implode('|', $preview['rows'][0]['errors']);
return $preview['summary']['invalid'] === 1
&& str_contains($errors, '快递名称不能为空')
&& str_contains($errors, '附加费类型不存在:虚构费')
&& str_contains($errors, '附加费类型已停用:超长费')
&& str_contains($errors, '生效日期必须是有效的')
&& str_contains($errors, '状态只能填写');
});
$preview = $previewResponse->getSession()->get('courierSurchargeImportPreview');
$this->withExceptionHandling()->post(route('business.courier-surcharge.import.commit'), [
'preview_token' => $preview['token'],
'duplicate_policy' => CourierSurchargeQuoteWorkbookImporter::DUPLICATE_SKIP,
])->assertSessionHasErrors('import');
$this->assertDatabaseMissing('business_courier_surcharge_quotes', ['reconciler' => '李会计', 'condition' => '测试条件']);
}
public function test_import_requires_exact_headers_and_enforces_row_limit(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$wrongHeaders = CourierSurchargeQuoteWorkbookImporter::HEADERS;
$wrongHeaders[0] = '快递公司';
$this->withExceptionHandling()->post(route('business.courier-surcharge.import.preview'), [
'import_file' => $this->workbook([], $wrongHeaders),
])->assertSessionHasErrors('import');
$rows = array_fill(0, CourierSurchargeQuoteWorkbookImporter::MAX_ROWS + 1, [
'测试快递', '自有快递', '周会计', '', '进仓费', '固定单价',
'1 元/单', '测试条件', '2026-09-22', '已启用', '',
]);
$this->withExceptionHandling()->post(route('business.courier-surcharge.import.preview'), [
'import_file' => $this->workbook($rows),
])->assertSessionHasErrors('import');
}
public function test_file_duplicates_are_reported_and_existing_rules_require_an_explicit_policy(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$duplicateRow = [
'文件内重复快递', '自有快递', '周会计', '重复测试供应商', '进仓费', '固定单价',
'5 元/单', '重复条件', '2026-09-22', '已启用', '',
];
$response = $this->post(route('business.courier-surcharge.import.preview'), [
'import_file' => $this->workbook([$duplicateRow, $duplicateRow]),
]);
$response->assertSessionHas('courierSurchargeImportPreview', function (array $preview): bool {
return $preview['summary']['invalid'] === 2
&& str_contains($preview['rows'][0]['errors'][0], '文件内规则重复')
&& str_contains($preview['rows'][1]['errors'][0], '文件内规则重复');
});
$existing = $this->createQuote();
$existingRow = [
$existing->courier, $existing->scope, $existing->reconciler, $existing->supplier, $existing->typeLabel(), $existing->mode,
'新计价标准', $existing->condition, $existing->effective_at->format('Y-m-d'), '已停用', '更新后的备注',
];
$skipPreview = $this->post(route('business.courier-surcharge.import.preview'), [
'import_file' => $this->workbook([$existingRow]),
])->getSession()->get('courierSurchargeImportPreview');
$this->post(route('business.courier-surcharge.import.commit'), [
'preview_token' => $skipPreview['token'],
'duplicate_policy' => CourierSurchargeQuoteWorkbookImporter::DUPLICATE_SKIP,
])->assertSessionHas('success', '附加费报价导入完成:新增 0 条,更新 0 条,跳过 1 条。');
$this->assertSame('最长边 > 160 cm:8 元/单', $existing->fresh()->standard);
$updatePreview = $this->post(route('business.courier-surcharge.import.preview'), [
'import_file' => $this->workbook([$existingRow]),
])->getSession()->get('courierSurchargeImportPreview');
$this->post(route('business.courier-surcharge.import.commit'), [
'preview_token' => $updatePreview['token'],
'duplicate_policy' => CourierSurchargeQuoteWorkbookImporter::DUPLICATE_UPDATE,
])->assertSessionHas('success', '附加费报价导入完成:新增 0 条,更新 1 条,跳过 0 条。');
$existing->refresh();
$this->assertSame('新计价标准', $existing->standard);
$this->assertSame('更新后的备注', $existing->remark);
$this->assertSame(CourierSurchargeQuote::STATUS_INACTIVE, $existing->status);
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
'target_id' => $existing->id,
'action' => CourierSurchargeAuditLog::ACTION_UPDATED,
'actor_id' => $this->user->id,
]);
}
public function test_import_duplicate_identity_includes_optional_supplier(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$base = [
'供应商区分快递', '自有快递', '周会计', '供应商甲', '进仓费', '固定单价',
'5 元/单', '相同条件', '2026-09-22', '已启用', '',
];
$otherSupplier = $base;
$otherSupplier[3] = '供应商乙';
$this->post(route('business.courier-surcharge.import.preview'), [
'import_file' => $this->workbook([$base, $otherSupplier]),
])->assertSessionHas('courierSurchargeImportPreview', function (array $preview): bool {
return $preview['summary'] === ['total' => 2, 'valid' => 2, 'new' => 2, 'duplicate' => 0, 'invalid' => 0];
});
}
public function test_import_commit_rejects_expired_or_other_context_preview(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$token = str_repeat('a', 48);
$this->withSession([
'courier_surcharge_import_previews' => [
$token => [
'company_id' => (int) company_id() + 999,
'user_id' => (int) $this->user->id,
'expires_at' => now()->subMinute()->timestamp,
'rows' => [],
],
],
])->post(route('business.courier-surcharge.import.commit'), [
'preview_token' => $token,
'duplicate_policy' => CourierSurchargeQuoteWorkbookImporter::DUPLICATE_SKIP,
])->assertRedirect()->assertSessionHasErrors('import');
}
/** @param list<list<mixed>> $rows @param list<string>|null $headers */
private function workbook(array $rows, ?array $headers = null): UploadedFile
{
$path = $this->temporaryPath('surcharge-import-');
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray($headers ?? CourierSurchargeQuoteWorkbookImporter::HEADERS, null, 'A1');
if ($rows !== []) {
$sheet->fromArray($rows, null, 'A2');
}
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
return new UploadedFile(
$path,
'附加费报价.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
null,
true,
);
}
private function temporaryPath(string $prefix): string
{
$path = tempnam(sys_get_temp_dir(), $prefix);
$this->temporaryImportFiles[] = $path;
return $path;
}
protected function tearDown(): void
{
foreach ($this->temporaryImportFiles as $path) {
@unlink($path);
}
parent::tearDown();
}
private function createQuote(): CourierSurchargeQuote
{
return CourierSurchargeQuote::query()->create([
'company_id' => company_id(),
'courier' => '曹县德邦',
'scope' => '自有快递',
'reconciler' => '张会计',
'supplier' => '曹县物流供应商',
'types' => ['超长费'],
'mode' => '按尺寸/最长边',
'standard' => '最长边 > 160 cm:8 元/单',
'condition' => '最长边超过 160 cm',
'effective_at' => '2026-09-01',
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
'remark' => '大件渠道标准',
]);
}
}