CaiWuXiTong/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php
WuYongJie 78857a51d9 feat: 店铺维度报表按主体期间拆分并支持期初数据维护
- 改为按期初数据+系统主体+爬虫新主体生成主体期间,一行一期间,修复流程历史重复行
- 变更日当天归新主体、老主体失效显示变更日前一天;关店和其他非正常状态两列显示 -
- 期初数据首次打开固化为系统库设置,可在「规则信息 → 店铺维度报表逻辑配置」在线增删改
- 爬虫店铺主体按税号归一,首次抓到的新主体写入期间变更表并自动生成新期间
- 新增期间变更表迁移与 crawl_shop_company 镜像数据集
2026-10-06 18:42:12 +08:00

304 lines
14 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', 'store_name' => '前台店铺A', 'store_id' => 'SHOP-001', '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'));
$this->assertSame(['前台店铺A', '前台店铺A'], array_column($rows, 'store_name'));
$this->assertSame(['SHOP-001', 'SHOP-001'], array_column($rows, 'store_id'));
}
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']);
$this->assertCount(1, $this->repository->rows([['erp_store_name' => '店铺A']], 2026, ['sales_07' => '30.00']));
$this->assertCount(0, $this->repository->rows([['erp_store_name' => '店铺A']], 2026, ['sales_07' => '130']));
}
public function test_store_name_sort_is_accepted_and_applied(): void
{
request()->merge(['sort' => 'store_name', 'direction' => 'asc']);
$filters = StoreReportRepository::requestFilters();
$rows = $this->repository->rows([
['erp_store_name' => '店铺A', 'store_name' => 'Z店'],
['erp_store_name' => '店铺B', 'store_name' => 'A店'],
], 2026, $filters);
$this->assertSame(['A店', 'Z店'], array_column($rows, 'store_name'));
}
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')->times(3)->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);
request()->merge(['income_08' => '70.00']);
$this->assertSame(1, $service->paginate('company-management', 'subject-operating-report', 50)->total());
request()->merge(['income_08' => '71.00']);
$this->assertSame(0, $service->paginate('company-management', 'subject-operating-report', 50)->total());
}
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(),
]);
}
}