Initial commit: ozon-mock Go server

- Mock Ozon Seller API endpoints
- Products, Postings, Finance, Returns, Analytics
- JSON fixtures for testing
- Docker support
- Gitea Actions CI/CD
This commit is contained in:
Rinat
2026-07-15 16:19:24 +03:00
commit 842f87f576
13 changed files with 1285 additions and 0 deletions

View File

@ -0,0 +1,180 @@
package handlers
import (
"encoding/json"
"net/http"
"time"
)
// FinanceTransactionListRequest - POST /v1/finance/transaction/list
type FinanceTransactionListRequest struct {
Filter struct {
Since string `json:"since"`
To string `json:"to"`
TransactionType string `json:"transaction_type"`
PostingNumber []string `json:"posting_number"`
} `json:"filter"`
Limit int `json:"limit"`
}
type Transaction struct {
TransactionType string `json:"transaction_type"`
Amount float64 `json:"amount"`
CurrencyCode string `json:"currency_code"`
PostingNumber string `json:"posting_number,omitempty"`
OrderID int64 `json:"order_id,omitempty"`
Items []TransactionItem `json:"items,omitempty"`
CreatedAt string `json:"created_at"`
}
type TransactionItem struct {
Type string `json:"type"`
Amount float64 `json:"amount"`
Quantity int `json:"quantity,omitempty"`
ProductID int64 `json:"product_id,omitempty"`
OfferID string `json:"offer_id,omitempty"`
}
func FinanceTransactionList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req FinanceTransactionListRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
transactions := GetTransactionsFixtures()
if req.Limit == 0 {
req.Limit = 100
}
if len(transactions) > req.Limit {
transactions = transactions[:req.Limit]
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"transactions": transactions,
"total": len(transactions),
})
}
// FinancePayoutListRequest - POST /v1/finance/payout/list
type FinancePayoutListRequest struct {
Filter struct {
Since string `json:"since"`
To string `json:"to"`
} `json:"filter"`
Limit int `json:"limit"`
}
type Payout struct {
PayoutID int64 `json:"payout_id"`
Type string `json:"type"`
Status string `json:"status"`
Amount float64 `json:"amount"`
CurrencyCode string `json:"currency_code"`
CreatedAt string `json:"created_at"`
OperationsCount int `json:"operations_count"`
}
func FinancePayoutList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req FinancePayoutListRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
payouts := GetPayoutsFixtures()
if req.Limit == 0 {
req.Limit = 100
}
if len(payouts) > req.Limit {
payouts = payouts[:req.Limit]
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"payouts": payouts,
"total": len(payouts),
})
}
// AnalyticsDataRequest - POST /v1/analytics/data
type AnalyticsDataRequest struct {
DateFrom string `json:"date_from"`
DateTo string `json:"date_to"`
Metrics []string `json:"metrics"`
Dimension []string `json:"dimension"`
}
func AnalyticsData(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"data": []map[string]any{
{
"date": time.Now().Format("2006-01-02"),
"orders_count": 42,
"revenue": 125000.50,
"avg_order": 2976.20,
},
},
"total": 1,
})
}
// TurnoverStocksRequest - POST /v1/analytics/turnover/stocks
type TurnoverStocksRequest struct {
Date string `json:"date"`
SKU []string `json:"sku"`
}
type TurnoverStocksResponse struct {
Items []struct {
SKU string `json:"sku"`
ProductID int64 `json:"product_id"`
OnHand int `json:"orders_count_canceled"`
Reserved int `json:"reserved"`
InWayToCustomer int `json:"in_way_to_customer"`
InWayFromCustomer int `json:"in_way_from_customer"`
} `json:"items"`
}
func TurnoverStocks(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(TurnoverStocksResponse{
Items: []struct {
SKU string `json:"sku"`
ProductID int64 `json:"product_id"`
OnHand int `json:"orders_count_canceled"`
Reserved int `json:"reserved"`
InWayToCustomer int `json:"in_way_to_customer"`
InWayFromCustomer int `json:"in_way_from_customer"`
}{
{SKU: "TEST-SKU-001", ProductID: 99001234001, OnHand: 150, Reserved: 0, InWayToCustomer: 10, InWayFromCustomer: 2},
{SKU: "TEST-SKU-002", ProductID: 99001234002, OnHand: 75, Reserved: 5, InWayToCustomer: 15, InWayFromCustomer: 1},
},
})
}

View File

