This commit is contained in:
diPhantxm
2023-03-12 22:37:40 +03:00
commit ca40ab4559
10 changed files with 1152 additions and 0 deletions

19
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: tests
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Unit Tests
run: go test -v ./...

124
client.go Normal file
View File

@@ -0,0 +1,124 @@
package core
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
)
type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}
type Client struct {
baseUrl string
ctx context.Context
Options map[string]string
client HttpClient
}
func NewClient(baseUrl string, opts map[string]string) *Client {
return &Client{
Options: opts,
ctx: context.Background(),
client: http.DefaultClient,
baseUrl: baseUrl,
}
}
func NewMockClient(handler http.HandlerFunc) *Client {
return &Client{
ctx: context.Background(),
client: NewMockHttpClient(handler),
}
}
func (c Client) newRequest(method string, url string, body interface{}) (*http.Request, error) {
bodyJson, err := json.Marshal(body)
if err != nil {
return nil, err
}
url = c.baseUrl + url
req, err := http.NewRequestWithContext(c.ctx, method, url, bytes.NewBuffer(bodyJson))
if err != nil {
return nil, err
}
for k, v := range c.Options {
req.Header.Add(k, v)
}
return req, nil
}
func (c Client) Request(method string, path string, req, resp interface{}) (*Response, error) {
httpReq, err := c.newRequest(method, path, req)
if err != nil {
return nil, err
}
rawQuery, err := buildRawQuery(httpReq, req)
if err != nil {
return nil, err
}
httpReq.URL.RawQuery = rawQuery
httpResp, err := c.client.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := ioutil.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
response := &Response{}
response.Data = resp
response.StatusCode = httpResp.StatusCode
if httpResp.StatusCode == http.StatusOK {
err = json.Unmarshal(body, &response)
} else {
err = json.Unmarshal(body, &response.Data)
}
if err != nil {
return nil, err
}
return response, nil
}
type MockHttpClient struct {
handler http.HandlerFunc
}
func NewMockHttpClient(handler http.HandlerFunc) *MockHttpClient {
return &MockHttpClient{
handler: handler,
}
}
func (c MockHttpClient) Do(req *http.Request) (*http.Response, error) {
rr := httptest.NewRecorder()
c.handler.ServeHTTP(rr, req)
return rr.Result(), nil
}
func NewMockHttpHandler(statusCode int, json string, headers map[string]string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if len(headers) > 0 {
for key, value := range headers {
w.Header().Add(key, value)
}
}
w.WriteHeader(statusCode)
w.Write([]byte(json))
}
}

101
core.go Normal file
View File

@@ -0,0 +1,101 @@
package core
import (
"fmt"
"net/http"
"reflect"
)
type CommonResponse struct {
StatusCode int
Code int `json:"code"`
Details []CommonResponseDetail `json:"details"`
Message string `json:"message"`
}
type CommonResponseDetail struct {
TypeUrl string `json:"typeUrl"`
Value string `json:"value"`
}
type Response struct {
CommonResponse
Data interface{}
}
func (r Response) CopyCommonResponse(rhs *CommonResponse) {
rhs.Code = r.Code
rhs.Details = r.Details
rhs.StatusCode = r.StatusCode
}
func getDefaultValues(v interface{}) (map[string]string, error) {
isNil, err := isZero(v)
if err != nil {
return make(map[string]string), err
}
if isNil {
return make(map[string]string), nil
}
out := make(map[string]string)
vType := reflect.TypeOf(v).Elem()
vValue := reflect.ValueOf(v).Elem()
//re := regexp.MustCompile(`default:*`)
for i := 0; i < vType.NumField(); i++ {
field := vType.Field(i)
tag := field.Tag.Get("json")
defaultValue := field.Tag.Get("default")
if field.Type.Kind() == reflect.Slice {
// Attach any slices as query params
fieldVal := vValue.Field(i)
for j := 0; j < fieldVal.Len(); j++ {
out[tag] = fmt.Sprintf("%v", fieldVal.Index(j))
}
} else {
// Add any scalar values as query params
fieldVal := fmt.Sprintf("%v", vValue.Field(i))
// If no value was set by the user, use the default
// value specified in the struct tag.
if fieldVal == "" || fieldVal == "0" {
if defaultValue == "" {
continue
}
fieldVal = defaultValue
}
out[tag] = fmt.Sprintf("%v", fieldVal)
}
}
return out, nil
}
func buildRawQuery(req *http.Request, v interface{}) (string, error) {
query := req.URL.Query()
values, err := getDefaultValues(v)
if err != nil {
return "", err
}
for k, v := range values {
query.Add(k, v)
}
return query.Encode(), nil
}
func isZero(v interface{}) (bool, error) {
t := reflect.TypeOf(v)
if !t.Comparable() {
return false, fmt.Errorf("type is not comparable: %v", t)
}
return v == reflect.Zero(t).Interface(), nil
}

