From c307bc31bd2f5ae3e59e4b1a0bd00991eb242474 Mon Sep 17 00:00:00 2001 From: diPhantxm Date: Sat, 18 Mar 2023 23:38:13 +0300 Subject: [PATCH] add some more additional endpoints for working with fbs --- ENDPOINTS.md | 12 +- ozon/fbs.go | 307 +++++++++++++++++++++++++++++++++++++--- ozon/fbs_test.go | 359 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 655 insertions(+), 23 deletions(-) diff --git a/ENDPOINTS.md b/ENDPOINTS.md index a454dc9..921520e 100644 --- a/ENDPOINTS.md +++ b/ENDPOINTS.md @@ -93,14 +93,14 @@ - [x] Shipments list (version 3) - [x] Get shipment details by identifier (version 3) - [x] Get shipment data by barcode -- [ ] List of manufacturing countries -- [ ] Set the manufacturing country +- [x] List of manufacturing countries +- [x] Set the manufacturing country - [ ] Specify number of boxes for multi-box shipments - [x] Get drop-off point restrictions -- [ ] Partial pack the order +- [x] 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 +- [x] Available freights list - [x] Get acceptance and transfer certificate and waybill - [ ] Generating status of digital acceptance and transfer certificate and waybill - [ ] Get digital shipment certificate @@ -123,8 +123,8 @@ - [x] Change the status to "Last Mile" - [x] Change the status to "Delivered" - [x] Change status to "Sent by seller" -- [ ] Dates available for delivery reschedule -- [ ] Reschedule shipment delivery date +- [x] Dates available for delivery reschedule +- [x] Reschedule shipment delivery date - [ ] ETGB customs declarations ## Returns diff --git a/ozon/fbs.go b/ozon/fbs.go index eba8bcc..10dfb91 100644 --- a/ozon/fbs.go +++ b/ozon/fbs.go @@ -369,7 +369,7 @@ type PackOrderResponse struct { // // the products do not fit in one package, // the products cannot be put in one package. -// Differs from /v2/posting/fbs/ship by the presence of the field exemplar_info in the request. +// Differs from /v2/posting/fbs/ship by the presence of the field `exemplar_info` in the request. // // 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 func (c FBS) PackOrder(params *PackOrderParams) (*PackOrderResponse, error) { @@ -424,22 +424,7 @@ type ValidateLabelingCodesResponse struct { Error string `json:"error"` // Product items data - Exemplars []struct { - // Product item validation errors - Errors []string `json:"errors"` - - // Сustoms cargo declaration (CCD) number - GTD string `json:"gtd"` - - // Mandatory “Chestny ZNAK” labeling - MandatoryMark string `json:"mandatory_mark"` - - // Check result. true if the labeling code of product item meets the requirements - Valid bool `json:"valid"` - - // Product batch registration number - RNPT string `json:"rnpt"` - } `json:"exemplars"` + Exemplars []FBSProductExemplar `json:"exemplars"` // Product identifier ProductId int64 `json:"product_id"` @@ -1451,3 +1436,291 @@ func (c FBS) GetProductItemsCheckStatuses(params *GetProductItemsCheckStatusesPa return resp, nil } + +type RescheduleShipmentDeliveryDateParams struct { + // New delivery date period + NewTimeslot RescheduleShipmentDeliveryDateTimeslot `json:"new_timeslot"` + + // Shipment number + PostingNumber string `json:"posting_number"` +} + +type RescheduleShipmentDeliveryDateTimeslot struct { + // Period start date + DeliveryDateBegin time.Time `json:"delivery_date_begin"` + + // Period end date + DeliveryDateEnd time.Time `json:"delivery_date_end"` +} + +type RescheduleShipmentDeliveryDateResponse struct { + core.CommonResponse + + // true, if the date was changed + Result bool `json:"result"` +} + +// You can change the delivery date of a shipment up to two times +func (c FBS) RescheduleShipmentDeliveryDate(params *RescheduleShipmentDeliveryDateParams) (*RescheduleShipmentDeliveryDateResponse, error) { + url := "/v1/posting/fbs/timeslot/set" + + resp := &RescheduleShipmentDeliveryDateResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type DateAvailableForDeliveryScheduleParams struct { + // Shipment number + PostingNumber string `json:"posting_number"` +} + +type DateAvailableForDeliveryScheduleResponse struct { + core.CommonResponse + + // Number of delivery date reschedules made + AvailableChangecount int64 `json:"available_change_count"` + + // Period of dates available for reschedule + DeliveryInterval struct { + // Period start date + Begin time.Time `json:"begin"` + + // Period end date + End time.Time `json:"end"` + } `json:"delivery_interval"` + + // Number of delivery date reschedules left + RemainingChangeCount int64 `json:"remaining_change_count"` +} + +// Method for getting the dates and number of times available for delivery reschedule +func (c FBS) DateAvailableForDeliverySchedule(params *DateAvailableForDeliveryScheduleParams) (*DateAvailableForDeliveryScheduleResponse, error) { + url := "/v1/posting/fbs/timeslot/change-restrictions" + + resp := &DateAvailableForDeliveryScheduleResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type ListManufacturingCountriesParams struct { + // Filtering by line + NameSearch string `json:"name_search"` +} + +type ListManufacturingCountriesResponse struct { + core.CommonResponse + + // List of manufacturing countries and their ISO codes + Result []struct { + // Country name in Russian + Name string `json:"name"` + + // Country ISO code + CountriISOCode string `json:"country_iso_code"` + } `json:"result"` +} + +// Method for getting a list of available manufacturing countries and their ISO codes +func (c FBS) ListManufacturingCountries(params *ListManufacturingCountriesParams) (*ListManufacturingCountriesResponse, error) { + url := "/v2/posting/fbs/product/country/list" + + resp := &ListManufacturingCountriesResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type SetManufacturingCountryParams struct { + // Shipment identifier + PostingNumber string `json:"posting_number"` + + // Product identifier + ProductId int64 `json:"product_id"` + + // Country ISO code from the `/v2/posting/fbs/product/country/list` method response + CountryISOCode string `json:"country_iso_code"` +} + +type SetManufacturingCountryResponse struct { + core.CommonResponse + + // Product identifier + ProductId int64 `json:"product_id"` + + // Indication that you need to pass the сustoms cargo declaration (CCD) number for the product and shipment + IsGTDNeeded bool `json:"is_gtd_needed"` +} + +// The method to set the manufacturing country to the product if it hasn't been specified +func (c FBS) SetManufacturingCountry(params *SetManufacturingCountryParams) (*SetManufacturingCountryResponse, error) { + url := "/v2/posting/fbs/product/country/set" + + resp := &SetManufacturingCountryResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type PartialPackOrderParams struct { + // Shipment ID + PostingNumber string `json:"posting_number"` + + // Array of products + Products []PartialPackOrderProduct `json:"products"` +} + +type PartialPackOrderProduct struct { + // Data array on product items + ExemplarInfo []FBSProductExemplar `json:"exemplar_info"` + + // FBS product identifier in the Ozon system, SKU + ProductId int64 `json:"product_id"` + + // Product quantity + Quantity int32 `json:"quantity"` +} + +type PartialPackOrderResponse struct { + core.CommonResponse + + // Additional data about shipments + AdditionalData []struct { + // Shipment identifier + PostingNumber string `json:"posting_number"` + + // List of products in the shipment + Products []PostingProduct `json:"products"` + } `json:"additional_data"` + + // Identifiers of shipments that were created after package + Result []string `json:"result"` +} + +// If you pass to the request a part of the products from the shipment, the primary shipment will split into two parts. +// The primary unassembled shipment will contain some of the products that were not passed to the request. +// +// The status of the original shipment will only change when the split shipments status changes +func (c FBS) PartialPackOrder(params *PartialPackOrderParams) (*PartialPackOrderResponse, error) { + url := "/v3/posting/fbs/ship/package" + + resp := &PartialPackOrderResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type AvailableFreightsListParams struct { + // Filter by delivery method identifier + DeliveryMethodId int64 `json:"delivery_method_id"` + + // Shipping date. The default value is current date + DepartureDate time.Time `json:"departure_date"` +} + +type AvailableFreightsListResponse struct { + core.CommonResponse + + // Method result + Result []struct{ + // Freight identifier (document generation task number) + CarriageId int64 `json:"carriage_id"` + + // Number of shipments in the freight + CarriagePostingsCount int32 `json:"carriage_postings_count"` + + // Freight status for requested delivery method and shipping date + CarriageStatus string `json:"carriage_status"` + + // Date and time before a shipment must be packaged + CutoffAt time.Time `json:"cutoff_at"` + + // Delivery method identifier + DeliveryMethodId int64 `json:"delivery_method_id"` + + // Delivery method name + DeliveryMethodName string `json:"delivery_method_name"` + + // Errors list + Errors []struct{ + // Error code + Code string `json:"code"` + + // Error type: + // - warning + // - critical + Status string `json:"status"` + } `json:"errors"` + + // First mile type + FirstMileType string `json:"first_mile_type"` + + // Trusted acceptance attribute. true if trusted acceptance is enabled in the warehouse + HasEntrustedAcceptance bool `json:"has_entrusted_acceptance"` + + // Number of shipments to be packaged + MandatoryPostingsCount int32 `json:"mandatory_postings_count"` + + // Number of already packaged shipments + MandatoryPackagedCount int32 `json:"mandatory_packaged_count"` + + // Delivery service icon link + TPLProviderIconURL string `json:"tpl_provider_icon_url"` + + // Delivery service name + TPLProviderName string `json:"tpl_provider_name"` + + // Warehouse city + WarehouseCity string `json:"warehouse_city"` + + // Warehouse identifier + WarehouseId int64 `json:"warehouse_id"` + + // Warehouse name + WarehouseName string `json:"warehouse_name"` + + // Warehouse timezone + WarehouseTimezone string `json:"warehouse_timezone"` + } `json:"result"` +} + +// Method for getting freights that require printing acceptance and transfer certificates and a waybill +func (c FBS) AvailableFreightsList(params *AvailableFreightsListParams) (*AvailableFreightsListResponse, error) { + url := "/v1/posting/carriage-available/list" + + resp := &AvailableFreightsListResponse{} + + 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 11b6ec8..cac26a9 100644 --- a/ozon/fbs_test.go +++ b/ozon/fbs_test.go @@ -1463,3 +1463,362 @@ func TestGetProductItemsCheckStatuses(t *testing.T) { } } } + +func TestRescheduleShipmentDeliveryDate(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *RescheduleShipmentDeliveryDateParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &RescheduleShipmentDeliveryDateParams{ + PostingNumber: "23281294-0063-2", + NewTimeslot: RescheduleShipmentDeliveryDateTimeslot{ + DeliveryDateBegin: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2023-03-03T11:07:00.381Z"), + DeliveryDateEnd: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2023-03-03T11:07:00.381Z"), + }, + }, + `{ + "result": true + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &RescheduleShipmentDeliveryDateParams{}, + `{ + "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().RescheduleShipmentDeliveryDate(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 TestDateAvailableForDeliverySchedule(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *DateAvailableForDeliveryScheduleParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &DateAvailableForDeliveryScheduleParams{ + PostingNumber: "23281294-0063-2", + }, + `{ + "available_change_count": 0, + "delivery_interval": { + "begin": "2019-08-24T14:15:22Z", + "end": "2019-08-24T14:15:22Z" + }, + "remaining_change_count": 0 + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &DateAvailableForDeliveryScheduleParams{}, + `{ + "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().DateAvailableForDeliverySchedule(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 TestListManufactoruingCountries(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *ListManufacturingCountriesParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &ListManufacturingCountriesParams{ + NameSearch: "some name", + }, + `{ + "result": [ + { + "name": "Алжир", + "country_iso_code": "DZ" + }, + { + "name": "Ангилья", + "country_iso_code": "AI" + }, + { + "name": "Виргинские Острова (Великобритания)", + "country_iso_code": "VG" + } + ] + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &ListManufacturingCountriesParams{}, + `{ + "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().ListManufacturingCountries(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 len(resp.Result) > 0 { + if resp.Result[0].Name == "" { + t.Errorf("Name cannot be empty") + } + if resp.Result[0].CountriISOCode == "" { + t.Errorf("ISO code cannot be empty") + } + } + } + } +} + +func TestSetManufacturingCountry(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *SetManufacturingCountryParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &SetManufacturingCountryParams{ + PostingNumber: "57195475-0050-3", + ProductId: 180550365, + CountryISOCode: "NO", + }, + `{ + "product_id": 180550365, + "is_gtd_needed": true + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &SetManufacturingCountryParams{}, + `{ + "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().SetManufacturingCountry(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.ProductId != test.params.ProductId { + t.Errorf("Product ids in request and response are not equal") + } + } + } +} + +func TestPartialPackOrder(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *PartialPackOrderParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &PartialPackOrderParams{ + PostingNumber: "48173252-0034-4", + Products: []PartialPackOrderProduct{ + { + ExemplarInfo: []FBSProductExemplar{ + { + MandatoryMark: "mark", + GTD: "gtd", + IsGTDAbsest: true, + }, + }, + ProductId: 247508873, + Quantity: 1, + }, + }, + }, + `{ + "result": [ + "48173252-0034-9" + ] + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &PartialPackOrderParams{}, + `{ + "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().PartialPackOrder(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 TestAvailableFreightsList(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *AvailableFreightsListParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &AvailableFreightsListParams{ + DeliveryMethodId: 0, + DepartureDate: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"), + }, + `{ + "result": [ + { + "carriage_id": 0, + "carriage_postings_count": 0, + "carriage_status": "string", + "cutoff_at": "2019-08-24T14:15:22Z", + "delivery_method_id": 0, + "delivery_method_name": "string", + "errors": [ + { + "code": "string", + "status": "string" + } + ], + "first_mile_type": "string", + "has_entrusted_acceptance": true, + "mandatory_postings_count": 0, + "mandatory_packaged_count": 0, + "tpl_provider_icon_url": "string", + "tpl_provider_name": "string", + "warehouse_city": "string", + "warehouse_id": 0, + "warehouse_name": "string", + "warehouse_timezone": "string" + } + ] + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &AvailableFreightsListParams{}, + `{ + "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().AvailableFreightsList(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) + } + } +}