From 89d690e00ae971e599fb7600c63a1b5c4f322ea1 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Mon, 4 May 2026 15:45:26 +0300 Subject: [PATCH 1/2] Update for StoreOffers * Update filters, update response, update Product DTO --- client.go | 33 ++++++++++++++++++++++ client_test.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++++- filters.go | 17 ++++++++++-- request.go | 2 ++ response.go | 4 +-- testutils.go | 36 +++++++++++++++++++++++- types.go | 25 +++++++++++------ 7 files changed, 176 insertions(+), 15 deletions(-) diff --git a/client.go b/client.go index 8778bf1..1756d41 100644 --- a/client.go +++ b/client.go @@ -7739,6 +7739,35 @@ func (c *Client) EditMGChannelTemplate(req EditMGChannelTemplateRequest) (int, e return code, nil } +// StoreOffers returns list of offers +// +// For more information see https://docs.retailcrm.ru/Developers/API/APIMethods#get--api-v5-store-offers +// +// Example: +// +// var client = retailcrm.New("https://demo.url", "09jIJ") +// +// active := 1 +// data, status, err := client.StoreOffers(retailcrm.OffersRequest{ +// OffersFilter: retailcrm.OffersFilter{ +// Ids: []int{76}, +// Active: &active, +// }, +// Limit: 20, +// Page: 1, +// }) +// +// if err != nil { +// if apiErr, ok := retailcrm.AsAPIError(err); ok { +// log.Fatalf("http status: %d, %s", status, apiErr.String()) +// } +// +// log.Fatalf("http status: %d, error: %s", status, err) +// } +// +// for _, value := range data.Offers { +// log.Printf("%v\n", value) +// } func (c *Client) StoreOffers(req OffersRequest) (StoreOffersResponse, int, error) { var result StoreOffersResponse @@ -7748,6 +7777,10 @@ func (c *Client) StoreOffers(req OffersRequest) (StoreOffersResponse, int, error return StoreOffersResponse{}, 0, err } + for code, value := range req.Properties { + filter.Set(fmt.Sprintf("filter[properties][%s]", code), value) + } + resp, status, err := c.GetRequest(fmt.Sprintf("/store/offers?%s", filter.Encode())) if err != nil { diff --git a/client_test.go b/client_test.go index 1b07ca9..45f2353 100644 --- a/client_test.go +++ b/client_test.go @@ -9219,11 +9219,46 @@ func TestClient_StoreOffers(t *testing.T) { Get(prefix+"/store/offers"). MatchParam("filter[active]", "1"). MatchParam("filter[ids][]", "76"). + MatchParam("filter[externalIds][]", "offer-external-id"). + MatchParam("filter[xmlIds][]", "offer-xml-id"). + MatchParam("filter[name]", "Название"). + MatchParam("filter[sites][]", "main"). + MatchParam("filter[catalogs][]", "2"). + MatchParam("filter[groups][]", "10"). + MatchParam("filter[priceType]", "base"). + MatchParam("filter[properties][color]", "red"). + MatchParam("filter[sinceId]", "75"). + MatchParam("filter[minPrice]", "100"). + MatchParam("filter[maxPrice]", "10000"). + MatchParam("filter[minQuantity]", "1"). + MatchParam("filter[maxQuantity]", "10"). + MatchParam("limit", "20"). + MatchParam("page", "1"). Reply(http.StatusOK). JSON(getStoreOfferResponse()) a := 1 - f := OffersRequest{OffersFilter{Ids: []int{76}, Active: &a}} + f := OffersRequest{ + OffersFilter: OffersFilter{ + Ids: []int{76}, + ExternalIDs: []string{"offer-external-id"}, + XMLIDs: []string{"offer-xml-id"}, + Name: "Название", + Sites: []string{"main"}, + Catalogs: []int{2}, + Groups: []int{10}, + PriceType: "base", + Active: &a, + Properties: map[string]string{"color": "red"}, + SinceID: 75, + MinPrice: 100, + MaxPrice: 10000, + MinQuantity: 1, + MaxQuantity: 10, + }, + Limit: 20, + Page: 1, + } resp, status, err := cl.StoreOffers(f) @@ -9241,9 +9276,46 @@ func TestClient_StoreOffers(t *testing.T) { assert.Len(t, resp.Offers, 1) assert.Equal(t, 76, resp.Offers[0].ID) + assert.Equal(t, "offer-external-id", resp.Offers[0].ExternalID) + assert.Equal(t, "offer-xml-id", resp.Offers[0].XMLID) + assert.Equal(t, "main", resp.Offers[0].Site) assert.Equal(t, "Название\nПеревод строки", resp.Offers[0].Name) + assert.Equal(t, "Артикул", resp.Offers[0].Article) + assert.Equal(t, "20", resp.Offers[0].VatRate) assert.Equal(t, 222, resp.Offers[0].Product.ID) + assert.Equal(t, 2, resp.Offers[0].Product.CatalogID) + assert.Equal(t, ProductType("product"), resp.Offers[0].Product.Type) + assert.Equal(t, "product-article", resp.Offers[0].Product.Article) + assert.Equal(t, "Товар", resp.Offers[0].Product.Name) + assert.Equal(t, "https://example.com/product", resp.Offers[0].Product.URL) + assert.Equal(t, "https://example.com/product.jpg", resp.Offers[0].Product.ImageURL) + assert.Equal(t, "Описание товара", resp.Offers[0].Product.Description) + assert.True(t, resp.Offers[0].Product.Popular) + assert.True(t, resp.Offers[0].Product.Stock) + assert.True(t, resp.Offers[0].Product.Novelty) + assert.True(t, resp.Offers[0].Product.Recommended) + assert.Len(t, resp.Offers[0].Product.Options, 1) + assert.Len(t, resp.Offers[0].Product.Groups, 1) + assert.Equal(t, 10, resp.Offers[0].Product.Groups[0].ID) + assert.Equal(t, "group-external-id", resp.Offers[0].Product.Groups[0].ExternalID) + assert.Equal(t, "product-external-id", resp.Offers[0].Product.ExternalID) + assert.Equal(t, "Производитель", resp.Offers[0].Product.Manufacturer) + assert.Equal(t, "2024-01-02 03:04:05", resp.Offers[0].Product.UpdatedAt) + assert.True(t, resp.Offers[0].Product.Active) + assert.Equal(t, float32(5), resp.Offers[0].Product.Quantity) + assert.True(t, resp.Offers[0].Product.Markable) + assert.Equal(t, "chestny_znak", resp.Offers[0].Product.MarkingProvider) assert.Equal(t, "base", resp.Offers[0].Prices[0].PriceType) assert.Equal(t, float32(10000), resp.Offers[0].Prices[0].Price) assert.Equal(t, "RUB", resp.Offers[0].Prices[0].Currency) + assert.Equal(t, float32(10), resp.Offers[0].PurchasePrice) + assert.Equal(t, float32(5), resp.Offers[0].Quantity) + assert.Equal(t, float32(1.5), resp.Offers[0].Weight) + assert.Equal(t, float32(10), resp.Offers[0].Length) + assert.Equal(t, float32(20), resp.Offers[0].Width) + assert.Equal(t, float32(30), resp.Offers[0].Height) + assert.Equal(t, "red", resp.Offers[0].Properties["color"]) + assert.True(t, resp.Offers[0].Active) + assert.Equal(t, "1234567890", resp.Offers[0].Barcode) + assert.Equal(t, "pc", resp.Offers[0].Unit.Code) } diff --git a/filters.go b/filters.go index 7da1f15..aec83d0 100644 --- a/filters.go +++ b/filters.go @@ -480,8 +480,21 @@ type LoyaltyAPIFilter struct { } type OffersFilter struct { - Ids []int `url:"ids,omitempty,brackets"` - Active *int `url:"active,omitempty"` + Ids []int `url:"ids,omitempty,brackets"` + ExternalIDs []string `url:"externalIds,omitempty,brackets"` + XMLIDs []string `url:"xmlIds,omitempty,brackets"` + Name string `url:"name,omitempty"` + Sites []string `url:"sites,omitempty,brackets"` + Catalogs []int `url:"catalogs,omitempty,brackets"` + Groups []int `url:"groups,omitempty,brackets"` + PriceType string `url:"priceType,omitempty"` + Active *int `url:"active,omitempty"` + Properties map[string]string `url:"-"` + SinceID int `url:"sinceId,omitempty"` + MinPrice float32 `url:"minPrice,omitempty"` + MaxPrice float32 `url:"maxPrice,omitempty"` + MinQuantity float32 `url:"minQuantity,omitempty"` + MaxQuantity float32 `url:"maxQuantity,omitempty"` } type SiteFilter struct { diff --git a/request.go b/request.go index ed55dc8..d635940 100644 --- a/request.go +++ b/request.go @@ -339,4 +339,6 @@ func (r ConnectRequest) Verify(secret string) bool { type OffersRequest struct { OffersFilter `url:"filter,omitempty"` + Limit int `url:"limit,omitempty"` + Page int `url:"page,omitempty"` } diff --git a/response.go b/response.go index 1080ad4..5c35310 100644 --- a/response.go +++ b/response.go @@ -705,7 +705,7 @@ type MGChannelTemplatesResponse struct { } type StoreOffersResponse struct { - Pagination *Pagination `json:"pagination"` SuccessfulResponse - Offers []Offer `json:"offers,omitempty"` + Pagination *Pagination `json:"pagination"` + Offers []Offer `json:"offers,omitempty"` } diff --git a/testutils.go b/testutils.go index 1ab6ee7..e3b6607 100644 --- a/testutils.go +++ b/testutils.go @@ -535,13 +535,39 @@ func getStoreOfferResponse() string { "https://s3-s1.retailcrm.tech/ru-central1/retailcrm/dev-vega-d32aea7f9a5bc26eba6ad986077cea03/product/65a92fa0bb737-test.jpeg" ], "id": 76, + "externalId": "offer-external-id", + "xmlId": "offer-xml-id", "site": "main", "name": "Название\nПеревод строки", "article": "Артикул", + "vatRate": "20", "product": { "type": "product", "catalogId": 2, - "id": 222 + "id": 222, + "article": "product-article", + "name": "Товар", + "url": "https://example.com/product", + "imageUrl": "https://example.com/product.jpg", + "description": "Описание товара", + "popular": true, + "stock": true, + "novelty": true, + "recommended": true, + "options": ["option"], + "groups": [ + { + "id": 10, + "externalId": "group-external-id" + } + ], + "externalId": "product-external-id", + "manufacturer": "Производитель", + "updatedAt": "2024-01-02 03:04:05", + "active": true, + "quantity": 5, + "markable": true, + "markingProvider": "chestny_znak" }, "prices": [ { @@ -553,7 +579,15 @@ func getStoreOfferResponse() string { ], "purchasePrice": 10, "quantity": 5, + "weight": 1.5, + "length": 10, + "width": 20, + "height": 30, + "properties": { + "color": "red" + }, "active": true, + "barcode": "1234567890", "unit": { "code": "pc", "name": "Штука", diff --git a/types.go b/types.go index 8a2aa37..39567fd 100644 --- a/types.go +++ b/types.go @@ -761,6 +761,7 @@ type Offer struct { ExternalID string `json:"externalId,omitempty"` Name string `json:"name,omitempty"` XMLID string `json:"xmlId,omitempty"` + Site string `json:"site,omitempty"` Article string `json:"article,omitempty"` VatRate string `json:"vatRate,omitempty"` Price float32 `json:"price,omitempty"` @@ -774,6 +775,8 @@ type Offer struct { Properties StringMap `json:"properties,omitempty"` Prices []OfferPrice `json:"prices,omitempty"` Images []string `json:"images,omitempty"` + Active bool `json:"active,omitempty"` + Barcode string `json:"barcode,omitempty"` Unit *Unit `json:"unit,omitempty"` Product *Product `json:"product,omitempty"` } @@ -1270,15 +1273,19 @@ const ( // Product type. type Product struct { BaseProduct - ID int `json:"id,omitempty"` - Type ProductType `json:"type"` - MaxPrice float32 `json:"maxPrice,omitempty"` - MinPrice float32 `json:"minPrice,omitempty"` - ImageURL string `json:"imageUrl,omitempty"` - Quantity float32 `json:"quantity,omitempty"` - Offers []Offer `json:"offers,omitempty"` - Properties StringMap `json:"properties,omitempty"` - Groups []ProductGroup `json:"groups,omitempty"` + ID int `json:"id,omitempty"` + Type ProductType `json:"type"` + CatalogID int `json:"catalogId,omitempty"` + MaxPrice float32 `json:"maxPrice,omitempty"` + MinPrice float32 `json:"minPrice,omitempty"` + ImageURL string `json:"imageUrl,omitempty"` + Quantity float32 `json:"quantity,omitempty"` + Offers []Offer `json:"offers,omitempty"` + Properties StringMap `json:"properties,omitempty"` + Options []interface{} `json:"options,omitempty"` + Groups []ProductGroup `json:"groups,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` + MarkingProvider string `json:"markingProvider,omitempty"` } // ProductEditGroupInput type. From 5687bbd7ae0518bd5dcd54224c05594cd0c50e41 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Mon, 4 May 2026 15:58:53 +0300 Subject: [PATCH 2/2] tmp fix for linter --- filters.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filters.go b/filters.go index aec83d0..2ead571 100644 --- a/filters.go +++ b/filters.go @@ -480,7 +480,7 @@ type LoyaltyAPIFilter struct { } type OffersFilter struct { - Ids []int `url:"ids,omitempty,brackets"` + Ids []int `url:"ids,omitempty,brackets"` //nolint:revive ExternalIDs []string `url:"externalIds,omitempty,brackets"` XMLIDs []string `url:"xmlIds,omitempty,brackets"` Name string `url:"name,omitempty"`