Files
jiu/backend/internal/service/inventory_status_test.go
T
wangjia 73955a139c feat(backend): 库存三态落库(stock/on_sale/sold)——挂售可控 + 卖光留痕
- inventories 加 status 枚举列(默认 stock,AutoMigrate 存量即全部「库存」)+ 索引
- 出库审核扣光置 sold 不再软删(留痕台账);退单恢复行置回 stock;盘亏仍软删
- 新接口 PUT /inventory/:id/status(仅收 stock/on_sale,已售行拒改,租户校验)
- List 默认排除已售(显式 status=sold 才显示)+ status 多值筛选
- Summary:SKU/货值/数量排除已售,新增 sold_count
- 公开店铺页/API 只列 on_sale——酒单从全量裸奔改为人工精选
- 测试:卖光置售/部分不改/退单回库/接口边界/跨租户/汇总口径 8 用例
- 设计方案 docs/design/inventory-sale-status.html + db-schema/CLAUDE.md 同步

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-07 13:35:03 +08:00

96 lines
4.0 KiB
Go

package service
import (
"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"
)
// 库存三态(2026-07-07):出库卖光 → sold 留痕不软删;部分出库不改状态;退单恢复 → 回 stock。
func TestApproveStockOut_SellOutMarksSold(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STS001")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
p := testutil.CreateTestProduct(db, shop.ID, "茅台")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
inv := model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &p.ID, Quantity: 5, Status: "on_sale"}
require.NoError(t, db.Create(&inv).Error)
order := model.StockOutOrder{
TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "OUT-STS-1",
WarehouseID: wh.ID, OperatorID: user.ID, Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockOutItem{{ShopID: shop.ID, ProductID: p.ID, Quantity: 5}},
}
require.NoError(t, db.Create(&order).Error)
require.NoError(t, NewStockService(db).ApproveStockOut(shop.ID, order.ID, user.ID))
// 卖光:行保留(deleted_at 空)、qty=0、status=sold
var after model.Inventory
require.NoError(t, db.Unscoped().Where("id = ?", inv.ID).First(&after).Error)
assert.Equal(t, "sold", after.Status)
assert.Equal(t, float64(0), after.Quantity)
assert.Nil(t, after.DeletedAt, "卖光不再软删")
}
func TestApproveStockOut_PartialKeepsStatus(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STS002")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
p := testutil.CreateTestProduct(db, shop.ID, "五粮液")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
inv := model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &p.ID, Quantity: 10, Status: "on_sale"}
require.NoError(t, db.Create(&inv).Error)
order := model.StockOutOrder{
TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "OUT-STS-2",
WarehouseID: wh.ID, OperatorID: user.ID, Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockOutItem{{ShopID: shop.ID, ProductID: p.ID, Quantity: 3}},
}
require.NoError(t, db.Create(&order).Error)
require.NoError(t, NewStockService(db).ApproveStockOut(shop.ID, order.ID, user.ID))
var after model.Inventory
require.NoError(t, db.Where("id = ?", inv.ID).First(&after).Error)
assert.Equal(t, "on_sale", after.Status, "部分出库不改状态")
assert.Equal(t, float64(7), after.Quantity)
}
func TestReturnStockOut_RestoresToStock(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STS003")
wh := testutil.CreateTestWarehouse(db, shop.ID, "仓")
p := testutil.CreateTestProduct(db, shop.ID, "剑南春")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
inv := model.Inventory{ShopID: shop.ID, WarehouseID: &wh.ID, ProductID: &p.ID, Quantity: 2, Status: "on_sale"}
require.NoError(t, db.Create(&inv).Error)
order := model.StockOutOrder{
TenantBase: model.TenantBase{ShopID: shop.ID}, OrderNo: "OUT-STS-3",
WarehouseID: wh.ID, OperatorID: user.ID, Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockOutItem{{ShopID: shop.ID, ProductID: p.ID, Quantity: 2, SalePrice: 100}},
}
require.NoError(t, db.Create(&order).Error)
svc := NewStockService(db)
require.NoError(t, svc.ApproveStockOut(shop.ID, order.ID, user.ID))
// 退单 → 新建的恢复行应为 stock(保守:需重新手动挂售)
require.NoError(t, svc.ReturnStockOut(shop.ID, order.ID, user.ID, "admin", []uint64{order.Items[0].ID}))
var restored model.Inventory
require.NoError(t, db.Where("shop_id = ? AND product_id = ? AND quantity > 0 AND deleted_at IS NULL", shop.ID, p.ID).
Order("id DESC").First(&restored).Error)
assert.Equal(t, "stock", restored.Status)
assert.Equal(t, float64(2), restored.Quantity)
}