实际修改: - 店铺维度报表新增真实数据库条件查询:店铺字段走共享镜像 SQL WHERE,主体/能否变更/生效/失效走生命周期行级过滤(左闭右开) - 店铺负责人/部门负责人/利润中心改为店铺账号 PostgreSQL 镜像值, 按店铺名称关联,未匹配时回退经营分析副本库原值 - 能否变更按主表 platform 判定(唯品会MP/淘工厂=否,其余=是), 筛选改为 是/否 下拉;默认生效时间 2026-07-01,失效未记录显示存续中 - 主体维度经营报表接入生命周期收入窗口与注册地址解析市字段 - 同步更新视图测试并重写 StoreReportRepositoryTest 匹配 SQLite 实现 验证:容器内 tests/Feature/Business + tests/Unit/EcommerceFinance + tests/Unit/Business 共 324 passed / 4 skipped;真实数据端到端验证 (无筛选 1935 行、负责人/利润中心/失效区间筛选抽样全对)。 无新增数据库迁移,不涉及生产数据写入。
203 lines
9.1 KiB
PHP
203 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\EcommerceFinance;
|
|
|
|
use App\Business\UpstreamBusinessInformation;
|
|
use Illuminate\Support\Carbon;
|
|
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 PDO;
|
|
use RuntimeException;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 店铺维度报表数据组合:本地账单 SQLite 的生命周期拆行与收入归属。
|
|
*/
|
|
class StoreReportRepositoryTest extends TestCase
|
|
{
|
|
private string $databasePath;
|
|
private StoreReportRepository $repository;
|
|
private ?PDO $pdo = null;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->databasePath = sys_get_temp_dir() . '/store-report-rows-' . uniqid() . '.sqlite';
|
|
$pdo = new PDO('sqlite:' . $this->databasePath);
|
|
$pdo->exec('CREATE TABLE bills (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
platform TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
store_name TEXT NOT NULL,
|
|
occurred_at TEXT,
|
|
income NUMERIC NOT NULL DEFAULT 0,
|
|
expense NUMERIC NOT NULL DEFAULT 0,
|
|
bill_item TEXT,
|
|
record_code TEXT NOT NULL,
|
|
source_file TEXT NOT NULL,
|
|
raw_json TEXT NOT NULL,
|
|
imported_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(platform, record_code)
|
|
)');
|
|
$pdo->exec('CREATE TABLE store_lifecycles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
erp_store_name TEXT NOT NULL,
|
|
company_name TEXT,
|
|
effective_at TEXT,
|
|
expires_at TEXT,
|
|
source_file TEXT NOT NULL,
|
|
raw_json TEXT NOT NULL,
|
|
imported_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(erp_store_name, effective_at, expires_at)
|
|
)');
|
|
$this->pdo = $pdo;
|
|
config(['database.connections.bills_sqlite.database' => $this->databasePath]);
|
|
$this->repository = new StoreReportRepository(app('db'));
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Carbon::setTestNow();
|
|
$this->pdo = null;
|
|
@unlink($this->databasePath);
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function test_lifecycle_boundaries_split_monthly_income_without_double_counting(): void
|
|
{
|
|
$this->insertLifecycle('店铺A', '主体甲', '2026-05-01 00:00:00', '2026-07-01 00:00:00');
|
|
$this->insertLifecycle('店铺A', '主体乙', '2026-07-01 00:00:00', null);
|
|
$this->insertBill('店铺A', '2026-04-30 10:00:00', 9);
|
|
$this->insertBill('店铺A', '2026-05-01 00:00:00', 100);
|
|
$this->insertBill('店铺A', '2026-06-30 23:59:59', 60);
|
|
$this->insertBill('店铺A', '2026-07-01 00:00:00', 70);
|
|
$this->insertBill('店铺A', '2026-08-01 10:00:00', 80);
|
|
$this->insertBill('店铺A', '2027-01-01 10:00:00', 5);
|
|
|
|
$rows = $this->repository->rows([
|
|
['erp_store_name' => '店铺A', 'company_name' => '旧主体', 'store_status' => '3'],
|
|
], 2026);
|
|
|
|
$this->assertSame(['主体甲', '主体乙'], array_column($rows, 'entity'));
|
|
// 主体切换日 07-01 的账单只归主体乙(左闭右开),切换前的 04-30 不计入。
|
|
$this->assertSame([160.0, 155.0], array_column($rows, 'period_sales'));
|
|
$this->assertSame([100.0, 0.0], array_column($rows, 'sales_05'));
|
|
$this->assertSame([0.0, 70.0], array_column($rows, 'sales_07'));
|
|
$this->assertSame(['关店中', '关店中'], array_column($rows, 'status'));
|
|
$this->assertSame(['2026-07-01', '存续中'], array_column($rows, 'invalid_at'));
|
|
}
|
|
|
|
public function test_fallback_row_uses_company_table_subject_and_default_window(): void
|
|
{
|
|
// 未匹配生命周期时按默认窗口 [2026-07-01, 2999-01-01) 归属账单。
|
|
$this->insertBill('店铺A', '2026-05-03 10:00:00', 25.5);
|
|
$this->insertBill('店铺A', '2026-07-05 10:00:00', 30);
|
|
|
|
$rows = $this->repository->rows([
|
|
['erp_store_name' => '店铺A', 'company_name' => '公司表主体', '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_missing_sqlite_file_is_reported_instead_of_returning_misleading_zeroes(): void
|
|
{
|
|
config(['database.connections.bills_sqlite.database' => sys_get_temp_dir() . '/store-report-missing-' . uniqid() . '.sqlite']);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessage('店铺账单 SQLite 尚未挂载');
|
|
$this->repository->rows([['erp_store_name' => '店铺A']], 2026);
|
|
}
|
|
|
|
public function test_upstream_business_information_exposes_shared_store_report_rows(): void
|
|
{
|
|
$this->insertLifecycle('店铺A', '主体甲', '2026-01-01 00:00:00', null);
|
|
$this->insertBill('店铺A', '2026-06-30 10:00:00', 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' => '3',
|
|
]]);
|
|
$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->insertLifecycle('店铺A', '主体甲', '2026-01-01 00:00:00', null);
|
|
$this->insertLifecycle('店铺B', '主体甲', '2026-01-01 00:00:00', null);
|
|
$this->insertBill('店铺A', '2026-06-10 10:00:00', 10);
|
|
$this->insertBill('店铺A', '2026-07-10 10:00:00', 20);
|
|
$this->insertBill('店铺A', '2026-08-10 10:00:00', 30);
|
|
$this->insertBill('店铺B', '2026-08-11 10:00:00', 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(100.0, $rows[0]->rolling_12m_income);
|
|
$this->assertSame(10.0, $rows[0]->income_06);
|
|
$this->assertSame(20.0, $rows[0]->income_07);
|
|
$this->assertSame(70.0, $rows[0]->income_08);
|
|
}
|
|
|
|
private function insertBill(string $store, string $occurredAt, float $income): void
|
|
{
|
|
$statement = $this->pdo->prepare(
|
|
'INSERT INTO bills (platform, type, store_name, occurred_at, income, expense, record_code, source_file, raw_json)
|
|
VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?)',
|
|
);
|
|
$statement->execute(['test', 'income', $store, $occurredAt, $income, uniqid(), 'test', '{}']);
|
|
}
|
|
|
|
private function insertLifecycle(string $store, string $company, ?string $from, ?string $to): void
|
|
{
|
|
$statement = $this->pdo->prepare(
|
|
'INSERT INTO store_lifecycles (erp_store_name, company_name, effective_at, expires_at, source_file, raw_json)
|
|
VALUES (?, ?, ?, ?, ?, ?)',
|
|
);
|
|
$statement->execute([$store, $company, $from, $to, 'test', '{}']);
|
|
}
|
|
}
|