Files
jiu/backend/internal/service/stock_test.go
T
wangjia 1b4ca0c675 fix(test): 修复测试文件类型错误和测试数据库 schema 过期问题
- 修正 stock_in/out 测试中 WarehouseID/ProductID 指针误用(StockOrder 用 uint64,Inventory 用 *uint64)
- 更新 testutil/setup.go:inventories 表加入所有批次字段,stock_in_items 加 production_date,新增 finance_records 表
- 修正 inventory handler 中 DATE_FORMAT(MySQL 专属)→ DATE()(跨 DB 兼容)
- 更新断言:order_no 前缀改为 RK,库存总量用 SUM 替代 First
- 库存 List 新增 in_stock=1 过滤器

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 14:16:10 +08:00

299 lines
8.7 KiB
Go

package service
import (
"fmt"
"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"
)
func TestStockService_ApproveStockIn_Success(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STOCK001")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Main Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Beer")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
// 创建入库单
order := model.StockInOrder{
TenantBase: model.TenantBase{ShopID: shop.ID},
OrderNo: "IN20240101000001",
WarehouseID: warehouse.ID,
OperatorID: user.ID,
Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockInItem{
{
ShopID: shop.ID,
ProductID: product.ID,
Quantity: 10,
UnitPrice: 5.0,
TotalPrice: 50.0,
},
},
}
require.NoError(t, db.Create(&order).Error)
svc := NewStockService(db)
err := svc.ApproveStockIn(shop.ID, order.ID, user.ID)
require.NoError(t, err)
// 验证单据状态
var updated model.StockInOrder
db.First(&updated, order.ID)
assert.Equal(t, "approved", updated.Status)
// 验证库存
var inv model.Inventory
db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?",
shop.ID, warehouse.ID, product.ID).First(&inv)
assert.Equal(t, float64(10), inv.Quantity)
// 验证库存流水
var logs []model.InventoryLog
db.Where("shop_id = ? AND product_id = ?", shop.ID, product.ID).Find(&logs)
require.Len(t, logs, 1)
assert.Equal(t, "in", logs[0].Direction)
assert.Equal(t, float64(10), logs[0].Quantity)
assert.Equal(t, float64(0), logs[0].QtyBefore)
assert.Equal(t, float64(10), logs[0].QtyAfter)
}
func TestStockService_ApproveStockIn_NotPending(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STOCK002")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
_ = testutil.CreateTestProduct(db, shop.ID, "Wine")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
order := model.StockInOrder{
TenantBase: model.TenantBase{ShopID: shop.ID},
OrderNo: "IN20240101000002",
WarehouseID: warehouse.ID,
OperatorID: user.ID,
Status: "draft", // 非 pending 状态
OrderDate: model.Date{Time: time.Now()},
}
require.NoError(t, db.Create(&order).Error)
svc := NewStockService(db)
err := svc.ApproveStockIn(shop.ID, order.ID, user.ID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not in pending status")
}
func TestStockService_ApproveStockOut_Success(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STOCK003")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Whiskey")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
// 先入库
inv := model.Inventory{
ShopID: shop.ID,
WarehouseID: &warehouse.ID,
ProductID: &product.ID,
Quantity: 20,
}
require.NoError(t, db.Create(&inv).Error)
// 创建出库单
order := model.StockOutOrder{
TenantBase: model.TenantBase{ShopID: shop.ID},
OrderNo: "OUT20240101000001",
WarehouseID: warehouse.ID,
OperatorID: user.ID,
Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockOutItem{
{
ShopID: shop.ID,
ProductID: product.ID,
Quantity: 5,
UnitPrice: 10.0,
TotalPrice: 50.0,
},
},
}
require.NoError(t, db.Create(&order).Error)
svc := NewStockService(db)
err := svc.ApproveStockOut(shop.ID, order.ID, user.ID)
require.NoError(t, err)
// 验证库存减少
var updatedInv model.Inventory
db.First(&updatedInv, inv.ID)
assert.Equal(t, float64(15), updatedInv.Quantity)
// 验证单据状态
var updatedOrder model.StockOutOrder
db.First(&updatedOrder, order.ID)
assert.Equal(t, "approved", updatedOrder.Status)
}
func TestStockService_ApproveStockOut_InsufficientStock(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STOCK004")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Vodka")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
// 库存只有 3
inv := model.Inventory{
ShopID: shop.ID,
WarehouseID: &warehouse.ID,
ProductID: &product.ID,
Quantity: 3,
}
require.NoError(t, db.Create(&inv).Error)
// 要出库 10
order := model.StockOutOrder{
TenantBase: model.TenantBase{ShopID: shop.ID},
OrderNo: "OUT20240101000002",
WarehouseID: warehouse.ID,
OperatorID: user.ID,
Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockOutItem{
{
ShopID: shop.ID,
ProductID: product.ID,
Quantity: 10,
},
},
}
require.NoError(t, db.Create(&order).Error)
svc := NewStockService(db)
err := svc.ApproveStockOut(shop.ID, order.ID, user.ID)
assert.Error(t, err)
assert.ErrorIs(t, err, ErrInsufficientStock)
}
func TestStockService_ApproveStockOut_ProductNotInInventory(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STOCK005")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Rum")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
// 没有创建库存记录
order := model.StockOutOrder{
TenantBase: model.TenantBase{ShopID: shop.ID},
OrderNo: "OUT20240101000003",
WarehouseID: warehouse.ID,
OperatorID: user.ID,
Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockOutItem{
{
ShopID: shop.ID,
ProductID: product.ID,
Quantity: 5,
},
},
}
require.NoError(t, db.Create(&order).Error)
svc := NewStockService(db)
err := svc.ApproveStockOut(shop.ID, order.ID, user.ID)
assert.Error(t, err)
assert.ErrorIs(t, err, ErrInsufficientStock)
}
func TestStockService_GenerateOrderNo(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STOCK006")
svc := NewStockService(db)
no, err := svc.GenerateOrderNo(shop.ID, "stock_in")
require.NoError(t, err)
assert.NotEmpty(t, no)
// 单号格式: prefix + date + 6位序号
assert.Contains(t, no, "RK")
// 第二次生成序号应递增
no2, err := svc.GenerateOrderNo(shop.ID, "stock_in")
require.NoError(t, err)
assert.NotEqual(t, no, no2)
}
// TestStockService_GenerateOrderNo_Sequential 验证顺序调用生成唯一递增单号
// 注:并发安全由 MySQL 的事务锁保证,SQLite in-memory 不模拟此场景
func TestStockService_GenerateOrderNo_Sequential(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STOCK007")
svc := NewStockService(db)
const count = 5
seen := map[string]bool{}
for i := 0; i < count; i++ {
no, err := svc.GenerateOrderNo(shop.ID, "stock_in")
require.NoError(t, err)
assert.NotEmpty(t, no)
assert.False(t, seen[no], fmt.Sprintf("duplicate order no: %s", no))
seen[no] = true
}
assert.Len(t, seen, count, "should generate exactly %d unique order numbers", count)
}
func TestStockService_MultipleStockIn_AccumulatesInventory(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "STOCK008")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Gin")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
svc := NewStockService(db)
// 第一次入库
order1 := model.StockInOrder{
TenantBase: model.TenantBase{ShopID: shop.ID},
OrderNo: "IN001",
WarehouseID: warehouse.ID,
OperatorID: user.ID,
Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockInItem{
{ShopID: shop.ID, ProductID: product.ID, Quantity: 10},
},
}
require.NoError(t, db.Create(&order1).Error)
require.NoError(t, svc.ApproveStockIn(shop.ID, order1.ID, user.ID))
// 第二次入库
order2 := model.StockInOrder{
TenantBase: model.TenantBase{ShopID: shop.ID},
OrderNo: "IN002",
WarehouseID: warehouse.ID,
OperatorID: user.ID,
Status: "pending",
OrderDate: model.Date{Time: time.Now()},
Items: []model.StockInItem{
{ShopID: shop.ID, ProductID: product.ID, Quantity: 5},
},
}
require.NoError(t, db.Create(&order2).Error)
require.NoError(t, svc.ApproveStockIn(shop.ID, order2.ID, user.ID))
// 总库存应该是 15(两个批次之和)
var totalQty float64
db.Model(&model.Inventory{}).
Where("shop_id = ? AND warehouse_id = ? AND product_id = ? AND deleted_at IS NULL",
shop.ID, &warehouse.ID, &product.ID).
Select("COALESCE(SUM(quantity), 0)").Scan(&totalQty)
assert.Equal(t, float64(15), totalQty)
}