From 4c4427f26cc5372dcd04d30df70a9e7c87211fd1 Mon Sep 17 00:00:00 2001 From: diPhantxm Date: Mon, 13 Mar 2023 21:00:28 +0300 Subject: [PATCH] add get shipments lists methods for fbs and fbo --- ozon/fbo.go | 166 ++++++++++++++++++++++++++++++++++++++++ ozon/fbo_test.go | 132 ++++++++++++++++++++++++++++++++ ozon/fbs.go | 191 ++++++++++++++++++++++++++++++++++++----------- ozon/fbs_test.go | 101 ++++++++++++++++++++++++- 4 files changed, 545 insertions(+), 45 deletions(-) create mode 100644 ozon/fbo.go create mode 100644 ozon/fbo_test.go diff --git a/ozon/fbo.go b/ozon/fbo.go new file mode 100644 index 0000000..c173a96 --- /dev/null +++ b/ozon/fbo.go @@ -0,0 +1,166 @@ +package ozon + +import ( + "net/http" + "time" + + core "github.com/diphantxm/ozon-api-client" +) + +type GetFBOShipmentsListParams struct { + // Sorting direction + Direction string `json:"dir"` + + // Shipment search filter + Filter GetFBOShipmentsListFilter `json:"filter"` + + // Number of values in the response. Maximum is 1000, minimum is 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"` + + // true if the address transliteration from Cyrillic to Latin is enabled + Translit bool `json:"translit"` + + // Additional fields to add to the response + With GetFBOShipmentsListWith `json:"with"` +} + +// Shipment search filter +type GetFBOShipmentsListFilter struct { + // Period start in YYYY-MM-DD format + Since time.Time `json:"since"` + + // Shipment status + Status string `json:"status"` + + // Period end in YYYY-MM-DD format + To time.Time `json:"to"` +} + +// Additional fields to add to the response +type GetFBOShipmentsListWith struct { + // Specify true to add analytics data to the response + AnalyticsData bool `json:"analytics_data"` + + // Specify true to add financial data to the response + FinancialData bool `json:"financial_data"` +} + +type GetFBOShipmentsListResponse struct { + core.CommonResponse + + // Shipments list + Result []struct { + // Additional data for shipment list + AdditionalData []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"additional_data"` + + // Analytical data + AnalyticsData struct { + // Delivery city + City string `json:"city"` + + // Delivery method + DeliveryType string `json:"delivery_type"` + + // Indication that the recipient is a legal person + // * true — a legal person, + // * false — a natural person. + IsLegal bool `json:"is_legal"` + + // Premium subscription + IsPremium bool `json:"is_premium"` + + // Payment method + PaymentTypeGroupName string `json:"payment_type_group_name"` + + // Delivery region + Region string `json:"region"` + + // Warehouse identifier + WarehouseId int64 `json:"warehouse_id"` + + // Name of the warehouse from which the order is shipped + WarehouseName string `json:"warehouse_name"` + } `json:"analytics_data"` + + // Shipment cancellation reason identifier + CancelReasonId int64 `json:"cancel_reason_id"` + + // Date and time of shipment creation + CreatedAt time.Time `json:"created_at"` + + // Financial data + FinancialData struct { + // Identifier of the cluster, where the shipment is sent from + ClusterFrom string `json:"cluster_from"` + + // Identifier of the cluster, where the shipment is delivered to + ClusterTo string `json:"cluster_to"` + + // Services + PostingServices MarketplaceServices `json:"posting_services"` + + // Products list + Products []FinancialDataProduct `json:"products"` + } `json:"financial_data"` + + // Date and time of shipment processing start + InProccessAt time.Time `json:"in_process_at"` + + // Identifier of the order to which the shipment belongs + OrderId int64 `json:"order_id"` + + // Number of the order to which the shipment belongs + OrderNumber string `json:"order_number"` + + // Shipment number + PostingNumber string `json:"posting_number"` + + // Number of products in the shipment + Products []struct { + // Activation codes for services and digital products + DigitalCodes []string `json:"digital_codes"` + + // Currency of your prices. It matches the currency set in the personal account settings + CurrencyCode string `json:"currency_code"` + + // Product name + Name string `json:"name"` + + // Product identifier in the seller's system + OfferId string `json:"offer_id"` + + // Product price + Price string `json:"price"` + + // Quantity of products in the shipment + Quantity int64 `json:"quantity"` + + // Product identifier in the Ozon system, SKU + SKU int64 `json:"sku"` + } `json:"products"` + + // Shipment status + Status string `json:"status"` + } `json:"result"` +} + +// Returns a list of shipments for a specified period of time. You can additionally filter the shipments by their status +func (c Client) GetFBOShipmentsList(params *GetFBOShipmentsListParams) (*GetFBOShipmentsListResponse, error) { + url := "/v1/product/import/prices" + + resp := &GetFBOShipmentsListResponse{} + + 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/fbo_test.go b/ozon/fbo_test.go new file mode 100644 index 0000000..f5a4f47 --- /dev/null +++ b/ozon/fbo_test.go @@ -0,0 +1,132 @@ +package ozon + +import ( + "net/http" + "testing" + + core "github.com/diphantxm/ozon-api-client" +) + +func TestGetFBOShipmentsList(t *testing.T) { + tests := []struct { + statusCode int + headers map[string]string + params *GetFBOShipmentsListParams + response string + }{ + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetFBOShipmentsListParams{ + Direction: "ASC", + Filter: GetFBOShipmentsListFilter{ + Since: core.TimeFromString(t, "2021-09-01T00:00:00.000Z"), + Status: "awaiting_packaging", + To: core.TimeFromString(t, "2021-11-17T10:44:12.828Z"), + }, + Limit: 5, + Offset: 0, + Translit: true, + With: GetFBOShipmentsListWith{ + AnalyticsData: true, + FinancialData: true, + }, + }, + `{ + "result": [ + { + "order_id": 354680487, + "order_number": "16965409-0014", + "posting_number": "16965409-0014-1", + "status": "delivered", + "cancel_reason_id": 0, + "created_at": "2021-09-01T00:23:45.607Z", + "in_process_at": "2021-09-01T00:25:30.120Z", + "products": [ + { + "sku": 160249683, + "name": "Так говорил Омар Хайям. Жизнеописание. Афоризмы и рубайят. Классика в словах и картинках", + "quantity": 1, + "offer_id": "978-5-906864-56-7", + "price": "81.00", + "digital_codes": [], + "currency_code": "RUB" + } + ], + "analytics_data": { + "region": "РОСТОВСКАЯ ОБЛАСТЬ", + "city": "Ростов-на-Дону", + "delivery_type": "PVZ", + "is_premium": false, + "payment_type_group_name": "Карты оплаты", + "warehouse_id": 17717042026000, + "warehouse_name": "РОСТОВ-НА-ДОНУ_РФЦ", + "is_legal": false + }, + "financial_data": { + "products": [ + { + "commission_amount": 12.15, + "commission_percent": 15, + "payout": 68.85, + "product_id": 160249683, + "currency_code": "RUB", + "old_price": 115, + "price": 81, + "total_discount_value": 34, + "total_discount_percent": 29.57, + "actions": [ + "Системная виртуальная скидка селлера" + ], + "picking": null, + "quantity": 0, + "client_price": "", + "item_services": { + "marketplace_service_item_fulfillment": -31.5, + "marketplace_service_item_pickup": 0, + "marketplace_service_item_dropoff_pvz": 0, + "marketplace_service_item_dropoff_sc": 0, + "marketplace_service_item_dropoff_ff": 0, + "marketplace_service_item_direct_flow_trans": -5, + "marketplace_service_item_return_flow_trans": 0, + "marketplace_service_item_deliv_to_customer": -20, + "marketplace_service_item_return_not_deliv_to_customer": 0, + "marketplace_service_item_return_part_goods_customer": 0, + "marketplace_service_item_return_after_deliv_to_customer": 0 + } + } + ], + "posting_services": { + "marketplace_service_item_fulfillment": 0, + "marketplace_service_item_pickup": 0, + "marketplace_service_item_dropoff_pvz": 0, + "marketplace_service_item_dropoff_sc": 0, + "marketplace_service_item_dropoff_ff": 0, + "marketplace_service_item_direct_flow_trans": 0, + "marketplace_service_item_return_flow_trans": 0, + "marketplace_service_item_deliv_to_customer": 0, + "marketplace_service_item_return_not_deliv_to_customer": 0, + "marketplace_service_item_return_part_goods_customer": 0, + "marketplace_service_item_return_after_deliv_to_customer": 0 + } + }, + "additional_data": [] + } + ] + }`, + }, + } + + for _, test := range tests { + c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers)) + + resp, err := c.GetFBOShipmentsList(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) + } + } +} diff --git a/ozon/fbs.go b/ozon/fbs.go index db343cb..ad1ae72 100644 --- a/ozon/fbs.go +++ b/ozon/fbs.go @@ -40,11 +40,11 @@ type ListUnprocessedShipmentsResponse struct { } type ListUnprocessedShipmentsResult struct { - Count int64 `json:"count"` - Postings []ListUnprocessedShipmentsPosting `json:"postings"` + Count int64 `json:"count"` + Postings []FBSPosting `json:"postings"` } -type ListUnprocessedShipmentsPosting struct { +type FBSPosting struct { Addressee struct { Name string `json:"name"` Phone string `json:"phone"` @@ -79,26 +79,7 @@ type ListUnprocessedShipmentsPosting struct { CancelledAfterShip bool `json:"cancellation_after_ship"` } `json:"cancellation"` - Customer struct { - Address struct { - AddressTail string `json:"address_tail"` - City string `json:"city"` - Comment string `json:"comment"` - Country string `json:"country"` - District string `json:"district"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - ProviderPVZCode string `json:"provider_pvz_code"` - PVZCode int64 `json:"pvz_code"` - Region string `json:"region"` - ZIPCode string `json:"zip_code"` - } `json:"customer"` - - CustomerEmail string `json:"customer_email"` - CustomerId int64 `json:"customer_id"` - Name string `json:"name"` - Phone string `json:"phone"` - } `json:"customer"` + Customer FBSCustomer `json:"customer"` DeliveringDate time.Time `json:"delivering_date"` @@ -116,27 +97,7 @@ type ListUnprocessedShipmentsPosting struct { ClusterTo string `json:"cluster_to"` PostingServices MarketplaceServices `json:"posting_services"` - Products []struct { - Actions []string `json:"actions"` - ClientPrice string `json:"client_price"` - CommissionAmount float64 `json:"commission_amount"` - CommissionPercent int64 `json:"commission_percent"` - CommissionsCurrencyCode string `json:"commissions_currency_code"` - ItemServices MarketplaceServices `json:"item_services"` - CurrencyCode string `json:"currency_code"` - OldPrice float64 `json:"old_price"` - Payout float64 `json:"payout"` - Picking struct { - Amount float64 `json:"amount"` - Moment time.Time `json:"moment"` - Tag string `json:"tag"` - } `json:"picking"` - Price float64 `json:"price"` - ProductId int64 `json:"product_id"` - Quantity int64 `json:"quantity"` - TotalDiscountPercent float64 `json:"total_discount_percent"` - TotalDiscountValue float64 `json:"total_discount_value"` - } `json:"products"` + Products []FinancialDataProduct `json:"products"` } InProccessAt time.Time `json:"in_process_at"` @@ -171,6 +132,27 @@ type ListUnprocessedShipmentsPosting struct { TrackingNumber string `json:"tracking_number"` } +type FBSCustomer struct { + Address struct { + AddressTail string `json:"address_tail"` + City string `json:"city"` + Comment string `json:"comment"` + Country string `json:"country"` + District string `json:"district"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + ProviderPVZCode string `json:"provider_pvz_code"` + PVZCode int64 `json:"pvz_code"` + Region string `json:"region"` + ZIPCode string `json:"zip_code"` + } `json:"customer"` + + CustomerEmail string `json:"customer_email"` + CustomerId int64 `json:"customer_id"` + Name string `json:"name"` + Phone string `json:"phone"` +} + type MarketplaceServices struct { DeliveryToCustomer float64 `json:"marketplace_service_item_deliv_to_customer"` DirectFlowTrans float64 `json:"marketplace_service_item_direct_flow_trans"` @@ -185,6 +167,28 @@ type MarketplaceServices struct { ReturnPartGoodsCustomer float64 `json:"marketplace_service_item_return_part_goods_customer"` } +type FinancialDataProduct struct { + Actions []string `json:"actions"` + ClientPrice string `json:"client_price"` + CommissionAmount float64 `json:"commission_amount"` + CommissionPercent int64 `json:"commission_percent"` + CommissionsCurrencyCode string `json:"commissions_currency_code"` + ItemServices MarketplaceServices `json:"item_services"` + CurrencyCode string `json:"currency_code"` + OldPrice float64 `json:"old_price"` + Payout float64 `json:"payout"` + Picking struct { + Amount float64 `json:"amount"` + Moment time.Time `json:"moment"` + Tag string `json:"tag"` + } `json:"picking"` + Price float64 `json:"price"` + ProductId int64 `json:"product_id"` + Quantity int64 `json:"quantity"` + TotalDiscountPercent float64 `json:"total_discount_percent"` + TotalDiscountValue float64 `json:"total_discount_value"` +} + func (c Client) ListUnprocessedShipments(params *ListUnprocessedShipmentsParams) (*ListUnprocessedShipmentsResponse, error) { url := "/v3/posting/fbs/unfulfilled/list" @@ -198,3 +202,102 @@ func (c Client) ListUnprocessedShipments(params *ListUnprocessedShipmentsParams) return resp, nil } + +type GetFBSShipmentsListParams struct { + // Sorting direction + Direction string `json:"direction"` + + //Filter + Filter GetFBSShipmentsListFilter `json:"filter"` + + // Number of shipments in the response: + // - maximum is 50, + // - minimum is 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"` + + // Additional fields that should be added to the response + With GetFBSShipmentsListWith `json:"with"` +} + +type GetFBSShipmentsListFilter struct { + // Delivery method identifier + DeliveryMethodId []int64 `json:"delivery_method_id"` + + // Order identifier + OrderId int64 `json:"order_id"` + + // Delivery service identifier + ProviderId []int64 `json:"provider_id"` + + // Start date of the period for which a list of shipments should be generated. + // + // Format: YYYYY-MM-DDTHH:MM:SSZ. + // + // Example: 2019-08-24T14:15:22Z + Since time.Time `json:"since"` + + // End date of the period for which a list of shipments should be generated. + // + // Format: YYYYY-MM-DDTHH:MM:SSZ. + // + // Example: 2019-08-24T14:15:22Z. + To time.Time `json:"to"` + + // Shipment status + Status string `json:"status"` + + // Warehouse identifier + WarehouseId []int64 `json:"warehouse_id"` +} + +type GetFBSShipmentsListWith struct { + // Add analytics data to the response + AnalyticsData bool `json:"analytics_data"` + + // Add the shipment barcodes to the response + Barcodes bool `json:"barcodes"` + + // Add financial data to the response + FinancialData bool `json:"financial_data"` + + // Transliterate the return values + Translit bool `json:"translit"` +} + +type GetFBSShipmentsListResponse struct { + core.CommonResponse + + // Array of shipments + Result struct { + // Indicates that the response returned not the entire array of shipments: + // + // - true — it is necessary to make a new request with a different offset value to get information on the remaining shipments; + // - false — the entire array of shipments for the filter specified in the request was returned in the response + HasNext bool `json:"has_next"` + + // Shipment details + Postings FBSPosting `json:"postings"` + } `json:"result"` +} + +// Returns a list of shipments for the specified time period: it shouldn't be longer than one year. +// +// You can filter shipments by their status. The list of available statuses is specified in the description of the filter.status parameter. +// +// The true value of the has_next parameter in the response means there is not the entire array of shipments in the response. To get information on the remaining shipments, make a new request with a different offset value. +func (c Client) GetFBSShipmentsList(params *GetFBSShipmentsListParams) (*GetFBSShipmentsListResponse, error) { + url := "/v3/posting/fbs/list" + + resp := &GetFBSShipmentsListResponse{} + + 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 7a8431b..483670d 100644 --- a/ozon/fbs_test.go +++ b/ozon/fbs_test.go @@ -1,6 +1,7 @@ package ozon import ( + "net/http" "testing" core "github.com/diphantxm/ozon-api-client" @@ -14,7 +15,7 @@ func TestListUnprocessedShipments(t *testing.T) { response string }{ { - 200, + http.StatusOK, map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, &ListUnprocessedShipmentsParams{ Direction: "ASC", @@ -163,3 +164,101 @@ func TestListUnprocessedShipments(t *testing.T) { } } } + +func TestGetFBSShipmentsList(t *testing.T) { + tests := []struct { + statusCode int + headers map[string]string + params *GetFBSShipmentsListParams + response string + }{ + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetFBSShipmentsListParams{ + Direction: "ASC", + Filter: GetFBSShipmentsListFilter{ + Since: core.TimeFromString(t, "2021-11-01T00:00:00.000Z"), + To: core.TimeFromString(t, "2021-12-01T23:59:59.000Z"), + Status: "awaiting_packaging", + }, + Limit: 100, + Offset: 0, + With: GetFBSShipmentsListWith{ + AnalyticsData: true, + FinancialData: true, + Translit: true, + }, + }, + `{ + "result": { + "postings": [ + { + "posting_number": "05708065-0029-1", + "order_id": 680420041, + "order_number": "05708065-0029", + "status": "awaiting_deliver", + "delivery_method": { + "id": 21321684811000, + "name": "Ozon Логистика самостоятельно, Красногорск", + "warehouse_id": 21321684811000, + "warehouse": "Стим Тойс Нахабино", + "tpl_provider_id": 24, + "tpl_provider": "Ozon Логистика" + }, + "tracking_number": "", + "tpl_integration_type": "ozon", + "in_process_at": "2022-05-13T07:07:32Z", + "shipment_date": "2022-05-13T10:00:00Z", + "delivering_date": null, + "cancellation": { + "cancel_reason_id": 0, + "cancel_reason": "", + "cancellation_type": "", + "cancelled_after_ship": false, + "affect_cancellation_rating": false, + "cancellation_initiator": "" + }, + "customer": null, + "products": [ + { + "currency_code": "RUB", + "price": "1390.000000", + "offer_id": "205953", + "name": " Электронный конструктор PinLab Позитроник", + "sku": 358924380, + "quantity": 1, + "mandatory_mark": [] + } + ], + "addressee": null, + "barcodes": null, + "analytics_data": null, + "financial_data": null, + "is_express": false, + "requirements": { + "products_requiring_gtd": [], + "products_requiring_country": [], + "products_requiring_mandatory_mark": [] + } + } + ], + "has_next": true + } + }`, + }, + } + + for _, test := range tests { + c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers)) + + resp, err := c.GetFBSShipmentsList(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) + } + } +}