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_id": product.ID, "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. 验证库存变化 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) // 6. 验证库存流水 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) } 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_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_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)) }