*/ 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>')); $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="超长费">超长费', $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('曹县德邦', false) ->assertDontSee('霸州百世快运-雅柏', 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> $rows @param list|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' => '大件渠道标准', ]); } }