Files
jiu/backend/internal/handler/stock_in_test.go
T
wangjia 7a78448a25
Deploy Server / release-deploy-server (push) Successful in 56s
chore: release server-v1.0.72
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
2026-06-21 19:09:30 +08:00

442 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 TestStockInHandler_FullFlow(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI001")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Main")
product := testutil.CreateTestProduct(db, shop.ID, "Test Beer")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 1. 创建入库单(草稿)
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{
"product_name": "Test Beer",
"series": "普通",
"spec": "500ml",
"quantity": 10.0,
"unit_price": 5.0,
},
},
})
require.Equal(t, http.StatusCreated, w.Code)
orderID := extractID(w)
assert.NotZero(t, orderID)
// 验证状态是 draft
respData := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, "draft", respData["status"])
assert.NotEmpty(t, respData["order_no"])
// 2. 提交(draft → pending
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), token, nil)
require.Equal(t, http.StatusOK, w.Code)
// 3. 获取单据详情
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/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-in/orders/%d/approve", orderID), token, nil)
require.Equal(t, http.StatusOK, w.Code)
// 5. 验证库存变化:入库为明细新建了独立产品,库存指向它(不是预设的 product)
var inv model.Inventory
db.Where("shop_id = ? AND warehouse_id = ?",
shop.ID, warehouse.ID).First(&inv)
assert.Equal(t, float64(10), inv.Quantity)
assert.NotZero(t, inv.ProductID)
assert.NotEqual(t, product.ID, inv.ProductID)
// 6. 验证库存流水
var logs []model.InventoryLog
db.Where("shop_id = ? AND product_id = ?", shop.ID, inv.ProductID).Find(&logs)
require.Len(t, logs, 1)
assert.Equal(t, "in", logs[0].Direction)
assert.Equal(t, float64(10), logs[0].Quantity)
}
func TestStockInHandler_List(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI002")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Wine")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建 2 个入库单
for i := 0; i < 2; i++ {
makeRequest(r, "POST", "/api/v1/stock-in/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, "unit_price": 10.0},
},
})
}
w := makeRequest(r, "GET", "/api/v1/stock-in/orders", token, nil)
assert.Equal(t, http.StatusOK, w.Code)
resp := parseResponse(w)
assert.Equal(t, float64(2), resp["total"].(float64))
}
func TestStockInHandler_Submit_WrongStatus(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI003")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Gin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建
w := makeRequest(r, "POST", "/api/v1/stock-in/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-in/orders/%d/submit", orderID), token, nil)
// 再次提交(应该失败,因为已经是 pending)
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), token, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_Approve_NotPending(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI004")
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)
// 创建但不提交(状态是 draft)
w := makeRequest(r, "POST", "/api/v1/stock-in/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-in/orders/%d/approve", orderID), token, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_Reject(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI005")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Tequila")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建并提交
w := makeRequest(r, "POST", "/api/v1/stock-in/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-in/orders/%d/submit", orderID), token, nil)
// 驳回
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/reject", orderID), token, nil)
assert.Equal(t, http.StatusOK, w.Code)
// 验证状态
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), token, nil)
detailData := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, "rejected", detailData["status"])
}
func TestStockInHandler_Withdraw(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI006")
admin := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
operator := testutil.CreateTestUser(db, shop.ID, "op", "pass", "operator")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Tequila")
adminToken := getAuthToken(admin.ID, shop.ID, "admin")
opToken := getAuthToken(operator.ID, shop.ID, "operator")
r := setupProtectedRouter(db)
// 创建并提交(进入 pending)
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", adminToken, 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-in/orders/%d/submit", orderID), adminToken, nil)
// 1. 非管理员撤回 → 403
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), opToken, nil)
assert.Equal(t, http.StatusForbidden, w.Code)
// 2. 管理员撤回 → 200,状态回到 draft
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), adminToken, nil)
assert.Equal(t, http.StatusOK, w.Code)
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), adminToken, nil)
assert.Equal(t, "draft", parseResponse(w)["data"].(map[string]interface{})["status"])
// 3. 撤回后已是 draft,再撤回 → 400(仅 pending 可撤回)
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/withdraw", orderID), adminToken, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
// 4. 撤回为 draft 后可再次修改并提交
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), adminToken, nil)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestStockInHandler_GetNotFound(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI006")
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-in/orders/99999", token, nil)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestStockInHandler_TotalAmount(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI007")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product1 := testutil.CreateTestProduct(db, shop.ID, "ProductA")
product2 := testutil.CreateTestProduct(db, shop.ID, "ProductB")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_id": product1.ID, "quantity": 10.0, "unit_price": 5.0}, // 50
{"product_id": product2.ID, "quantity": 3.0, "unit_price": 20.0}, // 60
},
})
require.Equal(t, http.StatusCreated, w.Code)
data := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, float64(110), data["total_amount"])
}
func TestStockInHandler_NoAuth(t *testing.T) {
db := testutil.SetupTestDB()
r := setupProtectedRouter(db)
w := makeRequest(r, "GET", "/api/v1/stock-in/orders", "", nil)
assert.Equal(t, http.StatusUnauthorized, w.Code)
w = makeRequest(r, "POST", "/api/v1/stock-in/orders", "", map[string]interface{}{
"warehouse_id": 1,
})
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestStockInHandler_TenantIsolation(t *testing.T) {
db := testutil.SetupTestDB()
shopA := testutil.CreateTestShop(db, "SI_A")
userA := testutil.CreateTestUser(db, shopA.ID, "adminA", "pass", "admin")
warehouseA := testutil.CreateTestWarehouse(db, shopA.ID, "WA")
productA := testutil.CreateTestProduct(db, shopA.ID, "BeerA")
tokenA := getAuthToken(userA.ID, shopA.ID, "admin")
shopB := testutil.CreateTestShop(db, "SI_B")
userB := testutil.CreateTestUser(db, shopB.ID, "adminB", "pass", "admin")
tokenB := getAuthToken(userB.ID, shopB.ID, "admin")
r := setupProtectedRouter(db)
// 门店 A 创建入库单
w := makeRequest(r, "POST", "/api/v1/stock-in/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-in/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-in/orders/%d", orderAID), tokenB, nil)
assert.Equal(t, http.StatusNotFound, w.Code)
// 门店 B 不能提交门店 A 的订单
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderAID), tokenB, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
// 门店 B 不能审核门店 A 的订单
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", orderAID), tokenB, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_Create_MissingWarehouse(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI008")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 缺少 warehouse_id,应该返回 400
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{},
})
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_Reject_NotPending(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI009")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Whiskey")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建但不提交(draft 状态)
w := makeRequest(r, "POST", "/api/v1/stock-in/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-in/orders/%d/reject", orderID), token, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_List_FilterByKeyword(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI011")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Brandy")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 往来单位:茅台供应商
partner := &model.Partner{Code: "P001", Name: "茅台供应商", Type: "supplier"}
partner.ShopID = shop.ID
require.NoError(t, db.Create(partner).Error)
// 单据1:挂往来单位
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"partner_id": partner.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{{"product_id": product.ID, "quantity": 5.0}},
})
require.Equal(t, http.StatusCreated, w.Code)
order1No := parseResponse(w)["data"].(map[string]interface{})["order_no"].(string)
// 单据2:无往来单位
w = makeRequest(r, "POST", "/api/v1/stock-in/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": 3.0}},
})
require.Equal(t, http.StatusCreated, w.Code)
// 按往来单位名搜索 → 只命中单据1
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?keyword=茅台", token, nil)
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
// 按单号搜索 → 只命中单据1
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?keyword="+order1No, token, nil)
assert.Equal(t, float64(1), parseResponse(w)["total"].(float64))
// 无关键词 → 两条都在
w = makeRequest(r, "GET", "/api/v1/stock-in/orders", token, nil)
assert.Equal(t, float64(2), parseResponse(w)["total"].(float64))
}
func TestStockInHandler_List_FilterByStatus(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI010")
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)
// 创建一个 draft 和一个 pending 订单
w := makeRequest(r, "POST", "/api/v1/stock-in/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}},
})
draftOrderID := extractID(w)
w = makeRequest(r, "POST", "/api/v1/stock-in/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": 3.0}},
})
pendingOrderID := extractID(w)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", pendingOrderID), token, nil)
_ = draftOrderID
// 过滤 pending 状态
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?status=pending", token, nil)
resp := parseResponse(w)
assert.Equal(t, float64(1), resp["total"].(float64))
// 过滤 draft 状态
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?status=draft", token, nil)
resp = parseResponse(w)
assert.Equal(t, float64(1), resp["total"].(float64))
// 逗号分隔的多状态:pending + draft 应返回两条(审核标签页用法)
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?status=pending,draft", token, nil)
resp = parseResponse(w)
assert.Equal(t, float64(2), resp["total"].(float64))
}