*/ private array $temporaryFiles = []; protected function setUp(): void { parent::setUp(); $this->user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail(), ); $this->withoutMiddleware(ThrottleRequests::class); } public function test_page_exposes_import_and_filtered_export_actions(): void { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '操作按钮测试店', 'store_entity_name' => '按钮测试主体', ]); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '天猫', ])) ->assertOk() ->assertSeeText('导入') ->assertSeeText('导出') ->assertSee('data-withdrawal-import-open', false) ->assertSee(route('business.withdrawal-management.export', ['platform' => '天猫']), false) ->assertSee(route('business.withdrawal-management.template'), false); } public function test_export_contains_all_filtered_rows_and_excludes_passwords(): void { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '导出目标店', 'store_id' => '00001234', 'store_entity_name' => '导出主体', 'login_password' => 'never-export-this-password', 'withdrawal_path' => '支付宝', 'withdrawal_account_name' => '目标账户', 'bank_card_last_four' => '0098', ]); StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '不应导出的店铺', 'store_entity_name' => '其他主体', ]); $response = $this->loginAs()->get(route('business.withdrawal-management.export', [ 'platform' => '天猫', ])); $response->assertOk()->assertDownload(); $path = $this->temporaryPath('withdrawal-export-'); file_put_contents($path, $response->streamedContent()); $spreadsheet = IOFactory::load($path); try { $sheet = $spreadsheet->getActiveSheet(); $this->assertSame([ ...self::HEADERS, now()->format('n月j日') . '店铺余额', '预留标准', '提现金额', '是否达标', ], $sheet->rangeToArray('A1:K1')[0]); $this->assertSame(2, $sheet->getHighestDataRow()); $this->assertSame('导出目标店', $sheet->getCell('B2')->getValue()); $this->assertSame('00001234', $sheet->getCell('C2')->getValue()); $this->assertSame(DataType::TYPE_STRING, $sheet->getCell('C2')->getDataType()); $this->assertSame('0098', $sheet->getCell('G2')->getValue()); $this->assertSame(DataType::TYPE_STRING, $sheet->getCell('G2')->getDataType()); $values = array_merge(...$sheet->toArray(null, false, false, false)); $this->assertNotContains('不应导出的店铺', $values); $this->assertNotContains('never-export-this-password', $values); } finally { $spreadsheet->disconnectWorksheets(); } } public function test_template_has_import_headers_and_instructions(): void { $response = $this->loginAs()->get(route('business.withdrawal-management.template')); $response->assertOk()->assertDownload('withdrawal_management_import_template.xlsx'); $path = $this->temporaryPath('withdrawal-template-'); file_put_contents($path, $response->streamedContent()); $spreadsheet = IOFactory::load($path); try { $this->assertSame(self::HEADERS, $spreadsheet->getSheet(0)->rangeToArray('A1:G1')[0]); $this->assertSame('填写说明', $spreadsheet->getSheet(1)->getTitle()); $this->assertStringContainsString('[清空]', (string) $spreadsheet->getSheet(1)->getCell('A4')->getValue()); } finally { $spreadsheet->disconnectWorksheets(); } } public function test_valid_import_is_previewed_before_allowed_fields_are_committed(): void { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '导入更新店', 'store_id' => 'TM-IMPORT-1', 'store_entity_name' => '原主体', 'login_password' => 'password-remains-private', 'withdrawal_path' => '银行卡', 'withdrawal_account_name' => '原账户', 'bank_card_last_four' => '1111', ]); $this->loginAs(); $file = $this->workbook([[ '天猫', '导入更新店', 'TM-IMPORT-1', '新主体', '支付宝', '新账户', '0022', ]]); $previewResponse = $this->post(route('business.withdrawal-management.import.preview', [ 'platform' => '天猫', ]), ['import' => $file]); $previewResponse ->assertRedirect(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '天猫', ])) ->assertSessionHas('withdrawalImportPreview', function (array $preview): bool { return $preview['total'] === 1 && $preview['changed'] === 1 && $preview['unchanged'] === 0 && $preview['preview'][0]['changes'] === '店铺主体、提现路径、提现账户名称、银行卡号后四位'; }); $this->assertSame('原主体', $store->fresh()->store_entity_name); $preview = $previewResponse->getSession()->get('withdrawalImportPreview'); $this->post(route('business.withdrawal-management.import.commit', [ 'platform' => '天猫', ]), ['preview_token' => $preview['token']]) ->assertRedirect() ->assertSessionHas('success', '提现配置导入完成,共更新 1 家店铺。'); $store = $store->fresh(); $this->assertSame('新主体', $store->store_entity_name); $this->assertSame('支付宝', $store->withdrawal_path); $this->assertSame('新账户', $store->withdrawal_account_name); $this->assertSame('0022', $store->bank_card_last_four); $this->assertSame('password-remains-private', $store->login_password); } public function test_invalid_bank_card_rejects_the_entire_preview_without_writes(): void { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '非法卡号测试店', 'store_id' => 'JD-INVALID-1', 'store_entity_name' => '原主体', 'withdrawal_path' => '银行卡', ]); $this->loginAs(); $this->withExceptionHandling() ->post(route('business.withdrawal-management.import.preview'), ['import' => $this->workbook([[ '京东', '非法卡号测试店', 'JD-INVALID-1', '不应写入的主体', '', '', '12ab', ]])]) ->assertRedirect() ->assertSessionHasErrors('import'); $this->assertSame('原主体', $store->fresh()->store_entity_name); } public function test_clear_marker_clears_nullable_and_required_configuration_fields(): void { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '拼多多', 'erp_store_name' => '清空配置测试店', 'store_id' => 'PDD-CLEAR-1', 'store_entity_name' => '待清空主体', 'withdrawal_path' => '银行卡', 'withdrawal_account_name' => '待清空账户', 'bank_card_last_four' => '8899', ]); $this->loginAs(); $previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [ 'import' => $this->workbook([[ '拼多多', '清空配置测试店', 'PDD-CLEAR-1', '[清空]', '[清空]', '[清空]', '[清空]', ]]), ]); $preview = $previewResponse->getSession()->get('withdrawalImportPreview'); $this->post(route('business.withdrawal-management.import.commit'), [ 'preview_token' => $preview['token'], ])->assertRedirect()->assertSessionHasNoErrors(); $store = $store->fresh(); $this->assertSame('', $store->getRawOriginal('store_entity_name')); $this->assertSame('', $store->getRawOriginal('receiving_method')); $this->assertNull($store->withdrawal_account_name); $this->assertNull($store->bank_card_last_four); } public function test_broken_workbook_is_reported_as_an_import_error(): void { $this->loginAs(); $this->withExceptionHandling() ->post(route('business.withdrawal-management.import.preview'), [ 'import' => UploadedFile::fake()->createWithContent('损坏文件.xlsx', "PK\x03\x04broken"), ]) ->assertRedirect() ->assertSessionHasErrors([ 'import' => 'Excel 文件无法读取,请确认文件未损坏并使用最新模板。', ]); } public function test_commit_rejects_a_preview_when_store_changed_after_preview(): void { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '抖音', 'erp_store_name' => '并发修改测试店', 'store_id' => 'DY-STALE-1', 'store_entity_name' => '预览前主体', ]); $this->loginAs(); $previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [ 'import' => $this->workbook([[ '抖音', '并发修改测试店', 'DY-STALE-1', '导入主体', '', '', '', ]]), ]); $preview = $previewResponse->getSession()->get('withdrawalImportPreview'); DB::table('business_store_receiving_accounts')->where('id', $store->id)->update([ 'store_entity_name' => '他人已修改主体', 'updated_at' => now()->addMinute(), ]); $this->withExceptionHandling() ->post(route('business.withdrawal-management.import.commit'), [ 'preview_token' => $preview['token'], ]) ->assertRedirect() ->assertSessionHasErrors('import'); $this->assertSame('他人已修改主体', $store->fresh()->store_entity_name); } /** @param list> $rows */ private function workbook(array $rows): UploadedFile { $path = $this->temporaryPath('withdrawal-import-'); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('提现管理'); $sheet->fromArray(self::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->temporaryFiles[] = $path; return $path; } protected function tearDown(): void { foreach ($this->temporaryFiles as $path) { @unlink($path); } parent::tearDown(); } }