408 lines
26 KiB
PHP
408 lines
26 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use Akaunting\Firewall\Middleware\Xss;
|
||
use App\Models\Auth\Permission;
|
||
use App\Models\Business\CourierQuoteChange;
|
||
use App\Models\Business\CourierStockQuoteEntry;
|
||
use App\Support\CourierStockQuoteBatchService;
|
||
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 PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
class CourierStockQuoteBatchTest extends FeatureTestCase
|
||
{
|
||
private array $temporaryFiles = [];
|
||
|
||
private array $sourceImages = [];
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
Storage::fake('uploads');
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->mirror([
|
||
['code' => 'EXP-001', 'name' => '同名网点'], ['code' => 'EXP-002', 'name' => '同名网点'],
|
||
['code' => 'EXP-003', 'name' => '第三网点'],
|
||
]);
|
||
app()->instance(QuoteManagementSnapshot::class, new QuoteManagementSnapshot('/nonexistent-batch-quotes.json'));
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
foreach ($this->temporaryFiles as $path) {
|
||
if (is_file($path)) {
|
||
unlink($path);
|
||
}
|
||
}
|
||
parent::tearDown();
|
||
}
|
||
|
||
public function test_preview_keeps_workbook_without_creating_quotes_and_never_guesses_ambiguous_names(): void
|
||
{
|
||
$this->loginAs()->get(route('business.courier-stock-quotes.create'))->assertOk()
|
||
->assertSeeText('共用计费规则')->assertSeeText('只补录一家');
|
||
$batch = $this->preview(['EXP-001', '同名网点', '第三网点', '说明']);
|
||
$this->get(route('business.courier-stock-quotes.batch', $batch))->assertOk()
|
||
->assertSeeText('名称重复,请选编号')->assertSeeText('保存并提交审核');
|
||
$service = app(CourierStockQuoteBatchService::class);
|
||
$manifest = $service->load($batch, (int) $this->user->id);
|
||
$rows = $service->rows($manifest, app(\App\Support\CourierStockQuoteEntryService::class)->courierOptions());
|
||
$this->assertSame(['EXP-001', '', 'EXP-003', ''], array_column($rows, 'courier_expcode'));
|
||
$this->assertSame('=B2/2', $manifest['sheets'][0]['formulas'][1][2]);
|
||
Storage::disk('uploads')->assertExists($manifest['source_file']['path']);
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
$this->assertDatabaseCount('business_courier_quote_changes', 0);
|
||
}
|
||
|
||
public function test_bulk_save_reuses_rule_groups_overrides_dates_and_preserves_separate_prices(): void
|
||
{
|
||
$batch = $this->preview(['EXP-001', 'EXP-002', '说明']);
|
||
$selection = $this->selection(['EXP-001', 'EXP-002', '']);
|
||
$selection['entries'][2]['selected'] = false;
|
||
$selection['rule_groups'][] = ['name' => '单独规则', 'rules' => '超长件按实际重量计费'];
|
||
$selection['entries'][1]['rule_group'] = 1;
|
||
$selection['entries'][1]['effective_date'] = '2025-11-01';
|
||
$url = route('business.courier-stock-quotes.batch-save', $batch);
|
||
$this->loginAs()->post($url, ['selection_json' => json_encode($selection)])->assertRedirect();
|
||
$entries = CourierStockQuoteEntry::with('change')->orderBy('id')->get();
|
||
$this->assertCount(2, $entries);
|
||
$this->assertSame('按原表重量段计费,面单另计', $entries[0]->change->quote_payload['billing_rules']);
|
||
$this->assertSame('超长件按实际重量计费', $entries[1]->change->quote_payload['billing_rules']);
|
||
$this->assertSame('2025-11-01', $entries[1]->change->effective_date->toDateString());
|
||
$this->assertEquals(5, $entries[0]->sourceQuote()['values'][1][1]);
|
||
$this->assertEquals(6, $entries[1]->sourceQuote()['values'][1][1]);
|
||
$this->assertSame($entries[0]->files[0]['path'], $entries[1]->files[0]['path']);
|
||
$this->assertSame(CourierQuoteChange::STATUS_DRAFT, $entries[0]->change->status);
|
||
$this->assertDatabaseCount('business_courier_quote_versions', 0);
|
||
$this->get(route('business.courier-stock-quotes.batch', $batch))->assertOk()->assertSeeText('已录入');
|
||
// 同一预览重发只返回原结果,不生成重复记录。
|
||
$this->post($url, ['selection_json' => json_encode($selection)])->assertRedirect();
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 2);
|
||
}
|
||
|
||
public function test_bulk_submit_requires_confirmation_and_does_not_approve_or_fake_signatures(): void
|
||
{
|
||
$batch = $this->preview(['EXP-001', 'EXP-002']);
|
||
$selection = [...$this->selection(['EXP-001', 'EXP-002']), 'submit_for_review' => true, 'confirmed' => false];
|
||
$url = route('business.courier-stock-quotes.batch-save', $batch);
|
||
$this->withExceptionHandling()->loginAs()->post($url, ['selection_json' => json_encode($selection)])->assertSessionHasErrors('confirmed');
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
$selection['confirmed'] = true;
|
||
$this->post($url, ['selection_json' => json_encode($selection)])->assertSessionHasNoErrors()->assertRedirect();
|
||
$this->assertSame([CourierQuoteChange::STATUS_PENDING_REVIEW], CourierQuoteChange::pluck('status')->unique()->all());
|
||
$this->assertDatabaseCount('business_courier_quote_versions', 0);
|
||
$this->assertDatabaseCount('business_courier_quote_signatures', 0);
|
||
}
|
||
|
||
public function test_structured_json_save_passes_real_xss_checks_and_returns_correctable_errors(): void
|
||
{
|
||
$batch = $this->preview(['EXP-001', 'EXP-002']);
|
||
$selection = $this->selection(['EXP-001', 'EXP-002']);
|
||
// 合法名称中出现 JavaScript,但没有协议或标签;序列化后会与后续字段的冒号串联误匹配。
|
||
$selection['summary'] = 'JavaScript物流报价补录';
|
||
$middleware = new Xss();
|
||
$middleware->prepare(\Illuminate\Http\Request::create('/', 'POST'));
|
||
$this->assertSame(1, $middleware->match($middleware->getPatterns()[3], ['selection_json' => json_encode($selection)]));
|
||
config(['firewall.middleware.xss.enabled' => true]);
|
||
$url = route('business.courier-stock-quotes.batch-save', $batch);
|
||
$selection['entries'][1]['courier_expcode'] = 'EXP-001';
|
||
$this->withExceptionHandling()->loginAs()->postJson($url, $selection)
|
||
->assertUnprocessable()->assertJsonValidationErrors('entries.1');
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
$selection['entries'][1]['courier_expcode'] = 'EXP-002';
|
||
$selection['submit_for_review'] = $selection['confirmed'] = true;
|
||
$this->postJson($url, $selection)->assertOk()
|
||
->assertJsonPath('redirect', route('business.courier-stock-quotes.batch', $batch));
|
||
$this->assertSame([CourierQuoteChange::STATUS_PENDING_REVIEW], CourierQuoteChange::pluck('status')->unique()->all());
|
||
$this->postJson($url, $selection)->assertOk();
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 2);
|
||
$this->assertDatabaseCount('business_courier_quote_versions', 0);
|
||
}
|
||
|
||
public function test_structured_json_keeps_malicious_fields_blocked_by_real_xss_middleware(): void
|
||
{
|
||
$batch = $this->preview(['EXP-001']);
|
||
$selection = $this->selection(['EXP-001']);
|
||
$selection['rule_groups'][0]['rules'] = '<script>alert(1)</script>';
|
||
config(['firewall.middleware.xss.enabled' => true, 'firewall.logs.enabled' => false]);
|
||
$this->withExceptionHandling()->loginAs()->postJson(route('business.courier-stock-quotes.batch-save', $batch), $selection)
|
||
->assertForbidden();
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
}
|
||
|
||
public function test_row_errors_are_atomic_keep_preview_and_do_not_override_existing_quotes(): void
|
||
{
|
||
$batch = $this->preview(['EXP-001', 'EXP-002']);
|
||
$selection = $this->selection(['EXP-001', 'EXP-001']);
|
||
$this->withExceptionHandling()->loginAs()->from(route('business.courier-stock-quotes.batch', $batch))
|
||
->post(route('business.courier-stock-quotes.batch-save', $batch), ['selection_json' => json_encode($selection)])
|
||
->assertSessionHasErrors('entries.1')->assertSessionHasInput('selection_json');
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
$selection['entries'][1]['courier_expcode'] = 'EXP-002';
|
||
$this->post(route('business.courier-stock-quotes.batch-save', $batch), ['selection_json' => json_encode($selection)])->assertSessionHasNoErrors();
|
||
$another = $this->preview(['EXP-001']);
|
||
$this->post(route('business.courier-stock-quotes.batch-save', $another), ['selection_json' => json_encode($this->selection(['EXP-001']))])
|
||
->assertSessionHasErrors('entries.0');
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 2);
|
||
$this->get(route('business.courier-stock-quotes.batch', $another))->assertOk()->assertSeeText('已录入,默认跳过');
|
||
}
|
||
|
||
public function test_real_date_and_known_courier_are_required_but_notes_can_be_skipped(): void
|
||
{
|
||
$batch = $this->preview(['EXP-001', '说明']);
|
||
$selection = $this->selection(['EXP-001', 'UNKNOWN']);
|
||
$selection['effective_date'] = '';
|
||
$this->withExceptionHandling()->loginAs()->post(route('business.courier-stock-quotes.batch-save', $batch), ['selection_json' => json_encode($selection)])
|
||
->assertSessionHasErrors(['entries.0', 'entries.1']);
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
$selection['entries'][0]['effective_date'] = '2024-06-01';
|
||
$selection['entries'][1]['selected'] = false;
|
||
$this->post(route('business.courier-stock-quotes.batch-save', $batch), ['selection_json' => json_encode($selection)])->assertSessionHasNoErrors();
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 1);
|
||
}
|
||
|
||
public function test_preview_is_scoped_to_company_and_uploader_and_rejects_fake_sheet_indices(): void
|
||
{
|
||
$batch = $this->preview(['EXP-001']);
|
||
$manifest = app(CourierStockQuoteBatchService::class)->load($batch, (int) $this->user->id);
|
||
$manifestPath = dirname($manifest['source_file']['path']) . '/manifest.json';
|
||
$selection = $this->selection(['EXP-001']);
|
||
$selection['entries'] = [50 => $selection['entries'][0]];
|
||
$this->withExceptionHandling()->loginAs()->post(route('business.courier-stock-quotes.batch-save', $batch), ['selection_json' => json_encode($selection)])
|
||
->assertSessionHasErrors('entries.50');
|
||
$manifest['user_id'] = (int) $this->user->id + 1;
|
||
Storage::disk('uploads')->put($manifestPath, json_encode($manifest));
|
||
$this->get(route('business.courier-stock-quotes.batch', $batch), ['X-Requested-With' => 'XMLHttpRequest'])->assertNotFound();
|
||
$manifest['user_id'] = (int) $this->user->id;
|
||
$manifest['company_id'] = (int) company_id() + 1;
|
||
Storage::disk('uploads')->put($manifestPath, json_encode($manifest));
|
||
$this->get(route('business.courier-stock-quotes.batch', $batch), ['X-Requested-With' => 'XMLHttpRequest'])->assertNotFound();
|
||
}
|
||
|
||
public function test_one_hundred_twenty_sheets_preview_and_save_as_one_batch(): void
|
||
{
|
||
$couriers = $names = [];
|
||
for ($index = 1; $index <= 120; $index++) {
|
||
$names[] = $code = sprintf('BULK-%03d', $index);
|
||
$couriers[] = ['code' => $code, 'name' => '批量网点' . $index];
|
||
}
|
||
$this->mirror($couriers);
|
||
$batch = $this->preview($names, array_fill(0, 120, 1));
|
||
$this->get(route('business.courier-stock-quotes.batch', $batch))->assertOk()->assertSeeText('120');
|
||
config(['firewall.middleware.xss.enabled' => true]);
|
||
$this->postJson(route('business.courier-stock-quotes.batch-save', $batch), $this->selection($names))->assertOk();
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 120);
|
||
$this->assertDatabaseCount('business_courier_quote_changes', 120);
|
||
$this->assertCount(1, CourierStockQuoteEntry::all()->pluck('files')->map(fn ($files) => $files[0]['path'])->unique());
|
||
$this->assertCount(120, CourierStockQuoteEntry::all()->pluck('files')->map(fn ($files) => $files[1]['path'])->unique());
|
||
}
|
||
|
||
public function test_empty_sheet_can_be_skipped_and_failed_result_storage_rolls_back_the_entire_batch(): void
|
||
{
|
||
$batch = $this->preview(['EXP-001', '空表']);
|
||
$this->get(route('business.courier-stock-quotes.batch', $batch))->assertOk()->assertSeeText('空工作表,请取消选择');
|
||
$selection = $this->selection(['EXP-001', 'EXP-002']);
|
||
$this->withExceptionHandling()->loginAs()->post(route('business.courier-stock-quotes.batch-save', $batch), ['selection_json' => json_encode($selection)])
|
||
->assertSessionHasErrors('entries.1');
|
||
$selection['entries'][1]['selected'] = false;
|
||
$disk = Storage::disk('uploads');
|
||
$failingDisk = \Mockery::mock($disk);
|
||
$failingDisk->shouldReceive('put')->once()->andReturn(false);
|
||
Storage::shouldReceive('disk')->with('uploads')->andReturn($failingDisk);
|
||
$this->post(route('business.courier-stock-quotes.batch-save', $batch), ['selection_json' => json_encode($selection)])
|
||
->assertSessionHasErrors('source_quote');
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
$this->assertDatabaseCount('business_courier_quote_changes', 0);
|
||
}
|
||
|
||
public function test_embedded_images_are_archived_byte_for_byte_per_sheet_and_never_auto_verified(): void
|
||
{
|
||
$file = $this->workbook(['EXP-001', 'EXP-002'], [2, 1]);
|
||
$response = $this->loginAs()->post(route('business.courier-stock-quotes.batch-preview'), [
|
||
'source_quote' => $file, 'quote_type' => '标准快递报价',
|
||
])->assertSessionHasNoErrors()->assertRedirect();
|
||
$batch = basename($response->headers->get('Location'));
|
||
$manifest = app(CourierStockQuoteBatchService::class)->load($batch, (int) $this->user->id);
|
||
$this->assertCount(2, $manifest['sheets'][0]['signed_images']);
|
||
$this->assertCount(1, $manifest['sheets'][1]['signed_images']);
|
||
foreach ($manifest['sheets'] as $sheetIndex => $sheet) {
|
||
foreach ($sheet['signed_images'] as $imageIndex => $image) {
|
||
$this->assertSame($this->sourceImages[$sheetIndex][$imageIndex], Storage::disk('uploads')->get($image['path']));
|
||
$this->assertSame(hash('sha256', $this->sourceImages[$sheetIndex][$imageIndex]), $image['sha256']);
|
||
$this->assertSame($sheet['sheet_name'], $image['sheet_name']);
|
||
$this->assertSame('G' . (2 + $imageIndex * 20), $image['anchor']);
|
||
$this->assertSame('image/png', $image['mime_type']);
|
||
$url = route('business.courier-stock-quotes.batch-image', [$batch, $sheetIndex, $imageIndex]);
|
||
$download = $this->get($url)->assertOk()->assertDownload()->assertStreamedContent($this->sourceImages[$sheetIndex][$imageIndex]);
|
||
$this->assertStringContainsString(rawurlencode($image['name']), $download->headers->get('Content-Disposition'));
|
||
$this->get($url . '?preview=1')->assertOk()->assertHeader('Content-Type', 'image/png')
|
||
->assertHeader('X-Content-Type-Options', 'nosniff')->assertStreamedContent($this->sourceImages[$sheetIndex][$imageIndex]);
|
||
}
|
||
}
|
||
$this->get(route('business.courier-stock-quotes.batch', $batch))->assertOk()->assertSeeText('已留存 2 张 · 待核对');
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
$selection = [...$this->selection(['EXP-001', 'EXP-002']), 'submit_for_review' => true, 'confirmed' => true];
|
||
$this->post(route('business.courier-stock-quotes.batch-save', $batch), ['selection_json' => json_encode($selection)])->assertSessionHasNoErrors();
|
||
$entries = CourierStockQuoteEntry::orderBy('id')->get();
|
||
$this->assertSame([3, 2], $entries->map(fn ($entry) => count($entry->files))->all());
|
||
$this->assertSame($manifest['sheets'][0]['signed_images'][0]['path'], $entries[0]->files[1]['path']);
|
||
$this->assertSame($manifest['sheets'][1]['signed_images'][0]['path'], $entries[1]->files[1]['path']);
|
||
$this->assertSame([CourierQuoteChange::STATUS_PENDING_REVIEW], CourierQuoteChange::pluck('status')->unique()->all());
|
||
$this->assertDatabaseCount('business_courier_quote_versions', 0);
|
||
$this->assertDatabaseCount('business_courier_quote_signatures', 0);
|
||
$this->get(route('business.courier-quote-changes.show', $entries[0]->change))->assertOk()->assertSeeText('查看原图');
|
||
$this->get(route('business.courier-stock-quotes.download', [$entries[0], 1, 'preview' => 1]))->assertOk()
|
||
->assertStreamedContent($this->sourceImages[0][0]);
|
||
}
|
||
|
||
public function test_image_storage_failure_does_not_publish_a_preview_or_create_quotes(): void
|
||
{
|
||
$file = $this->workbook(['EXP-001'], [1]);
|
||
$disk = Storage::disk('uploads');
|
||
$failingDisk = \Mockery::mock($disk);
|
||
$failingDisk->shouldReceive('put')->withArgs(fn ($path) => str_contains($path, '/images/'))->once()->andReturn(false);
|
||
Storage::shouldReceive('disk')->with('uploads')->andReturn($failingDisk);
|
||
$this->withExceptionHandling()->loginAs()->post(route('business.courier-stock-quotes.batch-preview'), [
|
||
'source_quote' => $file, 'quote_type' => '标准快递报价',
|
||
])->assertSessionHasErrors('source_quote');
|
||
$this->assertSame([], $disk->allFiles());
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
}
|
||
|
||
public function test_missing_archived_image_blocks_the_entire_save_and_image_routes_are_private(): void
|
||
{
|
||
$response = $this->loginAs()->post(route('business.courier-stock-quotes.batch-preview'), [
|
||
'source_quote' => $this->workbook(['EXP-001', 'EXP-002'], [1, 1]), 'quote_type' => '标准快递报价',
|
||
])->assertRedirect();
|
||
$batch = basename($response->headers->get('Location'));
|
||
$manifest = app(CourierStockQuoteBatchService::class)->load($batch, (int) $this->user->id);
|
||
Storage::disk('uploads')->delete($manifest['sheets'][1]['signed_images'][0]['path']);
|
||
$this->withExceptionHandling()->post(route('business.courier-stock-quotes.batch-save', $batch), [
|
||
'selection_json' => json_encode($this->selection(['EXP-001', 'EXP-002'])),
|
||
])->assertSessionHasErrors('entries.1');
|
||
$this->assertDatabaseCount('business_courier_stock_quote_entries', 0);
|
||
$url = route('business.courier-stock-quotes.batch-image', [$batch, 0, 0, 'preview' => 1]);
|
||
$this->get(route('business.courier-stock-quotes.batch-image', [$batch, 0, 99]), ['X-Requested-With' => 'XMLHttpRequest'])->assertNotFound();
|
||
$manifestPath = dirname($manifest['source_file']['path']) . '/manifest.json';
|
||
$manifest['user_id']++;
|
||
Storage::disk('uploads')->put($manifestPath, json_encode($manifest));
|
||
$this->get($url, ['X-Requested-With' => 'XMLHttpRequest'])->assertNotFound();
|
||
$manifest['user_id']--;
|
||
$manifest['company_id']++;
|
||
Storage::disk('uploads')->put($manifestPath, json_encode($manifest));
|
||
$this->get($url, ['X-Requested-With' => 'XMLHttpRequest'])->assertNotFound();
|
||
}
|
||
|
||
public function test_followup_price_change_keeps_all_original_images_and_unverified_images_do_not_activate_quote(): void
|
||
{
|
||
$response = $this->loginAs()->post(route('business.courier-stock-quotes.batch-preview'), [
|
||
'source_quote' => $this->workbook(['EXP-001'], [2]), 'quote_type' => '标准快递报价',
|
||
])->assertRedirect();
|
||
$batch = basename($response->headers->get('Location'));
|
||
$this->post(route('business.courier-stock-quotes.batch-save', $batch), [
|
||
'selection_json' => json_encode([...$this->selection(['EXP-001']), 'submit_for_review' => true, 'confirmed' => true]),
|
||
])->assertSessionHasNoErrors();
|
||
$entry = CourierStockQuoteEntry::firstOrFail();
|
||
$reviewer = user_model_class()::create([
|
||
'name' => '原件核验员', 'email' => 'image-reviewer@example.com', 'password' => 'local-test-password',
|
||
'locale' => 'zh-CN', 'enabled' => 1, 'landing_page' => 'dashboard', 'created_from' => 'core::test',
|
||
]);
|
||
$reviewer->companies()->attach($this->company->id);
|
||
$changes = app(CourierQuoteChangeService::class);
|
||
$approved = $changes->approve($entry->change, (int) $reviewer->id);
|
||
$this->assertSame('待确认/待补签', $approved->version->signature_status);
|
||
$change = $changes->create([
|
||
'source_quote_id' => $approved->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);
|
||
$changes->submit($change, (int) $this->user->id);
|
||
$changes->approve($change->fresh(), (int) $reviewer->id);
|
||
$this->assertCount(3, $entry->fresh()->files);
|
||
foreach ([1, 2] as $index) {
|
||
$this->get(route('business.courier-stock-quotes.download', [$entry, $index]))->assertOk()
|
||
->assertStreamedContent($this->sourceImages[0][$index - 1]);
|
||
}
|
||
\DB::table('business_courier_stock_quote_entries')->where('id', $entry->id)->update(['company_id' => $this->company->id + 999]);
|
||
$this->withExceptionHandling()->get(route('business.courier-stock-quotes.download', [$entry, 1, 'preview' => 1]), ['X-Requested-With' => 'XMLHttpRequest'])->assertNotFound();
|
||
}
|
||
|
||
public function test_legacy_xls_images_are_not_reencoded_and_falsely_labeled_originals(): void
|
||
{
|
||
$response = $this->loginAs()->post(route('business.courier-stock-quotes.batch-preview'), [
|
||
'source_quote' => $this->workbook(['EXP-001'], [1], 'Xls'), 'quote_type' => '标准快递报价',
|
||
])->assertSessionHasNoErrors()->assertRedirect();
|
||
$batch = basename($response->headers->get('Location'));
|
||
$manifest = app(CourierStockQuoteBatchService::class)->load($batch, (int) $this->user->id);
|
||
$this->assertSame([], $manifest['sheets'][0]['signed_images']);
|
||
$this->assertNotEmpty($manifest['sheets'][0]['image_warnings']);
|
||
Storage::disk('uploads')->assertExists($manifest['source_file']['path']);
|
||
$this->get(route('business.courier-stock-quotes.batch', $batch))->assertOk()->assertSeeText('无法无损提取');
|
||
}
|
||
|
||
private function preview(array $names, array $imageCounts = []): string
|
||
{
|
||
$response = $this->loginAs()->post(route('business.courier-stock-quotes.batch-preview'), [
|
||
'source_quote' => $this->workbook($names, $imageCounts), 'quote_type' => '标准快递报价',
|
||
'effective_date' => '2025-12-01', 'billing_rules' => '按原表重量段计费,面单另计',
|
||
])->assertRedirect();
|
||
|
||
return basename($response->headers->get('Location'));
|
||
}
|
||
|
||
private function selection(array $codes): array
|
||
{
|
||
return ['quote_type' => '标准快递报价', 'effective_date' => '2025-12-01', 'summary' => '整本报价历史补录',
|
||
'rule_groups' => [['name' => '共用规则', 'rules' => '按原表重量段计费,面单另计']],
|
||
'submit_for_review' => false, 'confirmed' => false,
|
||
'entries' => array_map(fn ($code) => ['selected' => true, 'courier_expcode' => $code, 'rule_group' => 0, 'effective_date' => ''], $codes),
|
||
];
|
||
}
|
||
|
||
private function workbook(array $names, array $imageCounts = [], string $format = 'Xlsx'): UploadedFile
|
||
{
|
||
$book = new Spreadsheet();
|
||
foreach ($names as $index => $name) {
|
||
$sheet = ($index === 0 ? $book->getActiveSheet() : $book->createSheet())->setTitle($name);
|
||
if ($name === '空表') {
|
||
continue;
|
||
}
|
||
$sheet->fromArray([['省份', '首重', '续重'], ['江苏省', 5 + $index, null]]);
|
||
$sheet->setCellValue('C2', '=B2/2');
|
||
for ($imageIndex = 0; $imageIndex < ($imageCounts[$index] ?? 0); $imageIndex++) {
|
||
$image = imagecreatetruecolor(24 + $index, 32 + $imageIndex);
|
||
imagefill($image, 0, 0, imagecolorallocate($image, (80 + $index * 20) % 256, 50, (30 + $imageIndex * 20) % 256));
|
||
$imagePath = tempnam(sys_get_temp_dir(), 'quote-image-');
|
||
rename($imagePath, $imagePath . '.png');
|
||
$imagePath .= '.png';
|
||
$this->temporaryFiles[] = $imagePath;
|
||
imagepng($image, $imagePath);
|
||
imagedestroy($image);
|
||
$this->sourceImages[$index][$imageIndex] = file_get_contents($imagePath);
|
||
(new Drawing())->setPath($imagePath)->setCoordinates('G' . (2 + $imageIndex * 20))->setWorksheet($sheet);
|
||
}
|
||
}
|
||
$path = tempnam(sys_get_temp_dir(), 'batch-quote-');
|
||
$this->temporaryFiles[] = $path;
|
||
IOFactory::createWriter($book, $format)->save($path);
|
||
$book->disconnectWorksheets();
|
||
|
||
return new UploadedFile($path, '整本存量报价.' . strtolower($format), null, null, true);
|
||
}
|
||
|
||
private function mirror(array $couriers): void
|
||
{
|
||
$mirror = \Mockery::mock(CourierInformationMirrorRepository::class);
|
||
$mirror->shouldReceive('overviewSource')->andReturn(['express' => array_map(fn ($courier) => (object) ['expcode' => $courier['code'], 'expname' => $courier['name']], $couriers)]);
|
||
app()->instance(CourierInformationMirrorRepository::class, $mirror);
|
||
}
|
||
}
|