CaiWuXiTong/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php
WuYongJie 561c42d830 feat: 店铺主体期间与爬虫数据逻辑
- 店铺维度报表按期初数据+系统主体+爬虫新主体生成主体期间,一行一期间,修复流程历史重复行
- 变更日当天归新主体、老主体失效显示变更日前一天;关店和其他非正常状态两列显示 -
- 新增店铺维度报表逻辑配置页(R1-R8)、主体商标数据逻辑配置页和预警页面
- 爬虫店铺主体按税号归一,首次抓到的新主体写入期间变更表并自动生成新期间
- 店铺基础信息、商标展示按爬虫优先;新增爬虫数据质量检查与预警处理
2026-10-03 13:28:08 +08:00

283 lines
13 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\UpstreamBusinessInformation;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\BusinessInformationMirrorRepository;
use Modules\EcommerceFinance\Repositories\InstallationDetailMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreAccountMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReportRepository;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use Mockery;
use RuntimeException;
use Tests\TestCase;
/**
* 店铺维度报表数据组合:期初数据、系统主体、爬虫变更的期间拆分与收入归属。
*/
class StoreReportRepositoryTest extends TestCase
{
private StoreReportRepository $repository;
private string $dailyGeneration = '22222222-2222-4222-8222-222222222222';
protected function setUp(): void
{
parent::setUp();
config(['database.connections.business_mirror' => [
'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
], 'upstream_mirrors.connection' => 'business_mirror']);
DB::purge('business_mirror');
$schema = Schema::connection('business_mirror');
$schema->create('store_report_import_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('active_generation')->nullable();
$table->dateTime('initial_completed_at')->nullable();
});
$schema->create('store_report_shop_period_changes', function (Blueprint $table): void {
$table->increments('id');
$table->string('erp_store_name');
$table->string('company_name')->nullable();
$table->string('unified_social_credit_code')->nullable();
$table->date('observed_on');
$table->string('source')->default('crawl_shop_company');
$table->string('business_key')->unique();
$table->timestamps();
});
$schema->create('store_report_daily_rows', function (Blueprint $table): void {
$table->increments('id');
$table->string('generation');
$table->string('store_name');
$table->date('occurred_on');
$table->decimal('income', 20, 4)->default(0);
$table->decimal('expense', 20, 4)->default(0);
});
$schema->create('ops_fys_shop_info', function (Blueprint $table): void {
$table->increments('Id');
$table->string('erpShopName');
$table->string('unifiedSocialCreditCode')->nullable();
$table->string('shopState')->nullable();
});
$schema->create('ops_fys_company_info', function (Blueprint $table): void {
$table->increments('Id');
$table->string('unifiedSocialCreditCode')->nullable();
$table->string('businessLicenseName')->nullable();
});
DB::connection('business_mirror')->table('store_report_import_states')->insert([
['dataset_key' => StoreReportRepository::LIFECYCLES_DATASET, 'active_generation' => 'lifecycle-live', 'initial_completed_at' => now()],
['dataset_key' => StoreReportRepository::DAILY_DATASET, 'active_generation' => $this->dailyGeneration, 'initial_completed_at' => now()],
]);
$this->repository = new StoreReportRepository(app('db'));
}
protected function tearDown(): void
{
Carbon::setTestNow();
DB::purge('business_mirror');
parent::tearDown();
}
public function test_initial_data_splits_shop_periods_and_income_at_change_day(): void
{
config(['store_report_initial_periods.shops' => [
'店铺A' => [
'previous_company' => '主体甲',
'company' => '主体乙',
'changed_at' => '2026-07-10',
],
]]);
$this->insertBill('店铺A', '2026-06-30', 9);
$this->insertBill('店铺A', '2026-07-01', 100);
$this->insertBill('店铺A', '2026-07-09', 60);
$this->insertBill('店铺A', '2026-07-10', 70);
$this->insertBill('店铺A', '2026-08-01', 80);
$this->insertBill('店铺A', '2027-01-01', 5);
$rows = $this->repository->rows([
['erp_store_name' => '店铺A', 'company_name' => '旧主体', 'store_status' => '0'],
], 2026);
$this->assertSame(['主体甲', '主体乙'], array_column($rows, 'entity'));
// 变更日前一天(07-09)仍归老主体,变更日(07-10)当天归新主体,窗口前的 06-30 不计入。
$this->assertSame([160.0, 155.0], array_column($rows, 'period_sales'));
$this->assertSame([0.0, 0.0], array_column($rows, 'sales_05'));
$this->assertSame([160.0, 70.0], array_column($rows, 'sales_07'));
$this->assertSame([0.0, 80.0], array_column($rows, 'sales_08'));
$this->assertSame(['2026-07-01', '2026-07-10'], array_column($rows, 'effective_at'));
$this->assertSame(['2026-07-09', '存续中'], array_column($rows, 'invalid_at'));
$this->assertSame(['开店中', '开店中'], array_column($rows, 'status'));
}
public function test_store_without_initial_data_uses_store_row_company_from_initial_date(): void
{
$this->insertBill('店铺A', '2026-05-03', 25.5);
$this->insertBill('店铺A', '2026-07-05', 30);
$rows = $this->repository->rows([
['erp_store_name' => '店铺A', 'company_name' => '公司表主体', 'store_status' => '0', 'opened_at' => '2026-05-01'],
], 2026);
$this->assertCount(1, $rows);
$this->assertSame(30.0, $rows[0]['period_sales']);
$this->assertSame(30.0, $rows[0]['sales_07']);
$this->assertSame('公司表主体', $rows[0]['entity']);
$this->assertSame('2026-07-01', $rows[0]['effective_at']);
$this->assertSame('存续中', $rows[0]['invalid_at']);
}
public function test_closed_store_shows_dash_for_period_columns_but_keeps_income(): void
{
$this->insertBill('店铺A', '2026-07-05', 42);
$rows = $this->repository->rows([
['erp_store_name' => '店铺A', 'company_name' => '主体甲', 'store_status' => '6'],
], 2026);
$this->assertCount(1, $rows);
$this->assertSame('关店', $rows[0]['status']);
$this->assertSame('-', $rows[0]['effective_at']);
$this->assertSame('-', $rows[0]['invalid_at']);
$this->assertSame('主体甲', $rows[0]['entity']);
$this->assertSame(42.0, $rows[0]['period_sales']);
}
public function test_crawler_change_appends_period_and_ignores_repeated_company(): void
{
$this->insertChange('店铺B', '主体新', '2026-08-20');
$this->insertChange('店铺B', '主体新', '2026-08-25');
$this->insertBill('店铺B', '2026-08-19', 10);
$this->insertBill('店铺B', '2026-08-20', 20);
$rows = $this->repository->rows([
['erp_store_name' => '店铺B', 'company_name' => '主体旧', 'store_status' => '0'],
], 2026);
$this->assertSame(['主体旧', '主体新'], array_column($rows, 'entity'));
$this->assertSame(['2026-07-01', '2026-08-20'], array_column($rows, 'effective_at'));
$this->assertSame(['2026-08-19', '存续中'], array_column($rows, 'invalid_at'));
$this->assertSame([10.0, 20.0], array_column($rows, 'period_sales'));
}
public function test_crawler_change_uses_system_entity_as_base_when_row_shows_crawler_company(): void
{
DB::connection('business_mirror')->table('ops_fys_shop_info')->insert([
'erpShopName' => '店铺C', 'unifiedSocialCreditCode' => 'CODE-C', 'shopState' => '0',
]);
DB::connection('business_mirror')->table('ops_fys_company_info')->insert([
'unifiedSocialCreditCode' => 'CODE-C', 'businessLicenseName' => '主体旧',
]);
$this->insertChange('店铺C', '主体新', '2026-09-29', 'CODE-NEW');
DB::connection('business_mirror')->table('ops_fys_company_info')->insert([
'unifiedSocialCreditCode' => 'CODE-NEW', 'businessLicenseName' => '主体新',
]);
$this->insertBill('店铺C', '2026-09-28', 10);
$this->insertBill('店铺C', '2026-09-29', 20);
// 店铺基础信息页按爬虫优先展示主体新,报表期间仍以系统主体为基数拆分。
$rows = $this->repository->rows([
['erp_store_name' => '店铺C', 'company_name' => '主体新', 'store_status' => '0'],
], 2026);
$this->assertSame(['主体旧', '主体新'], array_column($rows, 'entity'));
$this->assertSame([10.0, 20.0], array_column($rows, 'period_sales'));
}
public function test_unfinished_shared_daily_summary_is_reported_instead_of_returning_misleading_zeroes(): void
{
DB::connection('business_mirror')->table('store_report_import_states')
->where('dataset_key', StoreReportRepository::DAILY_DATASET)
->update(['active_generation' => null, 'initial_completed_at' => null]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('store_report_dataset_not_ready');
$this->repository->rows([['erp_store_name' => '店铺A']], 2026);
}
public function test_upstream_business_information_exposes_shared_store_report_rows(): void
{
$this->insertBill('店铺A', '2026-07-05', 60);
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
$mirror->shouldReceive('allStores')->once()->andReturn([[
'erp_store_name' => '店铺A', 'company_name' => '主体甲', 'store_status' => '0',
]]);
$accounts = Mockery::mock(StoreAccountMirrorRepository::class);
$accounts->shouldReceive('ownershipByStoreName')->andReturn([]);
$service = new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
Mockery::mock(InstallationDetailMirrorRepository::class),
$this->repository,
$accounts,
);
$paginator = $service->paginate('store-management', 'store-report', 50);
$this->assertSame(1, $paginator->total());
$this->assertSame('主体甲', $paginator->items()[0]->entity);
$this->assertSame(60.0, $paginator->items()[0]->period_sales);
}
public function test_subject_operating_report_aggregates_income_by_entity(): void
{
Carbon::setTestNow('2026-09-01 10:00:00');
$this->insertBill('店铺A', '2026-07-10', 20);
$this->insertBill('店铺A', '2026-08-10', 30);
$this->insertBill('店铺B', '2026-08-11', 40);
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
$mirror->shouldReceive('allStores')->once()->andReturn([
['erp_store_name' => '店铺A', 'company_name' => '主体甲', 'store_status' => '0'],
['erp_store_name' => '店铺B', 'company_name' => '主体甲', 'store_status' => '0'],
]);
$mirror->shouldReceive('ready')->with('company-information.read')->andReturnTrue();
$mirror->shouldReceive('companiesByNames')->andReturn([]);
$accounts = Mockery::mock(StoreAccountMirrorRepository::class);
$accounts->shouldReceive('ownershipByStoreName')->andReturn([]);
$service = new UpstreamBusinessInformation(
Mockery::mock(UpstreamQueryCacheFactory::class),
$mirror,
Mockery::mock(InstallationDetailMirrorRepository::class),
$this->repository,
$accounts,
);
$rows = $service->paginate('company-management', 'subject-operating-report', 50)->items();
$this->assertCount(1, $rows);
$this->assertSame('主体甲', $rows[0]->entity);
$this->assertSame(90.0, $rows[0]->rolling_12m_income);
$this->assertSame(20.0, $rows[0]->income_07);
$this->assertSame(70.0, $rows[0]->income_08);
}
private function insertBill(string $store, string $occurredOn, float $income): void
{
DB::connection('business_mirror')->table('store_report_daily_rows')->insert([
'generation' => $this->dailyGeneration,
'store_name' => $store,
'occurred_on' => $occurredOn,
'income' => $income,
'expense' => 0,
]);
}
private function insertChange(string $store, string $company, string $observedOn, ?string $taxNo = null): void
{
DB::connection('business_mirror')->table('store_report_shop_period_changes')->insert([
'erp_store_name' => $store,
'company_name' => $company,
'unified_social_credit_code' => $taxNo,
'observed_on' => $observedOn,
'source' => 'crawl_shop_company',
'business_key' => hash('sha256', implode('|', [$store, $company, $observedOn])),
'created_at' => now(),
'updated_at' => now(),
]);
}
}