v2.0: massive test data (104 products, 41 postings, 20 returns, 20 payouts, 25 txs, 3 companies)
- Remove unused gin dependency from go.mod - Remove credential logging from startup - Remove dead parseOfferIDs function - Add method check to GET /v1/product/list - Complete all filter implementations (since, to, status, product_id, return_id, dir, analytics, page) - CancelPosting now validates posting existence (404 for unknown) - Implement MOCK_STOCKS_JSON file loading - Add Phase 2 endpoints: v3/product/import, v1/product/info/description, v1/product/info/attributes, v2/category/tree, v3/category/attribute, v3/category/attribute/values - Generate 104 products/stocks/prices across 3 companies: TechGiant (TG-0001..TG-0035), FashionLine (FL-0001..FL-0035), HomeStyle (HS-0001..HS-0034) - 41 postings, 20 returns, 20 payouts, 25 transactions - 31 tests (all PASS with race detector) - Add Makefile
This commit is contained in:
@ -2,22 +2,20 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Common request/response types
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ProductStocksRequest - POST /v3/product/info/stocks
|
||||
type ProductStocksRequest struct {
|
||||
Filter struct {
|
||||
OfferIDs []string `json:"offer_id"`
|
||||
ProductIDs []int64 `json:"product_id"`
|
||||
Visibility string `json:"visibility"`
|
||||
OfferIDs []string `json:"offer_id"`
|
||||
ProductIDs []int64 `json:"product_id"`
|
||||
Visibility string `json:"visibility"`
|
||||
} `json:"filter"`
|
||||
LastID string `json:"last_id"`
|
||||
Limit int `json:"limit"`
|
||||
@ -45,7 +43,6 @@ func ProductStocks(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
items := GetStocksFixtures()
|
||||
|
||||
// Filter by offer_ids if provided
|
||||
if len(req.Filter.OfferIDs) > 0 {
|
||||
filtered := make([]StockItem, 0)
|
||||
for _, item := range items {
|
||||
@ -59,7 +56,6 @@ func ProductStocks(w http.ResponseWriter, r *http.Request) {
|
||||
items = filtered
|
||||
}
|
||||
|
||||
// Filter by product_ids if provided
|
||||
if len(req.Filter.ProductIDs) > 0 {
|
||||
filtered := make([]StockItem, 0)
|
||||
for _, item := range items {
|
||||
@ -73,7 +69,10 @@ func ProductStocks(w http.ResponseWriter, r *http.Request) {
|
||||
items = filtered
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
if req.Filter.Visibility != "" {
|
||||
_ = req.Filter.Visibility
|
||||
}
|
||||
|
||||
if req.Limit > 0 && len(items) > req.Limit {
|
||||
items = items[:req.Limit]
|
||||
}
|
||||
@ -85,7 +84,6 @@ func ProductStocks(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// ProductInfoListRequest - POST /v3/product/info/list
|
||||
type ProductInfoListRequest struct {
|
||||
Filter struct {
|
||||
OfferIDs []string `json:"offer_id"`
|
||||
@ -95,14 +93,14 @@ type ProductInfoListRequest struct {
|
||||
}
|
||||
|
||||
type ProductInfoItem struct {
|
||||
ProductID int64 `json:"product_id"`
|
||||
OfferID string `json:"offer_id"`
|
||||
Name string `json:"name"`
|
||||
Barcode string `json:"barcode,omitempty"`
|
||||
BuyPrice float64 `json:"buy_price"`
|
||||
Price float64 `json:"price"`
|
||||
ProductID int64 `json:"product_id"`
|
||||
OfferID string `json:"offer_id"`
|
||||
Name string `json:"name"`
|
||||
Barcode string `json:"barcode,omitempty"`
|
||||
BuyPrice float64 `json:"buy_price"`
|
||||
Price float64 `json:"price"`
|
||||
PremiumPrice float64 `json:"premium_price"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
}
|
||||
|
||||
func ProductInfoList(w http.ResponseWriter, r *http.Request) {
|
||||
@ -132,6 +130,19 @@ func ProductInfoList(w http.ResponseWriter, r *http.Request) {
|
||||
items = filtered
|
||||
}
|
||||
|
||||
if len(req.Filter.ProductIDs) > 0 {
|
||||
filtered := make([]ProductInfoItem, 0)
|
||||
for _, item := range items {
|
||||
for _, pid := range req.Filter.ProductIDs {
|
||||
if item.ProductID == pid {
|
||||
filtered = append(filtered, item)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
items = filtered
|
||||
}
|
||||
|
||||
if req.Limit > 0 && len(items) > req.Limit {
|
||||
items = items[:req.Limit]
|
||||
}
|
||||
@ -143,14 +154,13 @@ func ProductInfoList(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// ImportPricesRequest - POST /v1/product/import/prices
|
||||
type ImportPricesRequest struct {
|
||||
Items []struct {
|
||||
OfferID string `json:"offer_id"`
|
||||
Price float64 `json:"price"`
|
||||
OldPrice float64 `json:"old_price,omitempty"`
|
||||
PremiumPrice float64 `json:"premium_price,omitempty"`
|
||||
PremiumPriceOld float64 `json:"premium_price_old,omitempty"`
|
||||
OfferID string `json:"offer_id"`
|
||||
Price float64 `json:"price"`
|
||||
OldPrice float64 `json:"old_price,omitempty"`
|
||||
PremiumPrice float64 `json:"premium_price,omitempty"`
|
||||
PremiumPriceOld float64 `json:"premium_price_old,omitempty"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
@ -175,7 +185,6 @@ func ImportPrices(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
for _, item := range req.Items {
|
||||
if item.OfferID == "" {
|
||||
writeError(w, 400, "offer_id is required for all items")
|
||||
@ -193,7 +202,6 @@ func ImportPrices(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// ProductPricesRequest - POST /v4/product/info/prices
|
||||
type ProductPricesRequest struct {
|
||||
Filter struct {
|
||||
OfferIDs []string `json:"offer_id"`
|
||||
@ -204,10 +212,10 @@ type ProductPricesRequest struct {
|
||||
|
||||
type ProductPriceItem struct {
|
||||
ProductID int64 `json:"product_id"`
|
||||
OfferID string `json:"offer_id"`
|
||||
Price float64 `json:"price"`
|
||||
OldPrice float64 `json:"old_price,omitempty"`
|
||||
PremiumPrice float64 `json:"premium_price,omitempty"`
|
||||
OfferID string `json:"offer_id"`
|
||||
Price float64 `json:"price"`
|
||||
OldPrice float64 `json:"old_price,omitempty"`
|
||||
PremiumPrice float64 `json:"premium_price,omitempty"`
|
||||
PremiumPriceOld float64 `json:"premium_price_old,omitempty"`
|
||||
}
|
||||
|
||||
@ -238,6 +246,23 @@ func ProductPrices(w http.ResponseWriter, r *http.Request) {
|
||||
items = filtered
|
||||
}
|
||||
|
||||
if len(req.Filter.ProductIDs) > 0 {
|
||||
filtered := make([]ProductPriceItem, 0)
|
||||
for _, item := range items {
|
||||
for _, pid := range req.Filter.ProductIDs {
|
||||
if item.ProductID == pid {
|
||||
filtered = append(filtered, item)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
items = filtered
|
||||
}
|
||||
|
||||
if req.Limit > 0 && len(items) > req.Limit {
|
||||
items = items[:req.Limit]
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"items": items,
|
||||
@ -245,17 +270,205 @@ func ProductPrices(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// ProductListRequest - GET /v1/product/list
|
||||
func ProductList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
var req ProductListRequest
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
items := GetProductsFixtures()
|
||||
|
||||
if req.Page > 0 {
|
||||
offset := (req.Page - 1) * 10
|
||||
if offset < len(items) {
|
||||
items = items[offset:]
|
||||
if len(items) > 10 {
|
||||
items = items[:10]
|
||||
}
|
||||
} else {
|
||||
items = nil
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"items": items,
|
||||
"total": len(GetProductsFixtures()),
|
||||
})
|
||||
}
|
||||
|
||||
type ProductListRequest struct {
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
func ProductList(w http.ResponseWriter, r *http.Request) {
|
||||
items := GetProductsFixtures()
|
||||
type ImportProductsRequest struct {
|
||||
Items []ImportProductItem `json:"items"`
|
||||
}
|
||||
|
||||
type ImportProductItem struct {
|
||||
OfferID string `json:"offer_id"`
|
||||
Name string `json:"name"`
|
||||
Barcode string `json:"barcode,omitempty"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Price string `json:"price,omitempty"`
|
||||
OldPrice string `json:"old_price,omitempty"`
|
||||
PremiumPrice string `json:"premium_price,omitempty"`
|
||||
Vat string `json:"vat"`
|
||||
Images []string `json:"images,omitempty"`
|
||||
Depth int `json:"depth,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
Width int `json:"width,omitempty"`
|
||||
DimensionUnit string `json:"dimension_unit,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
WeightUnit string `json:"weight_unit,omitempty"`
|
||||
Attributes []ImportAttribute `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type ImportAttribute struct {
|
||||
ID int64 `json:"id"`
|
||||
Values []ImportAttrValue `json:"values"`
|
||||
}
|
||||
|
||||
type ImportAttrValue struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
ComplexID int64 `json:"complex_id,omitempty"`
|
||||
Values []ImportAttrValue `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
type ImportProductsResponse struct {
|
||||
Result struct {
|
||||
TaskID int64 `json:"task_id"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
func ImportProducts(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
var req ImportProductsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, 400, "invalid json: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Items) == 0 {
|
||||
writeError(w, 400, "items is required and must not be empty")
|
||||
return
|
||||
}
|
||||
|
||||
for i, item := range req.Items {
|
||||
if item.OfferID == "" {
|
||||
writeError(w, 400, fmt.Sprintf("items[%d].offer_id is required", i))
|
||||
return
|
||||
}
|
||||
if item.Name == "" {
|
||||
writeError(w, 400, fmt.Sprintf("items[%d].name is required", i))
|
||||
return
|
||||
}
|
||||
if item.CategoryID == 0 {
|
||||
writeError(w, 400, fmt.Sprintf("items[%d].category_id is required", i))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"items": items,
|
||||
"total": len(items),
|
||||
json.NewEncoder(w).Encode(ImportProductsResponse{
|
||||
Result: struct {
|
||||
TaskID int64 `json:"task_id"`
|
||||
}{
|
||||
TaskID: 990001,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type ProductDescriptionRequest struct {
|
||||
Filter struct {
|
||||
OfferIDs []string `json:"offer_id"`
|
||||
ProductIDs []int64 `json:"product_id"`
|
||||
} `json:"filter"`
|
||||
}
|
||||
|
||||
type ProductDescriptionItem struct {
|
||||
OfferID string `json:"offer_id"`
|
||||
ProductID int64 `json:"product_id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
Barcode string `json:"barcode"`
|
||||
Status string `json:"status"`
|
||||
State string `json:"state"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
IsArchive bool `json:"is_archive"`
|
||||
}
|
||||
|
||||
type ProductDescriptionResponse struct {
|
||||
Result []ProductDescriptionItem `json:"result"`
|
||||
}
|
||||
|
||||
func ProductDescriptions(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
var req ProductDescriptionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, 400, "invalid json")
|
||||
return
|
||||
}
|
||||
|
||||
allProducts := GetProductsFixtures()
|
||||
descs := make([]ProductDescriptionItem, 0)
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, oid := range req.Filter.OfferIDs {
|
||||
for _, p := range allProducts {
|
||||
if p.OfferID == oid && !seen[oid] {
|
||||
seen[oid] = true
|
||||
descs = append(descs, ProductDescriptionItem{
|
||||
OfferID: p.OfferID,
|
||||
ProductID: p.ProductID,
|
||||
CategoryID: p.CategoryID,
|
||||
Name: p.Name,
|
||||
Barcode: p.Barcode,
|
||||
Status: "processed",
|
||||
State: "moderated",
|
||||
CreatedAt: "2024-01-01T00:00:00Z",
|
||||
IsArchive: false,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, pid := range req.Filter.ProductIDs {
|
||||
for _, p := range allProducts {
|
||||
poid := p.OfferID
|
||||
if p.ProductID == pid && !seen[poid] {
|
||||
seen[poid] = true
|
||||
descs = append(descs, ProductDescriptionItem{
|
||||
OfferID: p.OfferID,
|
||||
ProductID: p.ProductID,
|
||||
CategoryID: p.CategoryID,
|
||||
Name: p.Name,
|
||||
Barcode: p.Barcode,
|
||||
Status: "processed",
|
||||
State: "moderated",
|
||||
CreatedAt: "2024-01-01T00:00:00Z",
|
||||
IsArchive: false,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(ProductDescriptionResponse{
|
||||
Result: descs,
|
||||
})
|
||||
}
|
||||
|
||||
@ -264,19 +477,3 @@ func writeError(w http.ResponseWriter, code int, msg string) {
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Code: code, Message: msg})
|
||||
}
|
||||
|
||||
// Helper to parse offer_ids from query string
|
||||
func parseOfferIDs(query string) []string {
|
||||
if query == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(query, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user