CaiWuXiTong/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php
WuYongJie b8175cfb00 feat: 店铺维度报表无业绩月份列默认隐藏
- 所有店铺当月都没有业绩的销量月份列默认隐藏,仍保留在「筛选字段」中可手动勾选显示
- 无数据月份列计算统一为 columnsWithoutData:收入以 null/空表示无账单、0 算有账单,销量 0 视为无业绩
- 补充单元测试与页面测试;页面测试断言改为只匹配表头标签,避免命中内联样式中的同名字符串
2026-10-07 10:00:55 +08:00

334 lines
16 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_store_report_hides_sales_months_without_data_by_default(): void
{
Carbon::setTestNow('2026-09-15 10:00:00');
$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);
// 只有 7 月有业绩,其余月份列默认隐藏。
$this->assertSame(
['sales_01', 'sales_02', 'sales_03', 'sales_04', 'sales_05', 'sales_06', 'sales_08', 'sales_09', 'sales_10', 'sales_11', 'sales_12'],
$this->app['request']->attributes->get('store-report.default_hidden_columns'),
);
$this->assertSame(1, $paginator->total());
$this->assertSame(60.0, (float) $paginator->items()[0]->sales_07);
}
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(),
]);
}
}