CaiWuXiTong/tests/Unit/EcommerceFinance/UpstreamBusinessInformationTest.php
DoDo 1b79fd9abf feat: 公司基础信息引用盘点税务工商类型
税务和工商类型优先引用公司盘点,并保持筛选、排序与下拉选项同口径。
2026-11-10 13:57:37 +08:00

619 lines
27 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\Unit\EcommerceFinance;
use App\Business\CompanyPlan\CompanyPlanCompanyRepository;
use App\Business\CompanyPlan\CompanyPlanRepository;
use App\Business\UpstreamBusinessInformation;
use Closure;
use Mockery;
use Modules\EcommerceFinance\Repositories\BusinessInformationMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreAccountMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReportRepository;
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use Tests\TestCase;
class UpstreamBusinessInformationTest extends TestCase
{
/**
* @dataProvider businessInformationPages
*/
public function test_local_direct_page_resolver_passes_pagination_to_its_fixed_snapshot(
string $section,
string $page,
string $scope,
): void {
config(['upstreams.local_configuration.enabled' => true]);
$cache = new class extends UpstreamQueryCache {
public array $calls = [];
public function __construct()
{
}
public function remember(string $scope, array $parameters, Closure $resolver): mixed
{
$this->calls[] = compact('scope', 'parameters');
return $resolver();
}
};
$caches = new class($cache) extends UpstreamQueryCacheFactory {
public function __construct(private readonly UpstreamQueryCache $cache)
{
}
public function forSource(string $source): UpstreamQueryCache
{
return $this->cache;
}
};
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->once()->with($scope)->andReturnFalse();
$information = new class($caches, $mirror) extends UpstreamBusinessInformation {
public array $snapshots = [];
public function snapshot(string $scope, array $parameters): array
{
$this->snapshots[] = compact('scope', 'parameters');
return ['data' => [], 'total' => 0];
}
};
$this->app['request']->merge(['page' => 3]);
$paginator = $information->paginate($section, $page, 25);
$expected = ['scope' => $scope, 'parameters' => ['page' => 3, 'perPage' => 25]];
$this->assertSame(3, $paginator->currentPage());
$this->assertSame($expected, $cache->calls[0]);
$this->assertSame($expected, $information->snapshots[0]);
}
/**
* @dataProvider paginatedBusinessInformationPages
*/
public function test_ready_business_mirror_is_used_without_registering_a_page_snapshot(
string $section,
string $page,
string $scope,
): void {
config(['operations_replica.enabled' => false, 'upstreams.stored_configuration.enabled' => false]);
$cache = new class extends UpstreamQueryCache {
public function __construct() {}
public function remember(string $scope, array $parameters, Closure $resolver): mixed
{
throw new \RuntimeException('镜像就绪后不应再读取分页快照。');
}
};
$caches = new class($cache) extends UpstreamQueryCacheFactory {
public function __construct(private readonly UpstreamQueryCache $cache) {}
public function forSource(string $source): UpstreamQueryCache { return $this->cache; }
};
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->once()->with($scope)->andReturnTrue();
$mirror->shouldReceive('paginate')->once()->with($scope, 2, 25, [])->andReturn([
'data' => [['id' => 10]],
'total' => 51,
]);
$this->app['request']->merge(['page' => 2]);
$paginator = (new UpstreamBusinessInformation($caches, $mirror))
->paginate($section, $page, 25);
$this->assertSame(2, $paginator->currentPage());
$this->assertSame(51, $paginator->total());
$this->assertSame(10, $paginator->items()[0]->id);
}
public function test_store_basic_information_uses_account_owner_and_department_head(): void
{
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->once()->with('store-information.read')->andReturnTrue();
$mirror->shouldReceive('allStores')->once()->with([])->andReturn([
[
'id' => 7,
'erp_store_name' => '拼多多_测试店铺',
'store_manager' => '旧店铺负责人',
'department_manager' => '旧部门负责人',
'store_status' => '0',
'platform' => '拼多多',
],
[
'id' => 8,
'erp_store_name' => '拼多多-其他店铺',
'store_manager' => '其他负责人',
'department_manager' => '其他部门负责人',
'store_status' => '6',
'platform' => '拼多多',
],
]);
$accounts = Mockery::mock(StoreAccountMirrorRepository::class);
$accounts->shouldReceive('ownershipByStoreName')->once()->andReturn([
StoreAccountMirrorRepository::storeNameKey('拼多多-测试店铺') => [
'operation_owner' => '正确店铺负责人',
'operation_department_head' => '正确部门负责人',
'operation_director' => '运营总监',
'profit_center' => '运营中心A',
],
]);
$this->app['request']->merge([
'store_manager' => '正确店铺负责人',
'department_manager' => '正确部门负责人',
]);
$paginator = (new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
null,
$accounts,
))->paginate('store-management', 'basic-information', 50);
$this->assertSame(1, $paginator->total());
$this->assertSame('正确店铺负责人', $paginator->items()[0]->store_manager);
$this->assertSame('正确部门负责人', $paginator->items()[0]->department_manager);
$this->assertNotSame('运营总监', $paginator->items()[0]->department_manager);
$this->assertSame('正常', $paginator->items()[0]->store_status);
$summaryCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
$this->assertSame([], $summaryCards->all());
}
public function test_trademark_classification_is_normalized_to_digits_only(): void
{
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->once()->with('trademark-information.read')->andReturnTrue();
$mirror->shouldReceive('paginate')->once()->with('trademark-information.read', 1, 50, [])->andReturn([
'data' => [
['id' => 1, 'trademark_type' => '第20'],
['id' => 2, 'trademark_type' => '第20类;第35类'],
['id' => 3, 'trademark_type' => null],
],
'total' => 3,
]);
$paginator = (new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
null,
))->paginate('trademark-management', 'information', 50);
$this->assertSame('20', $paginator->items()[0]->trademark_type);
$this->assertSame('20,35', $paginator->items()[1]->trademark_type);
$this->assertNull($paginator->items()[2]->trademark_type);
}
public function test_base_information_pages_do_not_render_summary_cards(): void
{
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->times(2)->andReturnUsing(
static fn (string $scope): bool => in_array($scope, ['company-information.read', 'trademark-information.read'], true),
);
$mirror->shouldReceive('paginate')->once()->with('company-information.read', 1, 50, [])->andReturn([
'data' => [],
'total' => 818,
'summary' => [
'total' => 818,
'normal' => 520,
'deregistered' => 298,
'general_taxpayer' => 123,
'small_taxpayer' => 606,
],
]);
$mirror->shouldReceive('paginate')->once()->with('trademark-information.read', 1, 50, [])->andReturn([
'data' => [],
'total' => 593,
'summary' => [
'total' => 593,
'enabled' => 548,
'disabled' => 45,
'holders' => 5,
'departments' => 6,
],
]);
$information = new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
null,
);
$information->paginate('company-management', 'basic-information', 50);
$companyCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
$this->assertSame([], $companyCards->all());
$information->paginate('trademark-management', 'information', 50);
$trademarkCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
$this->assertSame([], $trademarkCards->all());
}
public function test_company_registration_date_is_hidden_for_deregistered_and_invalid_pre_2000_values(): void
{
$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, 'company_status' => '1', 'establishment_date' => '2024-04-28'],
['id' => 2, 'company_status' => '已注销', 'establishment_date' => null],
['id' => 3, 'company_status' => '0', 'establishment_date' => '1900-01-01'],
['id' => 4, 'company_status' => '正常', 'establishment_date' => '1999-12-31'],
['id' => 5, 'company_status' => '0', 'establishment_date' => '2000-01-01'],
['id' => 6, 'company_status' => null, 'establishment_date' => null],
],
'total' => 6,
]);
$items = (new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
null,
))->paginate('company-management', 'basic-information', 50)->items();
$this->assertSame('已注销', $items[0]->company_status);
$this->assertSame('/', $items[0]->establishment_date);
$this->assertSame('/', $items[1]->establishment_date);
$this->assertSame('/', $items[2]->establishment_date);
$this->assertSame('/', $items[3]->establishment_date);
$this->assertSame('2000-01-01', $items[4]->establishment_date);
$this->assertNull($items[5]->establishment_date);
}
public function test_company_page_filters_and_overlays_shareholding_relations(): void
{
$path = sys_get_temp_dir() . '/caiwu-shareholding-' . bin2hex(random_bytes(6)) . '.sqlite';
config(['company_plan.database_path' => $path]);
$shareholder = '常州测试母体有限公司';
$relatedCompany = '常州测试子公司有限公司';
try {
$relations = app(CompanyPlanCompanyRepository::class);
$pdo = $relations->pdo();
$statement = $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 (?, ?, ?, ?, ?, ?, ?, ?, ?)
SQL);
$now = now()->toDateTimeString();
$statement->execute([
1,
$shareholder,
$relatedCompany,
'TEST-CHILD-UNIT',
'100.00',
$shareholder . ':100.00',
'主体明细',
$now,
$now,
]);
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')
->once()
->with('company-information.read')
->andReturnTrue();
$mirror->shouldReceive('paginate')
->once()
->withArgs(static fn (
string $scope,
int $page,
int $perPage,
array $filters,
): bool => $scope === 'company-information.read'
&& $page === 1
&& $perPage === 50
&& $filters['_company_names'] === [$relatedCompany])
->andReturn([
'data' => [[
'id' => 901,
'company_name' => $relatedCompany,
'company_status' => '0',
'taxpayer_category' => '小规模纳税人',
]],
'total' => 1,
]);
$this->app['request']->merge(['shareholder_company' => $shareholder]);
$paginator = (new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
null,
))->paginate('company-management', 'basic-information', 50);
$this->assertSame(1, $paginator->total());
$this->assertSame($relatedCompany, $paginator->items()[0]->company_name);
$this->assertSame($shareholder . ':100.00', $paginator->items()[0]->shareholders);
} finally {
@unlink($path);
@unlink($path . '-wal');
@unlink($path . '-shm');
}
}
public function test_company_tax_and_business_types_prefer_company_plan_by_name_or_credit_code(): void
{
$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,
'company_name' => '名称匹配公司',
'unified_social_credit_code' => 'CODE-1',
'taxpayer_category' => '旧税务类型',
'business_registration_type' => '/',
],
[
'id' => 2,
'company_name' => '未盘点公司',
'unified_social_credit_code' => 'CODE-2',
'taxpayer_category' => '一般纳税人',
'business_registration_type' => '个人独资企业',
],
[
'id' => 3,
'company_name' => '基础资料旧名称',
'unified_social_credit_code' => 'CODE-3',
'taxpayer_category' => '旧税务类型',
'business_registration_type' => '/',
],
],
'total' => 3,
]);
$companyPlans = Mockery::mock(CompanyPlanRepository::class);
$companyPlans->shouldReceive('classificationFilterOptions')
->once()
->with((int) (company_id() ?: 1))
->andReturn([
'taxpayer_type' => ['小规模纳税人', '一般纳税人'],
'business_registration_type' => ['核定', '有限责任公司'],
]);
$companyPlans->shouldReceive('classificationReferencesForCompanies')
->once()
->with(
(int) (company_id() ?: 1),
['名称匹配公司', '未盘点公司', '基础资料旧名称'],
['CODE-1', 'CODE-2', 'CODE-3'],
)
->andReturn([
[
'company_name' => '名称匹配公司',
'unified_social_credit_code' => 'CODE-1',
'taxpayer_type' => '小规模纳税人',
'business_registration_type' => '核定',
],
[
'company_name' => '盘点中的新名称',
'unified_social_credit_code' => 'CODE-3',
'taxpayer_type' => '一般纳税人',
'business_registration_type' => '有限责任公司',
],
]);
$items = (new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
null,
null,
null,
$companyPlans,
))->paginate('company-management', 'basic-information', 50)->items();
$this->assertSame('小规模纳税人', $items[0]->taxpayer_category);
$this->assertSame('核定', $items[0]->business_registration_type);
$this->assertSame('一般纳税人', $items[1]->taxpayer_category);
$this->assertSame('个人独资企业', $items[1]->business_registration_type);
$this->assertSame('一般纳税人', $items[2]->taxpayer_category);
$this->assertSame('有限责任公司', $items[2]->business_registration_type);
}
public function test_company_tax_filter_uses_company_plan_values(): void
{
$this->app['request']->merge([
'company_status' => '正常',
'taxpayer_type' => '一般纳税人',
]);
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('allCompaniesForClassification')
->once()
->with(['company_status' => '正常'])
->andReturn([
[
'id' => 2,
'company_name' => '旧口径为小规模',
'unified_social_credit_code' => 'CODE-1',
'taxpayer_category' => '小规模纳税人',
'business_registration_type' => '/',
],
[
'id' => 1,
'company_name' => '盘点为一般纳税人',
'unified_social_credit_code' => 'CODE-2',
'taxpayer_category' => '小规模纳税人',
'business_registration_type' => '/',
],
]);
$companyPlans = Mockery::mock(CompanyPlanRepository::class);
$companyPlans->shouldReceive('classificationFilterOptions')
->once()
->with((int) (company_id() ?: 1))
->andReturn([
'taxpayer_type' => ['小规模纳税人', '一般纳税人'],
'business_registration_type' => ['/'],
]);
$companyPlans->shouldReceive('allClassificationReferences')
->once()
->with((int) (company_id() ?: 1))
->andReturn([
[
'company_name' => '旧口径为小规模',
'unified_social_credit_code' => 'CODE-1',
'taxpayer_type' => '一般纳税人',
'business_registration_type' => '/',
],
[
'company_name' => '盘点为一般纳税人',
'unified_social_credit_code' => 'CODE-2',
'taxpayer_type' => '小规模纳税人',
'business_registration_type' => '/',
],
]);
$paginator = (new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
null,
null,
null,
$companyPlans,
))->paginate('company-management', 'basic-information', 50);
$this->assertSame(1, $paginator->total());
$this->assertSame('旧口径为小规模', $paginator->items()[0]->company_name);
$this->assertSame('一般纳税人', $paginator->items()[0]->taxpayer_category);
}
public function test_bootstrap_without_mirror_explains_shared_connection_instead_of_requesting_upstream_credentials(): void
{
config(['system_database.mode' => 'bootstrap', 'upstreams.local_configuration.enabled' => false]);
$caches = Mockery::mock(UpstreamQueryCacheFactory::class);
$caches->shouldNotReceive('forSource');
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->once()->with('store-information.read')->andReturnFalse();
$this->expectException(\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class);
$this->expectExceptionMessage('无需另配上游 SQL Server');
(new UpstreamBusinessInformation($caches, $mirror))
->paginate('store-management', 'basic-information');
}
public function test_subject_operating_report_hides_income_months_without_data_by_default(): void
{
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
$mirror->shouldReceive('allStores')->andReturn([['erp_store_name' => '测试店铺']]);
$mirror->shouldReceive('ready')->with('company-information.read')->andReturnTrue();
$mirror->shouldReceive('companiesByNames')->andReturn([]);
$mirror->shouldReceive('companyClassificationsByNames')->andReturn([]);
$storeReports = Mockery::mock(StoreReportRepository::class);
$storeReports->shouldReceive('rows')->andReturn([[
'entity' => '测试主体',
'company_type' => '店铺-普通公司',
'rolling_12m_income' => 188.5,
'income_01' => null,
'income_02' => 88.5,
'income_03' => 0.0,
'income_04' => null,
'income_05' => null,
'income_06' => null,
'income_07' => null,
'income_08' => null,
'income_09' => null,
'income_10' => null,
'income_11' => null,
'income_12' => null,
]]);
$storeAccounts = Mockery::mock(StoreAccountMirrorRepository::class);
$storeAccounts->shouldReceive('ownershipByStoreName')->andReturn([]);
$information = new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
$storeReports,
$storeAccounts,
);
$paginator = $information->paginate('company-management', 'subject-operating-report', 50);
$this->assertSame(
['income_01', 'income_04', 'income_05', 'income_06', 'income_07', 'income_08', 'income_09', 'income_10', 'income_11', 'income_12'],
$this->app['request']->attributes->get('subject-operating-report.default_hidden_columns'),
);
$this->assertSame(1, $paginator->total());
$this->assertSame('测试主体', $paginator->items()[0]->entity);
}
public function test_subject_status_counts_use_all_rows_in_the_current_filter_result(): void
{
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
$mirror->shouldReceive('allStores')->once()->andReturn([['erp_store_name' => '测试店铺']]);
$mirror->shouldReceive('ready')->with('company-information.read')->andReturnTrue();
$mirror->shouldReceive('companiesByNames')->once()->andReturn([]);
$mirror->shouldReceive('companyClassificationsByNames')->once()->andReturn([
'主体甲' => [
'status' => '保留',
'tax_type' => '小规模纳税人',
'business_registration_type' => '个人独资企业',
],
'主体乙' => [
'status' => '待注销',
'tax_type' => '一般纳税人',
'business_registration_type' => '核定',
],
]);
$storeReports = Mockery::mock(StoreReportRepository::class);
$storeReports->shouldReceive('rows')->once()->andReturn([
[
'entity' => '主体甲',
'company_type' => '店铺-普通公司',
'rolling_12m_income' => 100,
],
[
'entity' => '主体乙',
'company_type' => '店铺-国标公司',
'rolling_12m_income' => 200,
],
]);
$storeAccounts = Mockery::mock(StoreAccountMirrorRepository::class);
$storeAccounts->shouldReceive('ownershipByStoreName')->once()->andReturn([]);
$information = new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
$storeReports,
$storeAccounts,
);
$paginator = $information->paginate('company-management', 'subject-operating-report', 1);
$counts = $this->app['request']->attributes->get('directory.status-counts');
$fields = collect($counts['fields'])->keyBy('name');
$valueCounts = static fn (string $field): array => collect($fields[$field]['values'])->pluck('count', 'value')->all();
$this->assertSame(2, $paginator->total());
$this->assertCount(1, $paginator->items());
$this->assertSame(2, $counts['total']);
$this->assertSame(1, $valueCounts('company_status')['正常']);
$this->assertSame(1, $valueCounts('company_status')['已注销']);
$this->assertSame(1, $valueCounts('company_type')['店铺-普通公司']);
$this->assertSame(1, $valueCounts('company_type')['店铺-国标公司']);
$this->assertSame(1, $valueCounts('tax_type')['小规模纳税人']);
$this->assertSame(1, $valueCounts('tax_type')['一般纳税人']);
$this->assertSame(2, $valueCounts('plan')['']);
}
public static function businessInformationPages(): array
{
return [
'company' => ['company-management', 'basic-information', 'company-information.read'],
'store' => ['store-management', 'basic-information', 'store-information.read'],
'trademark' => ['trademark-management', 'information', 'trademark-information.read'],
];
}
public static function paginatedBusinessInformationPages(): array
{
return [
'company' => ['company-management', 'basic-information', 'company-information.read'],
'trademark' => ['trademark-management', 'information', 'trademark-information.read'],
];
}
}