CaiWuXiTong/tests/Feature/Business/CourierStockQuoteEntryTest.php

240 lines
14 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\Models\Auth\Permission;
use App\Models\Auth\Role;
use App\Models\Business\CourierQuoteChange;
use App\Models\Business\CourierQuoteVersion;
use App\Models\Business\CourierStockQuoteEntry;
use App\Support\CourierQuoteChangeService;
use App\Support\QuoteManagementSnapshot;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use Tests\Feature\FeatureTestCase;
class CourierStockQuoteEntryTest extends FeatureTestCase
{
private array $temporaryFiles = [];
protected function setUp(): void
{
parent::setUp();
Storage::fake('uploads');
$this->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()
->assertSeeText('EXP-001')->assertSeeText('EXP-002')->assertSeeText('实际开始执行日期');
$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']);
}
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_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_expcode');
$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_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_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();
}
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;
}
}