CaiWuXiTong/tests/Feature/Business/CompanyPlanPageTest.php
DoDo 49ef5ff892 feat: 公司盘点备注关联店铺与持股公司 (#1036)
规划注销主体备注关联店铺未办结并支持跳转;名下有子公司主体关联公司基础信息与持股公司。
2026-11-10 13:50:23 +08:00

519 lines
21 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\Business\CompanyPlan\CompanyPlanCompanyRepository;
use App\Business\CompanyPlan\CompanyPlanRepository;
use App\Business\CompanyPlan\CompanyPlanShopRepository;
use App\Business\CompanyPlan\CompanyPlanWorkbookImporter;
use App\Business\UpstreamBusinessInformation;
use Illuminate\Http\UploadedFile;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Mockery;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Tests\Feature\FeatureTestCase;
class CompanyPlanPageTest extends FeatureTestCase
{
private string $tempDirectory;
protected function setUp(): void
{
parent::setUp();
$this->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)
->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 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('/<a[^>]*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('/<a[^>]*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,
]);
}
}