From af7c167edf4d9d6629135f2566dfd725c86de95c Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 30 Nov 2023 18:16:34 +0300 Subject: [PATCH] Update November 30, 2023 (#50) Method for RFBS returns operations --- ozon/common.go | 47 +++++ ozon/returns.go | 386 ++++++++++++++++++++++++++++++++++++++++ ozon/returns_test.go | 414 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 847 insertions(+) diff --git a/ozon/common.go b/ozon/common.go index e6a4d2c..515315b 100644 --- a/ozon/common.go +++ b/ozon/common.go @@ -587,3 +587,50 @@ const ( // SKU is deleted SKUAvailabilityUnavailable = "UNAVAILABLE" ) + +type RFBSReturnsGroupState string + +const ( + // All requests + RFBSReturnsGroupStateAll RFBSReturnsGroupState = "All" + + // New + RFBSReturnsGroupStateNew RFBSReturnsGroupState = "New" + + // Returned product is on the way for check + RFBSReturnsGroupStateDelivering RFBSReturnsGroupState = "Delivering" + + // Returned product is being checked + RFBSReturnsGroupStateCheckout RFBSReturnsGroupState = "Checkout" + + // Disputed + RFBSReturnsGroupStateArbitration RFBSReturnsGroupState = "Arbitration" + + // Approved + RFBSReturnsGroupStateApproved RFBSReturnsGroupState = "Approved" + + // Rejected + RFBSReturnsGroupStateRejected RFBSReturnsGroupState = "Rejected" +) + +type GetRFBSReturnsCurrency string + +const ( + // Russian ruble + GetRFBSReturnsCurrencyRUB GetRFBSReturnsCurrency = "RUB" + + // Belarusian ruble + GetRFBSReturnsCurrencyBYN GetRFBSReturnsCurrency = "BYN" + + // Tenge + GetRFBSReturnsCurrencyKZT GetRFBSReturnsCurrency = "KZT" + + // Euro + GetRFBSReturnsCurrencyEUR GetRFBSReturnsCurrency = "EUR" + + // US dollar + GetRFBSReturnsCurrencyUSD GetRFBSReturnsCurrency = "USD" + + // Yuan + GetRFBSReturnsCurrencyCNY GetRFBSReturnsCurrency = "CNY" +) diff --git a/ozon/returns.go b/ozon/returns.go index 4b56251..7dc3345 100644 --- a/ozon/returns.go +++ b/ozon/returns.go @@ -261,3 +261,389 @@ func (c Returns) GetFBSReturns(ctx context.Context, params *GetFBSReturnsParams) return resp, nil } + +type GetRFBSReturnsParams struct { + // Filter + Filter GetRFBSReturnsFilter `json:"filter"` + + // Identifier of the last value on the page. + // Leave this field blank in the first request + LastId int32 `json:"last_id"` + + // Number of values per page + Limit int32 `json:"limit"` +} + +type GetRFBSReturnsFilter struct { + // Product identifier in the seller's system + OfferId string `json:"offer_id"` + + // Shipment number + PostingNumber string `json:"posting_number"` + + // Filter by request statuses + GroupState []RFBSReturnsGroupState `json:"group_state"` + + // Period of request creation + CreatedAt GetRFBSReturnsFilterCreatedAt `json:"created_at"` +} + +type GetRFBSReturnsFilterCreatedAt struct { + // Period start date + From time.Time `json:"from"` + + // Period end date + To time.Time `json:"to"` +} + +type GetRFBSReturnsResponse struct { + core.CommonResponse + + // Information on return requests + Returns GetRFBSReturnsReturn `json:"returns"` +} + +type GetRFBSReturnsReturn struct { + // Customer name + ClientName string `json:"client_name"` + + // Request creation date + CreatedAt time.Time `json:"created_at"` + + // Order number + OrderNumber string `json:"order_number"` + + // Shipment number + PostingNumber string `json:"posting_number"` + + // Product details + Product GetRFBSReturnsProduct `json:"product"` + + // Return request identifier + ReturnId int64 `json:"return_id"` + + // Return request number + ReturnNumber string `json:"return_number"` + + // Request and refund statuses + State GetRFBSReturnsState `json:"state"` +} + +type GetRFBSReturnsProduct struct { + // Product name + Name string `json:"name"` + + // Product identifier in the seller's system + OfferId string `json:"offer_id"` + + // Currency of your prices. It matches the currency set in your personal account + CurrencyCode GetRFBSReturnsCurrency `json:"currency_code"` + + // Product price + Price string `json:"price"` + + // Product identifier in the Ozon system, SKU + SKU int64 `json:"sku"` +} + +type GetRFBSReturnsState struct { + // Request status by the applied filter + GroupState RFBSReturnsGroupState `json:"group_state"` + + // Refund status + MoneyReturnStateName string `json:"money_return_state_name"` + + // Request status + State string `json:"state"` + + // Request status name in Russian + StateName string `json:"state_name"` +} + +// Get a list of return requests +func (c Returns) GetRFBSReturns(ctx context.Context, params *GetRFBSReturnsParams) (*GetRFBSReturnsResponse, error) { + url := "/v2/returns/rfbs/list" + + resp := &GetRFBSReturnsResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type GetRFBSReturnParams struct { + // Request identifier + ReturnId int64 `json:"return_id"` +} + +type GetRFBSReturnResponse struct { + core.CommonResponse + + // List of available actions on the request + AvailableActions []GetRFBSReturnAction `json:"available_actions"` + + // Customer name + ClientName string `json:"client_name"` + + // Links to product images + ClientPhoto []string `json:"client_photo"` + + // Information on return method + ClientReturnMethodType GetRFBSReturnMethodType `json:"client_return_method_type"` + + // Customer comment + Comment string `json:"comment"` + + // Request creation date + CreatedAt time.Time `json:"created_at"` + + // Order number + OrderNumber string `json:"order_number"` + + // Shipment number + PostingNumber string `json:"posting_number"` + + // Product details + Product GetRFBSReturnsProduct `json:"product"` + + // Comment on request rejection + RejectionComment string `json:"rejection_comment"` + + // Information on rejection reason + RejectionReason []GetRFBSReturnRejectionReason `json:"rejection_reason"` + + // Method of product return + ReturnMethodDescription string `json:"return_method_description"` + + // Return request number + ReturnNumber string `json:"return_number"` + + // Information on return reason + ReturnReason GetRFBSReturnReason `json:"return_reason"` + + // Postal tracking number + RUPostTrackingNumber string `json:"ru_post_tracking_number"` + + // Information on return status + State GetRFBSReturnState `json:"state"` + + // Warehouse identifier + WarehouseId int64 `json:"warehouse_id"` +} + +type GetRFBSReturnAction struct { + // Action identifier + Id int32 `json:"id"` + + // Action name + Name string `json:"name"` +} + +type GetRFBSReturnMethodType struct { + // Identifier + Id int32 `json:"id"` + + // Name + Name string `json:"name"` +} + +type GetRFBSReturnRejectionReason struct { + // Hint on further actions with the return + Hint string `json:"hint"` + + // Reason identifier + Id int32 `json:"id"` + + // `true` if you need to attach a comment + IsCommentRequired bool `json:"is_comment_required"` + + // Reason description + Name string `json:"name"` +} + +type GetRFBSReturnReason struct { + // Reason identifier + Id int32 `json:"id"` + + // `true` if the product is defective + IsDefect bool `json:"is_defect"` + + // Reason description + Name string `json:"name"` +} + +type GetRFBSReturnState struct { + // Status + State string `json:"state"` + + // Status name in Russian + StateName string `json:"state_name"` +} + +// Get information about a return request +func (c Returns) GetRFBSReturn(ctx context.Context, params *GetRFBSReturnParams) (*GetRFBSReturnResponse, error) { + url := "/v2/returns/rfbs/get" + + resp := &GetRFBSReturnResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type RejectRFBSReturnParams struct { + // Return request identifier + ReturnId int64 `json:"return_id"` + + // Comment + // + // The comment is required if the + // `rejection_reason.is_comment_required` parameter is `true` + // in the response of the `/v2/returns/rfbs/get` method + Comment string `json:"comment"` + + // Rejection reason identifier. + // + // Pass the value from the list of reasons received in the response + // of the `/v2/returns/rfbs/get` method in the `rejection_reason` parameter + RejectionReasonId int64 `json:"rejection_reason_id"` +} + +type RejectRFBSReturnResponse struct { + core.CommonResponse +} + +// The method rejects an rFBS return request. Explain your decision in the `comment` parameter +func (c Returns) RejectRFBSReturn(ctx context.Context, params *RejectRFBSReturnParams) (*RejectRFBSReturnResponse, error) { + url := "/v2/returns/rfbs/reject" + + resp := &RejectRFBSReturnResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type CompensateRFBSReturnParams struct { + // Compensation amount + CompensationAmount string `json:"compensation_amount"` + + // Return request identifier + ReturnId int64 `json:"return_id"` +} + +type CompensateRFBSReturnResponse struct { + core.CommonResponse +} + +// Using this method you can confirm the partial compensation and agree to keep the product with the customer +func (c Returns) CompensateRFBSReturn(ctx context.Context, params *CompensateRFBSReturnParams) (*CompensateRFBSReturnResponse, error) { + url := "/v2/returns/rfbs/compensate" + + resp := &CompensateRFBSReturnResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type ApproveRFBSReturnParams struct { + // Return request identifier + ReturnId int64 `json:"return_id"` + + // Method of product return + ReturnMethodDescription string `json:"return_method_description"` +} + +type ApproveRFBSReturnResponse struct { + core.CommonResponse +} + +// The method allows to approve an rFBS return request and agree to receive products for verification. +// +// Confirm that you've received the product using the `/v2/returns/rfbs/receive-return` method. +func (c Returns) ApproveRFBSReturn(ctx context.Context, params *ApproveRFBSReturnParams) (*ApproveRFBSReturnResponse, error) { + url := "/v2/returns/rfbs/verify" + + resp := &ApproveRFBSReturnResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type ReceiveRFBSReturnParams struct { + // Return request identifier + ReturnId int64 `json:"return_id"` +} + +type ReceiveRFBSReturnResponse struct { + core.CommonResponse +} + +// Confirm receipt of a product for check +func (c Returns) ReceiveRFBSReturn(ctx context.Context, params *ReceiveRFBSReturnParams) (*ReceiveRFBSReturnResponse, error) { + url := "/v2/returns/rfbs/receive-return" + + resp := &ReceiveRFBSReturnResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type RefundRFBSParams struct { + // Return request identifier + ReturnId int64 `json:"return_id"` + + // Refund amount for shipping the product + ReturnForBackWay int64 `json:"return_for_back_way"` +} + +type RefundRFBSResponse struct { + core.CommonResponse +} + +// The method confirms the refund of the full product cost. +// Use the method if you agree to refund the customer: +// +// Immediately without receiving the product. +// After you received and checked the product. +// If the product is defective or damaged, you also refund its return shipment cost. +func (c Returns) RefundRFBS(ctx context.Context, params *RefundRFBSParams) (*RefundRFBSResponse, error) { + url := "/v2/returns/rfbs/return-money" + + resp := &RefundRFBSResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + 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 index a065c11..9b9c496 100644 --- a/ozon/returns_test.go +++ b/ozon/returns_test.go @@ -189,3 +189,417 @@ func TestGetFBSReturns(t *testing.T) { } } } + +func TestGetRFBSReturns(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *GetRFBSReturnsParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetRFBSReturnsParams{ + LastId: 999, + Limit: 555, + Filter: GetRFBSReturnsFilter{ + OfferId: "123", + PostingNumber: "111", + GroupState: []RFBSReturnsGroupState{RFBSReturnsGroupStateAll}, + CreatedAt: GetRFBSReturnsFilterCreatedAt{ + From: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"), + To: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"), + }, + }, + }, + `{ + "returns": { + "client_name": "string", + "created_at": "2019-08-24T14:15:22Z", + "order_number": "string", + "posting_number": "111", + "product": { + "name": "string", + "offer_id": "123", + "currency_code": "string", + "price": "string", + "sku": 123 + }, + "return_id": 0, + "return_number": "string", + "state": { + "group_state": "All", + "money_return_state_name": "string", + "state": "string", + "state_name": "string" + } + } + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &GetRFBSReturnsParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Returns().GetRFBSReturns(ctx, 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) + } + + if resp.StatusCode == http.StatusOK { + if resp.Returns.Product.OfferId != test.params.Filter.OfferId { + t.Errorf("expected offer ID %s, but got: %s", test.params.Filter.OfferId, resp.Returns.Product.OfferId) + } + if resp.Returns.PostingNumber != test.params.Filter.PostingNumber { + t.Errorf("expected posting number %s, but got: %s", test.params.Filter.PostingNumber, resp.Returns.PostingNumber) + } + if resp.Returns.State.GroupState != test.params.Filter.GroupState[0] { + t.Errorf("expected group state %s, but got: %s", test.params.Filter.GroupState[0], resp.Returns.State.GroupState) + } + } + } +} + +func TestGetRFBSReturn(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *GetRFBSReturnParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetRFBSReturnParams{ + ReturnId: 123, + }, + `{ + "returns": { + "available_actions": [ + { + "id": 0, + "name": "string" + } + ], + "client_name": "string", + "client_photo": [ + "string" + ], + "client_return_method_type": { + "id": 0, + "name": "string" + }, + "comment": "string", + "created_at": "2019-08-24T14:15:22Z", + "order_number": "string", + "posting_number": "string", + "product": { + "name": "string", + "offer_id": "string", + "currency_code": "string", + "price": "string", + "sku": 0 + }, + "rejection_comment": "string", + "rejection_reason": [ + { + "hint": "string", + "id": 0, + "is_comment_required": true, + "name": "string" + } + ], + "return_method_description": "string", + "return_number": "string", + "return_reason": { + "id": 0, + "is_defect": true, + "name": "string" + }, + "ru_post_tracking_number": "string", + "state": { + "state": "string", + "state_name": "string" + }, + "warehouse_id": 0 + } + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &GetRFBSReturnParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Returns().GetRFBSReturn(ctx, 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 TestRejectRFBSReturn(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *RejectRFBSReturnParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &RejectRFBSReturnParams{ + ReturnId: 123, + Comment: "No comment", + RejectionReasonId: 111, + }, + `{}`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &RejectRFBSReturnParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Returns().RejectRFBSReturn(ctx, 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 TestCompensateRFBSreturn(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *CompensateRFBSReturnParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &CompensateRFBSReturnParams{ + ReturnId: 123, + CompensationAmount: "11", + }, + `{}`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &CompensateRFBSReturnParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Returns().CompensateRFBSReturn(ctx, 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 TestApproveRFBSReturn(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *ApproveRFBSReturnParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &ApproveRFBSReturnParams{ + ReturnId: 123, + ReturnMethodDescription: "some description", + }, + `{}`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &ApproveRFBSReturnParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Returns().ApproveRFBSReturn(ctx, 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 TestReceiveRFBSReturn(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *ReceiveRFBSReturnParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &ReceiveRFBSReturnParams{ + ReturnId: 123, + }, + `{}`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &ReceiveRFBSReturnParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Returns().ReceiveRFBSReturn(ctx, 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 TestRefundRFBS(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *RefundRFBSParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &RefundRFBSParams{ + ReturnId: 123, + ReturnForBackWay: 111, + }, + `{}`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &RefundRFBSParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Returns().RefundRFBS(ctx, 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) + } + } +}