*/ private array $temporaryTables = []; protected function setUp(): void { parent::setUp(); $schema = Schema::connection($this->connection); $this->createTableIfMissing('ops_jst_sku', function (Blueprint $table): void { $table->string('sku_id'); $table->decimal('cost_price', 20, 8)->nullable(); }); $this->createTableIfMissing('js_pm_price_adjustment', function (Blueprint $table): void { $table->string('skuCode')->nullable(); $table->decimal('purchasePrice', 20, 8)->nullable(); $table->dateTime('effectTime')->nullable(); }); $this->createTableIfMissing('js_pm_price_adjustment_log', function (Blueprint $table): void { $table->string('skucode')->nullable(); $table->decimal('afterPrice', 20, 8)->nullable(); $table->decimal('originalPrice', 20, 8)->nullable(); $table->dateTime('effectStartTime')->nullable(); $table->dateTime('createTime')->nullable(); }); $this->createTableIfMissing('jushuitan_cost_price_history_rows', function (Blueprint $table): void { $table->string('sku_id')->nullable(); $table->string('sku_id_normalized')->nullable(); $table->dateTime('operated_at')->nullable(); }); DB::connection($this->connection)->table('ops_jst_sku')->insert([ 'sku_id' => 'SKU-1', 'cost_price' => 9, ]); } protected function tearDown(): void { foreach (array_reverse($this->temporaryTables) as $table) { Schema::connection($this->connection)->dropIfExists($table); } parent::tearDown(); } public function test_compute_outputs_reconciliation_freeze_and_auto_skip(): void { $this->seedSourceMonth(); DB::connection($this->connection)->table('product_monthly_weighted_average_states')->insert([ 'report_month' => '2026-09-01', 'status' => 'base_imported', 'created_at' => now(), 'updated_at' => now(), ]); DB::connection($this->connection)->table('product_monthly_weighted_average_rows')->insert([ 'report_month' => '2026-09-01', 'row_key' => str_repeat('a', 40), 'barcode_warehouse' => 'SKU-1测试仓', 'warehouse_short_name' => '测试仓', 'product_code' => 'SKU-1', 'opening_quantity' => 10, 'opening_unit_price' => 5, 'previous_pricing_status' => '已定价', 'base_source' => 'base', 'imported_at' => now(), ]); DB::connection($this->connection)->table('inventory_inbound_detail_rows')->insert([ 'report_month' => '2026-09-01', 'row_key' => hash('sha256', 'inbound-1'), 'barcode' => 'sku-1', 'warehouse_short_name' => '测试仓', 'quantity' => 2, 'amount' => 10, 'estimated_amount' => 2, 'imported_at' => now(), ]); $resultPath = tempnam(sys_get_temp_dir(), 'weighted-result-') . '.json'; try { $this->artisan('product:monthly-weighted-average:compute', [ 'month' => '2026-09', '--result-json' => $resultPath, ])->expectsOutputToContain('总行数 1')->assertSuccessful(); $row = DB::connection($this->connection)->table('product_monthly_weighted_average_rows') ->where('report_month', '2026-09-01') ->first(); $this->assertEquals(5.1667, (float) $row->weighted_average_unit_price); $this->assertEquals(50, (float) $row->opening_amount); $this->assertEquals(12, (float) $row->purchase_amount); $result = json_decode((string) file_get_contents($resultPath), true); $this->assertSame('computed', $result['status']); $this->assertSame(1, $result['totals']['rows']); $this->assertSame(1, $result['totals']['weighted_price_rows']); $this->assertSame(1, $result['inbound']['matched_rows']); $this->assertSame(0, $result['source']['row_difference']); $this->assertSame(0, $result['anomalies']['affected_rows']); $this->artisan('product:monthly-weighted-average:compute', [ 'month' => '2026-09', ])->expectsOutputToContain('历史月份不自动覆盖')->assertFailed(); $autoPath = tempnam(sys_get_temp_dir(), 'weighted-auto-') . '.json'; $this->artisan('product:monthly-weighted-average:auto', [ '--month' => '2026-09', '--result-json' => $autoPath, ])->assertSuccessful(); $auto = json_decode((string) file_get_contents($autoPath), true); $this->assertSame('already_computed', $auto['status']); $this->assertSame(1, $auto['totals']['rows']); @unlink($autoPath); } finally { @unlink($resultPath); } } public function test_auto_blocks_when_inbound_data_is_missing(): void { $this->seedSourceMonth(); DB::connection($this->connection)->table('product_monthly_weighted_average_states')->insert([ ['report_month' => '2026-08-01', 'status' => 'computed', 'created_at' => now(), 'updated_at' => now()], ['report_month' => '2026-09-01', 'status' => 'base_imported', 'created_at' => now(), 'updated_at' => now()], ]); DB::connection($this->connection)->table('product_monthly_weighted_average_rows')->insert([ 'report_month' => '2026-08-01', 'row_key' => str_repeat('b', 40), 'barcode_warehouse' => 'SKU-1测试仓', 'warehouse_short_name' => '测试仓', 'product_code' => 'SKU-1', 'opening_quantity' => 1, 'weighted_average_unit_price' => 5, 'pricing_status' => '已定价', 'base_source' => 'base', 'imported_at' => now(), 'computed_at' => now(), ]); $path = tempnam(sys_get_temp_dir(), 'weighted-blocked-') . '.json'; try { $this->artisan('product:monthly-weighted-average:auto', [ '--month' => '2026-09', '--result-json' => $path, ])->expectsOutputToContain('上游数据未更新或不完整')->assertFailed(); $result = json_decode((string) file_get_contents($path), true); $this->assertSame('blocked', $result['status']); $check = collect($result['checks'])->firstWhere('name', '入库明细已更新'); $this->assertFalse($check['passed']); $this->assertSame( 'blocked', DB::connection($this->connection)->table('product_monthly_weighted_average_states') ->where('report_month', '2026-09-01')->value('status'), ); } finally { @unlink($path); } } private function seedSourceMonth(): void { $generation = (string) Str::uuid(); DB::connection($this->connection)->table('inventory_report_authoritative_months')->insert([ 'dataset_key' => 'jiansun.inventory-report.authoritative-month', 'source_month' => '2026-09-01', 'status' => 'succeeded', 'active_generation' => $generation, 'source_filename' => 'inventory-2026-09.xlsx', 'source_row_count' => 1, 'last_succeeded_at' => now(), 'updated_at' => now(), ]); DB::connection($this->connection)->table('inventory_report_authoritative_rows')->insert([ 'generation' => $generation, 'row_key' => hash('sha256', 'source-row-1'), 'report_month' => '2026-09-01', 'owner' => '测试货主', 'warehouse_provider' => '测试仓', 'warehouse' => '测试仓主仓', 'product_code' => 'SKU-1', 'opening_quantity' => 10, 'closing_quantity' => 10, 'imported_at' => now(), ]); } private function createTableIfMissing(string $table, \Closure $definition): void { if (Schema::connection($this->connection)->hasTable($table)) { return; } Schema::connection($this->connection)->create($table, $definition); $this->temporaryTables[] = $table; } }