tempDirectory = sys_get_temp_dir() . '/caiwu-company-plan-' . bin2hex(random_bytes(6)); File::ensureDirectoryExists($this->tempDirectory); Storage::fake('local'); config([ 'company_plan.database_path' => $this->tempDirectory . '/company-plan.sqlite', 'company_plan.preview_disk' => 'local', ]); } protected function tearDown(): void { File::deleteDirectory($this->tempDirectory); parent::tearDown(); } public function testPageRendersExactWorkbookHeadersClickableSummaryAndRemarkSearch(): void { $this->seedRows(); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'company-plan', ])); $response ->assertOk() ->assertSee('data-company-plan-page', false) ->assertSee('data-company-plan-summary="all"', false) ->assertSee('data-company-plan-summary="plan:retained"', false) ->assertSee('data-company-plan-summary="status:cancelling"', false) ->assertSee('data-company-plan-sort-menu', false) ->assertSee('aria-label="公司名称排序和字段操作"', false) ->assertSeeTextInOrder([ '序号', '公司名称', '类型', '税务类型', '注册时间', '工商类型', '统一社会信用代码', '规划', '公司状态', '注销-税务', '注销-工商', '注销-银行', '备注', ]) ->assertSeeText('备注搜索') ->assertSee('placeholder="只搜索备注内容', false) ->assertSeeText('公司总数') ->assertSeeText('规划保留') ->assertSeeText('已注销') ->assertSeeText('注销中') ->assertSeeText('规划注销未发起') ->assertSeeText('未完结店铺'); } public function testFiltersUseServerSideCompanyAndRemarkKeywords(): void { $this->seedRows(); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'company-plan', 'remark_keyword' => '银行卡', 'plan' => '注销', 'company_status' => '注销中', ])) ->assertOk() ->assertSeeText('上海测试注销主体') ->assertDontSeeText('常州测试保留主体') ->assertSeeText('备注含:银行卡'); $this->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'company-plan', 'keyword' => '常州测试', ])) ->assertOk() ->assertSeeText('常州测试保留主体') ->assertDontSeeText('上海测试注销主体'); $this->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'company-plan', 'remark_keyword' => '常州测试', ])) ->assertOk() ->assertDontSeeText('常州测试保留主体') ->assertDontSeeText('上海测试注销主体'); } public function testClassificationReferencesCanMatchCompanyNamesAndCreditCodes(): void { $this->seedRows(); $repository = app(CompanyPlanRepository::class); $references = collect($repository->classificationReferencesForCompanies( (int) company_id(), ['常州测试保留主体', '不存在的公司'], ['TEST-CREDIT-002'], ))->keyBy('company_name'); $this->assertCount(2, $references); $this->assertSame('小规模纳税人', $references['常州测试保留主体']['taxpayer_type']); $this->assertSame('一般纳税人', $references['上海测试注销主体']['taxpayer_type']); $this->assertCount(2, $repository->allClassificationReferences((int) company_id())); $this->assertEqualsCanonicalizing( ['小规模纳税人', '一般纳税人'], $repository->classificationFilterOptions((int) company_id())['taxpayer_type'], ); } public function testCompanyBasicInformationRendersTaxAndBusinessTypesFromCompanyPlan(): void { $companyId = (int) (company_id() ?: 1); app(CompanyPlanRepository::class)->importRows($companyId, [[ 'sequence_no' => 1, 'company_name' => '常州盘点口径主体', 'company_type' => '店铺-普通公司', 'taxpayer_type' => '小规模纳税人', 'registered_at' => '2024-03-01', 'business_registration_type' => '有限责任公司', 'unified_social_credit_code' => 'TEST-CREDIT-009', 'plan' => '保留', 'company_status' => '正常', 'tax_cancellation' => null, 'business_cancellation' => null, 'bank_cancellation' => null, 'notes' => null, ]], 'update', null); $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->once()->with('company-information.read')->andReturnTrue(); $mirror->shouldReceive('paginate')->once()->with('company-information.read', 1, 50, [])->andReturn([ 'data' => [[ 'id' => 1, 'unified_social_credit_code' => 'TEST-CREDIT-009', 'company_name' => '常州盘点口径主体', 'company_type' => '店铺-普通公司', 'company_status' => '0', 'taxpayer_category' => '旧税务类型', 'business_registration_type' => '个人独资企业', ]], 'total' => 1, ]); $this->app->instance(BusinessInformationMirrorRepository::class, $mirror); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'basic-information', ])) ->assertOk() ->assertSeeText('小规模纳税人') ->assertSeeText('有限责任公司') ->assertSee('value="有限责任公司"', false) ->assertDontSeeText('旧税务类型'); } public function testEditUpdatesOneCompanyPlanRow(): void { $this->seedRows(); $repository = app(CompanyPlanRepository::class); $row = $repository->paginate((int) company_id(), [], 1, 25)['rows'][0]; $this->loginAs() ->patch(route('business.company-plan.update', ['row' => $row['id']]), [ 'sequence_no' => 9, 'company_name' => '常州测试保留主体', 'company_type' => '店铺-普通公司', 'taxpayer_type' => '小规模纳税人', 'registered_at' => '2024-03-02', 'business_registration_type' => '/', 'unified_social_credit_code' => 'TEST-CREDIT-001', 'plan' => '待定', 'company_status' => '正常', 'tax_cancellation' => '', 'business_cancellation' => '', 'bank_cancellation' => '', 'notes' => '已改为保留观察', ]) ->assertRedirect(); $updated = $repository->findForCompany((int) company_id(), (int) $row['id']); $this->assertSame('待定', $updated['plan']); $this->assertSame('已改为保留观察', $updated['notes']); $this->assertSame('2024-03-02', $updated['registered_at']); } public function testPendingCompanyWithLinkedShopRendersNavigableNoteOnlyWhenShopExists(): void { $this->seedRows(); $repository = app(CompanyPlanRepository::class); $companyId = (int) company_id(); $row = $repository->paginate($companyId, ['keyword' => '上海测试注销主体'], 1, 25)['rows'][0]; $repository->update($companyId, (int) $row['id'], [ ...$row, 'plan' => '注销', 'company_status' => '正常', 'notes' => '关联店铺未办结', ], (int) auth()->id()); $repository->importRows($companyId, [[ 'sequence_no' => 3, 'company_name' => '常州测试无店铺主体', 'company_type' => '普通公司', 'taxpayer_type' => '小规模纳税人', 'registered_at' => '2024-03-03', 'business_registration_type' => '/', 'unified_social_credit_code' => 'TEST-CREDIT-003', 'plan' => '注销', 'company_status' => '正常', 'tax_cancellation' => null, 'business_cancellation' => null, 'bank_cancellation' => null, 'notes' => '关联店铺未办结', ]], 'update', (int) auth()->id()); $this->insertShop($companyId, (int) $row['id'], '上海测试注销主体', '上海测试店铺'); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'company-plan', 'plan' => '注销', 'company_status' => '正常', 'shop_keyword' => '旧关键词', 'shop_status' => '正常', ]))->assertOk(); $html = $response->getContent(); $this->assertSame(1, substr_count($html, 'data-company-plan-shop-link')); $this->assertSame( 1, preg_match('/]*data-company-plan-shop-link="\d+"[^>]*href="([^"]+)"/', $html, $matches), ); $link = html_entity_decode($matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'); parse_str((string) parse_url($link, PHP_URL_QUERY), $query); $this->assertSame('上海测试注销主体', $query['shop_company'] ?? null); $this->assertSame('1', (string) ($query['shop_page'] ?? '')); $this->assertSame('', (string) ($query['shop_keyword'] ?? '')); $this->assertSame('', (string) ($query['shop_status'] ?? '')); $this->assertSame('company-plan-shops', parse_url($link, PHP_URL_FRAGMENT)); } public function testShareholdingNoteLinksToCompanyInformationFilteredByShareholder(): void { $this->seedRows(); $companyId = (int) company_id(); $companyName = '常州测试母体有限公司'; $relatedCompany = '常州测试子公司有限公司'; $repository = app(CompanyPlanRepository::class); $repository->importRows($companyId, [[ 'sequence_no' => 4, 'company_name' => $companyName, 'company_type' => '母公司', 'taxpayer_type' => '小规模纳税人', 'registered_at' => '2024-03-04', 'business_registration_type' => '/', 'unified_social_credit_code' => 'TEST-PARENT-004', 'plan' => '注销', 'company_status' => '正常', 'tax_cancellation' => null, 'business_cancellation' => null, 'bank_cancellation' => null, 'notes' => CompanyPlanCompanyRepository::SUBSIDIARY_NOTE, ]], 'update', (int) auth()->id()); $this->insertCompanyRelation($companyId, $companyName, $relatedCompany); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'company-plan', 'keyword' => $companyName, 'plan' => '注销', 'company_status' => '正常', ]))->assertOk(); $html = $response->getContent(); $this->assertSame(1, substr_count($html, 'data-company-plan-shareholding-link')); $this->assertSame( 1, preg_match('/]*data-company-plan-shareholding-link="\d+"[^>]*href="([^"]+)"/', $html, $matches), ); $link = html_entity_decode($matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'); parse_str((string) parse_url($link, PHP_URL_QUERY), $query); $this->assertSame($companyName, $query['shareholder_company'] ?? null); $paginator = new LengthAwarePaginator([(object) [ 'id' => 1001, 'unified_social_credit_code' => 'TEST-CHILD-1001', 'company_name' => $relatedCompany, 'subscribed_capital' => 5, 'company_type' => '店铺-普通公司', 'establishment_date' => '2025-01-02', 'legal_representative' => '测试法人', 'registered_address' => '测试地址', 'business_scope' => '测试经营范围', 'basic_deposit_account_number' => null, 'company_status' => '正常', 'financial_officer' => '测试财务', 'supervisor' => null, 'taxpayer_category' => '小规模纳税人', 'business_registration_type' => '/', 'shareholders' => $companyName . ':100.00', ]], 1, 50, 1, ['path' => '/']); $service = Mockery::mock(UpstreamBusinessInformation::class); $service->shouldReceive('paginate') ->once() ->with('company-management', 'basic-information', 50) ->andReturn($paginator); $this->app->instance(UpstreamBusinessInformation::class, $service); $this->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'basic-information', 'shareholder_company' => $companyName, ])) ->assertOk() ->assertSeeText($relatedCompany) ->assertSeeText($companyName) ->assertSeeText($companyName . ':100.00') ->assertSeeText('作为股东持股的其他公司') ->assertSee('name="shareholder_company"', false); } public function testShareholdingWorkbookImportParsesCompanyShareholderAndRatio(): void { $companyId = (int) company_id(); $companyName = '常州测试母体有限公司'; $relatedCompany = '常州测试子公司有限公司'; app(CompanyPlanRepository::class)->importRows($companyId, [[ 'sequence_no' => 1, 'company_name' => $companyName, 'company_type' => '母公司', 'taxpayer_type' => '小规模纳税人', 'registered_at' => '2024-03-04', 'business_registration_type' => '/', 'unified_social_credit_code' => 'TEST-PARENT-IMPORT', 'plan' => '注销', 'company_status' => '正常', 'tax_cancellation' => null, 'business_cancellation' => null, 'bank_cancellation' => null, 'notes' => CompanyPlanCompanyRepository::SUBSIDIARY_NOTE, ]], 'update', (int) auth()->id()); $path = $this->tempDirectory . '/company-details.xlsx'; $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('主体明细'); $sheet->fromArray([ ['公司名称', '统一社会信用代码', '股东'], [$relatedCompany, 'TEST-CHILD-IMPORT', $companyName . ':100.00'], ], null, 'A1'); (new Xlsx($spreadsheet))->save($path); $spreadsheet->disconnectWorksheets(); $repository = app(CompanyPlanCompanyRepository::class); $result = $repository->importFromWorkbook($path, $companyId); $relations = $repository->relationsForShareholder($companyId, $companyName); $this->assertSame(1, $result['relationships']); $this->assertSame(1, $result['shareholders']); $this->assertArrayHasKey( CompanyPlanCompanyRepository::normalizeCompanyName($relatedCompany), $relations, ); $this->assertSame( $companyName . ':100.00', $relations[CompanyPlanCompanyRepository::normalizeCompanyName($relatedCompany)]['shareholders_text'], ); } public function testWorkbookPreviewAndConfirmImportsOnlyValidRows(): void { $path = $this->tempDirectory . '/subject-inventory.xlsx'; $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('主体盘点'); $sheet->fromArray([ [ '序号', '公司名称', '类型', '税务类型', '注册时间', '工商类型', '统一社会信用代码', '规划', '公司状态', '注销-税务', '注销-工商', '注销-银行', '备注', ], [1, '新增测试主体', '店铺-普通公司', '一般纳税人', '2025-01-02', '/', 'TEST-NEW-001', '保留', '正常', '', '', '', '重点观察'], [2, '', '店铺-普通公司', '小规模纳税人', '2025-01-03', '/', 'TEST-BAD-001', '注销', '注销中', '', '', '', ''], [3, '新增测试主体', '店铺-普通公司', '一般纳税人', '2025-01-04', '/', 'TEST-DUP-001', '注销', '注销中', '', '', '', '文件内重复'], ], null, 'A1'); (new Xlsx($spreadsheet))->save($path); $spreadsheet->disconnectWorksheets(); $previewResponse = $this->loginAs() ->post(route('business.company-plan.import.preview'), [ 'import' => new UploadedFile( $path, 'subject-inventory.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', null, true, ), 'duplicate_mode' => 'update', ]) ->assertRedirect(); $token = session('company_plan_preview_token'); $this->assertIsString($token); $this->assertSame(40, strlen($token)); $this->get(route('business.pages.show', [ 'section' => 'company-management', 'page' => 'company-plan', ])) ->assertOk() ->assertSeeText('导入预览') ->assertSeeText('有效主体') ->assertSeeText('新增测试主体') ->assertSeeText('公司名称为空') ->assertSeeText('文件内公司名称重复') ->assertDontSeeText('缺少表头'); $this->post(route('business.company-plan.import.confirm'), [ 'preview_token' => $token, 'duplicate_mode' => 'update', ])->assertRedirect(); $repository = app(CompanyPlanRepository::class); $result = $repository->paginate((int) company_id(), ['keyword' => '新增测试主体'], 1, 25); $this->assertSame(1, $result['total']); $this->assertSame('重点观察', $result['rows'][0]['notes']); $this->assertNull(session('company_plan_preview_token')); } public function testExportUsesCurrentFilterAndSelectedColumns(): void { $this->seedRows(); $response = $this->loginAs()->get(route('business.company-plan.export', [ 'plan' => '保留', 'columns' => 'sequence_no,company_name,plan,notes', ])); $response ->assertOk() ->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); $content = $response->streamedContent(); $this->assertNotSame('', $content); $this->assertStringContainsString( '公司盘点_', urldecode($response->headers->get('content-disposition', '')) ); $path = $this->tempDirectory . '/company-plan-export.xlsx'; file_put_contents($path, $content); $spreadsheet = IOFactory::load($path); $sheet = $spreadsheet->getActiveSheet(); $this->assertSame(['序号', '公司名称', '规划', '备注'], [ $sheet->getCell('A1')->getValue(), $sheet->getCell('B1')->getValue(), $sheet->getCell('C1')->getValue(), $sheet->getCell('D1')->getValue(), ]); $this->assertSame(2, $sheet->getHighestDataRow()); $this->assertSame('常州测试保留主体', $sheet->getCell('B2')->getValue()); $spreadsheet->disconnectWorksheets(); } private function seedRows(): void { app(CompanyPlanRepository::class)->importRows((int) company_id(), [ [ 'sequence_no' => 1, 'company_name' => '常州测试保留主体', 'company_type' => '店铺-普通公司', 'taxpayer_type' => '小规模纳税人', 'registered_at' => '2024-03-01', 'business_registration_type' => '/', 'unified_social_credit_code' => 'TEST-CREDIT-001', 'plan' => '保留', 'company_status' => '正常', 'tax_cancellation' => null, 'business_cancellation' => null, 'bank_cancellation' => null, 'notes' => '先继续使用', ], [ 'sequence_no' => 2, 'company_name' => '上海测试注销主体', 'company_type' => '资产型', 'taxpayer_type' => '一般纳税人', 'registered_at' => '2023-04-05', 'business_registration_type' => '/', 'unified_social_credit_code' => 'TEST-CREDIT-002', 'plan' => '注销', 'company_status' => '注销中', 'tax_cancellation' => '✅', 'business_cancellation' => null, 'bank_cancellation' => null, 'notes' => '税务已注销,银行卡待注销', ], ], 'update', (int) auth()->id()); } private function insertShop(int $companyId, int $planRowId, string $companyName, string $shopName): void { $statement = app(CompanyPlanShopRepository::class)->pdo()->prepare(<<<'SQL' INSERT INTO company_plan_shops ( company_id, plan_row_id, company_name, shop_name, shop_status, profit_center, owner, opened_on, successor, source_sheet, created_at, updated_at ) VALUES ( :company_id, :plan_row_id, :company_name, :shop_name, :shop_status, :profit_center, :owner, :opened_on, :successor, :source_sheet, :created_at, :updated_at ) SQL); $now = now()->toDateTimeString(); $statement->execute([ ':company_id' => $companyId, ':plan_row_id' => $planRowId, ':company_name' => $companyName, ':shop_name' => $shopName, ':shop_status' => '正常', ':profit_center' => null, ':owner' => null, ':opened_on' => null, ':successor' => null, ':source_sheet' => 'Sheet3', ':created_at' => $now, ':updated_at' => $now, ]); } private function insertCompanyRelation(int $companyId, string $companyName, string $relatedCompany): void { $statement = app(CompanyPlanCompanyRepository::class)->pdo()->prepare(<<<'SQL' INSERT INTO company_plan_company_relations ( company_id, shareholder_company_name, related_company_name, related_unified_social_credit_code, share_ratio, shareholders_text, source_sheet, created_at, updated_at ) VALUES ( :company_id, :shareholder_company_name, :related_company_name, :related_unified_social_credit_code, :share_ratio, :shareholders_text, :source_sheet, :created_at, :updated_at ) SQL); $now = now()->toDateTimeString(); $statement->execute([ ':company_id' => $companyId, ':shareholder_company_name' => $companyName, ':related_company_name' => $relatedCompany, ':related_unified_social_credit_code' => 'TEST-CHILD-1001', ':share_ratio' => '100.00', ':shareholders_text' => $companyName . ':100.00', ':source_sheet' => '主体明细', ':created_at' => $now, ':updated_at' => $now, ]); } }