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) }