diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa80932..f3f7372 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.19', '1.20', '1.21', '1.22', '1.23', 'stable'] + go-version: ['1.19', '1.20', '1.21', '1.22', '1.23', '1.24', '1.25', 'stable'] include: - go-version: '1.23' coverage: 1 @@ -55,25 +55,27 @@ jobs: run: | go mod tidy cp .env.dist .env + - name: Install gotestsum + run: | + if [[ "${{ matrix.go-version }}" < "1.24" && "${{ matrix.go-version }}" != "stable" ]]; then + go install gotest.tools/gotestsum@v1.12.0 + else + go install gotest.tools/gotestsum@latest + fi - name: Tests env: COVERAGE: ${{ matrix.coverage }} if: env.COVERAGE != 1 run: | - go install gotest.tools/gotestsum@latest gotestsum --format testdox ./... -tags=testutils -v -cpu 2 -timeout 60s -race - name: Tests with coverage env: COVERAGE: ${{ matrix.coverage }} if: env.COVERAGE == 1 run: | - go install gotest.tools/gotestsum@latest gotestsum --format testdox ./... -tags=testutils -v -cpu 2 -timeout 60s -race -cover -coverprofile=coverage.txt -covermode=atomic "$d" - name: Coverage env: COVERAGE: ${{ matrix.coverage }} if: env.COVERAGE == 1 - run: | - go install github.com/axw/gocov/gocov@latest - gocov convert ./coverage.txt | gocov report - bash <(curl -s https://codecov.io/bash) + run: bash <(curl -s https://codecov.io/bash) diff --git a/.golangci.yml b/.golangci.yml index 4aa1153..a74486d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -23,6 +23,7 @@ issues: - unused - errchkjson - varnamelen + - dupl exclude-use-default: true exclude-case-sensitive: false max-issues-per-linter: 0 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..f0d2c8d --- /dev/null +++ b/.tool-versions @@ -0,0 +1,3 @@ +go 1.23.12 +gotestsum 1.13.0 +golangci-lint 1.62.2 \ No newline at end of file diff --git a/client.go b/client.go index 96a788d..49cbb9e 100644 --- a/client.go +++ b/client.go @@ -1959,7 +1959,7 @@ func (c *Client) CorporateCustomerEdit(customer CorporateCustomer, by string, si // ClearCart clears the current customer's shopping cart // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-customer-interaction-site-cart-clear +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-customer-interaction-site-cart-clear // // Example: // @@ -2021,7 +2021,7 @@ func (c *Client) ClearCart(site string, filter SiteFilter, req ClearCartRequest) // SetCart creates or overwrites shopping cart data // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-customer-interaction-site-cart-set +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-customer-interaction-site-cart-set // // Example: // @@ -2091,7 +2091,7 @@ func (c *Client) SetCart(site string, filter SiteFilter, req SetCartRequest) ( // GetCart returns the current customer's shopping cart // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-customer-interaction-site-cart-customerId +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#get--api-v5-customer-interaction-site-cart-customerId // // Example: // @@ -2125,6 +2125,146 @@ func (c *Client) GetCart(site, customer string, filter GetCartFilter) (CartRespo return resp, status, nil } +// GetFavorites returns the current customer's list of favorite offers +// +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#get--api-v5-customer-interaction-site-favorites-customerId +// +// Example: +// +// var client = retailcrm.New("https://demo.url", "09jIJ") +// +// data, status, err := client.GetFavorites("site_id","customer_id", +// retailcrm.GetFavoritesFilter{ SiteBy: "code", By: "externalId"}) +// +// 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) +// } +func (c *Client) GetFavorites(site, customer string, filter FavoritesFilter) (FavoritesResponse, int, error) { + var resp FavoritesResponse + + params, _ := query.Values(filter) + + data, status, err := c.GetRequest(fmt.Sprintf("/customer-interaction/%s/favorites/%s?%s", site, customer, params.Encode())) + if err != nil { + return resp, status, err + } + + err = json.Unmarshal(data, &resp) + if err != nil { + return resp, status, err + } + + return resp, status, nil +} + +// AddFavorite adds favorite products for customer +// +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-customer-interaction-site-favorites-customerId-add +// +// Example: +// +// var client = retailcrm.New("https://demo.url", "09jIJ") +// +// data, status, err := client.AddFavorite("site_id", "customer_external_id", retailcrm.FavoritesFilter{SiteBy: "id", By: "externalId"}, +// retailcrm.AddFavoriteRequest{ +// ID: 1, +// ExternalID: "ext_id", +// XMLID: "xml_id", +// }, +// ) +// +// 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) +// } +func (c *Client) AddFavorite(site, customer string, filter FavoritesFilter, req AddFavoriteRequest) ( + SuccessfulResponse, int, error, +) { + var resp SuccessfulResponse + + updateJSON, err := json.Marshal(&req) + if err != nil { + return SuccessfulResponse{}, 0, err + } + + p := url.Values{ + "favorite": {string(updateJSON)}, + } + + params, _ := query.Values(filter) + + data, status, err := c.PostRequest(fmt.Sprintf("/customer-interaction/%s/favorites/%s/add?%s", site, customer, params.Encode()), p) + if err != nil { + return resp, status, err + } + + err = json.Unmarshal(data, &resp) + if err != nil { + return resp, status, err + } + + return resp, status, nil +} + +// RemoveFavorite removes favorite products from customer +// +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-customer-interaction-site-favorites-customerId-remove +// +// Example: +// +// var client = retailcrm.New("https://demo.url", "09jIJ") +// +// data, status, err := client.RemoveFavorite("site_id", "customer_external_id", retailcrm.FavoritesFilter{SiteBy: "id", By: "externalId"}, +// retailcrm.AddFavoriteRequest{ +// ID: 1, +// ExternalID: "ext_id", +// XMLID: "xml_id", +// }, +// ) +// +// 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) +// } +func (c *Client) RemoveFavorite(site, customer string, filter FavoritesFilter, req AddFavoriteRequest) ( + SuccessfulResponse, int, error, +) { + var resp SuccessfulResponse + + updateJSON, err := json.Marshal(&req) + if err != nil { + return SuccessfulResponse{}, 0, err + } + + p := url.Values{ + "favorite": {string(updateJSON)}, + } + + params, _ := query.Values(filter) + + data, status, err := c.PostRequest(fmt.Sprintf("/customer-interaction/%s/favorites/%s/remove?%s", site, customer, params.Encode()), p) + if err != nil { + return resp, status, err + } + + err = json.Unmarshal(data, &resp) + if err != nil { + return resp, status, err + } + + return resp, status, nil +} + // DeliveryTracking updates tracking data // // For more information see http://www.simla.com/docs/Developers/API/APIVersions/APIv5#post--api-v5-delivery-generic-subcode-tracking @@ -6277,7 +6417,7 @@ func (c *Client) AccountBonusOperations(id int, parameters AccountBonusOperation // ProductsBatchEdit perform editing products batch // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-store-products-batch-edit +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-store-products-batch-edit // // Example: // @@ -6333,7 +6473,7 @@ func (c *Client) ProductsBatchEdit(products []ProductEdit) (ProductsBatchEditRes // ProductsBatchCreate perform adding products batch // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-store-products-batch-create +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-store-products-batch-create // // Example: // @@ -6387,7 +6527,7 @@ func (c *Client) ProductsBatchCreate(products []ProductCreate) (ProductsBatchEdi // LoyaltyAccountCreate аdd a client to the loyalty program // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-loyalty-account-create +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-loyalty-account-create // // Example: // @@ -6441,7 +6581,7 @@ func (c *Client) LoyaltyAccountCreate(site string, loyaltyAccount SerializedCrea // LoyaltyAccountEdit edit a client in the loyalty program // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-loyalty-account-id-edit +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-loyalty-account-id-edit // // Example: // @@ -6491,7 +6631,7 @@ func (c *Client) LoyaltyAccountEdit(id int, loyaltyAccount SerializedEditLoyalty // LoyaltyAccount return information about client in the loyalty program // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-loyalty-account-id +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#get--api-v5-loyalty-account-id // // Example: // @@ -6530,7 +6670,7 @@ func (c *Client) LoyaltyAccount(id int) (LoyaltyAccountResponse, int, error) { // LoyaltyAccountActivate activate participation in the loyalty program for client // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-loyalty-account-id-activate +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-loyalty-account-id-activate // // Example: // @@ -6569,7 +6709,7 @@ func (c *Client) LoyaltyAccountActivate(id int) (LoyaltyAccountActivateResponse, // LoyaltyBonusCredit accrue bonus participation in the program of loyalty // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-loyalty-account-id-bonus-credit +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-loyalty-account-id-bonus-credit // // Example: // @@ -6615,7 +6755,7 @@ func (c *Client) LoyaltyBonusCredit(id int, req LoyaltyBonusCreditRequest) (Loya // LoyaltyBonusStatusDetails get details on the bonus account // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-loyalty-account-id-bonus-status-details +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#get--api-v5-loyalty-account-id-bonus-status-details // // Example: // @@ -6665,7 +6805,7 @@ func (c *Client) LoyaltyBonusStatusDetails( // LoyaltyAccounts return list of participations in the loyalty program // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-loyalty-accounts +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#get--api-v5-loyalty-accounts // // Example: // @@ -6719,7 +6859,7 @@ func (c *Client) LoyaltyAccounts(req LoyaltyAccountsRequest) (LoyaltyAccountsRes // LoyaltyCalculate calculations of the maximum discount // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-loyalty-calculate +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-loyalty-calculate // // Example: // @@ -6786,7 +6926,7 @@ func (c *Client) LoyaltyCalculate(req LoyaltyCalculateRequest) (LoyaltyCalculate // GetLoyalties returns list of loyalty programs // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-loyalty-loyalties +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#get--api-v5-loyalty-loyalties // // Example: // @@ -6838,7 +6978,7 @@ func (c *Client) GetLoyalties(req LoyaltiesRequest) (LoyaltiesResponse, int, err // GetLoyaltyByID return program of loyalty by id // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-loyalty-loyalties-id +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#get--api-v5-loyalty-loyalties-id // // Example: // @@ -6878,7 +7018,7 @@ func (c *Client) GetLoyaltyByID(id int) (LoyaltyResponse, int, error) { // OrderIntegrationDeliveryCancel cancels of integration delivery // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-orders-externalId-delivery-cancel +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-orders-externalId-delivery-cancel // // Example: // @@ -6922,7 +7062,7 @@ func (c *Client) OrderIntegrationDeliveryCancel(by string, force bool, id string // CreateProductsGroup adds a product group // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-store-product-groups-create +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-store-product-groups-create // // Example: // @@ -6975,7 +7115,7 @@ func (c *Client) CreateProductsGroup(group ProductGroup) (ActionProductsGroupRes // EditProductsGroup edits a product group // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-store-product-groups-externalId-edit +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-store-product-groups-externalId-edit // // Example: // @@ -7105,7 +7245,7 @@ func (c *Client) GetOrderPlate(by, orderID, site string, plateID int) (io.ReadCl // NotificationsSend send a notification // -// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-notifications-send +// For more information see https://docs.retailcrm.ru/Developers/API/APIv5#post--api-v5-notifications-send // // Example: // diff --git a/client_test.go b/client_test.go index bb6abbc..734d8c7 100644 --- a/client_test.go +++ b/client_test.go @@ -5,8 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/retailcrm/api-client-go/v2/constant" - "github.com/stretchr/testify/require" "io" "io/ioutil" "log" @@ -20,6 +18,9 @@ import ( "testing" "time" + "github.com/retailcrm/api-client-go/v2/constant" + "github.com/stretchr/testify/require" + "github.com/google/go-querystring/query" "github.com/stretchr/testify/assert" @@ -2110,6 +2111,176 @@ func TestClient_GetCart(t *testing.T) { } +func TestClient_GetFavorites(t *testing.T) { + c := client() + + site := "site_id" + customer := "customer_id" + filter := FavoritesFilter{ + SiteBy: "code", + By: "externalId", + } + + favoritesResp := FavoritesResponse{ + SuccessfulResponse: SuccessfulResponse{Success: true}, + Favorites: []FavoriteCustomerOffer{ + { + Offer: Offer{ + ID: 1, + ExternalID: "ext_id_1", + XMLID: "xml_id_1", + }, + CreatedAt: "2025-04-14 14:32:14+03:00", + }, + { + Offer: Offer{ + ID: 2, + ExternalID: "ext_id_2", + XMLID: "xml_id_2", + }, + CreatedAt: "2025-04-14 15:32:14+03:00", + }, + }, + } + + defer gock.Off() + gock.New(crmURL). + Get(fmt.Sprintf("/customer-interaction/%s/favorites/%s", site, customer)). + MatchParams(map[string]string{ + "siteBy": filter.SiteBy, + "by": filter.By, + }). + Reply(http.StatusOK). + JSON(favoritesResp) + + data, status, err := c.GetFavorites(site, customer, filter) + if err != nil { + t.Errorf("%v", err) + } + + if status >= http.StatusBadRequest { + t.Errorf("(%d) %v", status, err) + } + + if data.Success != true { + t.Errorf("%v", err) + } + + if !reflect.DeepEqual(favoritesResp.Favorites, data.Favorites) { + t.Errorf("%v", err) + } +} + +func TestClient_AddFavorite(t *testing.T) { + c := client() + + site := "site_id" + customer := "customer_id" + filter := FavoritesFilter{SiteBy: "id"} + request := AddFavoriteRequest{ + ID: 1, + ExternalID: "ext_id", + XMLID: "xml_id", + } + + defer gock.Off() + gock.New(crmURL). + Post(fmt.Sprintf("/customer-interaction/%s/favorites/%s/add", site, customer)). + AddMatcher(func(req *http.Request, _ *gock.Request) (bool, error) { + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + req.Body = io.NopCloser(bytes.NewBuffer(body)) + + val, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + data := val.Get("favorite") + var incoming AddFavoriteRequest + require.NoError(t, json.Unmarshal([]byte(data), &incoming)) + + equal := assert.Equal(t, 1, incoming.ID) && + assert.Equal(t, "ext_id", incoming.ExternalID) && + assert.Equal(t, "xml_id", incoming.XMLID) + + if !equal { + return false, errors.New("unequal values") + } + + return true, nil + }). + MatchParam("siteBy", filter.SiteBy). + Reply(http.StatusOK). + BodyString(`{"success":true}`) + + data, status, err := c.AddFavorite(site, customer, filter, request) + if err != nil { + t.Errorf("%v", err) + } + + if status >= http.StatusBadRequest { + t.Errorf("(%d) %v", status, err) + } + + if data.Success != true { + t.Errorf("%v", err) + } +} + +func TestClient_RemoveFavorite(t *testing.T) { + c := client() + + site := "site_id" + customer := "customer_id" + filter := FavoritesFilter{SiteBy: "id"} + request := AddFavoriteRequest{ + ID: 1, + ExternalID: "ext_id", + XMLID: "xml_id", + } + + defer gock.Off() + gock.New(crmURL). + Post(fmt.Sprintf("/customer-interaction/%s/favorites/%s/remove", site, customer)). + AddMatcher(func(req *http.Request, _ *gock.Request) (bool, error) { + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + req.Body = io.NopCloser(bytes.NewBuffer(body)) + + val, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + data := val.Get("favorite") + var incoming AddFavoriteRequest + require.NoError(t, json.Unmarshal([]byte(data), &incoming)) + + equal := assert.Equal(t, 1, incoming.ID) && + assert.Equal(t, "ext_id", incoming.ExternalID) && + assert.Equal(t, "xml_id", incoming.XMLID) + + if !equal { + return false, errors.New("unequal values") + } + + return true, nil + }). + MatchParam("siteBy", filter.SiteBy). + Reply(http.StatusOK). + BodyString(`{"success":true}`) + + data, status, err := c.RemoveFavorite(site, customer, filter, request) + if err != nil { + t.Errorf("%v", err) + } + + if status >= http.StatusBadRequest { + t.Errorf("(%d) %v", status, err) + } + + if data.Success != true { + t.Errorf("%v", err) + } +} + func TestClient_NotesNotes(t *testing.T) { c := client() diff --git a/filters.go b/filters.go index 88bd576..11b90c2 100644 --- a/filters.go +++ b/filters.go @@ -495,3 +495,12 @@ type GetCartFilter struct { // id|externalId, default is externalId. By string `url:"by,omitempty"` } + +type FavoritesFilter struct { + // SiteBy contains information about what is betrayed site id or site code. + // id|code, default is code. + SiteBy string `url:"siteBy,omitempty"` + // By contains information about what is betrayed: customer id or customer externalId. + // id|externalId, default is externalId. + By string `url:"by,omitempty"` +} diff --git a/request.go b/request.go index 96872f3..88b8582 100644 --- a/request.go +++ b/request.go @@ -210,6 +210,12 @@ type SetCartRequest struct { Items []SetCartItem `json:"items,omitempty"` } +type AddFavoriteRequest struct { + ID int `json:"id,omitempty"` + ExternalID string `json:"externalId,omitempty"` + XMLID string `json:"xmlId,omitempty"` +} + // CostsRequest type. type CostsRequest struct { Filter CostsFilter `url:"filter,omitempty"` diff --git a/response.go b/response.go index 4a1c334..23731d8 100644 --- a/response.go +++ b/response.go @@ -413,6 +413,12 @@ type CartResponse struct { Cart Cart `json:"cart"` } +// FavoritesResponse type. +type FavoritesResponse struct { + SuccessfulResponse + Favorites []FavoriteCustomerOffer `json:"cart"` +} + // DeliveryShipmentsResponse type. type DeliveryShipmentsResponse struct { Success bool `json:"success"` diff --git a/types.go b/types.go index 1ce97bc..bb7f075 100644 --- a/types.go +++ b/types.go @@ -538,6 +538,12 @@ type CartUnit struct { Sym string `json:"sym"` } +// FavoriteCustomerOffer type. +type FavoriteCustomerOffer struct { + Offer Offer `json:"offer,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` +} + // UnmarshalJSON method. func (v *OrderDeliveryData) UnmarshalJSON(b []byte) error { var additionalData map[string]interface{}