@ -0,0 +1,242 @@
package handlers
import (
"log"
"os"
)
func GetStocksFixtures() []StockItem {
data := []StockItem{
{ProductID: 99001234001, OfferID: "TEST-SKU-001", Stock: 150, WarehouseID: 99100001001, WarehouseName: "Moscow FBO"},
{ProductID: 99001234002, OfferID: "TEST-SKU-002", Stock: 75, WarehouseID: 99100001001, WarehouseName: "Moscow FBO"},
{ProductID: 99001234003, OfferID: "TEST-SKU-003", Stock: 200, WarehouseID: 99100001002, WarehouseName: "St. Petersburg FBO"},
{ProductID: 99001234004, OfferID: "TEST-SKU-004", Stock: 0, WarehouseID: 99100001003, WarehouseName: "Kazan FBO"},
{ProductID: 99001234005, OfferID: "TEST-SKU-005", Stock: 50, WarehouseID: 99100001001, WarehouseName: "Moscow FBO"},
}
// Try to load from file if exists
if dataStr := os.Getenv("MOCK_STOCKS_JSON"); dataStr != "" {
log.Printf("MOCK_STOCKS_JSON is set, ignoring fixtures")
}
return data
}
func GetProductsFixtures() []ProductInfoItem {
return []ProductInfoItem{
{
ProductID: 99001234001,
OfferID: "TEST-SKU-001",
Name: "Тестовый товар 1",
Barcode: "4600000000001",
BuyPrice: 1000.00,
Price: 1990.00,
PremiumPrice: 2190.00,
CategoryID: 17000000,
},
{
ProductID: 99001234002,
OfferID: "TEST-SKU-002",
Name: "Тестовый товар 2",
Barcode: "4600000000002",
BuyPrice: 500.00,
Price: 990.00,
PremiumPrice: 1090.00,
CategoryID: 17000001,
},
{
ProductID: 99001234003,
OfferID: "TEST-SKU-003",
Name: "Тестовый товар 3",
Barcode: "4600000000003",
BuyPrice: 2000.00,
Price: 3990.00,
PremiumPrice: 4390.00,
CategoryID: 17000000,
},
{
ProductID: 99001234004,
OfferID: "TEST-SKU-004",
Name: "Тестовый товар 4",
Barcode: "4600000000004",
BuyPrice: 300.00,
Price: 590.00,
PremiumPrice: 650.00,
CategoryID: 17000002,
},
{
ProductID: 99001234005,
OfferID: "TEST-SKU-005",
Name: "Тестовый товар 5",
Barcode: "4600000000005",
BuyPrice: 800.00,
Price: 1590.00,
PremiumPrice: 1750.00,
CategoryID: 17000001,
},
}
}
func GetPricesFixtures() []ProductPriceItem {
return []ProductPriceItem{
{OfferID: "TEST-SKU-001", Price: 1990.00, OldPrice: 2190.00, PremiumPrice: 2190.00, PremiumPriceOld: 2390.00},
{OfferID: "TEST-SKU-002", Price: 990.00, OldPrice: 1090.00, PremiumPrice: 1090.00, PremiumPriceOld: 1190.00},
{OfferID: "TEST-SKU-003", Price: 3990.00, OldPrice: 4490.00, PremiumPrice: 4390.00, PremiumPriceOld: 4890.00},
{OfferID: "TEST-SKU-004", Price: 590.00, OldPrice: 690.00, PremiumPrice: 650.00, PremiumPriceOld: 750.00},
{OfferID: "TEST-SKU-005", Price: 1590.00, OldPrice: 1790.00, PremiumPrice: 1750.00, PremiumPriceOld: 1950.00},
}
}
func GetPostingsFixtures() []Posting {
return []Posting{
{
PostingNumber: "TEST-POST-001",
OrderID: 99000000001,
OrderNumber: "TEST-ORD-001",
Status: "delivered",
Products: []PostingItem{
{ProductID: 99001234001, OfferID: "TEST-SKU-001", Name: "Тестовый товар 1", Quantity: 2, Price: 1990.00, ItemsCount: 1},
},
CreatedAt: "2024-01-15T10:30:00Z",
UpdatedAt: "2024-01-17T14:45:00Z",
WarehouseID: 99100001001,
OrderType: "fbs",
},
{
PostingNumber: "TEST-POST-002",
OrderID: 99000000002,
OrderNumber: "TEST-ORD-002",
Status: "delivered",
Products: []PostingItem{
{ProductID: 99001234002, OfferID: "TEST-SKU-002", Name: "Тестовый товар 2", Quantity: 1, Price: 990.00, ItemsCount: 1},
{ProductID: 99001234003, OfferID: "TEST-SKU-003", Name: "Тестовый товар 3", Quantity: 1, Price: 3990.00, ItemsCount: 1},
},
CreatedAt: "2024-01-16T11:00:00Z",
UpdatedAt: "2024-01-18T09:30:00Z",
WarehouseID: 99100001001,
OrderType: "fbs",
},
{
PostingNumber: "TEST-POST-003",
OrderID: 99000000003,
OrderNumber: "TEST-ORD-003",
Status: "awaiting_deliver",
Products: []PostingItem{
{ProductID: 99001234004, OfferID: "TEST-SKU-004", Name: "Тестовый товар 4", Quantity: 3, Price: 590.00, ItemsCount: 1},
},
CreatedAt: "2024-01-18T08:15:00Z",
UpdatedAt: "2024-01-18T08:15:00Z",
WarehouseID: 99100001002,
OrderType: "fbs",
},
{
PostingNumber: "TEST-POST-004",
OrderID: 99000000004,
OrderNumber: "TEST-ORD-004",
Status: "cancelled",
Products: []PostingItem{
{ProductID: 99001234005, OfferID: "TEST-SKU-005", Name: "Тестовый товар 5", Quantity: 1, Price: 1590.00, ItemsCount: 1},
},
CreatedAt: "2024-01-17T16:00:00Z",
UpdatedAt: "2024-01-17T18:30:00Z",
WarehouseID: 99100001001,
OrderType: "fbs",
},
}
}
func GetReturnsFixtures() []Return {
return []Return{
{
ReturnID: 99000001,
PostingNumber: "TEST-POST-001",
Status: "completed",
Reason: "wrong_item",
ReasonID: 10,
CreatedAt: "2024-01-20T10:00:00Z",
DeliveredAt: "2024-01-22T15:00:00Z",
Items: []ReturnItem{
{ProductID: 99001234001, OfferID: "TEST-SKU-001", Name: "Тестовый товар 1", Quantity: 1, IsOptional: false},
},
},
{
ReturnID: 99000002,
PostingNumber: "TEST-POST-002",
Status: "pending",
Reason: "buyer_remorse",
ReasonID: 20,
CreatedAt: "2024-01-21T14:30:00Z",
Items: []ReturnItem{
{ProductID: 99001234002, OfferID: "TEST-SKU-002", Name: "Тестовый товар 2", Quantity: 1, IsOptional: true},
},
},
}
}
func GetTransactionsFixtures() []Transaction {
return []Transaction{
{
TransactionType: "orders",
Amount: 3980.00,
CurrencyCode: "RUB",
PostingNumber: "TEST-POST-001",
OrderID: 99000000001,
CreatedAt: "2024-01-17T00:00:00Z",
Items: []TransactionItem{
{Type: "sale", Amount: 1990.00, Quantity: 1, ProductID: 99001234001, OfferID: "TEST-SKU-001"},
{Type: "sale", Amount: 1990.00, Quantity: 1, ProductID: 99001234001, OfferID: "TEST-SKU-001"},
},
},
{
TransactionType: "orders",
Amount: 4980.00,
CurrencyCode: "RUB",
PostingNumber: "TEST-POST-002",
OrderID: 99000000002,
CreatedAt: "2024-01-18T00:00:00Z",
Items: []TransactionItem{
{Type: "sale", Amount: 990.00, Quantity: 1, ProductID: 99001234002, OfferID: "TEST-SKU-002"},
{Type: "sale", Amount: 3990.00, Quantity: 1, ProductID: 99001234003, OfferID: "TEST-SKU-003"},
},
},
{
TransactionType: "commission",
Amount: -597.00,
CurrencyCode: "RUB",
PostingNumber: "TEST-POST-001",
OrderID: 99000000001,
CreatedAt: "2024-01-17T00:00:00Z",
},
{
TransactionType: "refund",
Amount: -990.00,
CurrencyCode: "RUB",
PostingNumber: "TEST-POST-004",
OrderID: 99000000004,
CreatedAt: "2024-01-17T00:00:00Z",
},
}
}
func GetPayoutsFixtures() []Payout {
return []Payout{
{
PayoutID: 99001,
Type: "payout",
Status: "completed",
Amount: 15000.00,
CurrencyCode: "RUB",
CreatedAt: "2024-01-15T00:00:00Z",
OperationsCount: 10,
},
{
PayoutID: 99002,
Type: "payout",
Status: "completed",
Amount: 22500.00,
CurrencyCode: "RUB",
CreatedAt: "2024-01-22T00:00:00Z",
OperationsCount: 15,
},
}
}

