700 lines
32 KiB
PHP
700 lines
32 KiB
PHP
<?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('附加费报价')
|
||
->assertSee('aria-label="附加费报价维护"', false)
|
||
->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' => '大件渠道标准',
|
||
]);
|
||
}
|
||
}
|