feat: 主体报表接入公司盘点税务工商类型

This commit is contained in:
DoDo 2026-11-11 15:36:16 +08:00
parent 11db14c758
commit 178bf2840f
4 changed files with 161 additions and 23 deletions

View File

@ -275,25 +275,14 @@ class UpstreamBusinessInformation
}
$references ??= $this->companyPlanClassificationReferences($rows);
$byCreditCode = [];
$byCompanyName = [];
foreach ($references as $reference) {
$creditCode = $this->normalizeCreditCode($reference['unified_social_credit_code'] ?? null);
if ($creditCode !== '' && ! isset($byCreditCode[$creditCode])) {
$byCreditCode[$creditCode] = $reference;
}
$companyName = $this->normalizeCompanyName($reference['company_name'] ?? null);
if ($companyName !== '' && ! isset($byCompanyName[$companyName])) {
$byCompanyName[$companyName] = $reference;
}
}
$index = $this->companyPlanClassificationIndex($references);
foreach ($rows as &$row) {
$creditCode = $this->normalizeCreditCode($row['unified_social_credit_code'] ?? null);
$companyName = $this->normalizeCompanyName($row['company_name'] ?? null);
$reference = $byCreditCode[$creditCode] ?? $byCompanyName[$companyName] ?? null;
$reference = $this->matchedCompanyPlanClassification(
$index,
$row['company_name'] ?? null,
[$row],
);
$planTaxType = trim((string) ($reference['taxpayer_type'] ?? ''));
$row['taxpayer_category'] = $this->normalizeTaxType(
@ -319,6 +308,10 @@ class UpstreamBusinessInformation
*/
private function companyPlanClassificationReferences(array $rows = [], bool $all = false): array
{
if ($this->companyPlans === null) {
return [];
}
try {
$companyId = (int) (company_id() ?: 1);
if ($all) {
@ -337,6 +330,61 @@ class UpstreamBusinessInformation
}
}
/**
* @param list<array<string,mixed>> $references
* @return array{
* by_credit_code:array<string,array<string,mixed>>,
* by_company_name:array<string,array<string,mixed>>
* }
*/
private function companyPlanClassificationIndex(array $references): array
{
$byCreditCode = [];
$byCompanyName = [];
foreach ($references as $reference) {
$creditCode = $this->normalizeCreditCode($reference['unified_social_credit_code'] ?? null);
if ($creditCode !== '' && ! isset($byCreditCode[$creditCode])) {
$byCreditCode[$creditCode] = $reference;
}
$companyName = $this->normalizeCompanyName($reference['company_name'] ?? null);
if ($companyName !== '' && ! isset($byCompanyName[$companyName])) {
$byCompanyName[$companyName] = $reference;
}
}
return [
'by_credit_code' => $byCreditCode,
'by_company_name' => $byCompanyName,
];
}
/**
* @param array{
* by_credit_code:array<string,array<string,mixed>>,
* by_company_name:array<string,array<string,mixed>>
* } $index
* @param list<array<string,mixed>> $companyRows
* @return array<string,mixed>|null
*/
private function matchedCompanyPlanClassification(
array $index,
mixed $companyName,
array $companyRows = [],
): ?array {
foreach ($companyRows as $companyRow) {
$creditCode = $this->normalizeCreditCode($companyRow['unified_social_credit_code'] ?? null);
if ($creditCode !== '' && isset($index['by_credit_code'][$creditCode])) {
return $index['by_credit_code'][$creditCode];
}
}
$normalizedCompanyName = $this->normalizeCompanyName($companyName);
return $index['by_company_name'][$normalizedCompanyName] ?? null;
}
/**
* @param array<string,string|list<string>> $filters
*/
@ -819,15 +867,29 @@ SQL);
} catch (Throwable) {
$classifications = [];
}
$planClassifications = $this->companyPlanClassificationIndex(
$this->companyPlanClassificationReferences(all: true),
);
foreach ($rows as &$row) {
$row['company_details'] = $details[$row['entity']] ?? [];
$row['city'] = $this->cityFromDetails($row['company_details']);
$row['company_status'] = $row['company_details'][0]['company_status'] ?? null;
$classification = $classifications[$row['entity']] ?? [];
$taxType = $classification['tax_type']
$planClassification = $this->matchedCompanyPlanClassification(
$planClassifications,
$row['entity'],
$row['company_details'],
);
$planTaxType = trim((string) ($planClassification['taxpayer_type'] ?? ''));
$planBusinessType = trim((string) ($planClassification['business_registration_type'] ?? ''));
$taxType = $planTaxType !== ''
? $planTaxType
: $classification['tax_type']
?? $row['company_details'][0]['taxpayer_category']
?? null;
$businessType = $classification['business_registration_type']
$businessType = $planBusinessType !== ''
? $planBusinessType
: $classification['business_registration_type']
?? $row['company_details'][0]['business_registration_type']
?? null;
$classificationStatus = trim((string) ($classification['status'] ?? ''));
@ -839,7 +901,7 @@ SQL);
$row['tax_type'] = $this->normalizeTaxType($taxType) ?? '/';
$businessType = trim((string) $businessType);
$row['business_registration_type'] = $retained
&& in_array($businessType, ['核定', '个人独资企业'], true)
&& ($planBusinessType !== '' || in_array($businessType, ['核定', '个人独资企业'], true))
? $businessType
: '/';
}

View File