View File

@ -0,0 +1,235 @@
package handlers
import (
"encoding/json"
"net/http"
"time"
)
// PostingFBSListRequest - POST /v2/posting/fbs/list
type PostingFBSListRequest struct {
Dir string `json:"dir"` // "asc" or "desc"
Filter struct {
Since string `json:"since"`
To string `json:"to"`
Status string `json:"status"`
PostingNumber []string `json:"posting_number"`
DeliveryMethod int `json:"delivery_method"`
} `json:"filter"`
Limit int `json:"limit"`
With struct {
AnalyticsData bool `json:"analytics_data"`
} `json:"with"`
}
type PostingItem struct {
ProductID int64 `json:"product_id"`
OfferID string `json:"offer_id"`
Name string `json:"name"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
ItemsCount int `json:"items_count"`
}
type Posting struct {
PostingNumber string `json:"posting_number"`
OrderID int64 `json:"order_id"`
OrderNumber string `json:"order_number"`
Status string `json:"status"`
Products []PostingItem `json:"products"`
AnalyticsData any `json:"analytics_data,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Warehouses []int64 `json:"warehouses"`
WarehouseID int64 `json:"warehouse_id"`
OrderType string `json:"order_type"`
}
func PostingFBSList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req PostingFBSListRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
if req.Limit == 0 {
req.Limit = 100
}
if req.Limit > 1000 {
req.Limit = 1000
}
postings := GetPostingsFixtures()
// Filter by status if provided
if req.Filter.Status != "" {
filtered := make([]Posting, 0)
for _, p := range postings {
if p.Status == req.Filter.Status {
filtered = append(filtered, p)
}
}
postings = filtered
}
// Filter by posting_number if provided
if len(req.Filter.PostingNumber) > 0 {
filtered := make([]Posting, 0)
for _, p := range postings {
for _, num := range req.Filter.PostingNumber {
if p.PostingNumber == num {
filtered = append(filtered, p)
break
}
}
}
postings = filtered
}
// Apply limit
if len(postings) > req.Limit {
postings = postings[:req.Limit]
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"postings": postings,
"total": len(postings),
})
}
// PostingFBSGetRequest - POST /v2/posting/fbs/get
type PostingFBSGetRequest struct {
PostingNumber string `json:"posting_number"`
}
func PostingFBSGet(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req PostingFBSGetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
postings := GetPostingsFixtures()
for _, p := range postings {
if p.PostingNumber == req.PostingNumber {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"posting": p,
})
return
}
}
writeError(w, 404, "posting not found")
}
// CancelPostingRequest - POST /v1/posting/fbs/cancel
type CancelPostingRequest struct {
PostingNumber string `json:"posting_number"`
}
type CancelPostingResponse struct {
PostingNumber string `json:"posting_number"`
Status string `json:"status"`
}
func CancelPosting(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req CancelPostingRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
if req.PostingNumber == "" {
writeError(w, 400, "posting_number is required")
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(CancelPostingResponse{
PostingNumber: req.PostingNumber,
Status: "cancelled",
})
}
// Returns
type ReturnsListRequest struct {
Filter struct {
Since string `json:"since"`
To string `json:"to"`
Status string `json:"status"`
ReturnID int64 `json:"return_id"`
} `json:"filter"`
Limit int `json:"limit"`
}
type ReturnItem struct {
ProductID int64 `json:"product_id"`
OfferID string `json:"offer_id"`
Name string `json:"name"`
Quantity int `json:"quantity"`
IsOptional bool `json:"is_optional"`
}
type Return struct {
ReturnID int64 `json:"return_id"`
PostingNumber string `json:"posting_number"`
Status string `json:"status"`
Reason string `json:"reason"`
ReasonID int64 `json:"reason_id"`
CreatedAt string `json:"created_at"`
RejectedAt string `json:"rejected_at,omitempty"`
DeliveredAt string `json:"delivered_at,omitempty"`
Items []ReturnItem `json:"items"`
}
func ReturnsList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", 405)
return
}
var req ReturnsListRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, "invalid json")
return
}
returns := GetReturnsFixtures()
if req.Limit == 0 {
req.Limit = 100
}
if len(returns) > req.Limit {
returns = returns[:req.Limit]
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"result": returns,
"total": len(returns),
})
}
func init() {
// Ensure timestamps are consistent
_ = time.RFC3339
}

View File

@ -0,0 +1,282 @@
package handlers
import (
"encoding/json"
"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"`
} `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()
// Filter by offer_ids if provided
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
}
// Filter by product_ids if provided
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
}
// Apply limit
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),
})
}
// ProductInfoListRequest - POST /v3/product/info/list
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 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),
})
}
// 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"`
} `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
}
// Validate required fields
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,
})
}
// ProductPricesRequest - POST /v4/product/info/prices
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
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"items": items,
"total": len(items),
})
}
// ProductListRequest - GET /v1/product/list
type ProductListRequest struct {
Page int `json:"page"`
}
func ProductList(w http.ResponseWriter, r *http.Request) {
items := GetProductsFixtures()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"items": items,
"total": len(items),
})
}
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})
}
// 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
}