From f108c846b06daf11e3228b6b2c4775e053a264ee Mon Sep 17 00:00:00 2001 From: diPhantxm Date: Sat, 18 Mar 2023 20:34:04 +0300 Subject: [PATCH] add methods for labeling and product check statuses --- ENDPOINTS.md | 12 +-- ozon/fbs.go | 205 +++++++++++++++++++++++++++++++++++--- ozon/fbs_test.go | 249 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 444 insertions(+), 22 deletions(-) diff --git a/ENDPOINTS.md b/ENDPOINTS.md index 96d0ede..a454dc9 100644 --- a/ENDPOINTS.md +++ b/ENDPOINTS.md @@ -83,9 +83,9 @@ - [ ] Shipment details ## FBS and rFBS products labeling -- [ ] Validate labeling codes -- [ ] Check and save product items data -- [ ] Get product items check statuses +- [x] Validate labeling codes +- [x] Check and save product items data +- [x] Get product items check statuses - [x] Pack the order (version 4) ## FBS and rFBS @@ -96,16 +96,16 @@ - [ ] List of manufacturing countries - [ ] Set the manufacturing country - [ ] Specify number of boxes for multi-box shipments -- [ ] Get drop-off point restrictions +- [x] Get drop-off point restrictions - [ ] Partial pack the order - [x] Create an acceptance and transfer certificate and a waybill - [ ] Status of acceptance and transfer certificate and waybill - [ ] Available freights list -- [ ] Get acceptance and transfer certificate and waybill +- [x] Get acceptance and transfer certificate and waybill - [ ] Generating status of digital acceptance and transfer certificate and waybill - [ ] Get digital shipment certificate - [x] Print the labeling -- [ ] Create a task to generate labeling +- [x] Create a task to generate labeling - [x] Get a labeling file - [ ] Package unit labels - [ ] Open a dispute over a shipment diff --git a/ozon/fbs.go b/ozon/fbs.go index 0b47501..eba8bcc 100644 --- a/ozon/fbs.go +++ b/ozon/fbs.go @@ -728,22 +728,7 @@ type GetShipmentDataByIdentifierResponse struct { SKU int64 `json:"sku"` // Array of exemplars - Exemplars []struct { - // Mandatory “Chestny ZNAK” labeling - MandatoryMark string `json:"mandatory_mark"` - - // Сustoms cargo declaration (CCD) number - GTD string `json:"gtd"` - - // Indication that a сustoms cargo declaration (CCD) number hasn't been specified - IsGTDAbsest bool `json:"is_gtd_absent"` - - // Product batch registration number - RNPT string `json:"rnpt"` - - // Indication that a product batch registration number hasn't been specified - IsRNPTAbsent bool `json:"is_rnpt_absent"` - } `json:"exemplars"` + Exemplars []FBSProductExemplar `json:"exemplars"` } `json:"products"` } `json:"product_exemplars"` @@ -798,6 +783,23 @@ type GetShipmentDataByIdentifierResponse struct { } `json:"result"` } +type FBSProductExemplar struct { + // Mandatory “Chestny ZNAK” labeling + MandatoryMark string `json:"mandatory_mark"` + + // Сustoms cargo declaration (CCD) number + GTD string `json:"gtd"` + + // Indication that a сustoms cargo declaration (CCD) number hasn't been specified + IsGTDAbsest bool `json:"is_gtd_absent"` + + // Product batch registration number + RNPT string `json:"rnpt"` + + // Indication that a product batch registration number hasn't been specified + IsRNPTAbsent bool `json:"is_rnpt_absent"` +} + // Method for getting shipment details by identifier func (c FBS) GetShipmentDataByIdentifier(params *GetShipmentDataByIdentifierParams) (*GetShipmentDataByIdentifierResponse, error) { url := "/v3/posting/fbs/get" @@ -1278,3 +1280,174 @@ func (c FBS) PrintLabeling(params *PrintLabelingParams) (*PrintLabelingResponse, return resp, nil } + +type CreateTaskForGeneratingLabelParams struct { + // Numbers of shipments that need labeling + PostingNumber []string `json:"posting_number"` +} + +type CreateTaskForGeneratingLabelResponse struct { + core.CommonResponse + + // Method result + Result struct { + // Task identifier for labeling generation + TaskId int64 `json:"task_id"` + } `json:"result"` +} + +// Method for creating a task for asynchronous labeling generation. +// +// To get labels created as a result of the method, use the /v1/posting/fbs/package-label/get method +func (c FBS) CreateTaskForGeneratingLabel(params *CreateTaskForGeneratingLabelParams) (*CreateTaskForGeneratingLabelResponse, error) { + url := "/v2/posting/fbs/package-label" + + resp := &CreateTaskForGeneratingLabelResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type GetDropOffPointRestrictionsParams struct { + // The number of shipment for which you want to determine the restrictions + PostingNumber string `json:"posting_number"` +} + +type GetDropOffPointRestrictionsResponse struct { + core.CommonResponse + + // Method result + Result struct { + // Shipment number + PostingNumber string `json:"posting_number"` + + // Maximum weight limit in grams + MaxPostingWeight float64 `json:"max_posting_weight"` + + // Minimum weight limit in grams + MinPostingWeight float64 `json:"min_posting_weight"` + + // Width limit in centimeters + Width float64 `json:"width"` + + // Length limit in centimeters + Length float64 `json:"length"` + + // Height limit in centimeters + Height float64 `json:"height"` + + // Maximum shipment cost limit in rubles + MaxPostingPrice float64 `json:"max_posting_price"` + + // Minimum shipment cost limit in rubles + MinPostingPrice float64 `json:"min_posting_price"` + } `json:"result"` +} + +// Method for getting dimensions, weight, and other restrictions of the drop-off point by the shipment number. +// The method is applicable only for the FBS scheme +func (c FBS) GetDropOffPointRestrictions(params *GetDropOffPointRestrictionsParams) (*GetDropOffPointRestrictionsResponse, error) { + url := "/v1/posting/fbs/restrictions" + + resp := &GetDropOffPointRestrictionsResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type CheckProductItemsDataParams struct { + // Shipment number + PostingNumber string `json:"posting_number"` + + Products CheckProductItemsDataProduct `json:"products"` +} + +type CheckProductItemsDataProduct struct { + // Product items data + Exemplars []FBSProductExemplar `json:"exemplars"` + + // SKU, FBS product identifier in the Ozon system + ProductId int64 `json:"product_id"` +} + +type CheckProductItemsDataResponse struct { + core.CommonResponse + + // Method result. true if the request was processed successfully + Result bool `json:"result"` +} + +// Asynchronous method: +// - for checking the availability of product items in the “Chestny ZNAK” labeling system +// - for saving product items data +// To get the checks results, use the `/v4/fbs/posting/product/exemplar/status method` +// +// If necessary, specify the number of the cargo customs declaration in the gtd parameter. If it is missing, pass the value `is_gtd_absent` = true +// +// If you have multiple identical products in a shipment, specify one `product_id` and `exemplars` array for each product in the shipment +// +// Always pass a complete set of product items data +// +// For example, you have 10 product items in your system. +// You have passed them for checking and saving. Then they added another 60 product items to your system. +// When you pass product items for checking and saving again, pass all of them: both old and newly added +func (c FBS) CheckproductItemsData(params *CheckProductItemsDataParams) (*CheckProductItemsDataResponse, error) { + url := "/v4/fbs/posting/product/exemplar/set" + + resp := &CheckProductItemsDataResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type GetProductItemsCheckStatusesParams struct { + // Shipment number + PostingNumber string `json:"posting_number"` +} + +type GetProductItemsCheckStatusesResponse struct { + core.CommonResponse + + // Shipment number + PostingNumber string `json:"posting_number"` + + // Products list + Products []CheckProductItemsDataProduct `json:"products"` + + // Product items check statuses and order collection availability: + // - ship_available — order collection is available, + // - ship_not_available — order collection is unavailable, + // - validation_in_process — product items validation is in progress + Status string `json:"status"` +} + +// Method for getting check statuses of product items that were passed in the `/fbs/posting/product/exemplar/set` method. +// Also returns data on these product items. +func (c FBS) GetProductItemsCheckStatuses(params *GetProductItemsCheckStatusesParams) (*GetProductItemsCheckStatusesResponse, error) { + url := "/v4/fbs/posting/product/exemplar/status" + + resp := &GetProductItemsCheckStatusesResponse{} + + 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/fbs_test.go b/ozon/fbs_test.go index 5abe867..11b6ec8 100644 --- a/ozon/fbs_test.go +++ b/ozon/fbs_test.go @@ -1214,3 +1214,252 @@ func TestPrintLabeling(t *testing.T) { } } } + +func TestCreateTaskForGeneratingLabel(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *CreateTaskForGeneratingLabelParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &CreateTaskForGeneratingLabelParams{ + PostingNumber: []string{"48173252-0034-4"}, + }, + `{ + "result": { + "task_id": 5819327210249 + } + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &CreateTaskForGeneratingLabelParams{}, + `{ + "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.FBS().CreateTaskForGeneratingLabel(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.Result.TaskId == 0 { + t.Errorf("Task id cannot be 0") + } + } + } +} + +func TestGetDropOffPointRestrictions(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *GetDropOffPointRestrictionsParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetDropOffPointRestrictionsParams{ + PostingNumber: "48173252-0034-4", + }, + `{ + "result": { + "posting_number": "48173252-0034-4", + "max_posting_weight": 0, + "min_posting_weight": 0, + "width": 0, + "length": 0, + "height": 0, + "max_posting_price": 0, + "min_posting_price": 0 + } + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &GetDropOffPointRestrictionsParams{}, + `{ + "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.FBS().GetDropOffPointRestrictions(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.Result.PostingNumber != test.params.PostingNumber { + t.Errorf("Posting numbers in request and response are not equal") + } + } + } +} + +func TestCheckProductItemsData(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *CheckProductItemsDataParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &CheckProductItemsDataParams{ + PostingNumber: "48173252-0034-4", + Products: CheckProductItemsDataProduct{ + Exemplars: []FBSProductExemplar{ + { + IsGTDAbsest: true, + MandatoryMark: "010290000151642731tVMohkbfFgunB", + }, + }, + ProductId: 476925391, + }, + }, + `{ + "result": true + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &CheckProductItemsDataParams{}, + `{ + "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.FBS().CheckproductItemsData(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 TestGetProductItemsCheckStatuses(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *GetProductItemsCheckStatusesParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetProductItemsCheckStatusesParams{ + PostingNumber: "23281294-0063-2", + }, + `{ + "posting_number": "23281294-0063-2", + "products": [ + { + "product_id": 476925391, + "exemplars": [ + { + "mandatory_mark": "010290000151642731tVMohkbfFgunB", + "gtd": "", + "is_gtd_absent": true, + "mandatory_mark_check_status": "passed", + "mandatory_mark_error_codes": [], + "gtd_check_status": "passed", + "gtd_error_codes": [] + } + ] + } + ], + "status": "ship_available" + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &GetProductItemsCheckStatusesParams{}, + `{ + "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.FBS().GetProductItemsCheckStatuses(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.PostingNumber != test.params.PostingNumber { + t.Errorf("Posting numbers in request and response are not equal") + } + if resp.Status == "" { + t.Errorf("Status cannot be empty") + } + if len(resp.Products) > 0 { + if resp.Products[0].ProductId == 0 { + t.Errorf("Product id cannot be 0") + } + if len(resp.Products[0].Exemplars) > 0 { + if resp.Products[0].Exemplars[0].MandatoryMark == "" { + t.Errorf("Mandatory mark cannot be empty") + } + } + } + } + } +}