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'], ]; } }