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,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
}