From dc6e28497a64e82fc4b38e46171ef00bc086021a Mon Sep 17 00:00:00 2001 From: diPhantxm Date: Wed, 15 Mar 2023 23:07:29 +0300 Subject: [PATCH] add methods for returns --- ozon/ozon.go | 7 ++ ozon/products.go | 2 +- ozon/returns.go | 250 +++++++++++++++++++++++++++++++++++++++++++ ozon/returns_test.go | 152 ++++++++++++++++++++++++++ 4 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 ozon/returns.go create mode 100644 ozon/returns_test.go diff --git a/ozon/ozon.go b/ozon/ozon.go index b7c181c..07c12b1 100644 --- a/ozon/ozon.go +++ b/ozon/ozon.go @@ -21,6 +21,7 @@ type Client struct { promotions *Promotions rating *Rating warehouses *Warehouses + returns *Returns } func (c Client) Analytics() *Analytics { @@ -55,6 +56,10 @@ func (c Client) Warehouses() *Warehouses { return c.warehouses } +func (c Client) Returns() *Returns { + return c.returns +} + func NewClient(clientId, apiKey string) *Client { coreClient := core.NewClient(DefaultAPIBaseUrl, map[string]string{ "Client-Id": clientId, @@ -71,6 +76,7 @@ func NewClient(clientId, apiKey string) *Client { promotions: &Promotions{client: coreClient}, rating: &Rating{client: coreClient}, warehouses: &Warehouses{client: coreClient}, + returns: &Returns{client: coreClient}, } } @@ -87,5 +93,6 @@ func NewMockClient(handler http.HandlerFunc) *Client { promotions: &Promotions{client: coreClient}, rating: &Rating{client: coreClient}, warehouses: &Warehouses{client: coreClient}, + returns: &Returns{client: coreClient}, } } diff --git a/ozon/products.go b/ozon/products.go index ccce5af..533bdd7 100644 --- a/ozon/products.go +++ b/ozon/products.go @@ -883,7 +883,7 @@ type GetProductsRatingBySKUResponse struct { Description string `json:"description"` // Indication that the condition is met - Fulfilled bool `json:'fulfilled"` + Fulfilled bool `json:"fulfilled"` // Condition identifier Key string `json:"key"` diff --git a/ozon/returns.go b/ozon/returns.go new file mode 100644 index 0000000..a78bd76 --- /dev/null +++ b/ozon/returns.go @@ -0,0 +1,250 @@ +package ozon + +import ( + "net/http" + "time" + + core "github.com/diphantxm/ozon-api-client" +) + +type Returns struct { + client *core.Client +} + +type GetFBOReturnsParams struct { + // Filter + Filter GetFBOReturnsFilter `json:"filter"` + + // Identifier of the last value on the page. Leave this field blank in the first request. + // + // To get the next values, specify the recieved value in the next request in the `last_id` parameter + LastId int64 `json:"last_id"` + + // Number of values in the response + Limit int64 `json:"limit"` +} + +type GetFBOReturnsFilter struct { + // Shipment number + PostingNumber string `json:"posting_number"` + + Status []string `json:"status"` +} + +type GetFBOReturnsResponse struct { + core.CommonResponse + + // Identifier of the last value on the page + LastId int64 `json:"last_id"` + + // Returns information + Returns []struct { + // Time when a return was received from the customer + AcceptedFromCustomerMoment time.Time `json:"accepted_from_customer_moment"` + + // Seller identifier + CompanyId int64 `json:"company_id"` + + // Current return location + CurrentPlaceName string `json:"current_place_name"` + + // Return destination + DestinationPlaceName string `json:"dst_place_name"` + + // Return identifier + Id int64 `json:"id"` + + // Indication that the package has been opened. true, if it has been + IsOpened bool `json:"is_opened"` + + // Shipment number + PostingNumber string `json:"posting_number"` + + // Return reason + ReturnReasonName string `json:"return_reason_name"` + + // Return delivery time to the Ozon warehouse + ReturnedToOzonMoment time.Time `json:"returned_to_ozon_moment"` + + // Product identifier in the Ozon system, SKU + SKU int64 `json:"sku"` + + // Return status + Status string `json:"status_name"` + } `json:"returns"` +} + +// Method for getting information on returned products that are sold from the Ozon warehouse +func (c Returns) GetFBOReturns(params *GetFBOReturnsParams) (*GetFBOReturnsResponse, error) { + url := "/v3/returns/company/fbo" + + resp := &GetFBOReturnsResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type GetFBSReturnsParams struct { + // Filter + Filter GetFBSReturnsFilter `json:"filter"` + + // Number of values in the response: + // - maximum — 1000, + // - minimum — 1 + Limit int64 `json:"limit"` + + // Number of elements that will be skipped in the response. + // For example, if offset=10, the response will start with the 11th element found + Offset int64 `json:"offset"` +} + +type GetFBSReturnsFilter struct { + // Time of receiving the return from the customer + AcceptedFromCustomerMoment GetFBSReturnsFilterTimeRange `json:"accepted_from_customer_moment"` + + // Last day of free storage + LastFreeWaitingDay []GetFBSReturnsFilterTimeRange `json:"last_free_waiting_dat"` + + // Order ID + OrderId int64 `json:"order_id"` + + // Shipment ID + PostingNumber []string `json:"posting_number"` + + // Product name + ProductName string `json:"product_name"` + + // Product ID + ProductOfferId string `json:"product_offer_id"` + + // Return status: + // - returned_to_seller — returned to seller, + // - waiting_for_seller — waiting for seller, + // - accepted_from_customer — accepted from customer, + // - cancelled_with_compensation — cancelled with compensation, + // - ready_for_shipment — ready for shipment + Status string `json:"status"` +} + +type GetFBSReturnsFilterTimeRange struct { + // The beginning of the period. + // + // Format: YYYY-MM-DDTHH:mm:ss.sssZ. + // + // Example: 2019-11-25T10:43:06.51 + TimeFrom time.Time `json:"time_from"` + + // The end of the period + // + // Format: YYYY-MM-DDTHH:mm:ss.sssZ. + // + // Example: 2019-11-25T10:43:06.51 + TimeTo time.Time `json:"time_to"` +} + +type GetFBSReturnsResponse struct { + core.CommonResponse + + Result struct { + // Elements counter in the response + Count int64 `json:"count"` + + // Returns information + Returns []struct { + // Time of receiving the return from the customer + AcceptedFromCustomerAmount string `json:"accepted_from_customer_amount"` + + // Bottom barcode on the product label + ClearingId int64 `json:"clearing_id"` + + // Commission fee + Commission float64 `json:"commission"` + + // Commission percentage + CommissionPercent float64 `json:"commission_percent"` + + // Return identifier + Id int64 `json:"id"` + + // If the product is in transit — true + IsMoving bool `json:"is_moving"` + + // Indication that the package has been opened. true, if it has been + IsOpened bool `json:"is_opened"` + + // Last day of free storage + LastFreeWaitingDay string `json:"last_free_waiting_day"` + + // ID of the warehouse the product is being transported to + PlaceId int64 `json:"place_id"` + + // Name of the warehouse the product is being transported to + MovingToPlaceName string `json:"moving_to_place_name"` + + // Delivery cost + PickingAmount float64 `json:"picking_amount"` + + // Shipment number + PostingNumber string `json:"posting_number"` + + // Current product price without a discount + Price float64 `json:"price"` + + // Product price without commission + PriceWithoutCommission float64 `json:"price_without_commission"` + + // Product identifier + ProductId int64 `json:"product_id"` + + // Product name + ProductName string `json:"product_name"` + + // Product quantity + Quantity int64 `json:"quantity"` + + // Product return date + ReturnDate string `json:"return_date"` + + // Return reason + ReturnReasonName string `json:"return_reason_name"` + + // Date when the product is ready to be handed over to the seller + WaitingForSellerDate string `json:"waiting_for_seller_date_time"` + + // Date of handing over the product to the seller + ReturnedToSellerDate string `json:"returned_to_seller_date_time"` + + // Return storage period in days + WaitingForSellerDays int64 `json:"waiting_for_seller_days"` + + // Return storage cost + ReturnsKeepingCost float64 `json:"returns_keeping_cost"` + + // Product identifier in the Ozon system, SKU + SKU int64 `json:"sku"` + + // Return status + Status string `json:"status"` + } `json:"returns"` + } `json:"result"` +} + +// Method for getting information on returned products that are sold from the seller's warehouse +func (c Returns) GetFBSReturns(params *GetFBSReturnsParams) (*GetFBSReturnsResponse, error) { + url := "/v2/returns/company/fbs" + + resp := &GetFBSReturnsResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} diff --git a/ozon/returns_test.go b/ozon/returns_test.go new file mode 100644 index 0000000..828be7f --- /dev/null +++ b/ozon/returns_test.go @@ -0,0 +1,152 @@ +package ozon + +import ( + "net/http" + "testing" + + core "github.com/diphantxm/ozon-api-client" +) + +func TestGetFBOReturns(t *testing.T) { + tests := []struct { + statusCode int + headers map[string]string + params *GetFBOReturnsParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetFBOReturnsParams{ + Filter: GetFBOReturnsFilter{ + PostingNumber: "some number", + }, + LastId: 123, + Limit: 100, + }, + `{ + "last_id": 0, + "returns": [ + { + "accepted_from_customer_moment": "2019-08-24T14:15:22Z", + "company_id": 0, + "current_place_name": "my-place", + "dst_place_name": "that-place", + "id": 0, + "is_opened": true, + "posting_number": "some number", + "return_reason_name": "ripped", + "returned_to_ozon_moment": "2019-08-24T14:15:22Z", + "sku": 0, + "status_name": "delivering" + } + ] + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &GetFBOReturnsParams{}, + `{ + "code": 16, + "message": "Client-Id and Api-Key headers are required" + }`, + }, + } + + for _, test := range tests { + c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers)) + + resp, err := c.Returns().GetFBOReturns(test.params) + if err != nil { + t.Error(err) + } + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +} + +func TestGetFBSReturns(t *testing.T) { + tests := []struct { + statusCode int + headers map[string]string + params *GetFBSReturnsParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetFBSReturnsParams{ + Filter: GetFBSReturnsFilter{ + PostingNumber: []string{"07402477-0022-2"}, + Status: "returned_to_seller", + }, + Limit: 1000, + Offset: 0, + }, + `{ + "result": { + "returns": [ + { + "id": 19166541735000, + "clearing_id": 19166541725000, + "posting_number": "07402477-0022-2", + "product_id": 172423678, + "sku": 172423678, + "status": "returned_to_seller", + "returns_keeping_cost": 0, + "return_reason_name": "5.12 Заказ более не актуален: долгие сроки доставки", + "return_date": "2020-08-12T17:27:50+00:00", + "quantity": 1, + "product_name": "Кофе ароматизированный \"Лесной орех\" 250 гр", + "price": 294, + "waiting_for_seller_date_time": "2020-08-16T02:50:35+00:00", + "returned_to_seller_date_time": "2020-08-21T10:07:13+00:00", + "last_free_waiting_day": "2020-08-19T23:59:59+00:00", + "is_opened": false, + "place_id": 0, + "commission_percent": 0, + "commission": 0, + "price_without_commission": 0, + "is_moving": false, + "moving_to_place_name": "МОСКВА_ХАБ", + "waiting_for_seller_days": 2, + "picking_amount": null, + "accepted_from_customer_moment": null, + "picking_tag": null + } + ], + "count": 1 + } + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &GetFBSReturnsParams{}, + `{ + "code": 16, + "message": "Client-Id and Api-Key headers are required" + }`, + }, + } + + for _, test := range tests { + c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers)) + + resp, err := c.Returns().GetFBSReturns(test.params) + if err != nil { + t.Error(err) + } + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +}