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

475 lines
20 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\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_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'],
];
}
}