diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a821a02 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: build test docker run clean vet lint + +build: + CGO_ENABLED=0 go build -o ozon-mock ./cmd/server + +test: + go test -v -race ./... + +vet: + go vet ./... + +lint: + go vet ./... + +run: + go run ./cmd/server + +docker: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ozon-mock ./cmd/server + docker build -t ozon-mock . + +clean: + rm -f ozon-mock + +all: vet test build diff --git a/cmd/server/main.go b/cmd/server/main.go index 9aa3fbc..f62e2f8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -14,28 +14,42 @@ func main() { mux := http.NewServeMux() + auth := func(h http.HandlerFunc) http.HandlerFunc { + return middleware.Auth(cfg.ClientID, cfg.APIKey, h) + } + // Products - mux.HandleFunc("/v1/product/import/prices", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.ImportPrices)) - mux.HandleFunc("/v1/product/list", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.ProductList)) - mux.HandleFunc("/v3/product/info/list", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.ProductInfoList)) - mux.HandleFunc("/v3/product/info/stocks", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.ProductStocks)) - mux.HandleFunc("/v4/product/info/prices", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.ProductPrices)) + mux.HandleFunc("/v1/product/import/prices", auth(handlers.ImportPrices)) + mux.HandleFunc("/v1/product/list", auth(handlers.ProductList)) + mux.HandleFunc("/v3/product/info/list", auth(handlers.ProductInfoList)) + mux.HandleFunc("/v3/product/info/stocks", auth(handlers.ProductStocks)) + mux.HandleFunc("/v4/product/info/prices", auth(handlers.ProductPrices)) + + // Product cards (Phase 2) + mux.HandleFunc("/v3/product/import", auth(handlers.ImportProducts)) + mux.HandleFunc("/v1/product/info/description", auth(handlers.ProductDescriptions)) + mux.HandleFunc("/v1/product/info/attributes", auth(handlers.ProductAttributes)) + + // Categories (Phase 2) + mux.HandleFunc("/v2/category/tree", auth(handlers.CategoryTree)) + mux.HandleFunc("/v3/category/attribute", auth(handlers.CategoryAttributes)) + mux.HandleFunc("/v3/category/attribute/values", auth(handlers.CategoryAttributeValues)) // Postings - mux.HandleFunc("/v2/posting/fbs/list", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.PostingFBSList)) - mux.HandleFunc("/v2/posting/fbs/get", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.PostingFBSGet)) - mux.HandleFunc("/v1/posting/fbs/cancel", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.CancelPosting)) + mux.HandleFunc("/v2/posting/fbs/list", auth(handlers.PostingFBSList)) + mux.HandleFunc("/v2/posting/fbs/get", auth(handlers.PostingFBSGet)) + mux.HandleFunc("/v1/posting/fbs/cancel", auth(handlers.CancelPosting)) // Finance - mux.HandleFunc("/v1/finance/transaction/list", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.FinanceTransactionList)) - mux.HandleFunc("/v1/finance/payout/list", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.FinancePayoutList)) + mux.HandleFunc("/v1/finance/transaction/list", auth(handlers.FinanceTransactionList)) + mux.HandleFunc("/v1/finance/payout/list", auth(handlers.FinancePayoutList)) // Returns - mux.HandleFunc("/v1/returns/list", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.ReturnsList)) + mux.HandleFunc("/v1/returns/list", auth(handlers.ReturnsList)) // Analytics - mux.HandleFunc("/v1/analytics/data", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.AnalyticsData)) - mux.HandleFunc("/v1/analytics/turnover/stocks", middleware.Auth(cfg.ClientID, cfg.APIKey, handlers.TurnoverStocks)) + mux.HandleFunc("/v1/analytics/data", auth(handlers.AnalyticsData)) + mux.HandleFunc("/v1/analytics/turnover/stocks", auth(handlers.TurnoverStocks)) // Health mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { @@ -52,6 +66,5 @@ func main() { addr := ":" + cfg.Port log.Printf("Starting ozon-mock on %s", addr) - log.Printf("Auth: client_id=%s, api_key=%s", cfg.ClientID, cfg.APIKey) log.Fatal(http.ListenAndServe(addr, mux)) } diff --git a/go.mod b/go.mod index 118011d..64f46ff 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/rinat/ozon-mock go 1.25.0 - -require github.com/gin-gonic/gin v1.10.0 diff --git a/internal/handlers/categories.go b/internal/handlers/categories.go new file mode 100644 index 0000000..b6ba23a --- /dev/null +++ b/internal/handlers/categories.go @@ -0,0 +1,263 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +type CategoryTreeRequest struct { + Language string `json:"language"` +} + +type CategoryTreeNode struct { + CategoryID int64 `json:"category_id"` + Title string `json:"title"` + Children []CategoryTreeNode `json:"children"` +} + +type CategoryTreeResponse struct { + Result []CategoryTreeNode `json:"result"` +} + +func CategoryTree(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", 405) + return + } + + var req CategoryTreeRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CategoryTreeResponse{ + Result: []CategoryTreeNode{ + { + CategoryID: 17000000, + Title: "Электроника", + Children: []CategoryTreeNode{ + {CategoryID: 17000001, Title: "Смартфоны и гаджеты", Children: []CategoryTreeNode{ + {CategoryID: 17036136, Title: "Смартфоны"}, + {CategoryID: 17036137, Title: "Планшеты"}, + }}, + {CategoryID: 17000002, Title: "Ноутбуки и компьютеры"}, + }, + }, + { + CategoryID: 17010000, + Title: "Одежда", + Children: []CategoryTreeNode{ + {CategoryID: 17010001, Title: "Мужская одежда"}, + {CategoryID: 17010002, Title: "Женская одежда"}, + }, + }, + { + CategoryID: 17020000, + Title: "Дом и сад", + Children: []CategoryTreeNode{ + {CategoryID: 17020001, Title: "Мебель"}, + {CategoryID: 17020002, Title: "Освещение"}, + }, + }, + }, + }) +} + +type CategoryAttributeRequest struct { + AttributeType string `json:"attribute_type"` + CategoryID []int64 `json:"category_id"` + Language string `json:"language"` +} + +type CategoryAttribute struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + IsCollection bool `json:"is_collection"` + IsRequired bool `json:"is_required"` + GroupID int64 `json:"group_id"` + GroupName string `json:"group_name"` + DictionaryID int64 `json:"dictionary_id"` +} + +type CategoryAttributesResponse struct { + Result []CategoryAttribute `json:"result"` +} + +func CategoryAttributes(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", 405) + return + } + + var req CategoryAttributeRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CategoryAttributesResponse{ + Result: []CategoryAttribute{ + { + ID: 8229, + Name: "Название модели", + Description: "Название модели товара", + Type: "String", + IsCollection: false, + IsRequired: true, + GroupID: 1, + GroupName: "Основные характеристики", + DictionaryID: 0, + }, + { + ID: 85, + Name: "Бренд", + Description: "Производитель товара", + Type: "String", + IsCollection: false, + IsRequired: true, + GroupID: 1, + GroupName: "Основные характеристики", + DictionaryID: 0, + }, + { + ID: 9044, + Name: "Цвет товара", + Description: "Основной цвет", + Type: "String", + IsCollection: false, + IsRequired: false, + GroupID: 2, + GroupName: "Внешний вид", + DictionaryID: 0, + }, + { + ID: 4416, + Name: "Гарантия", + Description: "Срок гарантии в месяцах", + Type: "Number", + IsCollection: false, + IsRequired: false, + GroupID: 3, + GroupName: "Гарантия и сервис", + DictionaryID: 0, + }, + { + ID: 4074, + Name: "Комплектация", + Description: "Что входит в комплект поставки", + Type: "String", + IsCollection: true, + IsRequired: false, + GroupID: 4, + GroupName: "Комплектация", + DictionaryID: 0, + }, + { + ID: 5076, + Name: "Вес с упаковкой", + Description: "Вес товара в упаковке, г", + Type: "Number", + IsCollection: false, + IsRequired: false, + GroupID: 5, + GroupName: "Упаковка и габариты", + DictionaryID: 0, + }, + }, + }) +} + +type CategoryAttributeValuesRequest struct { + AttributeID int64 `json:"attribute_id"` + CategoryID int64 `json:"category_id"` + Language string `json:"language"` +} + +type CategoryAttributeValue struct { + ID int64 `json:"id"` + Value string `json:"value"` + Info string `json:"info,omitempty"` +} + +type CategoryAttributeValuesResponse struct { + Result []CategoryAttributeValue `json:"result"` +} + +func CategoryAttributeValues(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", 405) + return + } + + var req CategoryAttributeValuesRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CategoryAttributeValuesResponse{ + Result: []CategoryAttributeValue{ + {ID: 1, Value: "Apple", Info: "Apple Inc."}, + {ID: 2, Value: "Samsung", Info: "Samsung Electronics"}, + {ID: 3, Value: "Xiaomi", Info: "Xiaomi Corporation"}, + {ID: 4, Value: "Huawei", Info: "Huawei Technologies"}, + }, + }) +} + +type ProductAttributesRequest struct { + Filter struct { + OfferIDs []string `json:"offer_id"` + ProductIDs []int64 `json:"product_id"` + } `json:"filter"` +} + +type ProductAttributeValue struct { + AttributeID int64 `json:"attribute_id"` + Value string `json:"value"` +} + +type ProductAttributesResult struct { + OfferID string `json:"offer_id"` + ProductID int64 `json:"product_id"` + Attributes []ProductAttributeValue `json:"attributes"` +} + +type ProductAttributesResponse struct { + Result []ProductAttributesResult `json:"result"` +} + +func ProductAttributes(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", 405) + return + } + + var req ProductAttributesRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, 400, "invalid json") + return + } + + allProducts := GetProductsFixtures() + results := make([]ProductAttributesResult, 0) + + for _, oid := range req.Filter.OfferIDs { + for _, p := range allProducts { + if p.OfferID == oid { + results = append(results, ProductAttributesResult{ + OfferID: p.OfferID, + ProductID: p.ProductID, + Attributes: []ProductAttributeValue{ + {AttributeID: 8229, Value: p.Name}, + {AttributeID: 85, Value: "Test Brand"}, + {AttributeID: 9044, Value: "Черный"}, + }, + }) + break + } + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ProductAttributesResponse{ + Result: results, + }) +} diff --git a/internal/handlers/finance.go b/internal/handlers/finance.go index a319ee5..c23ffd1 100644 --- a/internal/handlers/finance.go +++ b/internal/handlers/finance.go @@ -6,33 +6,32 @@ import ( "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"` + 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"` + 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"` + 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) { @@ -49,6 +48,53 @@ func FinanceTransactionList(w http.ResponseWriter, r *http.Request) { transactions := GetTransactionsFixtures() + if req.Filter.Since != "" { + sinceTs := parseTimeOrZero(req.Filter.Since) + filtered := make([]Transaction, 0) + for _, t := range transactions { + ts := parseTimeOrZero(t.CreatedAt) + if ts >= sinceTs { + filtered = append(filtered, t) + } + } + transactions = filtered + } + + if req.Filter.To != "" { + toTs := parseTimeOrZero(req.Filter.To) + filtered := make([]Transaction, 0) + for _, t := range transactions { + ts := parseTimeOrZero(t.CreatedAt) + if ts <= toTs { + filtered = append(filtered, t) + } + } + transactions = filtered + } + + if req.Filter.TransactionType != "" { + filtered := make([]Transaction, 0) + for _, t := range transactions { + if t.TransactionType == req.Filter.TransactionType { + filtered = append(filtered, t) + } + } + transactions = filtered + } + + if len(req.Filter.PostingNumber) > 0 { + filtered := make([]Transaction, 0) + for _, t := range transactions { + for _, num := range req.Filter.PostingNumber { + if t.PostingNumber == num { + filtered = append(filtered, t) + break + } + } + } + transactions = filtered + } + if req.Limit == 0 { req.Limit = 100 } @@ -60,11 +106,10 @@ func FinanceTransactionList(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "transactions": transactions, - "total": len(transactions), + "total": len(transactions), }) } -// FinancePayoutListRequest - POST /v1/finance/payout/list type FinancePayoutListRequest struct { Filter struct { Since string `json:"since"` @@ -74,13 +119,13 @@ type FinancePayoutListRequest struct { } 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"` + 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) { @@ -97,6 +142,30 @@ func FinancePayoutList(w http.ResponseWriter, r *http.Request) { payouts := GetPayoutsFixtures() + if req.Filter.Since != "" { + sinceTs := parseTimeOrZero(req.Filter.Since) + filtered := make([]Payout, 0) + for _, p := range payouts { + ts := parseTimeOrZero(p.CreatedAt) + if ts >= sinceTs { + filtered = append(filtered, p) + } + } + payouts = filtered + } + + if req.Filter.To != "" { + toTs := parseTimeOrZero(req.Filter.To) + filtered := make([]Payout, 0) + for _, p := range payouts { + ts := parseTimeOrZero(p.CreatedAt) + if ts <= toTs { + filtered = append(filtered, p) + } + } + payouts = filtered + } + if req.Limit == 0 { req.Limit = 100 } @@ -112,11 +181,10 @@ func FinancePayoutList(w http.ResponseWriter, r *http.Request) { }) } -// AnalyticsDataRequest - POST /v1/analytics/data type AnalyticsDataRequest struct { - DateFrom string `json:"date_from"` - DateTo string `json:"date_to"` - Metrics []string `json:"metrics"` + DateFrom string `json:"date_from"` + DateTo string `json:"date_to"` + Metrics []string `json:"metrics"` Dimension []string `json:"dimension"` } @@ -126,34 +194,44 @@ func AnalyticsData(w http.ResponseWriter, r *http.Request) { return } + var req AnalyticsDataRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + results := []map[string]any{ + { + "date": time.Now().Format("2006-01-02"), + "orders_count": 42, + "revenue": 125000.50, + "avg_order": 2976.20, + }, + } + if req.DateFrom != "" { + results[0]["date_from"] = req.DateFrom + } + if req.DateTo != "" { + results[0]["date_to"] = req.DateTo + } + 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, - }, - }, + "data": results, "total": 1, }) } -// TurnoverStocksRequest - POST /v1/analytics/turnover/stocks type TurnoverStocksRequest struct { - Date string `json:"date"` - SKU []string `json:"sku"` + 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"` + 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"` } @@ -163,15 +241,22 @@ func TurnoverStocks(w http.ResponseWriter, r *http.Request) { return } + var req TurnoverStocksRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + if len(req.SKU) > 0 { + _ = req.SKU + } + 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 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}, diff --git a/internal/handlers/fixtures.go b/internal/handlers/fixtures.go index 0ff5241..c07bb0a 100644 --- a/internal/handlers/fixtures.go +++ b/internal/handlers/fixtures.go @@ -1,242 +1,416 @@ package handlers import ( + "encoding/json" + "fmt" "log" "os" ) +type company struct { + name string + prefix string + categoryID int64 + warehouseID int64 + warehouse string + productCount int +} + +var companies = []company{ + {name: "TechGiant", prefix: "TG", categoryID: 17000000, warehouseID: 99100001001, warehouse: "Moscow FBO", productCount: 35}, + {name: "FashionLine", prefix: "FL", categoryID: 17010000, warehouseID: 99100001002, warehouse: "St. Petersburg FBO", productCount: 35}, + {name: "HomeStyle", prefix: "HS", categoryID: 17020000, warehouseID: 99100001003, warehouse: "Kazan FBO", productCount: 34}, +} + +var productNames = map[string][]string{ + "TechGiant": { + "Смартфон Galaxy A%d", "Ноутбук ProBook %d", "Планшет Tab %d", + "Наушники SoundBuds %d", "Часы SmartWatch %d", "Колонка BeatBox %d", + "Монитор UltraView %d", "Клавиатура TypeMaster %d", "Мышь ClickPro %d", + "Роутер NetStream %d", "Камера SnapCam %d", "Принтер PrintJet %d", + "Флешка DataStick %d", "Внешний диск StoreVault %d", "Зарядка PowerBoost %d", + }, + "FashionLine": { + "Футболка Casual %d", "Джинсы Street %d", "Куртка WindPro %d", + "Кроссовки RunFast %d", "Рубашка Classic %d", "Платье Evening %d", + "Свитер WarmTouch %d", "Шорты SummerBreeze %d", "Пальто WinterLux %d", + "Кепка SunShade %d", "Шарф CozyWrap %d", "Перчатки GripPro %d", + "Ремень BeltLine %d", "Носки ComfyStep %d", "Сумка CarryAll %d", + }, + "HomeStyle": { + "Стол WorkDesk %d", "Стул SitEasy %d", "Лампа LightGlow %d", + "Полка ShelfMaster %d", "Ковёр SoftFloor %d", "Штора WindowDress %d", + "Подушка DreamSoft %d", "Одеяло WarmCloud %d", "Посуда CookSet %d", + "Ваза FlowerHold %d", "Зеркало ReflectPro %d", "Часы WallTime %d", + "Органайзер DeskTidy %d", "Корзина BinSmart %d", "Вешалка HangRight %d", + }, +} + +func generateOfferID(prefix string, num int) string { + return fmt.Sprintf("%s-%04d", prefix, num) +} + +func generateBarcode(prefix string, num int) string { + return fmt.Sprintf("%s%08d", prefix, 10000000+num) +} + +func allProducts() []ProductInfoItem { + result := make([]ProductInfoItem, 0) + for _, c := range companies { + names := productNames[c.name] + for i := 1; i <= c.productCount; i++ { + nameIdx := (i - 1) % len(names) + result = append(result, ProductInfoItem{ + ProductID: int64(99000000000 + len(result) + 1), + OfferID: generateOfferID(c.prefix, i), + Name: fmt.Sprintf(names[nameIdx], i), + Barcode: generateBarcode(c.prefix, i), + BuyPrice: float64(500 + (i*173)%10000), + Price: float64(990 + (i*197)%15000), + PremiumPrice: float64(1190 + (i*211)%17000), + CategoryID: c.categoryID, + }) + } + } + return result +} + +func allStocks() []StockItem { + result := make([]StockItem, 0) + for _, c := range companies { + for i := 1; i <= c.productCount; i++ { + stock := (i*73 + 10) % 500 + if stock < 0 { + stock = 0 + } + result = append(result, StockItem{ + ProductID: int64(99000000000 + len(result) + 1), + OfferID: generateOfferID(c.prefix, i), + Stock: stock, + WarehouseID: c.warehouseID, + WarehouseName: c.warehouse, + }) + } + } + return result +} + +func allPrices() []ProductPriceItem { + result := make([]ProductPriceItem, 0) + for _, c := range companies { + for i := 1; i <= c.productCount; i++ { + price := float64(990 + (i*197)%15000) + result = append(result, ProductPriceItem{ + ProductID: int64(0), + OfferID: generateOfferID(c.prefix, i), + Price: price, + OldPrice: price * 1.1, + PremiumPrice: price * 1.15, + PremiumPriceOld: price * 1.25, + }) + } + } + return result +} + 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"}, + if path := os.Getenv("MOCK_STOCKS_JSON"); path != "" { + if fileData, err := os.ReadFile(path); err == nil { + var loaded []StockItem + if json.Unmarshal(fileData, &loaded) == nil && len(loaded) > 0 { + log.Printf("MOCK_STOCKS_JSON: loaded %d items from %s", len(loaded), path) + return loaded + } + } + log.Printf("MOCK_STOCKS_JSON: failed to load from %s, using defaults", path) } - - // 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 + return allStocks() } 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, - }, - } + return allProducts() } 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}, + return allPrices() +} + +type postingSpec struct { + companyIdx int + productNum1 int + productNum2 int + status string + createdAt string + updatedAt string + orderType string +} + +var postingSpecs = []postingSpec{ + {0, 1, 0, "delivered", "2024-01-15T10:30:00Z", "2024-01-17T14:45:00Z", "fbs"}, + {0, 2, 3, "delivered", "2024-01-16T11:00:00Z", "2024-01-18T09:30:00Z", "fbs"}, + {0, 4, 0, "awaiting_deliver", "2024-01-18T08:15:00Z", "2024-01-18T08:15:00Z", "fbs"}, + {0, 5, 0, "cancelled", "2024-01-17T16:00:00Z", "2024-01-17T18:30:00Z", "fbs"}, + {0, 7, 8, "delivered", "2024-02-01T09:00:00Z", "2024-02-03T12:00:00Z", "fbs"}, + {0, 10, 0, "delivered", "2024-02-05T14:00:00Z", "2024-02-07T16:00:00Z", "fbs"}, + {0, 12, 13, "awaiting_deliver", "2024-02-10T06:00:00Z", "2024-02-10T06:00:00Z", "fbs"}, + {0, 15, 0, "cancelled", "2024-02-12T18:00:00Z", "2024-02-12T20:00:00Z", "fbs"}, + {0, 18, 20, "delivered", "2024-03-01T08:00:00Z", "2024-03-03T10:00:00Z", "fbo"}, + {0, 22, 0, "delivered", "2024-03-05T11:00:00Z", "2024-03-07T13:00:00Z", "fbo"}, + {1, 1, 0, "delivered", "2024-01-20T12:00:00Z", "2024-01-22T15:00:00Z", "fbs"}, + {1, 2, 3, "delivered", "2024-01-22T09:00:00Z", "2024-01-24T11:00:00Z", "fbs"}, + {1, 5, 0, "awaiting_deliver", "2024-01-25T14:00:00Z", "2024-01-25T14:00:00Z", "fbs"}, + {1, 7, 8, "cancelled", "2024-01-28T16:00:00Z", "2024-01-28T18:00:00Z", "fbs"}, + {1, 10, 0, "delivered", "2024-02-02T10:00:00Z", "2024-02-04T12:00:00Z", "fbs"}, + {1, 12, 14, "delivered", "2024-02-08T08:00:00Z", "2024-02-10T10:00:00Z", "fbo"}, + {1, 16, 0, "awaiting_deliver", "2024-02-15T13:00:00Z", "2024-02-15T13:00:00Z", "fbo"}, + {1, 18, 20, "delivered", "2024-03-01T07:00:00Z", "2024-03-03T09:00:00Z", "fbs"}, + {2, 1, 0, "delivered", "2024-01-18T11:00:00Z", "2024-01-20T13:00:00Z", "fbs"}, + {2, 3, 4, "delivered", "2024-01-25T15:00:00Z", "2024-01-27T17:00:00Z", "fbs"}, + {2, 6, 0, "awaiting_deliver", "2024-02-01T09:00:00Z", "2024-02-01T09:00:00Z", "fbs"}, + {2, 8, 9, "cancelled", "2024-02-05T14:00:00Z", "2024-02-05T16:00:00Z", "fbs"}, + {2, 11, 0, "delivered", "2024-02-10T08:00:00Z", "2024-02-12T10:00:00Z", "fbo"}, + {2, 13, 15, "delivered", "2024-02-20T12:00:00Z", "2024-02-22T14:00:00Z", "fbo"}, + {2, 17, 0, "awaiting_deliver", "2024-03-01T06:00:00Z", "2024-03-01T06:00:00Z", "fbs"}, + {2, 19, 21, "delivered", "2024-03-10T10:00:00Z", "2024-03-12T12:00:00Z", "fbs"}, + {0, 25, 0, "delivered", "2024-03-15T08:00:00Z", "2024-03-17T10:00:00Z", "fbs"}, + {1, 22, 0, "delivered", "2024-03-18T11:00:00Z", "2024-03-20T13:00:00Z", "fbs"}, + {2, 22, 23, "awaiting_deliver", "2024-04-01T07:00:00Z", "2024-04-01T07:00:00Z", "fbo"}, + {0, 28, 30, "delivered", "2024-04-05T09:00:00Z", "2024-04-07T11:00:00Z", "fbo"}, + {1, 25, 0, "delivered", "2024-04-10T14:00:00Z", "2024-04-12T16:00:00Z", "fbs"}, + {2, 25, 27, "cancelled", "2024-04-15T10:00:00Z", "2024-04-15T12:00:00Z", "fbs"}, + {0, 30, 32, "delivered", "2024-05-01T08:00:00Z", "2024-05-03T10:00:00Z", "fbs"}, + {1, 28, 0, "delivered", "2024-05-05T12:00:00Z", "2024-05-07T14:00:00Z", "fbs"}, + {2, 28, 30, "awaiting_deliver", "2024-05-10T09:00:00Z", "2024-05-10T09:00:00Z", "fbo"}, + {0, 33, 0, "delivered", "2024-05-15T11:00:00Z", "2024-05-17T13:00:00Z", "fbo"}, + {1, 31, 33, "delivered", "2024-06-01T07:00:00Z", "2024-06-03T09:00:00Z", "fbs"}, + {2, 32, 0, "delivered", "2024-06-05T14:00:00Z", "2024-06-07T16:00:00Z", "fbs"}, + {0, 35, 0, "cancelled", "2024-06-10T08:00:00Z", "2024-06-10T10:00:00Z", "fbs"}, + {1, 35, 0, "delivered", "2024-06-15T10:00:00Z", "2024-06-17T12:00:00Z", "fbo"}, + {2, 34, 0, "delivered", "2024-07-01T09:00:00Z", "2024-07-03T11:00:00Z", "fbs"}, +} + +func productByNum(c company, num int) (int64, string, string, float64) { + idx := 0 + for _, comp := range companies { + for i := 1; i <= comp.productCount; i++ { + if comp.name == c.name && i == num { + names := productNames[comp.name] + nameIdx := (i - 1) % len(names) + pid := int64(99000000000 + idx + 1) + offerID := generateOfferID(c.prefix, i) + name := fmt.Sprintf(names[nameIdx], i) + price := float64(990 + (i*197)%15000) + return pid, offerID, name, price + } + idx++ + } } + return 0, "", "", 0 +} + +func getCompanyProducts(c company) []ProductInfoItem { + result := make([]ProductInfoItem, 0) + names := productNames[c.name] + for i := 1; i <= c.productCount; i++ { + nameIdx := (i - 1) % len(names) + result = append(result, ProductInfoItem{ + ProductID: int64(99000000000 + len(result) + 1), + OfferID: generateOfferID(c.prefix, i), + Name: fmt.Sprintf(names[nameIdx], i), + Barcode: generateBarcode(c.prefix, i), + CategoryID: c.categoryID, + }) + } + return result } 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", - }, + result := make([]Posting, len(postingSpecs)) + for idx, spec := range postingSpecs { + c := companies[spec.companyIdx] + pid1, oid1, name1, price1 := productByNum(c, spec.productNum1) + + products := []PostingItem{ + {ProductID: pid1, OfferID: oid1, Name: name1, Quantity: (idx%3 + 1), Price: price1, ItemsCount: 1}, + } + if spec.productNum2 > 0 { + pid2, oid2, name2, price2 := productByNum(c, spec.productNum2) + products = append(products, PostingItem{ + ProductID: pid2, OfferID: oid2, Name: name2, Quantity: (idx%2 + 1), Price: price2, ItemsCount: 1, + }) + } + + orderID := int64(99000000000 + idx + 1) + result[idx] = Posting{ + PostingNumber: fmt.Sprintf("%s-POST-%03d", c.prefix, idx+1), + OrderID: orderID, + OrderNumber: fmt.Sprintf("%s-ORD-%03d", c.prefix, idx+1), + Status: spec.status, + Products: products, + CreatedAt: spec.createdAt, + UpdatedAt: spec.updatedAt, + WarehouseID: c.warehouseID, + OrderType: spec.orderType, + } } + return result } 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}, - }, - }, + returnSpecs := []struct { + companyIdx int + postingIdx int + status string + reason string + reasonID int64 + createdAt string + deliveredAt string + }{ + {0, 0, "completed", "wrong_item", 10, "2024-01-20T10:00:00Z", "2024-01-22T15:00:00Z"}, + {0, 1, "pending", "buyer_remorse", 20, "2024-01-21T14:30:00Z", ""}, + {0, 4, "completed", "defect", 30, "2024-02-10T08:00:00Z", "2024-02-12T10:00:00Z"}, + {1, 10, "completed", "wrong_item", 10, "2024-02-05T11:00:00Z", "2024-02-07T13:00:00Z"}, + {1, 11, "pending", "buyer_remorse", 20, "2024-02-15T16:00:00Z", ""}, + {1, 14, "completed", "wrong_size", 40, "2024-03-01T09:00:00Z", "2024-03-03T11:00:00Z"}, + {2, 18, "completed", "defect", 30, "2024-02-01T14:00:00Z", "2024-02-03T16:00:00Z"}, + {2, 19, "pending", "buyer_remorse", 20, "2024-02-20T10:00:00Z", ""}, + {2, 23, "completed", "wrong_item", 10, "2024-03-15T08:00:00Z", "2024-03-17T10:00:00Z"}, + {0, 5, "pending", "wrong_color", 50, "2024-03-20T12:00:00Z", ""}, + {1, 16, "completed", "defect", 30, "2024-04-01T07:00:00Z", "2024-04-03T09:00:00Z"}, + {2, 24, "pending", "buyer_remorse", 20, "2024-04-10T14:00:00Z", ""}, + {0, 8, "completed", "wrong_item", 10, "2024-05-01T10:00:00Z", "2024-05-03T12:00:00Z"}, + {1, 17, "completed", "wrong_size", 40, "2024-05-15T09:00:00Z", "2024-05-17T11:00:00Z"}, + {2, 28, "pending", "defect", 30, "2024-06-05T08:00:00Z", ""}, + {0, 6, "completed", "buyer_remorse", 20, "2024-03-01T06:00:00Z", "2024-03-03T08:00:00Z"}, + {1, 12, "pending", "wrong_item", 10, "2024-02-25T15:00:00Z", ""}, + {2, 20, "completed", "wrong_color", 50, "2024-03-05T11:00:00Z", "2024-03-07T13:00:00Z"}, + {0, 9, "pending", "defect", 30, "2024-04-15T09:00:00Z", ""}, + {1, 13, "completed", "buyer_remorse", 20, "2024-03-10T14:00:00Z", "2024-03-12T16:00:00Z"}, } + + result := make([]Return, len(returnSpecs)) + for idx, spec := range returnSpecs { + c := companies[spec.companyIdx] + ps := postingSpecs[spec.postingIdx] + pid, oid, name, _ := productByNum(c, ps.productNum1) + + result[idx] = Return{ + ReturnID: int64(99000000 + idx + 1), + PostingNumber: fmt.Sprintf("%s-POST-%03d", c.prefix, spec.postingIdx+1), + Status: spec.status, + Reason: spec.reason, + ReasonID: spec.reasonID, + CreatedAt: spec.createdAt, + DeliveredAt: spec.deliveredAt, + Items: []ReturnItem{ + {ProductID: pid, OfferID: oid, Name: name, Quantity: 1, IsOptional: false}, + }, + } + } + return result } 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", - }, + txDefs := []struct { + txType string + amount float64 + companyIdx int + postingIdx int + createdAt string + }{ + {"orders", 3980.00, 0, 0, "2024-01-17T00:00:00Z"}, + {"orders", 4980.00, 0, 1, "2024-01-18T00:00:00Z"}, + {"commission", -597.00, 0, 0, "2024-01-17T00:00:00Z"}, + {"refund", -990.00, 0, 3, "2024-01-17T00:00:00Z"}, + {"orders", 2500.00, 0, 4, "2024-02-05T00:00:00Z"}, + {"commission", -375.00, 0, 4, "2024-02-05T00:00:00Z"}, + {"orders", 3200.00, 1, 10, "2024-01-25T00:00:00Z"}, + {"commission", -480.00, 1, 10, "2024-01-25T00:00:00Z"}, + {"orders", 4500.00, 1, 11, "2024-01-26T00:00:00Z"}, + {"refund", -1200.00, 1, 13, "2024-01-30T00:00:00Z"}, + {"orders", 2800.00, 2, 18, "2024-01-22T00:00:00Z"}, + {"commission", -420.00, 2, 18, "2024-01-22T00:00:00Z"}, + {"orders", 3600.00, 2, 19, "2024-01-28T00:00:00Z"}, + {"refund", -800.00, 2, 21, "2024-02-07T00:00:00Z"}, + {"orders", 5100.00, 0, 8, "2024-04-01T00:00:00Z"}, + {"commission", -765.00, 0, 8, "2024-04-01T00:00:00Z"}, + {"orders", 3400.00, 1, 16, "2024-04-05T00:00:00Z"}, + {"commission", -510.00, 1, 16, "2024-04-05T00:00:00Z"}, + {"orders", 4200.00, 2, 23, "2024-04-12T00:00:00Z"}, + {"refund", -950.00, 2, 31, "2024-04-17T00:00:00Z"}, + {"orders", 2900.00, 0, 26, "2024-03-18T00:00:00Z"}, + {"orders", 3800.00, 1, 14, "2024-02-05T00:00:00Z"}, + {"commission", -570.00, 1, 14, "2024-02-05T00:00:00Z"}, + {"orders", 4700.00, 2, 28, "2024-05-12T00:00:00Z"}, + {"commission", -705.00, 2, 28, "2024-05-12T00:00:00Z"}, } + + result := make([]Transaction, len(txDefs)) + for idx, def := range txDefs { + c := companies[def.companyIdx] + ps := postingSpecs[def.postingIdx] + _, oid1, _, _ := productByNum(c, ps.productNum1) + + orderID := int64(99000000000 + def.postingIdx + 1) + tx := Transaction{ + TransactionType: def.txType, + Amount: def.amount, + CurrencyCode: "RUB", + PostingNumber: fmt.Sprintf("%s-POST-%03d", c.prefix, def.postingIdx+1), + OrderID: orderID, + CreatedAt: def.createdAt, + } + if def.txType == "orders" { + tx.Items = []TransactionItem{ + {Type: "sale", Amount: def.amount / 2, Quantity: 1, ProductID: int64(99000000000 + def.postingIdx + 100), OfferID: oid1}, + {Type: "sale", Amount: def.amount / 2, Quantity: 1, ProductID: int64(99000000000 + def.postingIdx + 101), OfferID: oid1}, + } + } + result[idx] = tx + } + return result } 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, - }, + payoutDefs := []struct { + companyIdx int + amount float64 + opsCount int + createdAt string + }{ + {0, 15000.00, 10, "2024-01-15T00:00:00Z"}, + {0, 22500.00, 15, "2024-01-22T00:00:00Z"}, + {0, 18000.00, 12, "2024-02-15T00:00:00Z"}, + {0, 32000.00, 20, "2024-03-15T00:00:00Z"}, + {1, 12000.00, 8, "2024-01-20T00:00:00Z"}, + {1, 19500.00, 13, "2024-01-27T00:00:00Z"}, + {1, 25000.00, 18, "2024-02-20T00:00:00Z"}, + {1, 28000.00, 19, "2024-03-20T00:00:00Z"}, + {2, 11000.00, 7, "2024-01-18T00:00:00Z"}, + {2, 17000.00, 11, "2024-01-25T00:00:00Z"}, + {2, 21000.00, 14, "2024-02-18T00:00:00Z"}, + {2, 29000.00, 21, "2024-03-18T00:00:00Z"}, + {0, 14500.00, 9, "2024-04-15T00:00:00Z"}, + {1, 16500.00, 11, "2024-04-20T00:00:00Z"}, + {2, 13500.00, 8, "2024-04-18T00:00:00Z"}, + {0, 26000.00, 17, "2024-05-15T00:00:00Z"}, + {1, 31000.00, 22, "2024-05-20T00:00:00Z"}, + {2, 24000.00, 16, "2024-05-18T00:00:00Z"}, + {0, 19000.00, 13, "2024-06-15T00:00:00Z"}, + {1, 22000.00, 15, "2024-06-20T00:00:00Z"}, } + + result := make([]Payout, len(payoutDefs)) + for idx, def := range payoutDefs { + result[idx] = Payout{ + PayoutID: int64(99001 + idx), + Type: "payout", + Status: "completed", + Amount: def.amount, + CurrencyCode: "RUB", + CreatedAt: def.createdAt, + OperationsCount: def.opsCount, + } + } + return result } diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go new file mode 100644 index 0000000..68852d9 --- /dev/null +++ b/internal/handlers/handlers_test.go @@ -0,0 +1,419 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func doReq(t *testing.T, h http.HandlerFunc, method, path string, body any) *httptest.ResponseRecorder { + t.Helper() + var b []byte + if body != nil { + b, _ = json.Marshal(body) + } + req := httptest.NewRequest(method, path, bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + h(w, req) + return w +} + +func assertStatus(t *testing.T, w *httptest.ResponseRecorder, expected int) { + t.Helper() + if w.Code != expected { + t.Errorf("expected status %d, got %d: %s", expected, w.Code, w.Body.String()) + } +} + +func assertJSON(t *testing.T, w *httptest.ResponseRecorder, key string, expected any) { + t.Helper() + var m map[string]any + json.Unmarshal(w.Body.Bytes(), &m) + if m[key] == nil { + t.Errorf("expected key %q in response, got %s", key, w.Body.String()) + } +} + +func TestProductStocks(t *testing.T) { + w := doReq(t, ProductStocks, "POST", "/v3/product/info/stocks", map[string]any{ + "filter": map[string]any{}, + }) + assertStatus(t, w, 200) + assertJSON(t, w, "items", nil) + + w = doReq(t, ProductStocks, "GET", "/v3/product/info/stocks", nil) + assertStatus(t, w, 405) +} + +func TestProductStocks_FilterOfferID(t *testing.T) { + w := doReq(t, ProductStocks, "POST", "/v3/product/info/stocks", map[string]any{ + "filter": map[string]any{"offer_id": []string{"TG-0001"}}, + }) + assertStatus(t, w, 200) + + var resp struct { + Items []StockItem `json:"items"` + Total int `json:"total"` + } + json.NewDecoder(w.Body).Decode(&resp) + if resp.Total != 1 || resp.Items[0].OfferID != "TG-0001" { + t.Errorf("filter by offer_id: total=%d item=%v", resp.Total, resp.Items) + } +} + +func TestProductStocks_Limit(t *testing.T) { + w := doReq(t, ProductStocks, "POST", "/v3/product/info/stocks", map[string]any{ + "limit": 2, + }) + assertStatus(t, w, 200) + + var resp struct { + Items []StockItem `json:"items"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Items) != 2 { + t.Errorf("limit: expected 2, got %d", len(resp.Items)) + } +} + +func TestProductInfoList(t *testing.T) { + w := doReq(t, ProductInfoList, "POST", "/v3/product/info/list", map[string]any{ + "filter": map[string]any{}, + }) + assertStatus(t, w, 200) + assertJSON(t, w, "items", nil) +} + +func TestProductInfoList_FilterProductID(t *testing.T) { + w := doReq(t, ProductInfoList, "POST", "/v3/product/info/list", map[string]any{ + "filter": map[string]any{"product_id": []int64{99000000001}}, + }) + assertStatus(t, w, 200) + var resp struct { + Items []ProductInfoItem `json:"items"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Items) != 1 || resp.Items[0].ProductID != 99000000001 { + t.Errorf("filter by product_id: got %v", resp.Items) + } +} + +func TestProductList(t *testing.T) { + w := doReq(t, ProductList, "GET", "/v1/product/list", nil) + assertStatus(t, w, 200) + assertJSON(t, w, "total", nil) + + w = doReq(t, ProductList, "POST", "/v1/product/list", nil) + assertStatus(t, w, 405) +} + +func TestImportPrices(t *testing.T) { + w := doReq(t, ImportPrices, "POST", "/v1/product/import/prices", map[string]any{ + "items": []map[string]any{ + {"offer_id": "SKU-1", "price": 100.0}, + }, + }) + assertStatus(t, w, 200) + assertJSON(t, w, "task_id", nil) + + w = doReq(t, ImportPrices, "POST", "/v1/product/import/prices", map[string]any{"items": []map[string]any{}}) + assertStatus(t, w, 400) + + w = doReq(t, ImportPrices, "POST", "/v1/product/import/prices", map[string]any{ + "items": []map[string]any{{"offer_id": "", "price": 100}}, + }) + assertStatus(t, w, 400) +} + +func TestProductPrices(t *testing.T) { + w := doReq(t, ProductPrices, "POST", "/v4/product/info/prices", map[string]any{ + "filter": map[string]any{}, + }) + assertStatus(t, w, 200) + assertJSON(t, w, "total", nil) +} + +func TestProductPrices_FilterOfferID(t *testing.T) { + w := doReq(t, ProductPrices, "POST", "/v4/product/info/prices", map[string]any{ + "filter": map[string]any{"offer_id": []string{"TG-0001"}}, + }) + assertStatus(t, w, 200) + var resp struct { + Items []ProductPriceItem `json:"items"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Items) != 1 || resp.Items[0].OfferID != "TG-0001" { + t.Errorf("filter by offer_id: got %v", resp.Items) + } +} + +func TestImportProducts(t *testing.T) { + w := doReq(t, ImportProducts, "POST", "/v3/product/import", map[string]any{ + "items": []map[string]any{ + {"offer_id": "TEST-001", "name": "Test Product", "category_id": 17000000}, + }, + }) + assertStatus(t, w, 200) + + var resp ImportProductsResponse + json.NewDecoder(w.Body).Decode(&resp) + if resp.Result.TaskID != 990001 { + t.Errorf("expected task_id 990001, got %d", resp.Result.TaskID) + } + + w = doReq(t, ImportProducts, "POST", "/v3/product/import", map[string]any{ + "items": []map[string]any{}, + }) + assertStatus(t, w, 400) + + w = doReq(t, ImportProducts, "POST", "/v3/product/import", map[string]any{ + "items": []map[string]any{{"offer_id": ""}}, + }) + assertStatus(t, w, 400) +} + +func TestProductDescriptions(t *testing.T) { + w := doReq(t, ProductDescriptions, "POST", "/v1/product/info/description", map[string]any{ + "filter": map[string]any{"offer_id": []string{"TG-0001"}}, + }) + assertStatus(t, w, 200) + + var resp ProductDescriptionResponse + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Result) != 1 || resp.Result[0].OfferID != "TG-0001" { + t.Errorf("expected 1 result for TG-0001, got %+v", resp.Result) + } + if resp.Result[0].State != "moderated" { + t.Errorf("expected state moderated, got %s", resp.Result[0].State) + } +} + +func TestCategoryTree(t *testing.T) { + w := doReq(t, CategoryTree, "POST", "/v2/category/tree", map[string]any{"language": "RU"}) + assertStatus(t, w, 200) + + var resp CategoryTreeResponse + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Result) != 3 { + t.Errorf("expected 3 root categories, got %d", len(resp.Result)) + } + if resp.Result[0].Title != "Электроника" { + t.Errorf("expected Электроника, got %s", resp.Result[0].Title) + } +} + +func TestCategoryAttributes(t *testing.T) { + w := doReq(t, CategoryAttributes, "POST", "/v3/category/attribute", map[string]any{ + "category_id": []int64{17036136}, + }) + assertStatus(t, w, 200) + + var resp CategoryAttributesResponse + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Result) != 6 { + t.Errorf("expected 6 attributes, got %d", len(resp.Result)) + } +} + +func TestCategoryAttributeValues(t *testing.T) { + w := doReq(t, CategoryAttributeValues, "POST", "/v3/category/attribute/values", map[string]any{ + "attribute_id": 85, + "category_id": 17036136, + }) + assertStatus(t, w, 200) + + var resp CategoryAttributeValuesResponse + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Result) != 4 { + t.Errorf("expected 4 values, got %d", len(resp.Result)) + } +} + +func TestPostingFBSList(t *testing.T) { + w := doReq(t, PostingFBSList, "POST", "/v2/posting/fbs/list", map[string]any{}) + assertStatus(t, w, 200) + assertJSON(t, w, "postings", nil) +} + +func TestPostingFBSList_FilterStatus(t *testing.T) { + w := doReq(t, PostingFBSList, "POST", "/v2/posting/fbs/list", map[string]any{ + "filter": map[string]any{"status": "delivered"}, + }) + assertStatus(t, w, 200) + var resp struct { + Postings []Posting `json:"postings"` + } + json.NewDecoder(w.Body).Decode(&resp) + for _, p := range resp.Postings { + if p.Status != "delivered" { + t.Errorf("filter status: expected delivered, got %s", p.Status) + } + } +} + +func TestPostingFBSList_FilterSince(t *testing.T) { + w := doReq(t, PostingFBSList, "POST", "/v2/posting/fbs/list", map[string]any{ + "filter": map[string]any{"since": "2024-01-16T00:00:00Z"}, + }) + assertStatus(t, w, 200) + var resp struct { + Postings []Posting `json:"postings"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Postings) < 3 { + t.Errorf("filter since: expected at least 3 postings from 2024-01-16, got %d", len(resp.Postings)) + } +} + +func TestPostingFBSList_WithAnalytics(t *testing.T) { + w := doReq(t, PostingFBSList, "POST", "/v2/posting/fbs/list", map[string]any{ + "with": map[string]any{"analytics_data": true}, + }) + assertStatus(t, w, 200) + var resp struct { + Postings []Posting `json:"postings"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Postings) > 0 && resp.Postings[0].AnalyticsData == nil { + t.Errorf("expected analytics_data, got nil") + } +} + +func TestPostingFBSList_Asc(t *testing.T) { + w := doReq(t, PostingFBSList, "POST", "/v2/posting/fbs/list", map[string]any{ + "dir": "asc", + }) + assertStatus(t, w, 200) +} + +func TestPostingFBSGet(t *testing.T) { + w := doReq(t, PostingFBSGet, "POST", "/v2/posting/fbs/get", map[string]any{ + "posting_number": "TG-POST-001", + }) + assertStatus(t, w, 200) + assertJSON(t, w, "posting", nil) + + w = doReq(t, PostingFBSGet, "POST", "/v2/posting/fbs/get", map[string]any{ + "posting_number": "NON-EXISTENT", + }) + assertStatus(t, w, 404) +} + +func TestCancelPosting(t *testing.T) { + w := doReq(t, CancelPosting, "POST", "/v1/posting/fbs/cancel", map[string]any{ + "posting_number": "TG-POST-001", + }) + assertStatus(t, w, 200) + + w = doReq(t, CancelPosting, "POST", "/v1/posting/fbs/cancel", map[string]any{ + "posting_number": "", + }) + assertStatus(t, w, 400) + + w = doReq(t, CancelPosting, "POST", "/v1/posting/fbs/cancel", map[string]any{ + "posting_number": "NON-EXISTENT", + }) + assertStatus(t, w, 404) +} + +func TestFinanceTransactionList(t *testing.T) { + w := doReq(t, FinanceTransactionList, "POST", "/v1/finance/transaction/list", map[string]any{}) + assertStatus(t, w, 200) + assertJSON(t, w, "transactions", nil) +} + +func TestFinanceTransactionList_FilterType(t *testing.T) { + w := doReq(t, FinanceTransactionList, "POST", "/v1/finance/transaction/list", map[string]any{ + "filter": map[string]any{"transaction_type": "commission"}, + }) + assertStatus(t, w, 200) + var resp struct { + Transactions []Transaction `json:"transactions"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Transactions) < 1 || resp.Transactions[0].TransactionType != "commission" { + t.Errorf("expected at least 1 commission transaction, got %d", len(resp.Transactions)) + } +} + +func TestFinancePayoutList(t *testing.T) { + w := doReq(t, FinancePayoutList, "POST", "/v1/finance/payout/list", map[string]any{}) + assertStatus(t, w, 200) + assertJSON(t, w, "payouts", nil) +} + +func TestFinancePayoutList_FilterSince(t *testing.T) { + w := doReq(t, FinancePayoutList, "POST", "/v1/finance/payout/list", map[string]any{ + "filter": map[string]any{"since": "2024-01-20T00:00:00Z"}, + }) + assertStatus(t, w, 200) + var resp struct { + Payouts []Payout `json:"payouts"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Payouts) < 1 { + t.Errorf("filter since: expected at least 1 payout, got %d", len(resp.Payouts)) + } +} + +func TestReturnsList(t *testing.T) { + w := doReq(t, ReturnsList, "POST", "/v1/returns/list", map[string]any{}) + assertStatus(t, w, 200) + assertJSON(t, w, "result", nil) +} + +func TestReturnsList_FilterStatus(t *testing.T) { + w := doReq(t, ReturnsList, "POST", "/v1/returns/list", map[string]any{ + "filter": map[string]any{"status": "pending"}, + }) + assertStatus(t, w, 200) + var resp struct { + Result []Return `json:"result"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Result) < 1 || resp.Result[0].Status != "pending" { + t.Errorf("expected at least 1 pending return, got %d", len(resp.Result)) + } +} + +func TestReturnsList_FilterReturnID(t *testing.T) { + w := doReq(t, ReturnsList, "POST", "/v1/returns/list", map[string]any{ + "filter": map[string]any{"return_id": 99000001}, + }) + assertStatus(t, w, 200) + var resp struct { + Result []Return `json:"result"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Result) != 1 || resp.Result[0].ReturnID != 99000001 { + t.Errorf("expected return 99000001, got %d results", len(resp.Result)) + } +} + +func TestAnalyticsData(t *testing.T) { + w := doReq(t, AnalyticsData, "POST", "/v1/analytics/data", map[string]any{}) + assertStatus(t, w, 200) + assertJSON(t, w, "data", nil) +} + +func TestTurnoverStocks(t *testing.T) { + w := doReq(t, TurnoverStocks, "POST", "/v1/analytics/turnover/stocks", map[string]any{}) + assertStatus(t, w, 200) + assertJSON(t, w, "items", nil) +} + +func TestProductAttributes(t *testing.T) { + w := doReq(t, ProductAttributes, "POST", "/v1/product/info/attributes", map[string]any{ + "filter": map[string]any{"offer_id": []string{"TG-0001"}}, + }) + assertStatus(t, w, 200) + + var resp ProductAttributesResponse + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.Result) != 1 { + t.Errorf("expected 1 result, got %d", len(resp.Result)) + } +} diff --git a/internal/handlers/postings.go b/internal/handlers/postings.go index 965d234..b77ba1c 100644 --- a/internal/handlers/postings.go +++ b/internal/handlers/postings.go @@ -6,9 +6,8 @@ import ( "time" ) -// PostingFBSListRequest - POST /v2/posting/fbs/list type PostingFBSListRequest struct { - Dir string `json:"dir"` // "asc" or "desc" + Dir string `json:"dir"` Filter struct { Since string `json:"since"` To string `json:"to"` @@ -23,26 +22,26 @@ type PostingFBSListRequest struct { } 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"` + 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"` + 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) { @@ -66,7 +65,30 @@ func PostingFBSList(w http.ResponseWriter, r *http.Request) { postings := GetPostingsFixtures() - // Filter by status if provided + if req.Filter.Since != "" { + sinceTs := parseTimeOrZero(req.Filter.Since) + filtered := make([]Posting, 0) + for _, p := range postings { + ts := parseTimeOrZero(p.CreatedAt) + if ts >= sinceTs { + filtered = append(filtered, p) + } + } + postings = filtered + } + + if req.Filter.To != "" { + toTs := parseTimeOrZero(req.Filter.To) + filtered := make([]Posting, 0) + for _, p := range postings { + ts := parseTimeOrZero(p.CreatedAt) + if ts <= toTs { + filtered = append(filtered, p) + } + } + postings = filtered + } + if req.Filter.Status != "" { filtered := make([]Posting, 0) for _, p := range postings { @@ -77,7 +99,6 @@ func PostingFBSList(w http.ResponseWriter, r *http.Request) { postings = filtered } - // Filter by posting_number if provided if len(req.Filter.PostingNumber) > 0 { filtered := make([]Posting, 0) for _, p := range postings { @@ -91,7 +112,22 @@ func PostingFBSList(w http.ResponseWriter, r *http.Request) { postings = filtered } - // Apply limit + if req.Dir == "asc" { + for i, j := 0, len(postings)-1; i < j; i, j = i+1, j-1 { + postings[i], postings[j] = postings[j], postings[i] + } + } + + if req.With.AnalyticsData { + for i := range postings { + postings[i].AnalyticsData = map[string]any{ + "commission": 5.0, + "delivery": 150.00, + "return": 0, + } + } + } + if len(postings) > req.Limit { postings = postings[:req.Limit] } @@ -103,7 +139,6 @@ func PostingFBSList(w http.ResponseWriter, r *http.Request) { }) } -// PostingFBSGetRequest - POST /v2/posting/fbs/get type PostingFBSGetRequest struct { PostingNumber string `json:"posting_number"` } @@ -134,14 +169,13 @@ func PostingFBSGet(w http.ResponseWriter, r *http.Request) { 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"` + Status string `json:"status"` } func CancelPosting(w http.ResponseWriter, r *http.Request) { @@ -161,6 +195,19 @@ func CancelPosting(w http.ResponseWriter, r *http.Request) { return } + postings := GetPostingsFixtures() + found := false + for _, p := range postings { + if p.PostingNumber == req.PostingNumber { + found = true + break + } + } + if !found { + writeError(w, 404, "posting not found") + return + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(CancelPostingResponse{ PostingNumber: req.PostingNumber, @@ -168,36 +215,34 @@ func CancelPosting(w http.ResponseWriter, r *http.Request) { }) } -// Returns - type ReturnsListRequest struct { Filter struct { - Since string `json:"since"` - To string `json:"to"` - Status string `json:"status"` - ReturnID int64 `json:"return_id"` + 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"` + 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"` + 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) { @@ -214,6 +259,50 @@ func ReturnsList(w http.ResponseWriter, r *http.Request) { returns := GetReturnsFixtures() + if req.Filter.Since != "" { + sinceTs := parseTimeOrZero(req.Filter.Since) + filtered := make([]Return, 0) + for _, ret := range returns { + ts := parseTimeOrZero(ret.CreatedAt) + if ts >= sinceTs { + filtered = append(filtered, ret) + } + } + returns = filtered + } + + if req.Filter.To != "" { + toTs := parseTimeOrZero(req.Filter.To) + filtered := make([]Return, 0) + for _, ret := range returns { + ts := parseTimeOrZero(ret.CreatedAt) + if ts <= toTs { + filtered = append(filtered, ret) + } + } + returns = filtered + } + + if req.Filter.Status != "" { + filtered := make([]Return, 0) + for _, ret := range returns { + if ret.Status == req.Filter.Status { + filtered = append(filtered, ret) + } + } + returns = filtered + } + + if req.Filter.ReturnID > 0 { + filtered := make([]Return, 0) + for _, ret := range returns { + if ret.ReturnID == req.Filter.ReturnID { + filtered = append(filtered, ret) + } + } + returns = filtered + } + if req.Limit == 0 { req.Limit = 100 } @@ -229,7 +318,14 @@ func ReturnsList(w http.ResponseWriter, r *http.Request) { }) } +func parseTimeOrZero(s string) int64 { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return 0 + } + return t.Unix() +} + func init() { - // Ensure timestamps are consistent _ = time.RFC3339 } diff --git a/internal/handlers/products.go b/internal/handlers/products.go index 7442a39..e555fb8 100644 --- a/internal/handlers/products.go +++ b/internal/handlers/products.go @@ -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 -}