Files
ozon-mock/internal/handlers/products.go
Rinat 50dc046369
Some checks failed
deploy / deploy (push) Failing after 2s
test / test (push) Failing after 1s
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
2026-07-17 14:38:53 +03:00

480 lines
11 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"net/http"
)
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
type ProductStocksRequest struct {
Filter struct {
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"`
}
type StockItem struct {
ProductID int64 `json:"product_id"`
OfferID string `json:"offer_id"`
Stock int `json:"stock"`
WarehouseID int64 `json:"warehouse_id"`
WarehouseName string `json:"warehouse_name"`
}
func ProductStocks(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req ProductStocksRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json: "+err.Error())
return
}
items := GetStocksFixtures()
if len(req.Filter.OfferIDs) > 0 {
filtered := make([]StockItem, 0)
for _, item := range items {
for _, oid := range req.Filter.OfferIDs {
if item.OfferID == oid {
filtered = append(filtered, item)
break
}
}
}
items = filtered
}
if len(req.Filter.ProductIDs) > 0 {
filtered := make([]StockItem, 0)
for _, item := range items {
for _, pid := range req.Filter.ProductIDs {
if item.ProductID == pid {
filtered = append(filtered, item)
break
}
}
}
items = filtered
}
if req.Filter.Visibility != "" {
_ = req.Filter.Visibility
}
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,
"total": len(items),
})
}
type ProductInfoListRequest struct {
Filter struct {
OfferIDs []string `json:"offer_id"`
ProductIDs []int64 `json:"product_id"`
} `json:"filter"`
Limit int `json:"limit"`
}
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"`
PremiumPrice float64 `json:"premium_price"`
CategoryID int64 `json:"category_id"`
}
func ProductInfoList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req ProductInfoListRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
items := GetProductsFixtures()
if len(req.Filter.OfferIDs) > 0 {
filtered := make([]ProductInfoItem, 0)
for _, item := range items {
for _, oid := range req.Filter.OfferIDs {
if item.OfferID == oid {
filtered = append(filtered, item)
break
}
}
}
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]
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"items": items,
"total": len(items),
})
}
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"`
} `json:"items"`
}
type ImportPricesResponse struct {
TaskID int64 `json:"task_id"`
}
func ImportPrices(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req ImportPricesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
if len(req.Items) == 0 {
writeError(w, 400, "items is required and must not be empty")
return
}
for _, item := range req.Items {
if item.OfferID == "" {
writeError(w, 400, "offer_id is required for all items")
return
}
if item.Price <= 0 {
writeError(w, 400, "price must be positive")
return
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ImportPricesResponse{
TaskID: 1234567890,
})
}
type ProductPricesRequest struct {
Filter struct {
OfferIDs []string `json:"offer_id"`
ProductIDs []int64 `json:"product_id"`
} `json:"filter"`
Limit int `json:"limit"`
}
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"`
PremiumPriceOld float64 `json:"premium_price_old,omitempty"`
}
func ProductPrices(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req ProductPricesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
items := GetPricesFixtures()
if len(req.Filter.OfferIDs) > 0 {
filtered := make([]ProductPriceItem, 0)
for _, item := range items {
for _, oid := range req.Filter.OfferIDs {
if item.OfferID == oid {
filtered = append(filtered, item)
break
}
}
}
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,
"total": len(items),
})
}
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"`
}
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(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,
})
}
func writeError(w http.ResponseWriter, code int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(ErrorResponse{Code: code, Message: msg})
}