package handler // 2026-07 定价字段消歧(cost_price/cost_amount/cost_total/sale_amount/sale_total/profit_total) // 的专项回归:建单三值落库、确认售价联动、确认进价→利润联动、operator 响应抹零。 import ( "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/wangjia/jiu/backend/internal/model" "github.com/wangjia/jiu/backend/testutil" ) // 建单即落三值:sale_amount(行)、sale_total、profit_total(单据) func TestStockOutHandler_Create_PricingFields(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "PRC1") user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "W") prod := testutil.CreateTestProduct(db, shop.ID, "Gin") token := getAuthToken(user.ID, shop.ID, "admin") r := setupProtectedRouter(db) db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &prod.ID, Quantity: 100}) // 成本 355 × 2,售价 400 × 2 —— 即线上暴露口径问题的那组数 w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{ "warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339), "items": []map[string]interface{}{ {"product_id": prod.ID, "quantity": 2.0, "cost_price": 355.0, "sale_price": 400.0}, }, }) require.Equal(t, http.StatusCreated, w.Code) orderID := extractID(w) var it model.StockOutItem db.Where("order_id = ?", orderID).First(&it) assert.Equal(t, float64(355), it.CostPrice) assert.Equal(t, float64(710), it.CostAmount) // 2×355 assert.Equal(t, float64(800), it.SaleAmount) // 2×400 var o model.StockOutOrder db.First(&o, orderID) assert.Equal(t, float64(800), o.SaleTotal) assert.Equal(t, float64(90), o.ProfitTotal) // (400-355)×2 } // 兼容回归:旧客户端发 unit_price(无 cost_price)→ 回落生效 func TestStockOutHandler_Create_LegacyUnitPriceKey(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "PRC2") user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "W") prod := testutil.CreateTestProduct(db, shop.ID, "Rum") token := getAuthToken(user.ID, shop.ID, "admin") r := setupProtectedRouter(db) db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &prod.ID, Quantity: 10}) w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{ "warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339), "items": []map[string]interface{}{ {"product_id": prod.ID, "quantity": 3.0, "unit_price": 20.0, "sale_price": 25.0}, }, }) require.Equal(t, http.StatusCreated, w.Code) var it model.StockOutItem db.Where("order_id = ?", extractID(w)).First(&it) assert.Equal(t, float64(20), it.CostPrice) assert.Equal(t, float64(60), it.CostAmount) } // 确认售价:sale_price/sale_amount/sale_total/profit_total 四值联动重算 func TestStockOutHandler_ConfirmSale_RecalcsAmounts(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "PRC3") user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "W") prod := testutil.CreateTestProduct(db, shop.ID, "Sake") token := getAuthToken(user.ID, shop.ID, "admin") r := setupProtectedRouter(db) db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &prod.ID, Quantity: 100}) // 待定价出库:成本 5 × 10 w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{ "warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339), "items": []map[string]interface{}{ {"product_id": prod.ID, "quantity": 10.0, "cost_price": 5.0}, }, }) require.Equal(t, http.StatusCreated, w.Code) orderID := extractID(w) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderID), token, nil) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", orderID), token, nil) var it model.StockOutItem db.Where("order_id = ?", orderID).First(&it) assert.Equal(t, float64(0), it.SaleAmount) // 待定价 w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-out/orders/%d/confirm-sale", orderID), token, map[string]interface{}{ "items": []map[string]interface{}{{"item_id": it.ID, "sale_price": 8.0}}, }) require.Equal(t, http.StatusOK, w.Code) var got model.StockOutItem db.First(&got, it.ID) assert.Equal(t, float64(8), got.SalePrice) assert.Equal(t, float64(80), got.SaleAmount) var o model.StockOutOrder db.First(&o, orderID) assert.Equal(t, float64(80), o.SaleTotal) assert.Equal(t, float64(30), o.ProfitTotal) // (8-5)×10 } // 确认进价(成本回填)后,受影响出库单的 profit_total 联动重算 func TestConfirmStockInCost_RecalcsStockOutProfit(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "PRC4") user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "W") token := getAuthToken(user.ID, shop.ID, "admin") r := setupProtectedRouter(db) // 0 价暂估入库(新建独立产品)→ 审核 w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{ "warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339), "items": []map[string]interface{}{ {"product_name": "调货茅台", "series": "普通", "spec": "500ml", "quantity": 10.0, "cost_price": 0.0}, }, }) require.Equal(t, http.StatusCreated, w.Code) inOrderID := extractID(w) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", inOrderID), token, nil) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", inOrderID), token, nil) var inItem model.StockInItem require.NoError(t, db.Where("order_id = ?", inOrderID).First(&inItem).Error) // 以 0 成本出库 2 瓶、售价 10 → 审核;利润此刻 = (10-0)×2 = 20 w = makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{ "warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339), "items": []map[string]interface{}{ {"product_id": inItem.ProductID, "quantity": 2.0, "cost_price": 0.0, "sale_price": 10.0}, }, }) require.Equal(t, http.StatusCreated, w.Code) outOrderID := extractID(w) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", outOrderID), token, nil) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", outOrderID), token, nil) var beforeO model.StockOutOrder db.First(&beforeO, outOrderID) assert.Equal(t, float64(20), beforeO.ProfitTotal) // 确认进价 4 → 出库成本快照回填 → 利润重算 (10-4)×2 = 12 w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/confirm-cost", inOrderID), token, map[string]interface{}{ "items": []map[string]interface{}{{"item_id": inItem.ID, "unit_price": 4.0}}, }) require.Equal(t, http.StatusOK, w.Code) var outItem model.StockOutItem db.Where("order_id = ?", outOrderID).First(&outItem) assert.Equal(t, float64(4), outItem.CostPrice) assert.Equal(t, float64(8), outItem.CostAmount) var afterO model.StockOutOrder db.First(&afterO, outOrderID) assert.Equal(t, float64(12), afterO.ProfitTotal) } // 成本/利润仅管理员可见:operator 的 List/Get 响应中 cost_price/cost_amount/profit_total 被抹零 func TestStockOutHandler_CostHiddenFromOperator(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "PRC5") admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") op := testutil.CreateTestUser(db, shop.ID, "op1", "pass", "operator") wh := testutil.CreateTestWarehouse(db, shop.ID, "W") prod := testutil.CreateTestProduct(db, shop.ID, "Vermouth") adminToken := getAuthToken(admin.ID, shop.ID, "admin") opToken := getAuthToken(op.ID, shop.ID, "operator") r := setupProtectedRouter(db) db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &prod.ID, Quantity: 10}) w := makeRequest(r, "POST", "/api/v1/stock-out/orders", adminToken, map[string]interface{}{ "warehouse_id": wh.ID, "order_date": time.Now().Format(time.RFC3339), "items": []map[string]interface{}{ {"product_id": prod.ID, "quantity": 2.0, "cost_price": 30.0, "sale_price": 50.0}, }, }) require.Equal(t, http.StatusCreated, w.Code) orderID := extractID(w) // 管理员可见成本与利润 w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-out/orders/%d", orderID), adminToken, nil) require.Equal(t, http.StatusOK, w.Code) data := parseResponse(w)["data"].(map[string]interface{}) assert.Equal(t, float64(40), data["profit_total"]) items := data["items"].([]interface{}) assert.Equal(t, float64(30), items[0].(map[string]interface{})["cost_price"]) // operator:成本/利润抹零,售价照常 w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-out/orders/%d", orderID), opToken, nil) require.Equal(t, http.StatusOK, w.Code) data = parseResponse(w)["data"].(map[string]interface{}) assert.Equal(t, float64(0), data["profit_total"]) items = data["items"].([]interface{}) line := items[0].(map[string]interface{}) assert.Equal(t, float64(0), line["cost_price"]) assert.Equal(t, float64(0), line["cost_amount"]) assert.Equal(t, float64(50), line["sale_price"]) assert.Equal(t, float64(100), line["sale_amount"]) }