1b4ca0c675
- 修正 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>
308 lines
11 KiB
Go
308 lines
11 KiB
Go
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"
|
|
)
|
|
|
|
func TestStockOutHandler_FullFlow(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SO001")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Main")
|
|
product := testutil.CreateTestProduct(db, shop.ID, "Whiskey")
|
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
|
r := setupProtectedRouter(db)
|
|
|
|
// 先建立库存
|
|
db.Create(&model.Inventory{
|
|
ShopID: shop.ID,
|
|
WarehouseID: &warehouse.ID,
|
|
ProductID: &product.ID,
|
|
Quantity: 100,
|
|
})
|
|
|
|
// 1. 创建出库单
|
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
|
"warehouse_id": warehouse.ID,
|
|
"order_date": time.Now().Format(time.RFC3339),
|
|
"items": []map[string]interface{}{
|
|
{
|
|
"product_id": product.ID,
|
|
"quantity": 15.0,
|
|
"unit_price": 20.0,
|
|
},
|
|
},
|
|
})
|
|
require.Equal(t, http.StatusCreated, w.Code)
|
|
orderID := extractID(w)
|
|
assert.NotZero(t, orderID)
|
|
|
|
data := parseResponse(w)["data"].(map[string]interface{})
|
|
assert.Equal(t, "draft", data["status"])
|
|
assert.NotEmpty(t, data["order_no"])
|
|
|
|
// 2. 提交
|
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderID), token, nil)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// 3. 获取详情
|
|
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-out/orders/%d", orderID), token, nil)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
detailData := parseResponse(w)["data"].(map[string]interface{})
|
|
assert.Equal(t, "pending", detailData["status"])
|
|
|
|
// 4. 审核通过
|
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", orderID), token, nil)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// 5. 验证库存减少
|
|
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(85), inv.Quantity)
|
|
}
|
|
|
|
func TestStockOutHandler_InsufficientStock(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SO002")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
|
product := testutil.CreateTestProduct(db, shop.ID, "Vodka")
|
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
|
r := setupProtectedRouter(db)
|
|
|
|
// 库存只有 5
|
|
db.Create(&model.Inventory{
|
|
ShopID: shop.ID,
|
|
WarehouseID: &warehouse.ID,
|
|
ProductID: &product.ID,
|
|
Quantity: 5,
|
|
})
|
|
|
|
// 创建出库单要出 10
|
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
|
"warehouse_id": warehouse.ID,
|
|
"order_date": time.Now().Format(time.RFC3339),
|
|
"items": []map[string]interface{}{
|
|
{"product_id": product.ID, "quantity": 10.0},
|
|
},
|
|
})
|
|
orderID := extractID(w)
|
|
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderID), token, nil)
|
|
|
|
// 审核应该失败
|
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", orderID), token, nil)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestStockOutHandler_List(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SO003")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
|
product := testutil.CreateTestProduct(db, shop.ID, "Beer")
|
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
|
r := setupProtectedRouter(db)
|
|
|
|
// 先建立库存
|
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product.ID, Quantity: 100})
|
|
|
|
// 创建 2 个出库单
|
|
for i := 0; i < 2; i++ {
|
|
makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
|
"warehouse_id": warehouse.ID,
|
|
"order_date": time.Now().Format(time.RFC3339),
|
|
"items": []map[string]interface{}{
|
|
{"product_id": product.ID, "quantity": 1.0},
|
|
},
|
|
})
|
|
}
|
|
|
|
w := makeRequest(r, "GET", "/api/v1/stock-out/orders", token, nil)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
resp := parseResponse(w)
|
|
assert.Equal(t, float64(2), resp["total"].(float64))
|
|
}
|
|
|
|
func TestStockOutHandler_Reject(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SO004")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
|
product := testutil.CreateTestProduct(db, shop.ID, "Rum")
|
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
|
r := setupProtectedRouter(db)
|
|
|
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product.ID, Quantity: 100})
|
|
|
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
|
"warehouse_id": warehouse.ID,
|
|
"order_date": time.Now().Format(time.RFC3339),
|
|
"items": []map[string]interface{}{
|
|
{"product_id": product.ID, "quantity": 5.0},
|
|
},
|
|
})
|
|
orderID := extractID(w)
|
|
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderID), token, nil)
|
|
|
|
// 驳回
|
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/reject", orderID), token, nil)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// 验证状态
|
|
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-out/orders/%d", orderID), token, nil)
|
|
detailData := parseResponse(w)["data"].(map[string]interface{})
|
|
assert.Equal(t, "rejected", detailData["status"])
|
|
}
|
|
|
|
func TestStockOutHandler_GetNotFound(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SO005")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
|
r := setupProtectedRouter(db)
|
|
|
|
w := makeRequest(r, "GET", "/api/v1/stock-out/orders/99999", token, nil)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestStockOutHandler_NoAuth(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
r := setupProtectedRouter(db)
|
|
|
|
w := makeRequest(r, "GET", "/api/v1/stock-out/orders", "", nil)
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
w = makeRequest(r, "POST", "/api/v1/stock-out/orders", "", map[string]interface{}{
|
|
"warehouse_id": 1,
|
|
})
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
func TestStockOutHandler_TenantIsolation(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
|
|
shopA := testutil.CreateTestShop(db, "SO_A")
|
|
userA := testutil.CreateTestUser(db, shopA.ID, "adminA", "pass", "admin")
|
|
warehouseA := testutil.CreateTestWarehouse(db, shopA.ID, "WA")
|
|
productA := testutil.CreateTestProduct(db, shopA.ID, "BrandyA")
|
|
tokenA := getAuthToken(userA.ID, shopA.ID, "admin")
|
|
|
|
shopB := testutil.CreateTestShop(db, "SO_B")
|
|
userB := testutil.CreateTestUser(db, shopB.ID, "adminB", "pass", "admin")
|
|
tokenB := getAuthToken(userB.ID, shopB.ID, "admin")
|
|
|
|
r := setupProtectedRouter(db)
|
|
|
|
// 建立门店 A 的库存
|
|
db.Create(&model.Inventory{
|
|
ShopID: shopA.ID,
|
|
WarehouseID: &warehouseA.ID,
|
|
ProductID: &productA.ID,
|
|
Quantity: 50,
|
|
})
|
|
|
|
// 门店 A 创建出库单
|
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", tokenA, map[string]interface{}{
|
|
"warehouse_id": warehouseA.ID,
|
|
"order_date": time.Now().Format(time.RFC3339),
|
|
"items": []map[string]interface{}{
|
|
{"product_id": productA.ID, "quantity": 5.0},
|
|
},
|
|
})
|
|
require.Equal(t, http.StatusCreated, w.Code)
|
|
orderAID := extractID(w)
|
|
|
|
// 门店 B 看不到门店 A 的出库单
|
|
w = makeRequest(r, "GET", "/api/v1/stock-out/orders", tokenB, nil)
|
|
resp := parseResponse(w)
|
|
assert.Equal(t, float64(0), resp["total"].(float64))
|
|
|
|
// 门店 B 不能获取门店 A 的出库单详情
|
|
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-out/orders/%d", orderAID), tokenB, nil)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
// 门店 B 不能提交门店 A 的出库单
|
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderAID), tokenB, nil)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestStockOutHandler_Create_MissingWarehouse(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SO006")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
|
r := setupProtectedRouter(db)
|
|
|
|
// 缺少 warehouse_id(必填)
|
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
|
"order_date": time.Now().Format(time.RFC3339),
|
|
})
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestStockOutHandler_Approve_NotPending(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SO007")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
|
product := testutil.CreateTestProduct(db, shop.ID, "Cognac")
|
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
|
r := setupProtectedRouter(db)
|
|
|
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product.ID, Quantity: 50})
|
|
|
|
// 创建但不提交
|
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
|
"warehouse_id": warehouse.ID,
|
|
"order_date": time.Now().Format(time.RFC3339),
|
|
"items": []map[string]interface{}{
|
|
{"product_id": product.ID, "quantity": 5.0},
|
|
},
|
|
})
|
|
orderID := extractID(w)
|
|
|
|
// 直接审核(应该失败,因为是 draft 状态)
|
|
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", orderID), token, nil)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestStockOutHandler_InventoryLog_OnApprove(t *testing.T) {
|
|
db := testutil.SetupTestDB()
|
|
shop := testutil.CreateTestShop(db, "SO008")
|
|
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
|
|
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
|
|
product := testutil.CreateTestProduct(db, shop.ID, "Moutai")
|
|
token := getAuthToken(user.ID, shop.ID, "admin")
|
|
r := setupProtectedRouter(db)
|
|
|
|
db.Create(&model.Inventory{ShopID: shop.ID, WarehouseID: &warehouse.ID, ProductID: &product.ID, Quantity: 100})
|
|
|
|
// 创建、提交、审核
|
|
w := makeRequest(r, "POST", "/api/v1/stock-out/orders", token, map[string]interface{}{
|
|
"warehouse_id": warehouse.ID,
|
|
"order_date": time.Now().Format(time.RFC3339),
|
|
"items": []map[string]interface{}{{"product_id": product.ID, "quantity": 20.0}},
|
|
})
|
|
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 logs []model.InventoryLog
|
|
db.Where("shop_id = ? AND product_id = ? AND direction = 'out'", shop.ID, product.ID).Find(&logs)
|
|
require.Len(t, logs, 1)
|
|
assert.Equal(t, float64(20), logs[0].Quantity)
|
|
assert.Equal(t, float64(100), logs[0].QtyBefore)
|
|
assert.Equal(t, float64(80), logs[0].QtyAfter)
|
|
}
|