CaiWuXiTong/tests/Feature/Business/CourierStockQuoteBatchTest.php

408 lines
26 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 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);
}
}