From d1565b9d00a914ed0f065610f6151b34d62273e7 Mon Sep 17 00:00:00 2001 From: diPhantxm Date: Mon, 13 Mar 2023 23:29:39 +0300 Subject: [PATCH] add analytics methods --- core.go | 4 +- ozon/analytics.go | 167 +++++++++++++++++++++++++++++++++++++++++ ozon/analytics_test.go | 105 ++++++++++++++++++++++++++ ozon/fbo_test.go | 4 +- ozon/fbs_test.go | 8 +- 5 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 ozon/analytics.go create mode 100644 ozon/analytics_test.go diff --git a/core.go b/core.go index 07956db..02e808d 100644 --- a/core.go +++ b/core.go @@ -102,8 +102,8 @@ func isZero(v interface{}) (bool, error) { return v == reflect.Zero(t).Interface(), nil } -func TimeFromString(t *testing.T, datetime string) time.Time { - dt, err := time.Parse("2006-01-02T15:04:05Z", datetime) +func TimeFromString(t *testing.T, format, datetime string) time.Time { + dt, err := time.Parse(format, datetime) if err != nil { t.Errorf("error when parsing time: %s", err) } diff --git a/ozon/analytics.go b/ozon/analytics.go new file mode 100644 index 0000000..2915bdb --- /dev/null +++ b/ozon/analytics.go @@ -0,0 +1,167 @@ +package ozon + +import ( + "net/http" + "time" + + core "github.com/diphantxm/ozon-api-client" +) + +type GetAnalyticsDataParams struct { + // Date from which the data will be in the report + DateFrom time.Time `json:"date_from"` + + // Date up to which the data will be in the report + DateTo time.Time `json:"date_to"` + + // Items Enum: "unknownDimension" "sku" "spu" "day" "week" "month" "year" "category1" "category2" "category3" "category4" "brand" "modelID" + Dimension []string `json:"dimension"` + + // Filters + Filters []struct { + // Sorting parameter. You can pass any attribute from the `dimension` and `metric` parameters except the `brand` attribute + Key string `json:"key"` + + // Comparison operation + // + // Enum: "EQ" "GT" "GTE" "LT" "LTE" + Operation string `json:"operation"` + + // Value for comparison + Value string `json:"value"` + } `json:"filters"` + + // Number of items in the respones: + // - maximum is 1000, + // - minimum is 1. + Limit int64 `json:"limit"` + + // Specify up to 14 metrics. If there are more, you will get an error with the InvalidArgument code + // + // Items Enum: "unknown_metric" "hits_view_search" "hits_view_pdp" "hits_view" "hits_tocart_search" "hits_tocart_pdp" "hits_tocart" "session_view_search" + // "session_view_pdp" "session_view" "conv_tocart_search" "conv_tocart_pdp" "conv_tocart" "revenue" "returns" "cancellations" "ordered_units" "delivered_units" + // "adv_view_pdp" "adv_view_search_category" "adv_view_all" "adv_sum_all" "position_category" "postings" "postings_premium" + Metrics []string `json:"metrics"` + + // 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"` + + // Report sorting settings + Sort []GetAnalyticsDataSort `json:"sort"` +} + +// Report sorting settings +type GetAnalyticsDataSort struct { + // Metric by which the method result will be sorted + Key string `json:"key"` + + // Sorting type + // - ASC — in ascending order, + // - DESC — in descending order. + Order string `json:"order"` +} + +type GetAnalyticsDataResponse struct { + core.CommonResponse + + // Method result + Result struct { + // Data array + Data []struct { + // Data grouping in the report + Dimensions []struct { + // Identifier + Id string `json:"id"` + + // Name + Name string `json:"name"` + } `json:"dimensions"` + + // Metric values list + Metrics []float64 `json:"metrics"` + } `json:"data"` + + // Total and average metrics values + Totals []float64 `json:"totals"` + } `json:"result"` + + // Report creation time + Timestamp string `json:"timestamp"` +} + +// Specify the period and metrics that are required. The response will contain analytical data grouped by the `dimensions` parameter. +func (c Client) GetAnalyticsData(params *GetAnalyticsDataParams) (*GetAnalyticsDataResponse, error) { + url := "/v1/analytics/data" + + resp := &GetAnalyticsDataResponse{} + + response, err := c.client.Request(http.MethodPost, url, params, resp) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type GetStocksOnWarehousesParams struct { + // Number of values per page. + // + // Default is 100 + 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"` + + // Warehouse type filter: + // - EXPRESS_DARK_STORE — Ozon warehouses with Fresh delivery. + // - NOT_EXPRESS_DARK_STORE — Ozon warehouses without Fresh delivery. + // - ALL — all Ozon warehouses. + WarehouseType string `json:"warehouse_type"` +} + +type GetStocksOnWarehousesResponse struct { + core.CommonResponse + + // Method result + Result struct { + // Information about products and stocks + Rows []struct { + // Product identifier in the Ozon system, SKU + SKU int64 `json:"sku"` + + // Product identifier in the seller's system + ItemCode string `json:"item_code"` + + // Product name in the Ozon system + ItemName string `json:"item_name"` + + // Product amount available for sale on Ozon + FreeToSellAmount int64 `json:"free_to_sell_amount"` + + // Product amount specified for confirmed future supplies + PromisedAmount int64 `json:"promised_amount"` + + // Product amount reserved for purchase, returns, and transportation between warehouses + ReservedAmount int64 `json:"reserved_amount"` + + // Name of the warehouse where the products are stored + WarehouseName string `json:"warehouse_name"` + } `json:"rows"` + } `json:"result"` +} + +// Report on stocks and products movement at Ozon warehouses +func (c Client) GetStocksOnWarehouses(params *GetStocksOnWarehousesParams) (*GetStocksOnWarehousesResponse, error) { + url := "/v2/analytics/stock_on_warehouses" + + resp := &GetStocksOnWarehousesResponse{} + + 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/analytics_test.go b/ozon/analytics_test.go new file mode 100644 index 0000000..aa7acf9 --- /dev/null +++ b/ozon/analytics_test.go @@ -0,0 +1,105 @@ +package ozon + +import ( + "net/http" + "testing" + + core "github.com/diphantxm/ozon-api-client" +) + +func TestGetAnalyticsData(t *testing.T) { + tests := []struct { + statusCode int + headers map[string]string + params *GetAnalyticsDataParams + response string + }{ + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetAnalyticsDataParams{ + DateFrom: core.TimeFromString(t, "2006-01-02", "2020-09-01"), + DateTo: core.TimeFromString(t, "2006-01-02", "2021-10-15"), + Dimension: []string{"sku", "day"}, + Metrics: []string{"hits_view_search"}, + Sort: []GetAnalyticsDataSort{ + { + Key: "hits_view_search", + Order: "DESC", + }, + }, + Limit: 1000, + Offset: 0, + }, + `{ + "result": { + "data": [], + "totals": [ + 0 + ] + }, + "timestamp": "2021-11-25 15:19:21" + }`, + }, + } + + for _, test := range tests { + c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers)) + + resp, err := c.GetAnalyticsData(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 TestGetStocksOnWarehouses(t *testing.T) { + tests := []struct { + statusCode int + headers map[string]string + params *GetStocksOnWarehousesParams + response string + }{ + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetStocksOnWarehousesParams{ + Limit: 1000, + Offset: 0, + WarehouseType: "ALL", + }, + `{ + "result": { + "rows": [ + { + "free_to_sell_amount": 15, + "item_code": "my-code", + "item_name": "my-name", + "promised_amount": 12, + "reserved_amount": 11, + "sku": 12345, + "warehouse_name": "my-warehouse" + } + ] + } + }`, + }, + } + + for _, test := range tests { + c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers)) + + resp, err := c.GetStocksOnWarehouses(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/fbo_test.go b/ozon/fbo_test.go index f5a4f47..6df1078 100644 --- a/ozon/fbo_test.go +++ b/ozon/fbo_test.go @@ -20,9 +20,9 @@ func TestGetFBOShipmentsList(t *testing.T) { &GetFBOShipmentsListParams{ Direction: "ASC", Filter: GetFBOShipmentsListFilter{ - Since: core.TimeFromString(t, "2021-09-01T00:00:00.000Z"), + Since: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2021-09-01T00:00:00.000Z"), Status: "awaiting_packaging", - To: core.TimeFromString(t, "2021-11-17T10:44:12.828Z"), + To: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2021-11-17T10:44:12.828Z"), }, Limit: 5, Offset: 0, diff --git a/ozon/fbs_test.go b/ozon/fbs_test.go index 483670d..ff49ff6 100644 --- a/ozon/fbs_test.go +++ b/ozon/fbs_test.go @@ -20,8 +20,8 @@ func TestListUnprocessedShipments(t *testing.T) { &ListUnprocessedShipmentsParams{ Direction: "ASC", Filter: ListUnprocessedShipmentsFilter{ - CutoffFrom: core.TimeFromString(t, "2021-08-24T14:15:22Z"), - CutoffTo: core.TimeFromString(t, "2021-08-31T14:15:22Z"), + CutoffFrom: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2021-08-24T14:15:22Z"), + CutoffTo: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2021-08-31T14:15:22Z"), Status: "awaiting_packaging", }, Limit: 100, @@ -178,8 +178,8 @@ func TestGetFBSShipmentsList(t *testing.T) { &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"), + Since: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2021-11-01T00:00:00.000Z"), + To: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2021-12-01T23:59:59.000Z"), Status: "awaiting_packaging", }, Limit: 100,