package main import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" "github.com/wangjia/jiu/backend/internal/model" "github.com/wangjia/jiu/backend/testutil" ) func f64(v float64) *float64 { return &v } // fixture 复刻线上错列形态: // - 占位商品 HIST-PLACEHOLDER,历史出库明细 product_id 均指向它 // - 明细 cost_price/cost_amount 实为售价口径(源「单价/金额」),sale_* = 0 // - 单头 sale_total = 源合计(售价口径,与明细"成本"合计相等) // // 四个成本回填场景:A=入库明细价 B=库存快照价 D=商品进价 C=查无。 type fixture struct { db *gorm.DB shopID uint64 placeholder *model.Product order1 *model.StockOutOrder // A/B/C 三行,sale_total 与明细一致 order2 *model.StockOutOrder // D 一行,sale_total 故意不一致 normalItem *model.StockOutItem // 非占位商品的正常明细,不得被动到 } func setupFixture(t *testing.T) *fixture { t.Helper() db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "FIX01") wh := testutil.CreateTestWarehouse(db, shop.ID, "主仓") // 复刻线上形态:占位商品已被界面软删,明细仍引用其 id,工具必须照常定位 ph := &model.Product{TenantBase: model.TenantBase{ShopID: shop.ID}, Code: placeholderProductCode, Name: "历史导入占位"} require.NoError(t, db.Create(ph).Error) require.NoError(t, db.Exec( "UPDATE products SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?", ph.ID).Error) // A:入库明细里有真实进价 13600(同时给商品主数据一个更低优先级的进价,验证①优先) prodA := &model.Product{TenantBase: model.TenantBase{ShopID: shop.ID}, Code: "ZXZ027628", Name: "茅台铁盖1995五星", PublicID: "pub-a", PurchasePrice: 1} require.NoError(t, db.Create(prodA).Error) sin := &model.StockInOrder{TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "RKTEST1", WarehouseID: wh.ID, OperatorID: 1, Status: "approved"} require.NoError(t, db.Create(sin).Error) require.NoError(t, db.Create(&model.StockInItem{ OrderID: sin.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "ZXZ027628", Quantity: 1, CostPrice: 13600, CostAmount: 13600, }).Error) // B:只有库存快照价 500 require.NoError(t, db.Create(&model.Inventory{ ShopID: shop.ID, ProductCode: "B001", Quantity: 2, UnitPrice: f64(500), }).Error) // D:只有商品主数据进价 1500 prodD := &model.Product{TenantBase: model.TenantBase{ShopID: shop.ID}, Code: "D001", Name: "商品D", PublicID: "pub-d", PurchasePrice: 1500} require.NoError(t, db.Create(prodD).Error) // 出库单 1:A(14300×1) + B(400×2) + C(999×1),sale_total 与明细合计一致 o1 := &model.StockOutOrder{TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "CKTEST1", WarehouseID: wh.ID, OperatorID: 1, Status: "approved", SaleTotal: 14300 + 800 + 999} require.NoError(t, db.Create(o1).Error) for _, it := range []*model.StockOutItem{ {OrderID: o1.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "ZXZ027628", Quantity: 1, CostPrice: 14300, CostAmount: 14300}, {OrderID: o1.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "B001", Quantity: 2, CostPrice: 400, CostAmount: 800}, {OrderID: o1.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "C001", Quantity: 1, CostPrice: 999, CostAmount: 999}, } { require.NoError(t, db.Create(it).Error) } // 出库单 2:D(2000×1),sale_total 故意与明细不一致(源数据脏),应只报不改 o2 := &model.StockOutOrder{TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "CKTEST2", WarehouseID: wh.ID, OperatorID: 1, Status: "pending", SaleTotal: 123} require.NoError(t, db.Create(o2).Error) require.NoError(t, db.Create(&model.StockOutItem{ OrderID: o2.ID, ShopID: shop.ID, ProductID: ph.ID, ProductCode: "D001", Quantity: 1, CostPrice: 2000, CostAmount: 2000, }).Error) // 正常单(非占位商品):sale_price=0 且 cost>0,但 product_id 非占位 → 不得被动 prodN := testutil.CreateTestProduct(db, shop.ID, "正常商品") o3 := &model.StockOutOrder{TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "CKTEST3", WarehouseID: wh.ID, OperatorID: 1, Status: "approved", SaleTotal: 0} require.NoError(t, db.Create(o3).Error) normal := &model.StockOutItem{OrderID: o3.ID, ShopID: shop.ID, ProductID: prodN.ID, ProductCode: "P-正常商品", Quantity: 1, CostPrice: 100, CostAmount: 100} require.NoError(t, db.Create(normal).Error) return &fixture{db: db, shopID: shop.ID, placeholder: ph, order1: o1, order2: o2, normalItem: normal} } func TestFixShop_DryRunDoesNotWrite(t *testing.T) { fx := setupFixture(t) st, err := fixShop(fx.db, fx.shopID, false) require.NoError(t, err) assert.Equal(t, 4, st.Items) assert.Equal(t, 1, st.CostFromStockIn) assert.Equal(t, 1, st.CostFromInventory) assert.Equal(t, 1, st.CostFromProduct) assert.Equal(t, 1, st.CostUnmatched) assert.Equal(t, 2, st.Orders) assert.Equal(t, []string{"CKTEST2"}, st.TotalMismatches) // 回滚后数据原样 var it model.StockOutItem require.NoError(t, fx.db.Where("product_code = ?", "ZXZ027628"). Where("order_id = ?", fx.order1.ID).First(&it).Error) assert.Equal(t, 14300.0, it.CostPrice) assert.Equal(t, 0.0, it.SalePrice) var o model.StockOutOrder require.NoError(t, fx.db.First(&o, fx.order1.ID).Error) assert.Equal(t, 0.0, o.ProfitTotal) } func TestFixShop_Apply(t *testing.T) { fx := setupFixture(t) st, err := fixShop(fx.db, fx.shopID, true) require.NoError(t, err) assert.Equal(t, 4, st.Items) get := func(orderID uint64, code string) model.StockOutItem { var it model.StockOutItem require.NoError(t, fx.db.Where("order_id = ? AND product_code = ?", orderID, code). First(&it).Error) return it } // A:售价归位 14300,成本回填入库价 13600 a := get(fx.order1.ID, "ZXZ027628") assert.Equal(t, 14300.0, a.SalePrice) assert.Equal(t, 14300.0, a.SaleAmount) assert.Equal(t, 13600.0, a.CostPrice) assert.Equal(t, 13600.0, a.CostAmount) // B:成本回填库存快照价 500,小计 = 2×500 b := get(fx.order1.ID, "B001") assert.Equal(t, 400.0, b.SalePrice) assert.Equal(t, 800.0, b.SaleAmount) assert.Equal(t, 500.0, b.CostPrice) assert.Equal(t, 1000.0, b.CostAmount) // C:查无进价 → 成本待定 0 c := get(fx.order1.ID, "C001") assert.Equal(t, 999.0, c.SalePrice) assert.Equal(t, 0.0, c.CostPrice) assert.Equal(t, 0.0, c.CostAmount) // D:成本回填商品进价 1500 d := get(fx.order2.ID, "D001") assert.Equal(t, 2000.0, d.SalePrice) assert.Equal(t, 1500.0, d.CostPrice) // 单头:利润按 recalcStockOutProfit 口径;sale_total 不动 var o1, o2 model.StockOutOrder require.NoError(t, fx.db.First(&o1, fx.order1.ID).Error) // (14300-13600)×1 + (400-500)×2 + (999-0)×1 = 700 - 200 + 999 assert.InDelta(t, 1499.0, o1.ProfitTotal, 0.001) assert.Equal(t, 16099.0, o1.SaleTotal) require.NoError(t, fx.db.First(&o2, fx.order2.ID).Error) assert.InDelta(t, 500.0, o2.ProfitTotal, 0.001) assert.Equal(t, 123.0, o2.SaleTotal) // 不一致只报不改 assert.Equal(t, []string{"CKTEST2"}, st.TotalMismatches) // 正常明细未被波及 var n model.StockOutItem require.NoError(t, fx.db.First(&n, fx.normalItem.ID).Error) assert.Equal(t, 100.0, n.CostPrice) assert.Equal(t, 0.0, n.SalePrice) // 幂等:重跑无待修行 st2, err := fixShop(fx.db, fx.shopID, true) require.NoError(t, err) assert.Equal(t, 0, st2.Items) assert.Equal(t, 0, st2.Orders) } func TestFixShop_NoPlaceholder(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "FIX02") _, err := fixShop(db, shop.ID, false) assert.Error(t, err) }