initial
This commit is contained in:
19
.github/workflows/go.yml
vendored
Normal file
19
.github/workflows/go.yml
vendored
Normal 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
124
client.go
Normal 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
101
core.go
Normal 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
34
core_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
198
ozon/fbs.go
Normal file
198
ozon/fbs.go
Normal 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
165
ozon/fbs_test.go
Normal 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
30
ozon/ozon.go
Normal 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
217
ozon/products.go
Normal 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
261
ozon/products_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user