@ -12,7 +12,7 @@ $navigation = [
'directory' => [
'icon' => 'query_stats',
'status' => '收入汇总',
'description' => '按主体生效、失效日期归属账单收入;上月店铺结算收入取业绩明细收入金额,按店铺、主体期间汇总。',
'description' => '按主体生效、失效日期归属账单收入;税务类型和工商类型优先引用公司盘点,上月店铺结算收入取业绩明细收入金额并按主体期间汇总。',
'empty' => '经营报表尚未同步。完成店铺主体和销量镜像接入后,这里会显示可追溯的主体数据。',
'fields' => [
['name' => 'city', 'label' => '市'],

View File

@ -27,8 +27,8 @@
| 1 | 市 | 由主体基础资料 `rpt.fys_companyInfo.registeredAddress` 解析地级市(`App\Business\RegisteredAddressCity::parse`):直辖市直接返回「XX市」;其余取省/自治区前缀后第一个地级行政区;本地地址省略省市前缀时按常州区划前缀兜底;识别不出按统一缺失值展示,不伪造数据 |
| 2 | 主体 | 主体名称;作为主体合并键 |
| 3 | 公司类型 | 主体公司类型,来源于 `rpt` |
| 4 | 税务类型 | 主体税务类型,优先读取 `fin_companies.tax_type`,缺失时使用 `rpt.fys_companyInfo.taxCategoryAndType` |
| 5 | 工商类型 | 主体工商类型,读取 `fin_companies.business_type`;仅“核定”和“个人独资企业”为有效值,其他统一显示 `/` |
| 4 | 税务类型 | 主体税务类型,优先读取当前公司盘点的 `taxpayer_type`;盘点未填写时回退 `fin_companies.tax_type`,再回退 `rpt.fys_companyInfo.taxCategoryAndType` |
| 5 | 工商类型 | 主体工商类型,优先读取当前公司盘点的 `business_registration_type`;盘点未填写时回退 `fin_companies.business_type` 或 `rpt.fys_companyInfo` |
| 6 | 最近12个月累计收入 | 最近12个完整自然月收入合计,排除当前月 |
| 7 | 上月店铺结算收入-客服公司 | 按运行时上一个完整自然月动态命名,例如当前为 11 月时显示「10月店铺结算收入-客服公司」;金额取业绩明细 `store_receipt_order_match_rows.income_amount`,按店铺主体期间拆分后汇总到主体 |
| 8 | 1月收入 | 当前年份 1 月收入,当前月或无账单时为空 |

View File

@ -2,6 +2,7 @@
namespace Tests\Unit\EcommerceFinance;
use App\Business\CompanyPlan\CompanyPlanRepository;
use App\Business\UpstreamBusinessInformation;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
@ -455,6 +456,81 @@ class StoreReportRepositoryTest extends TestCase
$this->assertSame(0, $service->paginate('company-management', 'subject-operating-report', 50)->total());
}
public function test_subject_operating_report_uses_company_plan_tax_and_business_types(): void
{
Carbon::setTestNow('2026-09-15 10:00:00');
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
$mirror->shouldReceive('allStores')->once()->andReturn([
['erp_store_name' => '店铺盘点类型', 'company_name' => '主体甲', 'store_status' => '0'],
]);
$mirror->shouldReceive('ready')->with('company-information.read')->andReturnTrue();
$mirror->shouldReceive('companiesByNames')->once()->with(['主体甲'])->andReturn([
'主体甲' => [[
'Id' => 1,
'businessLicenseName' => '主体甲',
'unifiedSocialCreditCode' => 'CODE-SUBJECT',
'companyStatus' => '0',
'taxCategoryAndType' => '一般纳税人',
'business_registration_type' => '核定',
]],
]);
$mirror->shouldReceive('companyClassificationsByNames')->once()->with(['主体甲'])->andReturn([
'主体甲' => [
'status' => '保留',
'tax_type' => '一般纳税人',
'business_registration_type' => '核定',
],
]);
$companyPlans = Mockery::mock(CompanyPlanRepository::class);
$companyPlans->shouldReceive('allClassificationReferences')
->once()
->with((int) (company_id() ?: 1))
->andReturn([[
'company_name' => '盘点中的主体名称',
'unified_social_credit_code' => 'CODE-SUBJECT',
'taxpayer_type' => '小规模',
'business_registration_type' => '有限责任公司',
]]);
$storeReports = Mockery::mock(StoreReportRepository::class);
$storeReports->shouldReceive('rows')->once()->andReturn([[
'entity' => '主体甲',
'company_type' => '店铺-普通公司',
'rolling_12m_income' => 88,
]]);
$storeAccounts = Mockery::mock(StoreAccountMirrorRepository::class);
$storeAccounts->shouldReceive('ownershipByStoreName')->once()->andReturn([]);
$service = new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
$storeReports,
$storeAccounts,
null,
$companyPlans,
);
$rows = $service->paginate('company-management', 'subject-operating-report', 50)->items();
$statusFields = collect(
$this->app['request']->attributes->get('directory.status-counts')['fields']
)->keyBy('name');
$summaryCards = collect(
$this->app['request']->attributes->get('business-page-summary-cards')
)->keyBy('label');
$this->assertCount(1, $rows);
$this->assertSame('小规模纳税人', $rows[0]->tax_type);
$this->assertSame('有限责任公司', $rows[0]->business_registration_type);
$this->assertSame(1, $statusFields['tax_type']['values'][0]['count']);
$this->assertSame('小规模纳税人', $statusFields['tax_type']['values'][0]['value']);
$this->assertSame(1, $statusFields['business_registration_type']['values'][0]['count']);
$this->assertSame('有限责任公司', $statusFields['business_registration_type']['values'][0]['value']);
$this->assertSame(1, $summaryCards['小规模纳税人']['value']);
$this->assertSame(0, $summaryCards['一般纳税人']['value']);
}
public function test_subject_cards_exclude_deregistered_entities_and_show_slash_for_their_types(): void
{
Carbon::setTestNow('2026-09-15 10:00:00');