CaiWuXiTong/tests/Feature/Business/CompanyPlanPageTest.php
DoDo 5554ce99b3 fix: 修复公司盘点表头排序菜单被裁切 (#1038)
解除表头裁切并按可视区域自动对齐,13 个表头菜单均可正常展开和点击。
2026-11-10 14:07:28 +08:00

590 lines
24 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 Modules\EcommerceFinance\Repositories\BusinessInformationMirrorRepository;
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)
->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('/<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,
]);
}
}