package handler 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" ) // 调货核心场景:0 价入库 → 审核 → 部分出库 → 确认进价(前向补偿)。 func TestConfirmCost_BackfillsCostAndAdjustsPayable(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "CC_MAIN") admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "仓") prod := testutil.CreateTestProduct(db, shop.ID, "茅台") adminToken := getAuthToken(admin.ID, shop.ID, "admin") r := setupProtectedRouter(db) // 0 价入库 10 → 审核 w := makeRequest(r, "POST", "/api/v1/stock-in/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": 10.0, "unit_price": 0.0}}, }) require.Equal(t, http.StatusCreated, w.Code) inID := extractID(w) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", inID), adminToken, nil) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", inID), adminToken, nil) var inItem model.StockInItem require.NoError(t, db.Where("order_id = ?", inID).First(&inItem).Error) pid := inItem.ProductID // 序列号模型:入库为该行新建独立 product // 库存批次成本待定(CostPrice 为 NULL) var inv model.Inventory require.NoError(t, db.Where("shop_id = ? AND stock_in_item_id = ?", shop.ID, inItem.ID).First(&inv).Error) assert.Nil(t, inv.UnitPrice, "0 价入库时库存成本应为待定(NULL)") // 出库 4(售价 80,成本暂为 0)→ 审核 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": pid, "quantity": 4.0, "unit_price": 0.0, "sale_price": 80.0}}, }) outID := extractID(w) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", outID), adminToken, nil) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", outID), adminToken, nil) // 确认进价 50 w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/confirm-cost", inID), adminToken, map[string]interface{}{"items": []map[string]interface{}{{"item_id": inItem.ID, "unit_price": 50.0}}}) require.Equal(t, http.StatusOK, w.Code) // 1. 入库明细成本回填 db.First(&inItem, inItem.ID) assert.Equal(t, 50.0, inItem.CostPrice) assert.Equal(t, 500.0, inItem.CostAmount) // 2. 剩余库存批次成本回填(6 件) db.First(&inv, inv.ID) require.NotNil(t, inv.UnitPrice) assert.Equal(t, 50.0, *inv.UnitPrice) // 3. 已出库行成本快照回填,售价/应收不变 var outItem model.StockOutItem require.NoError(t, db.Where("order_id = ?", outID).First(&outItem).Error) assert.Equal(t, 50.0, outItem.CostPrice, "已出库成本应回填") assert.Equal(t, 200.0, outItem.CostAmount, "成本小计 = 50×4") assert.Equal(t, 80.0, outItem.SalePrice, "售价不应被改动") var outOrder model.StockOutOrder db.First(&outOrder, outID) assert.Equal(t, 320.0, outOrder.SaleTotal, "应收按售价 4×80,确认进价不影响") // 4. 入库单总额按差额重算:0 → 500 var inOrder model.StockInOrder db.First(&inOrder, inID) assert.Equal(t, 500.0, inOrder.CostTotal) // 5. 应付差额调整流水:+500(balance 是应收应付混合滚动总账:0 入 + 320 应收 + 500 = 820) var adj model.FinanceRecord require.NoError(t, db.Where("shop_id = ? AND type = 'payable' AND ref_type = 'stock_in_cost_adjust'", shop.ID). First(&adj).Error) assert.Equal(t, 500.0, adj.Amount) assert.Equal(t, 820.0, adj.Balance) // 6. product 参考进价同步(pid 是入库新建的独立 product) var p2 model.Product require.NoError(t, db.First(&p2, pid).Error) assert.Equal(t, 50.0, p2.PurchasePrice) } // 非管理员无权确认进价。 func TestConfirmCost_Forbidden(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "CC_FORBID") admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") op := testutil.CreateTestUser(db, shop.ID, "op", "pass", "operator") wh := testutil.CreateTestWarehouse(db, shop.ID, "仓") prod := testutil.CreateTestProduct(db, shop.ID, "五粮液") adminToken := getAuthToken(admin.ID, shop.ID, "admin") opToken := getAuthToken(op.ID, shop.ID, "operator") r := setupProtectedRouter(db) w := makeRequest(r, "POST", "/api/v1/stock-in/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": 5.0, "unit_price": 0.0}}, }) inID := extractID(w) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", inID), adminToken, nil) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", inID), adminToken, nil) var inItem model.StockInItem db.Where("order_id = ?", inID).First(&inItem) w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/confirm-cost", inID), opToken, map[string]interface{}{"items": []map[string]interface{}{{"item_id": inItem.ID, "unit_price": 30.0}}}) assert.Equal(t, http.StatusForbidden, w.Code) } // 仅 approved 单可确认;草稿/待审核拒绝。 func TestConfirmCost_RejectsNonApproved(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "CC_DRAFT") admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "仓") prod := testutil.CreateTestProduct(db, shop.ID, "汾酒") adminToken := getAuthToken(admin.ID, shop.ID, "admin") r := setupProtectedRouter(db) w := makeRequest(r, "POST", "/api/v1/stock-in/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": 5.0, "unit_price": 0.0}}, }) inID := extractID(w) var inItem model.StockInItem db.Where("order_id = ?", inID).First(&inItem) // 仍是 draft → 400 w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/confirm-cost", inID), adminToken, map[string]interface{}{"items": []map[string]interface{}{{"item_id": inItem.ID, "unit_price": 30.0}}}) assert.Equal(t, http.StatusBadRequest, w.Code) } // 重复确认:按新旧差额增量补偿,不重复全量。 func TestConfirmCost_RepeatableByDiff(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "CC_REPEAT") admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "仓") prod := testutil.CreateTestProduct(db, shop.ID, "剑南春") adminToken := getAuthToken(admin.ID, shop.ID, "admin") r := setupProtectedRouter(db) w := makeRequest(r, "POST", "/api/v1/stock-in/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": 10.0, "unit_price": 0.0}}, }) inID := extractID(w) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", inID), adminToken, nil) makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", inID), adminToken, nil) var inItem model.StockInItem db.Where("order_id = ?", inID).First(&inItem) // 第一次确认 50(diff 500) makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/confirm-cost", inID), adminToken, map[string]interface{}{"items": []map[string]interface{}{{"item_id": inItem.ID, "unit_price": 50.0}}}) // 第二次修正为 60(diff 100) w = makeRequest(r, "POST", fmt.Sprintf("/api/v1/stock-in/orders/%d/confirm-cost", inID), adminToken, map[string]interface{}{"items": []map[string]interface{}{{"item_id": inItem.ID, "unit_price": 60.0}}}) require.Equal(t, http.StatusOK, w.Code) db.First(&inItem, inItem.ID) assert.Equal(t, 60.0, inItem.CostPrice) var inOrder model.StockInOrder db.First(&inOrder, inID) assert.Equal(t, 600.0, inOrder.CostTotal, "总额 = 60×10") // 两条调整流水:+500 与 +100,余额滚动到 600 var adjs []model.FinanceRecord db.Where("shop_id = ? AND ref_type = 'stock_in_cost_adjust'", shop.ID).Order("id ASC").Find(&adjs) require.Len(t, adjs, 2) assert.Equal(t, 500.0, adjs[0].Amount) assert.Equal(t, 100.0, adjs[1].Amount) assert.Equal(t, 600.0, adjs[1].Balance) }