['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''], 'upstream_mirrors.connection' => 'business_mirror', ]); DB::purge('business_mirror'); $this->createTables(); } protected function tearDown(): void { foreach ($this->temporaryFiles as $file) { @unlink($file); } parent::tearDown(); } public function test_it_imports_platform_income_and_lifecycles_into_shared_generations(): void { $bills = $this->xlsx('抖音平台原始账单', [ ['店铺', '记录编码', '结算时间', '发生时间', '收入', '支出', '账单项目'], ['店铺A', 'BILL-1', '2026-08-02 10:00:00', '2026-08-01 09:00:00', '100.25', '2.5', '订单收入'], ['店铺A', 'BILL-2', null, '2026-08-03 11:00:00', '50', '0', '订单收入'], ]); $lifecycles = $this->xlsx('店铺生命周期', [ ['ERP系统店铺名称', '公司名称', '生效时间', '失效时间'], ['店铺A', '主体甲', '2026-01-01', '存续中'], ]); $importer = app(StoreReportDataImporter::class); $billResult = $importer->importBills($bills, 100); $lifecycleResult = $importer->importLifecycles($lifecycles, 100); $this->assertSame(2, $billResult['row_count']); $this->assertSame(1, $lifecycleResult['row_count']); $this->assertDatabaseHas('store_report_bill_rows', [ 'record_code' => 'BILL-1', 'occurred_at' => '2026-08-02 10:00:00', 'income' => 100.25, ], 'business_mirror'); $this->assertDatabaseHas('store_report_bill_rows', [ 'record_code' => 'BILL-2', 'occurred_at' => '2026-08-03 11:00:00', 'income' => 50, ], 'business_mirror'); $this->assertDatabaseHas('store_report_import_states', [ 'dataset_key' => StoreReportRepository::BILLS_DATASET, 'status' => 'succeeded', 'row_count' => 2, ], 'business_mirror'); $this->assertDatabaseHas('store_report_lifecycle_rows', [ 'erp_store_name' => '店铺A', 'company_name' => '主体甲', 'expires_at' => null, ], 'business_mirror'); } public function test_failed_reimport_keeps_the_previous_active_generation(): void { $valid = $this->xlsx('拼多多平台原始账单', [ ['店铺', '记录编码', '发生时间', '收入金额', '支出金额'], ['店铺A', 'PDD-1', '2026-08-01', '10', '0'], ]); $importer = app(StoreReportDataImporter::class); $first = $importer->importBills($valid, 100); $invalid = $this->xlsx('拼多多错误账单', [ ['店铺', '记录编码', '发生时间', '收入金额'], ['店铺A', 'PDD-2', '不是时间', '20'], ]); try { $importer->importBills($invalid, 100); $this->fail('无效时间应导致导入失败。'); } catch (RuntimeException) { $state = DB::connection('business_mirror')->table('store_report_import_states') ->where('dataset_key', StoreReportRepository::BILLS_DATASET)->first(); $this->assertSame('failed', $state->status); $this->assertSame($first['generation'], $state->active_generation); $this->assertSame(1, DB::connection('business_mirror')->table('store_report_bill_rows')->count()); } } private function createTables(): void { $schema = Schema::connection('business_mirror'); $schema->create('store_report_import_states', function (Blueprint $table): void { $table->string('dataset_key')->primary(); $table->string('status')->default('pending'); $table->string('active_generation')->nullable(); $table->string('pending_generation')->nullable(); $table->unsignedBigInteger('row_count')->default(0); $table->string('source_sha256')->nullable(); $table->text('source_files_json')->nullable(); $table->dateTime('last_attempted_at')->nullable(); $table->dateTime('last_succeeded_at')->nullable(); $table->string('last_error_code')->nullable(); $table->timestamps(); }); $schema->create('store_report_bill_rows', function (Blueprint $table): void { $table->increments('id'); $table->string('generation'); $table->string('platform'); $table->string('platform_label'); $table->string('store_name'); $table->dateTime('occurred_at')->nullable(); $table->decimal('income', 20, 4); $table->decimal('expense', 20, 4); $table->string('bill_item')->nullable(); $table->string('record_code'); $table->string('source_file'); $table->string('row_hash'); $table->dateTime('imported_at'); $table->unique(['generation', 'platform', 'record_code']); }); $schema->create('store_report_lifecycle_rows', function (Blueprint $table): void { $table->increments('id'); $table->string('generation'); $table->string('business_key'); $table->string('erp_store_name'); $table->string('company_name')->nullable(); $table->dateTime('effective_at')->nullable(); $table->dateTime('expires_at')->nullable(); $table->string('source_file'); $table->string('row_hash'); $table->dateTime('imported_at'); $table->unique(['generation', 'business_key']); }); } private function xlsx(string $name, array $rows): string { $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $name . '-' . uniqid() . '.xlsx'; $spreadsheet = new Spreadsheet(); $spreadsheet->getActiveSheet()->fromArray($rows); (new Xlsx($spreadsheet))->save($path); $spreadsheet->disconnectWorksheets(); $this->temporaryFiles[] = $path; return $path; } }