34
core_test.go Normal file
View File

@@ -0,0 +1,34 @@
package core
import (
"log"
"testing"
)
type TestTagDefaultValueStruct struct {
TestString string `json:"test_string" default:"something"`
TestNumber int `json:"test_number" default:"12"`
}
func TestTagDefaultValue(t *testing.T) {
testStruct := &TestTagDefaultValueStruct{}
values, err := getDefaultValues(testStruct)
if err != nil {
log.Fatalf("error when getting default values from tags: %s", err)
}
expected := map[string]string{
"test_string": "something",
"test_number": "12",
}
if len(values) != len(expected) {
log.Fatalf("expected equal length of values and expected: expected: %d, got: %d", len(expected), len(values))
}
for expKey, expValue := range expected {
if expValue != values[expKey] {
log.Fatalf("not equal values for key %s", expKey)
}
}
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/diphantxm/ozon-api-client
go 1.18

198
ozon/fbs.go Normal file
View File

@@ -0,0 +1,198 @@
package ozon
import (
"net/http"
core "github.com/diphantxm/ozon-api-client"
)
type ListUnprocessedShipmentsParams struct {
Direction string `json:"dir"`
Filter ListUnprocessedShipmentsFilter `json:"filter"`
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
With ListUnprocessedShipmentsWith `json:"with"`
}
type ListUnprocessedShipmentsFilter struct {
CutoffFrom string `json:"cutoff_from"`
CutoffTo string `json:"cutoff_to"`
DeliveringDateFrom string `json:"delivering_date_from"`
DeliveringDateTo string `json:"delivering_date_to"`
DeliveryMethodId []int64 `json:"deliveryMethodId"`
ProviderId []int64 `json:"provider_id"`
Status string `json:"status"`
WarehouseId []int64 `json:"warehouse_id"`
}
type ListUnprocessedShipmentsWith struct {
AnalyticsData bool `json:"analytics_data"`
Barcodes bool `json:"barcodes"`
FinancialData bool `json:"financial_data"`
Translit bool `json:"translit"`
}
type ListUnprocessedShipmentsResponse struct {
core.CommonResponse
Result ListUnprocessedShipmentsResult `json:"result"`
}
type ListUnprocessedShipmentsResult struct {
Count int64 `json:"count"`
Postings []ListUnprocessedShipmentsPosting `json:"postings"`
}
type ListUnprocessedShipmentsPosting struct {
Addressee struct {
Name string `json:"name"`
Phone string `json:"phone"`
} `json:"addressee"`
AnalyticsData struct {
City string `json:"city"`
DeliveryDateBegin string `json:"delivery_date_begin"`
DeliveryDateEnd string `json:"delivery_date_end"`
DeliveryType string `json:"delivery_type"`
IsLegal bool `json:"is_legal"`
IsPremium bool `json:"is_premium"`
PaymentTypeGroupName string `json:"payment_type_group_name"`
Region string `json:"region"`
TPLProvider string `json:"tpl_provider"`
TPLProviderId int64 `json:"tpl_provider_id"`
Warehouse string `json:"warehouse"`
WarehouseId int64 `json:"warehouse_id"`
} `json:"analytics_data"`
Barcodes struct {
LowerBarcode string `json:"lower_barcode"`
UpperBarcode string `json:"upper_barcode"`
} `json:"barcodes"`
Cancellation struct {
AffectCancellationRating bool `json:"affect_cancellation_rating"`
CancelReason string `json:"cancel_reason"`
CancelReasonId int64 `json:"cancel_reason_id"`
CancellationInitiator string `json:"cancellation_initiator"`
CancellationType string `json:"cancellation_type"`
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"`
DeliveringDate string `json:"delivering_date"`
DeliveryMethod struct {
Id int64 `json:"id"`
Name string `json:"name"`
TPLProvider string `json:"tpl_provider"`
TPLProviderId int64 `json:"tpl_provider_id"`
Warehouse string `json:"warehouse"`
WarehouseId int64 `json:"warehouse_id"`
} `json:"delivery_method"`
FinancialData struct {
ClusterFrom string `json:"cluster_from"`
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 string `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"`
}
InProccessAt string `json:"in_process_at"`
IsExpress bool `json:"is_express"`
IsMultibox bool `json:"is_multibox"`
MultiBoxQuantity int32 `json:"multi_box_qty"`
OrderId int64 `json:"order_id"`
OrderNumber string `json:"order_number"`
ParentPostingNumber string `json:"parent_posting_number"`
PostingNumber string `json:"posting_number"`
Products struct {
MandatoryMark []string `json:"mandatory_mark"`
Name string `json:"name"`
OfferId string `json:"offer_id"`
CurrencyCode string `json:"currency_code"`
Price string `json:"price"`
Quantity int32 `json:"quantity"`
SKU int64 `json:"sku"`
} `json:"products"`
Requirements struct {
ProductsRequiringGTD []string `json:"products_requiring_gtd"`
ProductsRequiringCountry []string `json:"products_requiring_country"`
ProductsRequiringMandatoryMark []string `json:"products_requiring_mandatory_mark"`
ProductsRequiringRNPT []string `json:"products_requiring_rnpt"`
} `json:"requirements"`
ShipmentDate string `json:"shipment_date"`
Status string `json:"status"`
TPLIntegrationType string `json:"tpl_integration_type"`
TrackingNumber string `json:"tracking_number"`
}
type MarketplaceServices struct {
DeliveryToCustomer float64 `json:"marketplace_service_item_deliv_to_customer"`
DirectFlowTrans float64 `json:"marketplace_service_item_direct_flow_trans"`
DropoffFF float64 `json:"marketplace_service_item_item_dropoff_ff"`
DropoffPVZ float64 `json:"marketplace_service_item_dropoff_pvz"`
DropoffSC float64 `json:"marketplace_service_item_dropoff_sc"`
Fulfillment float64 `json:"marketplace_service_item_fulfillment"`
Pickup float64 `json:"marketplace_service_item_pickup"`
ReturnAfterDeliveryToCustomer float64 `json:"marketplace_service_item_return_after_deliv_to_customer"`
ReturnFlowTrans float64 `json:"marketplace_service_item_return_flow_trans"`
ReturnNotDeliveryToCustomer float64 `json:"marketplace_service_item_return_not_deliv_to_customer"`
ReturnPartGoodsCustomer float64 `json:"marketplace_service_item_return_part_goods_customer"`
}
func (c Client) ListUnprocessedShipments(params *ListUnprocessedShipmentsParams) (*ListUnprocessedShipmentsResponse, error) {
url := "/v3/posting/fbs/unfulfilled/list"
resp := &ListUnprocessedShipmentsResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}

165
ozon/fbs_test.go Normal file
View File

@@ -0,0 +1,165 @@
package ozon
import (
"testing"
core "github.com/diphantxm/ozon-api-client"
)
func TestListUnprocessedShipments(t *testing.T) {
tests := []struct {
statusCode int
headers map[string]string
params *ListUnprocessedShipmentsParams
response string
}{
{
200,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&ListUnprocessedShipmentsParams{
Direction: "ASC",
Filter: ListUnprocessedShipmentsFilter{
CutoffFrom: "2021-08-24T14:15:22Z",
CutoffTo: "2021-08-31T14:15:22Z",
Status: "awaiting_packaging",
},
Limit: 100,
With: ListUnprocessedShipmentsWith{
AnalyticsData: true,
Barcodes: true,
FinancialData: true,
Translit: true,
},
},
`{
"result": {
"postings": [
{
"posting_number": "23713478-0018-3",
"order_id": 559293114,
"order_number": "33713378-0051",
"status": "awaiting_packaging",
"delivery_method": {
"id": 15110442724000,
"name": "Ozon Логистика курьеру, Москва",
"warehouse_id": 15110442724000,
"warehouse": "Склад на Ленина",
"tpl_provider_id": 24,
"tpl_provider": "Ozon Логистика"
},
"tracking_number": "",
"tpl_integration_type": "ozon",
"in_process_at": "2021-08-25T10:48:38Z",
"shipment_date": "2021-08-26T10: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": "1259",
"offer_id": "УТ-0001365",
"name": "Мяч, цвет: черный, 5 кг",
"sku": 140048123,
"quantity": 1,
"mandatory_mark": []
}
],
"addressee": null,
"barcodes": {
"upper_barcode": "%101%806044518",
"lower_barcode": "23024930500000"
},
"analytics_data": {
"region": "Санкт-Петербург",
"city": "Санкт-Петербург",
"delivery_type": "PVZ",
"is_premium": false,
"payment_type_group_name": "Карты оплаты",
"warehouse_id": 15110442724000,
"warehouse": "Склад на Ленина",
"tpl_provider_id": 24,
"tpl_provider": "Ozon Логистика",
"delivery_date_begin": "2022-08-28T14:00:00Z",
"delivery_date_end": "2022-08-28T18:00:00Z",
"is_legal": false
},
"financial_data": {
"products": [
{
"commission_amount": 0,
"commission_percent": 0,
"payout": 0,
"product_id": 140048123,
"old_price": 1888,
"price": 1259,
"total_discount_value": 629,
"total_discount_percent": 33.32,
"actions": [
"Системная виртуальная скидка селлера"
],
"picking": null,
"quantity": 1,
"client_price": "",
"item_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
}
}
],
"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
}
},
"is_express": false,
"requirements": {
"products_requiring_gtd": [],
"products_requiring_country": []
}
}
],
"count": 55
}
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.ListUnprocessedShipments(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)
}
}
}

30
ozon/ozon.go Normal file
View File

@@ -0,0 +1,30 @@
package ozon
import (
"net/http"
core "github.com/diphantxm/ozon-api-client"
)
const (
DefaultAPIBaseUrl = "https://api-seller.ozon.ru"
)
type Client struct {
client *core.Client
}
func NewClient(clientId, apiKey string) *Client {
return &Client{
client: core.NewClient(DefaultAPIBaseUrl, map[string]string{
"Client-Id": clientId,
"Api-Key": apiKey,
}),
}
}
func NewMockClient(handler http.HandlerFunc) *Client {
return &Client{
client: core.NewMockClient(handler),
}
}

217
ozon/products.go Normal file
View File

@@ -0,0 +1,217 @@
package ozon
import (
"net/http"
core "github.com/diphantxm/ozon-api-client"
)
type GetStocksInfoParams struct {
// Identifier of the last value on the page. Leave this field blank in the first request.
//
// To get the next values, specify last_id from the response of the previous request.
LastId string `json:"last_id,omitempty"`
// Number of values per page. Minimum is 1, maximum is 1000
Limit int64 `json:"limit,omitempty"`
// Filter by product
Filter GetStocksInfoFilter `json:"filter,omitempty"`
}
type GetStocksInfoFilter struct {
// Filter by the offer_id parameter. It is possible to pass a list of values
OfferId string `json:"offer_id,omitempty"`
// Filter by the product_id parameter. It is possible to pass a list of values
ProductId int64 `json:"product_id,omitempty"`
// Filter by product visibility
Visibility string `json:"visibility,omitempty"`
}
type GetStocksInfoResponse struct {
core.CommonResponse
// Method Result
Result GetStocksInfoResponseResult `json:"result,omitempty"`
}
type GetStocksInfoResponseResult struct {
// Identifier of the last value on the page
//
// To get the next values, specify the recieved value in the next request in the last_id parameter
LastId string `json:"last_id,omitempty"`
// The number of unique products for which information about stocks is displayed
Total int32 `json:"total,omitempty"`
// Product details
Items []GetStocksInfoResponseItem `json:"items,omitempty"`
}
type GetStocksInfoResponseItem struct {
// Product identifier in the seller's system
OfferId string `json:"offer_id,omitempty"`
// Product identifier
ProductId int64 `json:"product_id,omitempty"`
// Stock details
Stocks []GetStocksInfoResponseStock `json:"stocks,omitempty"`
}
type GetStocksInfoResponseStock struct {
// In a warehouse
Present int32 `json:"present,omitempty"`
// Reserved
Reserved int32 `json:"reserved,omitempty"`
// Warehouse type
Type string `json:"type,omitempty" default:"ALL"`
}
func (c Client) GetStocksInfo(params *GetStocksInfoParams) (*GetStocksInfoResponse, error) {
url := "/v3/product/info/stocks"
resp := &GetStocksInfoResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}
type GetProductDetailsParams struct {
OfferId string `json:"offer_id"`
ProductId int64 `json:"product_id"`
SKU int64 `json:"sku"`
}
type GetProductDetailsResponse struct {
core.CommonResponse
Result GetProductDetailsResponseResult `json:"Result"`
}
type GetProductDetailsResponseResult struct {
Barcode string `json:"barcode"`
Barcodes []string `json:"barcodes"`
BuyboxPrice string `json:"buybox_price"`
CategoryId int64 `json:"category_id"`
ColorImage string `json:"color_image"`
Commissions []GetProductDetailsResponseCommission `json:"commissions"`
CreatedAt string `json:"created_at"`
FBOSKU int64 `json:"fbo_sku"`
FBSSKU int64 `json:"fbs_sku"`
Id int64 `json:"id"`
Images []string `json:"images"`
PrimaryImage string `json:"primary_image"`
Images360 []string `json:"images360"`
HasDiscountedItem bool `json:"has_discounted_item"`
IsDiscounted bool `json:"is_discounted"`
DiscountedStocks GetProductDetailsResponseDiscountedStocks `json:"discounted_stocks"`
IsKGT bool `json:"is_kgt"`
IsPrepayment bool `json:"is_prepayment"`
IsPrepaymentAllowed bool `json:"is_prepayment_allowed"`
CurrencyCode string `json:"currency_code"`
MarketingPrice string `json:"marketing_price"`
MinOzonPrice string `json:"min_ozon_price"`
MinPrice string `json:"min_price"`
Name string `json:"name"`
OfferId string `json:"offer_id"`
OldPrice string `json:"old_price"`
PremiumPrice string `json:"premium_price"`
Price string `json:"price"`
PriceIndex string `json:"price_idnex"`
RecommendedPrice string `json:"recommended_price"`
Status GetProductDetailsResponseStatus `json:"status"`
Sources []GetProductDetailsResponseSource `json:"sources"`
Stocks GetProductDetailsResponseStocks `json:"stocks"`
UpdatedAt string `json:"updated_at"`
VAT string `json:"vat"`
VisibilityDetails GetProductDetailsResponseDetails `json:"visibility_details"`
Visible bool `json:"visible"`
VolumeWeight float64 `json:"volume_weights"`
}
type GetProductDetailsResponseCommission struct {
DeliveryAmount float64 `json:"deliveryAmount"`
MinValue float64 `json:"minValue"`
Percent float64 `json:"percent"`
ReturnAmount float64 `json:"returnAmount"`
SaleSchema string `json:"saleSchema"`
Value float64 `json:"value"`
}
type GetProductDetailsResponseDiscountedStocks struct {
Coming int32 `json:"coming"`
Present int32 `json:"present"`
Reserved int32 `json:"reserved"`
}
type GetProductDetailsResponseStatus struct {
State string `json:"state"`
StateFailed string `json:"state_failed"`
ModerateStatus string `json:"moderate_status"`
DeclineReasons []string `json:"decline_reasons"`
ValidationsState string `json:"validation_state"`
StateName string `json:"state_name"`
StateDescription string `json:"state_description"`
IsFailed bool `json:"is_failed"`
IsCreated bool `json:"is_created"`
StateTooltip string `json:"state_tooltip"`
ItemErrors []GetProductDetailsResponseItemError `json:"item_errors"`
StateUpdatedAt string `json:"state_updated_at"`
}
type GetProductDetailsResponseItemError struct {
Code string `json:"code"`
State string `json:"state"`
Level string `json:"level"`
Description string `json:"description"`
Field string `json:"field"`
AttributeId int64 `json:"attribute_id"`
AttributeName string `json:"attribute_name"`
OptionalDescriptionElements GetProductDetailsResponseOptionalDescriptionElements `json:"optional_description_elements"`
}
type GetProductDetailsResponseOptionalDescriptionElements struct {
PropertyName string `json:"property_name"`
}
type GetProductDetailsResponseSource struct {
IsEnabled bool `json:"is_enabled"`
SKU int64 `json:"sku"`
Source string `json:"source"`
}
type GetProductDetailsResponseStocks struct {
Coming int32 `json:"coming"`
Present int32 `json:"present"`
Reserved int32 `json:"reserved"`
}
type GetProductDetailsResponseDetails struct {
ActiveProduct bool `json:"active_product"`
HasPrice bool `json:"has_price"`
HasStock bool `json:"has_stock"`
}
func (c Client) GetProductDetails(params *GetProductDetailsParams) (*GetProductDetailsResponse, error) {
url := "/v2/product/info"
resp := &GetProductDetailsResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}

261
ozon/products_test.go Normal file
View File

@@ -0,0 +1,261 @@
package ozon
import (
"net/http"
"testing"
core "github.com/diphantxm/ozon-api-client"
)
func TestGetStocksInfo(t *testing.T) {
tests := []struct {
statusCode int
headers map[string]string
params *GetStocksInfoParams
response string
}{
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&GetStocksInfoParams{
Limit: 100,
LastId: "",
Filter: GetStocksInfoFilter{
OfferId: "136834",
ProductId: 214887921,
Visibility: "ALL",
},
},
`{
"result": {
"items": [
{
"product_id": 214887921,
"offer_id": "136834",
"stocks": [
{
"type": "fbs",
"present": 170,
"reserved": 0
},
{
"type": "fbo",
"present": 0,
"reserved": 0
}
]
}
],
"total": 1,
"last_id": "anVsbA=="
}
}`,
},
{
400,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&GetStocksInfoParams{
Limit: 100,
LastId: "",
Filter: GetStocksInfoFilter{
OfferId: "136834",
ProductId: 214887921,
Visibility: "ALL",
},
},
`{
"code": 0,
"details": [
{
"typeUrl": "string",
"value": "string"
}
],
"message": "string"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.GetStocksInfo(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 TestGetProductDetails(t *testing.T) {
tests := []struct {
statusCode int
headers map[string]string
params *GetProductDetailsParams
response string
}{
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&GetProductDetailsParams{
ProductId: 137208233,
},
`{
"result": {
"id": 137208233,
"name": "Комплект защитных плёнок для X3 NFC. Темный хлопок",
"offer_id": "143210586",
"barcode": "",
"barcodes": [
"2335900005",
"7533900005"
],
"buybox_price": "",
"category_id": 17038062,
"created_at": "2021-10-21T15:48:03.529178Z",
"images": [
"https://cdn1.ozone.ru/s3/multimedia-5/6088931525.jpg",
"https://cdn1.ozone.ru/s3/multimedia-p/6088915813.jpg"
],
"has_discounted_item": true,
"is_discounted": true,
"discounted_stocks": {
"coming": 0,
"present": 0,
"reserved": 0
},
"currency_code": "RUB",
"marketing_price": "",
"min_price": "",
"old_price": "",
"premium_price": "",
"price": "590.0000",
"recommended_price": "",
"sources": [
{
"is_enabled": true,
"sku": 522759607,
"source": "fbo"
},
{
"is_enabled": true,
"sku": 522759608,
"source": "fbs"
}
],
"stocks": {
"coming": 0,
"present": 0,
"reserved": 0
},
"errors": [],
"updated_at": "2023-02-09T06:46:44.152Z",
"vat": "0.0",
"visible": false,
"visibility_details": {
"has_price": true,
"has_stock": false,
"active_product": false
},
"price_index": "0.00",
"commissions": [],
"volume_weight": 0.1,
"is_prepayment": false,
"is_prepayment_allowed": true,
"images360": [],
"is_kgt": false,
"color_image": "",
"primary_image": "https://cdn1.ozone.ru/s3/multimedia-p/6088931545.jpg",
"status": {
"state": "imported",
"state_failed": "imported",
"moderate_status": "",
"decline_reasons": [],
"validation_state": "pending",
"state_name": "Не продается",
"state_description": "Не создан",
"is_failed": true,
"is_created": false,
"state_tooltip": "",
"item_errors": [
{
"code": "error_attribute_values_empty",
"field": "attribute",
"attribute_id": 9048,
"state": "imported",
"level": "error",
"description": "Не заполнен обязательный атрибут. Иногда мы обновляем обязательные атрибуты или добавляем новые. Отредактируйте товар или загрузите новый XLS-шаблон с актуальными атрибутами. ",
"optional_description_elements": {},
"attribute_name": "Название модели"
},
{
"code": "error_attribute_values_empty",
"field": "attribute",
"attribute_id": 5076,
"state": "imported",
"level": "error",
"description": "Не заполнен обязательный атрибут. Иногда мы обновляем обязательные атрибуты или добавляем новые. Отредактируйте товар или загрузите новый XLS-шаблон с актуальными атрибутами. ",
"optional_description_elements": {},
"attribute_name": "Рекомендовано для"
},
{
"code": "error_attribute_values_empty",
"field": "attribute",
"attribute_id": 8229,
"state": "imported",
"level": "error",
"description": "Не заполнен обязательный атрибут. Иногда мы обновляем обязательные атрибуты или добавляем новые. Отредактируйте товар или загрузите новый XLS-шаблон с актуальными атрибутами. ",
"optional_description_elements": {},
"attribute_name": "Тип"
},
{
"code": "error_attribute_values_empty",
"field": "attribute",
"attribute_id": 85,
"state": "imported",
"level": "error",
"description": "Не заполнен обязательный атрибут. Иногда мы обновляем обязательные атрибуты или добавляем новые. Отредактируйте товар или загрузите новый XLS-шаблон с актуальными атрибутами. ",
"optional_description_elements": {},
"attribute_name": "Бренд"
}
],
"state_updated_at": "2021-10-21T15:48:03.927309Z"
}
}
}`,
},
{
400,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&GetProductDetailsParams{
ProductId: 137208233,
},
`{
"code": 0,
"details": [
{
"typeUrl": "string",
"value": "string"
}
],
"message": "string"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.GetProductDetails(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)
}
}
}