user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $mirror = \Mockery::mock(CourierInformationMirrorRepository::class); $mirror->shouldReceive('overviewSource')->andReturn(['express' => [ (object) ['expcode' => 'EXP-001', 'expname' => '同名快递网点'], (object) ['expcode' => 'EXP-002', 'expname' => '同名快递网点'], ]]); app()->instance(CourierInformationMirrorRepository::class, $mirror); app()->instance(QuoteManagementSnapshot::class, new QuoteManagementSnapshot('/nonexistent-stock-quote-fixture.json')); } protected function tearDown(): void { foreach ($this->temporaryFiles as $path) { if (is_file($path)) { unlink($path); } } parent::tearDown(); } public function test_entry_uses_existing_courier_and_retains_original_file_formula_and_real_date(): void { $this->loginAs()->get(route('business.courier-stock-quotes.single'))->assertOk() ->assertSee('value="同名快递网点"', false)->assertSeeText('实际开始执行日期')->assertSee('data-stock-courier-code', false); $data = $this->payload(); $originalHash = hash_file('sha256', $data['source_quote']->getRealPath()); $this->loginAs()->post(route('business.courier-stock-quotes.store'), $data)->assertRedirect(); $entry = CourierStockQuoteEntry::firstOrFail(); $this->assertSame('EXP-001', $entry->courier_expcode); $this->assertSame($originalHash, $entry->files[0]['sha256']); $this->assertSame('原始报价.xlsx', $entry->files[0]['name']); $this->assertSame('=B2/2', $entry->sourceQuote()['formulas'][1][2]); $this->assertSame('2025-12-01', $entry->change->effective_date->toDateString()); $this->assertFalse($entry->change->isRetrospective()); Storage::disk('uploads')->assertExists($entry->files[0]['path']); $this->assertDatabaseCount('business_courier_quote_versions', 0); $this->assertDatabaseCount('business_courier_onboardings', 0); $this->loginAs()->get(route('business.courier-quote-changes.show', $entry->change))->assertOk() ->assertSeeText('存量报价原文与核对')->assertSeeText('=B2/2')->assertSeeText('首重1kg'); } public function test_review_requires_confirmation_and_never_claims_unsigned_quote_is_signed(): void { $entry = $this->createEntry(); $this->loginAs()->post(route('business.courier-quote-changes.submit', $entry->change))->assertRedirect(); $reviewer = $this->reviewer(); $this->withExceptionHandling()->loginAs($reviewer, $this->company) ->post(route('business.courier-quote-changes.approve', $entry->change))->assertSessionHasErrors('stock_rules_confirmed'); $this->assertDatabaseCount('business_courier_quote_versions', 0); $this->loginAs($reviewer, $this->company)->post(route('business.courier-quote-changes.approve', $entry->change), ['stock_rules_confirmed' => '1'])->assertRedirect(); $version = CourierQuoteVersion::firstOrFail(); $this->assertSame('存量报价,审核生效(无需签章)', $version->signature_status); $this->assertSame('EXP-001', $version->courier_expcode); $this->assertSame(CourierQuoteChange::RECALCULATION_NOT_REQUIRED, $entry->change->fresh()->recalculation_status); $this->assertSame('2025-12-01', $version->effective_date->toDateString()); $this->assertDatabaseCount('business_courier_quote_signatures', 0); $this->assertSame('生效中', app(QuoteManagementSnapshot::class)->load()['quotes'][0]['status']); $this->assertSame($version->id, \App\Support\CourierRequoteService::resolveAt(collect([$version->fresh()]), now())->id); // 已上线旧记录无需改库或重新审核,也按存量审核规则生效。 $version->update(['signature_status' => '待确认/待补签']); $snapshot = app(QuoteManagementSnapshot::class)->load()['quotes'][0]; $this->assertSame('生效中', $snapshot['status']); $this->assertSame('存量报价,审核生效(无需签章)', $snapshot['signature_status']); } public function test_single_upload_can_create_disjoint_periods_and_cleans_up_files_if_any_period_conflicts(): void { $this->loginAs()->get(route('business.courier-stock-quotes.single'))->assertOk()->assertSeeText('添加执行区间'); $data = $this->payload(); unset($data['effective_date']); $data['periods'] = [ ['effective_date' => '2026-10-21', 'end_date' => '2026-10-24'], ['effective_date' => '2026-11-01', 'end_date' => '2026-11-04'], ['effective_date' => '2026-11-11', 'end_date' => null], ]; $this->post(route('business.courier-stock-quotes.store'), $data)->assertSessionHasNoErrors() ->assertRedirect(route('business.courier-quote-changes.index')); $entries = CourierStockQuoteEntry::orderBy('id')->get(); $this->assertCount(3, $entries); $this->assertCount(1, $entries->pluck('values_json')->unique()); $this->assertCount(1, $entries->pluck('files')->map(fn ($files) => $files[0]['path'])->unique()); $this->assertSame('2026-11-04', $entries[1]->change->end_date->toDateString()); $this->assertNull($entries[2]->end_date); $files = Storage::disk('uploads')->allFiles(); $this->assertCount(1, $files); $conflict = $this->payload(); $conflict['periods'] = [ ['effective_date' => '2026-10-01', 'end_date' => '2026-10-02'], ['effective_date' => '2026-10-24', 'end_date' => '2026-10-25'], ]; $this->withExceptionHandling()->post(route('business.courier-stock-quotes.store'), $conflict)->assertSessionHasErrors('courier_name'); $this->assertDatabaseCount('business_courier_stock_quote_entries', 3); $this->assertSame($files, Storage::disk('uploads')->allFiles()); $conflict['source_quote'] = $this->workbook(); $conflict['periods'][1] = ['effective_date' => '2026-11-04', 'end_date' => '2026-11-01']; $this->post(route('business.courier-stock-quotes.store'), $conflict)->assertSessionHasErrors('periods.1.end_date'); $this->assertDatabaseCount('business_courier_stock_quote_entries', 3); } public function test_historical_documents_require_reviewer_verification_and_remain_distinct_from_esign(): void { $entry = $this->createEntry(['signed_files' => [UploadedFile::fake()->create('历史合同.pdf', 5, 'application/pdf')]]); $this->loginAs()->post(route('business.courier-quote-changes.submit', $entry->change)); $this->loginAs($this->reviewer(), $this->company)->post(route('business.courier-quote-changes.approve', $entry->change), [ 'stock_rules_confirmed' => '1', 'stock_signature_verified' => '1', ])->assertRedirect(); $version = CourierQuoteVersion::firstOrFail(); $this->assertSame('历史签署资料已归档', $version->signature_status); $this->assertDatabaseCount('business_courier_quote_signatures', 0); $snapshot = app(QuoteManagementSnapshot::class)->load()['quotes'][0]; $this->assertSame('生效中', $snapshot['status']); $this->assertArrayHasKey('signed', $snapshot['signed_files']); $this->loginAs()->get(route('business.courier-quote-changes.show', $entry->change))->assertOk() ->assertSeeText('尚无本系统电子签署记录')->assertDontSeeText('生成签署 PDF'); } public function test_claiming_verified_history_without_documents_is_rejected(): void { $entry = $this->createEntry(); $this->loginAs()->post(route('business.courier-quote-changes.submit', $entry->change)); $this->withExceptionHandling()->loginAs($this->reviewer(), $this->company) ->post(route('business.courier-quote-changes.approve', $entry->change), ['stock_rules_confirmed' => '1', 'stock_signature_verified' => '1']) ->assertSessionHasErrors('stock_signature_verified'); $this->assertDatabaseCount('business_courier_quote_versions', 0); } public function test_approved_stock_quote_exposes_all_originals_without_requiring_signature_verification(): void { $entry = $this->createEntry(['signed_files' => [ UploadedFile::fake()->createWithContent('signed-original.pdf', "%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n%%EOF"), UploadedFile::fake()->image('签署原件二.png'), ]]); $this->loginAs()->post(route('business.courier-quote-changes.submit', $entry->change)); $this->loginAs($this->reviewer(), $this->company)->post(route('business.courier-quote-changes.approve', $entry->change), ['stock_rules_confirmed' => '1'])->assertRedirect(); $quote = app(QuoteManagementSnapshot::class)->load()['quotes'][0]; $this->assertSame('生效中', $quote['status']); $this->assertCount(2, $quote['stock_originals']); $this->assertSame([], $quote['signed_files']); $this->loginAs()->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'quote-management'])) ->assertOk()->assertSee('data-stock-original-links', false)->assertSee('预览签署原件'); foreach ($quote['stock_originals'] as $original) { $this->assertTrue($original['previewable']); $response = $this->loginAs()->get($original['url'])->assertOk()->assertHeader('X-Content-Type-Options', 'nosniff'); $this->assertStringStartsWith('inline;', $response->headers->get('Content-Disposition')); } $version = CourierQuoteVersion::firstOrFail(); $version->update(['status' => CourierQuoteVersion::STATUS_VOIDED]); $this->assertNull(\App\Support\CourierRequoteService::resolveAt(collect([$version->fresh()]), now())); } public function test_same_code_and_type_cannot_be_imported_twice_but_same_names_with_different_codes_can(): void { $this->createEntry(); $this->withExceptionHandling()->loginAs()->post(route('business.courier-stock-quotes.store'), $this->payload()) ->assertSessionHasErrors('courier_name'); $this->loginAs()->post(route('business.courier-stock-quotes.store'), [...$this->payload(), 'courier_expcode' => 'EXP-002'])->assertRedirect(); $this->assertDatabaseCount('business_courier_stock_quote_entries', 2); $this->withExceptionHandling()->loginAs()->post(route('business.courier-stock-quotes.store'), [...$this->payload(), 'courier_expcode' => 'UNREGISTERED']) ->assertSessionHasErrors('courier_expcode'); } public function test_returned_quote_releases_period_and_replacement_preserves_review_overlap_protection(): void { $old = $this->createEntry(); $files = $old->files; $changes = app(CourierQuoteChangeService::class); $changes->submit($old->change, (int) $this->user->id); $reviewer = $this->reviewer(); $changes->returnForChanges($old->change->fresh(), '原报价执行日期需重新录入', (int) $reviewer->id); $replacement = $this->createEntry(); $this->assertNotSame($old->id, $replacement->id); $identities = app(\App\Support\CourierStockQuoteEntryService::class)->existingIdentities(); $this->assertSame([0], array_keys($identities)); $this->assertStringStartsWith('[', json_encode($identities, JSON_THROW_ON_ERROR)); $this->assertSame(CourierQuoteChange::STATUS_RETURNED, $old->change->fresh()->status); $this->assertSame($files, $old->fresh()->files); Storage::disk('uploads')->assertExists($files[0]['path']); $this->withExceptionHandling()->loginAs()->post(route('business.courier-quote-changes.submit', $old->change)) ->assertSessionHasErrors('courier_name'); $this->assertSame(CourierQuoteChange::STATUS_RETURNED, $old->change->fresh()->status); $changes->submit($replacement->change, (int) $this->user->id); $this->post(route('business.courier-stock-quotes.store'), $this->payload())->assertSessionHasErrors('courier_name'); $changes->approve($replacement->change->fresh(), (int) $reviewer->id); $this->post(route('business.courier-stock-quotes.store'), $this->payload())->assertSessionHasErrors('courier_name'); $this->assertDatabaseCount('business_courier_stock_quote_entries', 2); $this->assertDatabaseCount('business_courier_quote_versions', 1); } public function test_returned_quote_can_be_resubmitted_when_its_period_is_still_available(): void { $entry = $this->createEntry(); $changes = app(CourierQuoteChangeService::class); $changes->submit($entry->change, (int) $this->user->id); $changes->returnForChanges($entry->change->fresh(), '重新核对后提交', (int) $this->reviewer()->id); $this->loginAs()->post(route('business.courier-quote-changes.submit', $entry->change->fresh())) ->assertSessionHasNoErrors()->assertRedirect(); $this->assertSame(CourierQuoteChange::STATUS_PENDING_REVIEW, $entry->change->fresh()->status); $this->assertDatabaseCount('business_courier_stock_quote_entries', 1); $this->assertDatabaseCount('business_courier_quote_versions', 0); } public function test_multi_sheet_upload_requires_exact_sheet_and_invalid_workbooks_leave_no_record(): void { $this->withExceptionHandling()->loginAs()->post(route('business.courier-stock-quotes.store'), [...$this->payload(), 'source_quote' => $this->workbook(true)]) ->assertSessionHasErrors('sheet_name'); $this->assertDatabaseCount('business_courier_stock_quote_entries', 0); $this->loginAs()->post(route('business.courier-stock-quotes.store'), [...$this->payload(), 'source_quote' => $this->workbook(true), 'sheet_name' => '报价']) ->assertRedirect(); } public function test_manual_name_can_be_corrected_in_draft_and_follows_review_and_later_price_change(): void { $entry = $this->createEntry(['courier_expcode' => null, 'courier_name' => '手填名称待修正', 'signed_files' => [UploadedFile::fake()->create('签署原件.pdf', 1, 'application/pdf')]]); $files = $entry->files; $this->assertNull($entry->courier_expcode); $this->loginAs()->patch(route('business.courier-stock-quotes.update', $entry), [ 'courier_name' => '发货数据完整快递名称', 'effective_date' => '2025-12-01', 'billing_rules' => '首重1kg,续重按原报价', 'summary' => '更正为发货名称', ])->assertRedirect(); $entry->refresh(); $this->assertSame('发货数据完整快递名称', $entry->courier_name); $this->assertSame('发货数据完整快递名称', $entry->change->carrier_name); $this->assertSame('报价', $entry->sheet_name); $this->assertSame($files, $entry->files); $savedDetails = json_decode($entry->change->events()->where('event_type', 'saved')->latest('id')->firstOrFail()->details_json, true); $this->assertSame('发货数据完整快递名称', $savedDetails['carrier_name']); $service = app(CourierQuoteChangeService::class); $reviewer = (int) $this->reviewer()->id; $service->submit($entry->change, (int) $this->user->id); $service->approve($entry->change->fresh(), $reviewer, true); $original = CourierQuoteVersion::firstOrFail(); $this->assertNull($original->courier_expcode); $this->assertSame('发货数据完整快递名称', $original->company_name); $this->assertSame('生效中', app(QuoteManagementSnapshot::class)->load()['quotes'][0]['status']); $change = $service->create([ 'source_quote_id' => $original->version_code, 'change_type' => '费率调整', 'effective_date' => '2026-01-01', 'summary' => '后续变价', 'items' => [['source_cell' => 'B2', 'source_row' => 1, 'source_column' => 1, 'original_amount' => 5, 'new_amount' => 7]], ], (int) $this->user->id); $service->submit($change, (int) $this->user->id); $changed = $service->approve($change->fresh(), $reviewer); $this->assertSame('发货数据完整快递名称', $changed->version->company_name); $this->assertNull($changed->version->courier_expcode); $this->assertEquals(5, json_decode($original->fresh()->values_json, true)[1][1]); $this->assertEquals(7, json_decode($changed->version->values_json, true)[1][1]); $this->assertSame($files, $entry->fresh()->files); } public function test_shipping_names_can_be_suggested_without_a_courier_directory_record(): void { $shipping = \Mockery::mock(\Modules\EcommerceFinance\Repositories\ShippingOrderRepository::class); $shipping->shouldReceive('recentCourierNames')->once()->with('缺资料', 20)->andReturn(['缺资料发货快递']); app()->instance(\Modules\EcommerceFinance\Repositories\ShippingOrderRepository::class, $shipping); $this->loginAs()->getJson(route('business.courier-stock-quotes.courier-names', ['q' => '缺资料']))->assertOk()->assertJsonPath('items.0', '缺资料发货快递'); $this->assertDatabaseCount('business_courier_onboardings', 0); } public function test_name_only_versions_of_different_quote_types_do_not_supersede_each_other(): void { $service = app(CourierQuoteChangeService::class); $reviewer = (int) $this->reviewer()->id; foreach (['标准快递报价', '大件快递报价'] as $type) { $entry = $this->createEntry(['courier_expcode' => null, 'courier_name' => '同家无编号快递', 'quote_type' => $type, 'signed_files' => [UploadedFile::fake()->create('签署原件.pdf', 1, 'application/pdf')]]); $service->submit($entry->change, (int) $this->user->id); $service->approve($entry->change->fresh(), $reviewer, true); } $this->assertSame(['生效中', '生效中'], array_column(app(QuoteManagementSnapshot::class)->load()['quotes'], 'status')); } public function test_draft_replacement_retains_previous_original_and_updates_preview_and_audit(): void { $entry = $this->createEntry(); $originalPath = $entry->files[0]['path']; $this->loginAs()->patch(route('business.courier-stock-quotes.update', $entry), [ 'effective_date' => '2025-11-01', 'billing_rules' => '修正后首重1kg', 'summary' => '更正原报价附件', 'source_quote' => $this->workbook(false, 8), ])->assertRedirect(); $entry->refresh(); $this->assertCount(2, $entry->files); $this->assertSame('previous_source_quote', $entry->files[0]['kind']); Storage::disk('uploads')->assertExists($originalPath); $this->assertEquals(8, $entry->sourceQuote()['values'][1][1]); $this->assertEquals(8, json_decode($entry->change->proposed_values_json, true)[1][1]); $details = json_decode($entry->change->events()->where('event_type', 'saved')->firstOrFail()->details_json, true); $this->assertSame('修正后首重1kg', $details['billing_rules']); $this->assertCount(2, $details['files']); } public function test_followup_price_change_reuses_version_and_keeps_exact_courier_identity(): void { $entry = $this->createEntry(); app(CourierQuoteChangeService::class)->submit($entry->change, (int) $this->user->id); app(CourierQuoteChangeService::class)->approve($entry->change->fresh(), (int) $this->reviewer()->id); $version = CourierQuoteVersion::firstOrFail(); $this->loginAs()->get(route('business.courier-quote-changes.start', ['source_quote_id' => $version->version_code])) ->assertOk()->assertSeeText('编辑新价格'); $change = app(CourierQuoteChangeService::class)->create([ 'source_quote_id' => $version->version_code, 'change_type' => '费率调整', 'effective_date' => '2026-10-16', 'summary' => '存量快递后续变价', 'items' => [['source_cell' => 'B2', 'source_row' => 1, 'source_column' => 1, 'original_amount' => 5, 'new_amount' => 6]], ], (int) $this->user->id); app(CourierQuoteChangeService::class)->submit($change, (int) $this->user->id); $approved = app(CourierQuoteChangeService::class)->approve($change->fresh(), (int) $this->reviewer()->id); $this->assertSame('EXP-001', $approved->version->courier_expcode); $this->assertSame('首重1kg,续重按原报价', $approved->version->billing_rules); $this->assertEquals(6, json_decode($approved->version->values_json, true)[1][1]); $this->assertEquals(5, json_decode($version->fresh()->values_json, true)[1][1]); } public function test_other_company_cannot_download_or_edit_stock_quote(): void { $entry = $this->createEntry(); \DB::table('business_courier_stock_quote_entries')->where('id', $entry->id)->update(['company_id' => $this->company->id + 999]); $this->withExceptionHandling()->loginAs() ->get(route('business.courier-stock-quotes.download', [$entry, 0]))->assertNotFound(); $this->loginAs()->get(route('business.courier-stock-quotes.edit', $entry))->assertNotFound(); $this->assertNull(CourierStockQuoteEntry::find($entry->id)); } public function test_same_name_couriers_keep_separate_effective_versions(): void { $reviewer = $this->reviewer(); foreach (['EXP-001', 'EXP-002'] as $code) { $entry = $this->createEntry(['courier_expcode' => $code, 'signed_files' => [UploadedFile::fake()->create('回签.pdf', 1, 'application/pdf')]]); app(CourierQuoteChangeService::class)->submit($entry->change, (int) $this->user->id); app(CourierQuoteChangeService::class)->approve($entry->change->fresh(), (int) $reviewer->id, true); } $quotes = app(QuoteManagementSnapshot::class)->load()['quotes']; $this->assertCount(2, $quotes); $this->assertSame(['生效中', '生效中'], array_column($quotes, 'status')); $this->assertSame(['EXP-001', 'EXP-002'], array_column($quotes, 'courier_expcode')); } private function createEntry(array $overrides = []): CourierStockQuoteEntry { $this->loginAs()->post(route('business.courier-stock-quotes.store'), [...$this->payload(), ...$overrides])->assertRedirect(); return CourierStockQuoteEntry::latest('id')->firstOrFail(); } public function test_same_courier_can_import_disjoint_periods_and_resolve_history_without_temporary_price_semantics(): void { $first = $this->createEntry(['effective_date' => '2025-12-01', 'end_date' => '2026-09-30']); $second = $this->createEntry(['effective_date' => '2026-10-01', 'end_date' => null]); $reviewer = $this->reviewer(); foreach ([$first, $second] as $entry) { app(CourierQuoteChangeService::class)->submit($entry->change, $this->user->id); app(CourierQuoteChangeService::class)->approve($entry->change->fresh(), $reviewer->id); } $versions = CourierQuoteVersion::with('change')->get(); $this->assertCount(2, $versions); $this->assertFalse($versions[0]->isTemporary()); $this->assertSame('2025-12-01 至 2026-09-30', $versions[0]->periodLabel()); $this->assertSame($versions[0]->id, \App\Support\CourierRequoteService::resolveAt($versions, new \DateTimeImmutable('2026-09-30'))->id); $this->assertSame($versions[1]->id, \App\Support\CourierRequoteService::resolveAt($versions, new \DateTimeImmutable('2026-10-01'))->id); $quotes = app(QuoteManagementSnapshot::class)->load()['quotes']; $this->assertSame($quotes[0]['group_key'], $quotes[1]['group_key']); $this->assertSame('已到期', $quotes[0]['status']); $this->assertSame('生效中', $quotes[1]['status']); } public function test_overlapping_periods_and_shared_boundary_day_are_rejected_but_earlier_history_is_allowed(): void { $this->createEntry(['effective_date' => '2026-10-01']); foreach ([['2026-09-01', '2026-10-01'], ['2026-10-02', null], ['2026-11-01', '2026-10-01']] as [$start, $end]) { $this->withExceptionHandling()->loginAs()->post(route('business.courier-stock-quotes.store'), [...$this->payload(), 'effective_date' => $start, 'end_date' => $end])->assertSessionHasErrors(); } $this->assertDatabaseCount('business_courier_stock_quote_entries', 1); $this->createEntry(['effective_date' => '2025-12-01', 'end_date' => '2026-09-30']); $this->assertDatabaseCount('business_courier_stock_quote_entries', 2); } public function test_explicit_period_end_preserves_originals_and_audits_change_before_next_period_import(): void { $entry = $this->createEntry(['effective_date' => '2025-12-01']); $reviewer = $this->reviewer(); app(CourierQuoteChangeService::class)->submit($entry->change, $this->user->id); app(CourierQuoteChangeService::class)->approve($entry->change->fresh(), $reviewer->id); $original = $entry->values_json; $files = $entry->files; $this->loginAs($reviewer, $this->company)->post(route('business.courier-stock-quotes.period-end', $entry), ['end_date' => '2026-09-30', 'reason' => '依据新旧报价交接日期'])->assertRedirect(); $this->assertSame('2026-09-30', $entry->fresh()->end_date->toDateString()); $this->assertSame('2026-09-30', $entry->change->fresh()->version->end_date->toDateString()); $this->assertSame($original, $entry->fresh()->values_json); $this->assertSame($files, $entry->fresh()->files); $this->assertDatabaseHas('business_courier_quote_change_events', ['quote_change_id' => $entry->source_change_id, 'event_type' => 'period_end_updated', 'created_by' => $reviewer->id]); $this->createEntry(['effective_date' => '2026-10-01']); $this->withExceptionHandling()->loginAs($reviewer, $this->company)->post(route('business.courier-stock-quotes.period-end', $entry), ['end_date' => '2026-10-01', 'reason' => '重叠边界验证'])->assertSessionHasErrors('courier_name'); $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'quote-management']))->assertOk()->assertSeeText('执行期间重叠'); $this->assertSame('2026-09-30', $entry->fresh()->end_date->toDateString()); } public function test_period_end_requires_admin_and_current_company(): void { $entry = $this->createEntry(); $reader = $this->reviewer(); $reader->roles()->detach(); $this->withExceptionHandling()->loginAs($reader, $this->company)->post(route('business.courier-stock-quotes.period-end', $entry), ['end_date' => '2026-09-30', 'reason' => '权限检查'])->assertForbidden(); \DB::table('business_courier_stock_quote_entries')->where('id', $entry->id)->update(['company_id' => $this->company->id + 999]); $this->withExceptionHandling()->loginAs($this->reviewer(), $this->company)->post(route('business.courier-stock-quotes.period-end', $entry), ['end_date' => '2026-09-30', 'reason' => '公司隔离检查'])->assertNotFound(); } public function test_period_prefill_preserves_selected_code_for_same_named_couriers(): void { $response = $this->loginAs()->get(route('business.courier-stock-quotes.single', ['courier_name' => '同名快递网点', 'courier_expcode' => 'EXP-002', 'quote_type' => '标准快递报价']))->assertOk(); $document = new \DOMDocument; @$document->loadHTML($response->getContent(), LIBXML_NONET); $code = (new \DOMXPath($document))->query('//input[@name="courier_expcode"]')->item(0)->getAttribute('value'); $this->assertSame('EXP-002', $code); $this->post(route('business.courier-stock-quotes.store'), [...$this->payload(), 'courier_name' => '同名快递网点', 'courier_expcode' => $code])->assertRedirect(); $this->assertSame('EXP-002', CourierStockQuoteEntry::firstOrFail()->courier_expcode); } private function payload(): array { return ['courier_expcode' => 'EXP-001', 'quote_type' => '标准快递报价', 'source_quote' => $this->workbook(), 'effective_date' => '2025-12-01', 'billing_rules' => '首重1kg,续重按原报价', 'summary' => '补录历史合作报价']; } private function workbook(bool $multipleSheets = false, int $price = 5): UploadedFile { $book = new Spreadsheet(); $sheet = $book->getActiveSheet()->setTitle('报价'); $sheet->fromArray([['省份', '首重', '续重'], ['江苏省', $price, null]]); $sheet->setCellValue('C2', '=B2/2'); if ($multipleSheets) { $book->createSheet()->setTitle('其他网点')->setCellValue('A1', '其他报价'); } $path = tempnam(sys_get_temp_dir(), 'stock-quote-'); $this->temporaryFiles[] = $path; IOFactory::createWriter($book, 'Xlsx')->save($path); $book->disconnectWorksheets(); return new UploadedFile($path, '原始报价.xlsx', null, null, true); } private function reviewer() { $user = user_model_class()::create([ 'name' => '报价审核员', 'email' => 'stock-reviewer-' . uniqid() . '@example.com', 'password' => 'local-test-password', 'locale' => 'zh-CN', 'enabled' => 1, 'landing_page' => 'dashboard', 'created_from' => 'core::test', ]); $user->companies()->attach($this->company->id); $user->attachRole(Role::where('name', 'admin')->firstOrFail()); $user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); return $user; } }