databasePath = tempnam(sys_get_temp_dir(), 'store-report-'); config(['database.connections.bills_sqlite' => [ 'driver' => 'sqlite', 'database' => $this->databasePath, 'prefix' => '', 'foreign_key_constraints' => false, ]]); DB::purge('bills_sqlite'); $schema = Schema::connection('bills_sqlite'); $schema->create('store_lifecycles', function (Blueprint $table): void { $table->increments('id'); $table->string('erp_store_name'); $table->string('company_name')->nullable(); $table->string('effective_at')->nullable(); $table->string('expires_at')->nullable(); }); $schema->create('bills', function (Blueprint $table): void { $table->increments('id'); $table->string('store_name'); $table->string('occurred_at')->nullable(); $table->decimal('income', 18, 2)->default(0); }); $this->repository = new StoreReportRepository(app('db')); } protected function tearDown(): void { DB::purge('bills_sqlite'); if (is_file($this->databasePath)) { unlink($this->databasePath); } parent::tearDown(); } public function test_lifecycle_boundaries_split_monthly_and_period_income_without_double_counting(): void { $db = DB::connection('bills_sqlite'); $db->table('store_lifecycles')->insert([ ['erp_store_name' => '店铺A', 'company_name' => '主体甲', 'effective_at' => '2026-05-01', 'expires_at' => '2026-07-01'], ['erp_store_name' => '店铺A', 'company_name' => '主体乙', 'effective_at' => '2026-07-01', 'expires_at' => null], ]); $db->table('bills')->insert([ ['store_name' => '店铺A', 'occurred_at' => '2026-04-30 23:59:59', 'income' => 9], ['store_name' => '店铺A', 'occurred_at' => '2026-05-01 00:00:00', 'income' => 100], ['store_name' => '店铺A', 'occurred_at' => '2026-06-30 23:59:59', 'income' => 60], ['store_name' => '店铺A', 'occurred_at' => '2026-07-01 00:00:00', 'income' => 70], ['store_name' => '店铺A', 'occurred_at' => '2026-08-01 00:00:00', 'income' => 80], ['store_name' => '店铺A', 'occurred_at' => '2027-01-01 00:00:00', 'income' => 5], ]); $rows = $this->repository->rows([ ['erp_store_name' => '店铺A', 'company_name' => '旧主体', 'store_status' => '3'], ]); $this->assertCount(2, $rows); $this->assertSame(['主体甲', '主体乙'], array_column($rows, 'entity')); $this->assertSame(['2026-05-01', '2026-07-01'], array_column($rows, 'effective_at')); $this->assertSame(['2026-07-01', '2999-01-01'], array_column($rows, 'invalid_at')); $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')); } public function test_missing_lifecycle_uses_upstream_company_and_default_period(): void { $db = DB::connection('bills_sqlite'); $db->table('bills')->insert([ ['store_name' => '无生命周期店铺', 'occurred_at' => '2026-05-03 12:00:00', 'income' => 25.5], ]); $rows = $this->repository->rows([ ['erp_store_name' => '无生命周期店铺', 'company_name' => '公司表主体', 'store_status' => '0'], ]); $this->assertCount(1, $rows); $this->assertSame('公司表主体', $rows[0]['entity']); $this->assertSame('2026-05-01', $rows[0]['effective_at']); $this->assertSame('2999-01-01', $rows[0]['invalid_at']); $this->assertSame(25.5, $rows[0]['period_sales']); $this->assertSame(25.5, $rows[0]['sales_05']); $this->assertSame('开店中', $rows[0]['status']); } public function test_shop_state_mapping_keeps_opening_closing_and_closed_labels(): void { $rows = $this->repository->rows([ ['erp_store_name' => '开店中店铺', 'store_status' => '0'], ['erp_store_name' => '关店中店铺', 'store_status' => '3'], ['erp_store_name' => '已关店店铺', 'store_status' => '6'], ]); $this->assertSame(['开店中', '关店中', '关店'], array_column($rows, 'status')); } public function test_upstream_business_information_exposes_expanded_rows_for_store_report(): void { DB::connection('bills_sqlite')->table('store_lifecycles')->insert([ ['erp_store_name' => '店铺A', 'company_name' => '主体甲', 'effective_at' => '2026-05-01', 'expires_at' => '2026-07-01'], ['erp_store_name' => '店铺A', 'company_name' => '主体乙', 'effective_at' => '2026-07-01', 'expires_at' => null], ]); DB::connection('bills_sqlite')->table('bills')->insert([ ['store_name' => '店铺A', 'occurred_at' => '2026-06-30 23:59:59', 'income' => 60], ['store_name' => '店铺A', 'occurred_at' => '2026-07-01 00:00:00', 'income' => 70], ]); $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', ]]); $service = new UpstreamBusinessInformation( Mockery::mock(UpstreamQueryCacheFactory::class), $mirror, Mockery::mock(InstallationDetailMirrorRepository::class), $this->repository, ); $paginator = $service->paginate('store-management', 'store-report', 50); $this->assertSame(2, $paginator->total()); $this->assertSame(['主体甲', '主体乙'], array_map(static fn (object $row): string => $row->entity, $paginator->items())); $this->assertSame([60.0, 70.0], array_map(static fn (object $row): float => $row->period_sales, $paginator->items())); $this->assertSame(['关店中', '关店中'], array_map(static fn (object $row): string => $row->status, $paginator->items())); } }