add cdek
parent
cb83eb5ff5
commit
3eea5e0579
|
@ -0,0 +1,12 @@
|
||||||
|
vendor
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# binary
|
||||||
|
cdek
|
||||||
|
|
||||||
|
# go test -coverprofile cover.out
|
||||||
|
cover.out
|
||||||
|
# go tool cover -html=cover.out -o cover.html
|
||||||
|
cover.html
|
||||||
|
|
||||||
|
v2/.env.local
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Feel free to browse the open issues and file new ones, all feedback welcome!
|
||||||
|
|
||||||
|
We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||||
|
|
||||||
|
We generally follow the standard [github pull request](https://help.github.com/articles/about-pull-requests/) process
|
||||||
|
|
||||||
|
All changes must be code reviewed.
|
||||||
|
Expect reviewers to request that you avoid [common go style
|
||||||
|
mistakes](https://github.com/golang/go/wiki/CodeReviewComments) in your pull requests.
|
||||||
|
|
||||||
|
New code must be covered with tests. Modified code should be checked by existing tests.
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 vseinstrumenti.ru
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,44 @@
|
||||||
|
# GO SDK for CDEK API v2
|
||||||
|
[](https://godoc.org/github.com/vseinstrumentiru/CDEK)
|
||||||
|
[](https://travis-ci.com/vseinstrumentiru/CDEK)
|
||||||
|
[](https://coveralls.io/github/vseinstrumentiru/CDEK?branch=travis)
|
||||||
|
[](https://goreportcard.com/report/github.com/vseinstrumentiru/CDEK)
|
||||||
|
[](https://github.com/vseinstrumentiru/CDEK/releases)
|
||||||
|
[](https://bestpractices.coreinfrastructure.org/projects/2990)
|
||||||
|
|
||||||
|
The Go language implementation of SDK for [integration with CDEK](https://www.cdek.ru/clients/integrator.html)
|
||||||
|
|
||||||
|
----
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
To install this package, you need to install Go and setup your Go workspace on
|
||||||
|
your computer. The simplest way to install the library is to run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get github.com/vseinstrumentiru/cdek
|
||||||
|
```
|
||||||
|
With Go module support (Go 1.11+), simply `import "github.com/vseinstrumentiru/cdek"` in
|
||||||
|
your source code and `go [build|run|test]` will automatically download the
|
||||||
|
necessary dependencies ([Go modules
|
||||||
|
ref](https://github.com/golang/go/wiki/Modules)).
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
- See [godoc](https://godoc.org/github.com/vseinstrumentiru/CDEK) for package and API
|
||||||
|
descriptions and examples.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------------
|
||||||
|
You cat get test `clientAccount` and `clientSecurePassword` from [the official CDEK documentation](https://confluence.cdek.ru/pages/viewpage.action?pageId=20264477#DataExchangeProtocol(v1.5)-TestAccount)
|
||||||
|
```
|
||||||
|
import "github.com/vseinstrumentiru/cdek"
|
||||||
|
...
|
||||||
|
|
||||||
|
client := cdek.NewClient("https://integration.edu.cdek.ru/").
|
||||||
|
SetAuth(clientAccount, clientSecurePassword)
|
||||||
|
|
||||||
|
cities, err := client.GetCities(map[cdek.CityFilter]string{
|
||||||
|
cdek.CityFilterPage: "1",
|
||||||
|
})
|
||||||
|
```
|
|
@ -0,0 +1,19 @@
|
||||||
|
- [ ] расчёт тарифов и обращения к справочникам
|
||||||
|
- [X] расчёт стоимости доставки по тарифам с приоритетом
|
||||||
|
- [ ] расчёт стоимости по тарифам без приоритета
|
||||||
|
- [X] получение списка пунктов выдачи заказов (ПВЗ) с фильтрацией
|
||||||
|
- [X] получение списка регионов-субъектов РФ
|
||||||
|
- [X] получение списка городов
|
||||||
|
- [ ] управление заказами
|
||||||
|
- [X] формирование новых заказов от ИМ
|
||||||
|
- [ ] оформление заказов на доставку
|
||||||
|
- [ ] получение квитанции в PDF
|
||||||
|
- [ ] получение почтовых этикеток в PDF
|
||||||
|
- [X] удаление заказов
|
||||||
|
- [X] изменение заказов
|
||||||
|
- [ ] получение информации по заказам (отчёт «Информация по заказам»)
|
||||||
|
- [ ] трекинг заказов (отчёт «Статусы заказов»)
|
||||||
|
- [ ] прозвон получателя
|
||||||
|
- [ ] вызов курьера
|
||||||
|
- [ ] создание преалерта
|
||||||
|
- [X] выбор базового URL интерфейса
|
|
@ -0,0 +1,70 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Credentials struct {
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Credentials) UrlValues() url.Values {
|
||||||
|
data := url.Values{}
|
||||||
|
|
||||||
|
data.Set("grant_type", "client_credentials")
|
||||||
|
data.Set("client_id", c.ClientID)
|
||||||
|
data.Set("client_secret", c.ClientSecret)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Jti string `json:"jti"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) getAccessToken(ctx context.Context) (string, error) {
|
||||||
|
d, err := time.ParseDuration(fmt.Sprintf("%ds", c.expireIn))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
isExpired := time.Now().After(time.Now().Add(d))
|
||||||
|
|
||||||
|
if len(c.accessToken) == 0 || isExpired {
|
||||||
|
resp, err := c.Auth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
c.accessToken = resp.AccessToken
|
||||||
|
c.expireIn = resp.ExpiresIn
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.accessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) Auth(ctx context.Context) (*AuthResponse, error) {
|
||||||
|
if len(c.opts.Credentials.ClientID) == 0 || len(c.opts.Credentials.ClientSecret) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx, http.MethodPost,
|
||||||
|
c.buildUri("/v2/oauth/token", nil),
|
||||||
|
strings.NewReader(c.opts.Credentials.UrlValues().Encode()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
return jsonReq[AuthResponse](req)
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_Auth(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := c.Auth(timedCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CalculatorTrafiffListRequest struct {
|
||||||
|
// Date Дата и время планируемой передачи заказа. По умолчанию - текущая
|
||||||
|
Date string `json:"date,omitempty"`
|
||||||
|
// Type Тип заказа: 1 - "интернет-магазин", 2 - "доставка". По умолчанию - 1
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
// Валюта, в которой необходимо произвести расчет. По умолчанию - валюта договора
|
||||||
|
Currency int `json:"currency,omitempty"`
|
||||||
|
// Lang Локализация офиса. По умолчанию "rus"
|
||||||
|
Lang string `url:"lang,omitempty"`
|
||||||
|
// FromLocation Адрес отправления
|
||||||
|
FromLocation Location `json:"from_location,omitempty"`
|
||||||
|
// ToLocation Адрес получения
|
||||||
|
ToLocation Location `json:"to_location"`
|
||||||
|
// Packages Список информации по местам (упаковкам)
|
||||||
|
Packages []Package `json:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tariff struct {
|
||||||
|
TariffCode int `json:"tariff_code"`
|
||||||
|
TariffName string `json:"tariff_name"`
|
||||||
|
TariffDescription string `json:"tariff_description"`
|
||||||
|
DeliveryMode int `json:"delivery_mode"`
|
||||||
|
DeliverySum float64 `json:"delivery_sum"`
|
||||||
|
PeriodMin int `json:"period_min"`
|
||||||
|
PeriodMax int `json:"period_max"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalculatorTrafiffListResponse struct {
|
||||||
|
TariffCodes []Tariff `json:"tariff_codes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) CalculatorTrafiffList(ctx context.Context, input *CalculatorTrafiffListRequest) (*CalculatorTrafiffListResponse, error) {
|
||||||
|
payload, err := json.Marshal(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodPost,
|
||||||
|
c.buildUri("/v2/calculator/tarifflist", nil),
|
||||||
|
bytes.NewReader(payload),
|
||||||
|
)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := c.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
return jsonReq[CalculatorTrafiffListResponse](req)
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_CalculatorTrafiffList(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := c.CalculatorTrafiffList(timedCtx, &CalculatorTrafiffListRequest{
|
||||||
|
Lang: "rus",
|
||||||
|
Currency: 1,
|
||||||
|
FromLocation: Location{Code: 44},
|
||||||
|
ToLocation: Location{Code: 287},
|
||||||
|
Packages: []Package{
|
||||||
|
{Weight: 1},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
require.Greater(t, len(resp.TariffCodes), 0)
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CitiesRequest struct {
|
||||||
|
// CountryCodes Массив кодов стран в формате ISO_3166-1_alpha-2
|
||||||
|
CountryCodes []string `url:"country_codes,omitempty"`
|
||||||
|
// RegionCode Код региона СДЭК
|
||||||
|
RegionCode int `url:"region_code,omitempty"`
|
||||||
|
// FiasGuid Уникальный идентификатор ФИАС населенного пункта UUID
|
||||||
|
FiasGuid string `url:"fias_guid,omitempty"`
|
||||||
|
// PostalCode Почтовый индекс
|
||||||
|
PostalCode string `url:"postal_code,omitempty"`
|
||||||
|
// Code Код населенного пункта СДЭК
|
||||||
|
Code string `url:"code,omitempty"`
|
||||||
|
// City Название населенного пункта. Должно соответствовать полностью
|
||||||
|
City string `url:"city,omitempty"`
|
||||||
|
// Size Ограничение выборки результата. По умолчанию 1000
|
||||||
|
Size int `url:"size,omitempty"`
|
||||||
|
// Page Номер страницы выборки результата. По умолчанию 0
|
||||||
|
Page int `url:"page,omitempty"`
|
||||||
|
// Lang Локализация офиса. По умолчанию "rus"
|
||||||
|
Lang string `url:"lang,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CitiesResponse []*City
|
||||||
|
|
||||||
|
type City struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
City string `json:"city"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
RegionCode int `json:"region_code"`
|
||||||
|
SubRegion string `json:"sub_region,omitempty"`
|
||||||
|
PostalCodes []string `json:"postal_codes,omitempty"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
TimeZone string `json:"time_zone"`
|
||||||
|
KladrCode string `json:"kladr_code,omitempty"`
|
||||||
|
PaymentLimit float64 `json:"payment_limit,omitempty"`
|
||||||
|
FiasGuid string `json:"fias_guid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) Cities(ctx context.Context, input *CitiesRequest) (*CitiesResponse, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
c.buildUri("/v2/location/cities", input),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "http.NewRequestWithContext")
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := c.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "getAccessToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
return jsonReq[CitiesResponse](req)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_Cities(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := c.Cities(timedCtx, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
|
||||||
|
resp, err = c.Cities(timedCtx, &CitiesRequest{Page: 1000, Size: 1000})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/go-querystring/query"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
Auth(ctx context.Context) (*AuthResponse, error)
|
||||||
|
DeliveryPoints(ctx context.Context, input *DeliveryPointsRequest) (*DeliveryPointsResponse, error)
|
||||||
|
Regions(ctx context.Context, input *RegionsRequest) (*RegionsResponse, error)
|
||||||
|
Cities(ctx context.Context, input *CitiesRequest) (*CitiesResponse, error)
|
||||||
|
CalculatorTrafiffList(ctx context.Context, input *CalculatorTrafiffListRequest) (*CalculatorTrafiffListResponse, error)
|
||||||
|
OrderRegister(ctx context.Context, input *OrderRegisterRequest) (*Response, error)
|
||||||
|
OrderDelete(ctx context.Context, uuid string) (*Response, error)
|
||||||
|
OrderUpdate(ctx context.Context, input *OrderUpdateRequest) (*OrderUpdateResponse, error)
|
||||||
|
OrderStatus(ctx context.Context, uuid string) (*Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Endpoint string
|
||||||
|
Credentials *Credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(opts *Options) Client {
|
||||||
|
return &clientImpl{opts: opts}
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientImpl struct {
|
||||||
|
opts *Options
|
||||||
|
accessToken string
|
||||||
|
expireIn int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) buildUri(p string, values interface{}) string {
|
||||||
|
v, _ := query.Values(values)
|
||||||
|
return strings.TrimRight(fmt.Sprintf(
|
||||||
|
"%s/%s?%s",
|
||||||
|
strings.TrimRight(c.opts.Endpoint, "/"),
|
||||||
|
strings.TrimLeft(p, "/"),
|
||||||
|
v.Encode(),
|
||||||
|
), "?")
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTestClient() Client {
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
godotenv.Load(fmt.Sprintf("%s/.env.local", wd))
|
||||||
|
|
||||||
|
clientId := os.Getenv("CDEK_CLIENT_ID")
|
||||||
|
clientSecretId := os.Getenv("CDEK_SECRET_ID")
|
||||||
|
|
||||||
|
// public cdek test credentials
|
||||||
|
if clientId == "" {
|
||||||
|
clientId = "EMscd6r9JnFiQ3bLoyjJY6eM78JrJceI"
|
||||||
|
}
|
||||||
|
if clientSecretId == "" {
|
||||||
|
clientSecretId = "PjLZkKBHEiLK3YsjtNrt3TGNG0ahs3kG"
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewClient(&Options{
|
||||||
|
Endpoint: EndpointTest,
|
||||||
|
Credentials: &Credentials{
|
||||||
|
ClientID: clientId,
|
||||||
|
ClientSecret: clientSecretId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
const EndpointTest = "https://api.edu.cdek.ru"
|
||||||
|
const EndpointProd = "https://api.cdek.ru"
|
|
@ -0,0 +1,122 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeliveryPointsResponse []DeliveryPoint
|
||||||
|
|
||||||
|
type DeliveryPointWorkTime struct {
|
||||||
|
Day int `json:"day"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryPointWorkTimeExceptions struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
IsWorking bool `json:"is_working"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryPointOfficeImage struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryPointLocation struct {
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
RegionCode int `json:"region_code"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
CityCode int `json:"city_code"`
|
||||||
|
City string `json:"city,omitempty"`
|
||||||
|
FiasGuid string `json:"fias_guid,omitempty"`
|
||||||
|
PostalCode string `json:"postal_code,omitempty"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
AddressFull string `json:"address_full,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryPoint struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
AddressComment string `json:"address_comment,omitempty"`
|
||||||
|
WorkTime string `json:"work_time,omitempty"`
|
||||||
|
Phones []Phone `json:"phones,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
OwnerCode string `json:"owner_code"`
|
||||||
|
TakeOnly bool `json:"take_only"`
|
||||||
|
IsHandout bool `json:"is_handout,omitempty"`
|
||||||
|
IsReception bool `json:"is_reception,omitempty"`
|
||||||
|
IsDressingRoom bool `json:"is_dressing_room,omitempty"`
|
||||||
|
HaveCashless bool `json:"have_cashless"`
|
||||||
|
HaveCash bool `json:"have_cash"`
|
||||||
|
AllowedCod bool `json:"allowed_cod"`
|
||||||
|
Site string `json:"site,omitempty"`
|
||||||
|
WorkTimeList []DeliveryPointWorkTime `json:"work_time_list,omitempty"`
|
||||||
|
WeightMin float64 `json:"weight_min,omitempty"`
|
||||||
|
WeightMax float64 `json:"weight_max,omitempty"`
|
||||||
|
Location DeliveryPointLocation `json:"location"`
|
||||||
|
Fulfillment bool `json:"fulfillment"`
|
||||||
|
NearestStation string `json:"nearest_station,omitempty"`
|
||||||
|
NearestMetroStation string `json:"nearest_metro_station,omitempty"`
|
||||||
|
OfficeImageList []DeliveryPointOfficeImage `json:"office_image_list,omitempty"`
|
||||||
|
WorkTimeExceptions []DeliveryPointWorkTimeExceptions `json:"work_time_exceptions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeliveryPointsRequest struct {
|
||||||
|
// PostalCode Почтовый индекс города, для которого необходим список офисов
|
||||||
|
PostalCode int `url:"postal_code,omitempty"`
|
||||||
|
// CityCode Код населенного пункта СДЭК (метод "Список населенных пунктов")
|
||||||
|
CityCode int `url:"city_code,omitempty"`
|
||||||
|
// Type Тип офиса, может принимать значения: «PVZ» - склады, «POSTAMAT» - постаматы, «ALL» - все.
|
||||||
|
Type string `url:"type,omitempty"`
|
||||||
|
// CountryCode Код страны в формате ISO_3166-1_alpha-2 (см. “Общероссийский классификатор стран мира”)
|
||||||
|
CountryCode string `url:"country_code,omitempty"`
|
||||||
|
// RegionCode Код региона по базе СДЭК
|
||||||
|
RegionCode int `url:"region_code,omitempty"`
|
||||||
|
// HaveCashless Наличие терминала оплаты
|
||||||
|
HaveCashless bool `url:"have_cashless,omitempty"`
|
||||||
|
// HaveCash Есть прием наличных
|
||||||
|
HaveCash bool `url:"have_cash,omitempty"`
|
||||||
|
// AllowedCod Разрешен наложенный платеж
|
||||||
|
AllowedCod bool `url:"allowed_cod,omitempty"`
|
||||||
|
// IsDressingRoom Наличие примерочной
|
||||||
|
IsDressingRoom bool `url:"is_dressing_room,omitempty"`
|
||||||
|
// WeightMax Максимальный вес в кг, который может принять офис (значения больше 0 - передаются офисы, которые принимают этот вес; 0 - офисы с нулевым весом не передаются; значение не указано - все офисы)
|
||||||
|
WeightMax bool `url:"weight_max,omitempty"`
|
||||||
|
// WeightMin Минимальный вес в кг, который принимает офис (при переданном значении будут выводиться офисы с минимальным весом до указанного значения)
|
||||||
|
WeightMin bool `url:"weight_min,omitempty"`
|
||||||
|
// Lang Локализация офиса. По умолчанию "rus"
|
||||||
|
Lang string `url:"lang,omitempty"`
|
||||||
|
// TakeOnly Является ли офис только пунктом выдачи
|
||||||
|
TakeOnly bool `url:"take_only,omitempty"`
|
||||||
|
// IsHandout Является пунктом выдачи, может принимать значения
|
||||||
|
IsHandout bool `url:"is_handout,omitempty"`
|
||||||
|
// IsReception Есть ли в офисе приём заказов
|
||||||
|
IsReception bool `url:"is_reception,omitempty"`
|
||||||
|
// FiasGuid Код города ФИАС UUID
|
||||||
|
FiasGuid string `url:"fias_guid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) DeliveryPoints(ctx context.Context, input *DeliveryPointsRequest) (*DeliveryPointsResponse, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
c.buildUri("/v2/deliverypoints", input),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := c.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
return jsonReq[DeliveryPointsResponse](req)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_DeliveryPoints(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := c.DeliveryPoints(timedCtx, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
|
||||||
|
resp, err = c.DeliveryPoints(timedCtx, &DeliveryPointsRequest{
|
||||||
|
CountryCode: "ru",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HelperCitiesAll(ctx context.Context, c Client, input *CitiesRequest, first int) (*CitiesResponse, error) {
|
||||||
|
resp := &CitiesResponse{}
|
||||||
|
|
||||||
|
if input == nil {
|
||||||
|
input = &CitiesRequest{Size: 500}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Size > 500 {
|
||||||
|
input.Size = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
chunk, err := c.Cities(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*chunk) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
*resp = append(*resp, *chunk...)
|
||||||
|
|
||||||
|
if len(*resp) >= first {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
input.Page += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_CitiesAll(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := HelperCitiesAll(ctx, c, nil, 100)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *clientImpl) OrderDelete(ctx context.Context, uuid string) (*Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodDelete,
|
||||||
|
c.buildUri(fmt.Sprintf("/v2/orders/%s", uuid), nil),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
accessToken, err := c.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
resp, err := jsonReq[Response](req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateResponse(resp.Requests); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_OrderDelete(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := c.OrderRegister(timedCtx, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, resp)
|
||||||
|
|
||||||
|
uid := uuid.NewString()
|
||||||
|
resp, err = c.OrderRegister(timedCtx, &OrderRegisterRequest{
|
||||||
|
Type: 0,
|
||||||
|
Number: uid,
|
||||||
|
Comment: "test",
|
||||||
|
TariffCode: 62,
|
||||||
|
FromLocation: Location{Code: 44, Address: "qwe"},
|
||||||
|
ToLocation: Location{Code: 287, Address: "qwe"},
|
||||||
|
Sender: RecipientSender{
|
||||||
|
Name: "test",
|
||||||
|
Company: "test",
|
||||||
|
Email: "test@test.com",
|
||||||
|
},
|
||||||
|
Recipient: RecipientSender{
|
||||||
|
Name: "test",
|
||||||
|
Phones: []Phone{
|
||||||
|
{Number: "123"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Packages: []Package{
|
||||||
|
{
|
||||||
|
Number: "test",
|
||||||
|
Weight: 1,
|
||||||
|
Comment: "test",
|
||||||
|
Items: []PackageItem{
|
||||||
|
{
|
||||||
|
Name: "test",
|
||||||
|
WareKey: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
require.Greater(t, len(resp.Requests), 0)
|
||||||
|
|
||||||
|
statusResp, err := c.OrderDelete(ctx, resp.Entity.Uuid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, statusResp.Entity.Uuid, resp.Entity.Uuid)
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderRegisterRequest struct {
|
||||||
|
// Type Тип заказа: 1 - "интернет-магазин" (только для договора типа "Договор с ИМ"), 2 - "доставка" (для любого договора)
|
||||||
|
Type int `json:"type,omitempty"`
|
||||||
|
// Number Только для заказов "интернет-магазин". Номер заказа в ИС Клиента (если не передан, будет присвоен номер заказа в ИС СДЭК - uuid)
|
||||||
|
// Может содержать только цифры, буквы латинского алфавита или спецсимволы (формат ASCII)
|
||||||
|
Number string `json:"number"`
|
||||||
|
// Comment Комментарий к заказу
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
// TariffCode Код тарифа (подробнее см. приложение 1)
|
||||||
|
TariffCode int `json:"tariff_code"`
|
||||||
|
// OrderDeliveryRecipientCost Доп. сбор за доставку, которую ИМ берет с получателя. Только для заказов "интернет-магазин".
|
||||||
|
DeliveryRecipientCost Payment `json:"delivery_recipient_cost"`
|
||||||
|
// DeliveryRecipientCostAdv Доп. сбор за доставку (которую ИМ берет с получателя) в зависимости от суммы заказа
|
||||||
|
// Только для заказов "интернет-магазин". Возможно указать несколько порогов.
|
||||||
|
DeliveryRecipientCostAdv Cost `json:"delivery_recipient_cost_adv"`
|
||||||
|
// FromLocation Адрес отправления. Не может использоваться одновременно с shipment_point
|
||||||
|
FromLocation Location `json:"from_location"`
|
||||||
|
// ToLocation Адрес получения. Не может использоваться одновременно с delivery_point
|
||||||
|
ToLocation Location `json:"to_location"`
|
||||||
|
// Packages Список информации по местам (упаковкам). Количество мест в заказе может быть от 1 до 255
|
||||||
|
Packages []Package `json:"packages,omitempty"`
|
||||||
|
// Recipient Получатель
|
||||||
|
Recipient RecipientSender `json:"recipient"`
|
||||||
|
// Sender Отправитель. Обязателен если:
|
||||||
|
// нет, если заказ типа "интернет-магазин"
|
||||||
|
// да, если заказ типа "доставка"
|
||||||
|
Sender RecipientSender `json:"sender,omitempty"`
|
||||||
|
// Services Дополнительные услуги
|
||||||
|
Services []Service `json:"services,omitempty"`
|
||||||
|
// Seller Реквизиты истинного продавца. Только для заказов "интернет-магазин"
|
||||||
|
Seller Seller `json:"seller,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) OrderRegister(ctx context.Context, input *OrderRegisterRequest) (*Response, error) {
|
||||||
|
payload, err := json.Marshal(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodPost,
|
||||||
|
c.buildUri("/v2/orders", nil),
|
||||||
|
bytes.NewReader(payload),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
accessToken, err := c.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
resp, err := jsonReq[Response](req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateResponse(resp.Requests); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_OrderRegisterStatus(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := c.OrderRegister(timedCtx, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, resp)
|
||||||
|
|
||||||
|
resp, err = c.OrderRegister(timedCtx, &OrderRegisterRequest{
|
||||||
|
Type: 0,
|
||||||
|
Number: uuid.NewString(),
|
||||||
|
Comment: "test",
|
||||||
|
TariffCode: 62,
|
||||||
|
FromLocation: Location{Code: 44, Address: "qwe"},
|
||||||
|
ToLocation: Location{Code: 287, Address: "qwe"},
|
||||||
|
Sender: RecipientSender{
|
||||||
|
Name: "test",
|
||||||
|
Company: "test",
|
||||||
|
Email: "test@test.com",
|
||||||
|
},
|
||||||
|
Recipient: RecipientSender{
|
||||||
|
Name: "test",
|
||||||
|
Phones: []Phone{
|
||||||
|
{Number: "123"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Packages: []Package{
|
||||||
|
{
|
||||||
|
Number: "test",
|
||||||
|
Weight: 1,
|
||||||
|
Comment: "test",
|
||||||
|
Items: []PackageItem{
|
||||||
|
{
|
||||||
|
Name: "test",
|
||||||
|
WareKey: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
require.Greater(t, len(resp.Requests), 0)
|
||||||
|
|
||||||
|
statusResp, err := c.OrderStatus(ctx, resp.Entity.Uuid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, statusResp.Entity.Comment, "test")
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderStatusFailedCall struct {
|
||||||
|
// DateTime Дата и время создания недозвона datetime
|
||||||
|
DateTime string `json:"date_time"`
|
||||||
|
// ReasonCode Причина недозвона (подробнее см. приложение 5)
|
||||||
|
ReasonCode int `json:"reason_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderStatusRescheduledCall struct {
|
||||||
|
// DateTime Дата и время создания переноса прозвона
|
||||||
|
DateTime string `json:"date_time"`
|
||||||
|
// DateNext Дата, на которую согласован повторный прозвон
|
||||||
|
DateNext string `json:"date_next"`
|
||||||
|
// TimeNext Время, на которое согласован повторный прозвон
|
||||||
|
TimeNext string `json:"time_next"`
|
||||||
|
// Comment Комментарий к переносу прозвона
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderStatusCall struct {
|
||||||
|
// FailedCalls Информация о неуспешных прозвонах (недозвонах)
|
||||||
|
FailedCalls OrderStatusFailedCall `json:"failed_calls,omitempty"`
|
||||||
|
// RescheduledCalls Информация о переносах прозвонов
|
||||||
|
RescheduledCalls OrderStatusRescheduledCall `json:"rescheduled_calls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderStatusDeliveryProblem struct {
|
||||||
|
// Code Код проблемы (подробнее см. приложение 4) https://api-docs.cdek.ru/29923975.html
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
// CreateDate Дата создания проблемы
|
||||||
|
CreateDate string `json:"create_date,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderStatusPaymentInfo struct {
|
||||||
|
// Type Тип оплаты: CARD - картой, CASH - наличными
|
||||||
|
Type string `json:"type"`
|
||||||
|
// Sum Сумма в валюте страны получателя
|
||||||
|
Sum float64 `json:"sum"`
|
||||||
|
// DeliverySum Стоимость услуги доставки (по тарифу)
|
||||||
|
DeliverySum float64 `json:"delivery_sum"`
|
||||||
|
// TotalSum Итоговая стоимость заказа
|
||||||
|
TotalSum float64 `json:"total_sum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderStatusDeliveryDetail struct {
|
||||||
|
// Date Дата доставки
|
||||||
|
Date string `json:"date"`
|
||||||
|
// RecipientName получатель при доставке
|
||||||
|
RecipientName string `json:"recipient_name"`
|
||||||
|
// PaymentSum Сумма наложенного платежа, которую взяли с получателя, в валюте страны получателя с учетом частичной доставки
|
||||||
|
PaymentSum float64 `json:"payment_sum,omitempty"`
|
||||||
|
// PaymentInfo Тип оплаты наложенного платежа получателем
|
||||||
|
PaymentInfo []OrderStatusPaymentInfo `json:"payment_info,omitempty"`
|
||||||
|
// DeliverySum Стоимость услуги доставки (по тарифу)
|
||||||
|
DeliverySum float64 `json:"delivery_sum"`
|
||||||
|
TotalSum float64 `json:"total_sum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderStatusInfo struct {
|
||||||
|
// Code Код статуса (подробнее см. приложение 1)
|
||||||
|
Code string `json:"code"`
|
||||||
|
// Name Название статуса
|
||||||
|
Name string `json:"name"`
|
||||||
|
// DateTime Дата и время установки статуса (формат yyyy-MM-dd'T'HH:mm:ssZ)
|
||||||
|
DateTime string `json:"date_time"`
|
||||||
|
// ReasonCode Дополнительный код статуса (подробнее см. приложение 2)
|
||||||
|
ReasonCode string `json:"reason_code,omitempty"`
|
||||||
|
// City Наименование места возникновения статуса
|
||||||
|
City string `json:"city"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderStatusEntity struct {
|
||||||
|
// Uuid Идентификатор заказа в ИС СДЭК
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
// IsReturn Признак возвратного заказа: true - возвратный, false - прямой
|
||||||
|
IsReturn bool `json:"is_return"`
|
||||||
|
// IsReverse Признак реверсного заказа: true - реверсный, false - не реверсный
|
||||||
|
IsReverse bool `json:"is_reverse"`
|
||||||
|
// Type Тип заказа: 1 - "интернет-магазин" (только для договора типа "Договор с ИМ"), 2 - "доставка" (для любого договора)
|
||||||
|
Type int `json:"type"`
|
||||||
|
// CdekNumber Номер заказа СДЭК
|
||||||
|
CdekNumber string `json:"cdek_number,omitempty"`
|
||||||
|
// Number Номер заказа в ИС Клиента. При запросе информации по данному полю возможны варианты:
|
||||||
|
// - если не передан, будет присвоен номер заказа в ИС СДЭК - uuid;
|
||||||
|
// - если найдено больше 1, то выбирается созданный с самой последней датой.
|
||||||
|
// Может содержать только цифры, буквы латинского алфавита или спецсимволы (формат ASCII)
|
||||||
|
Number string `json:"number,omitempty"`
|
||||||
|
// DeliveryMode Истинный режим заказа:
|
||||||
|
// 1 - дверь-дверь
|
||||||
|
// 2 - дверь-склад
|
||||||
|
// 3 - склад-дверь
|
||||||
|
// 4 - склад-склад
|
||||||
|
// 6 - дверь-постамат
|
||||||
|
// 7 - склад-постамат
|
||||||
|
DeliveryMode string `json:"delivery_mode"`
|
||||||
|
//// TariffCode Код тарифа
|
||||||
|
//TariffCode int `json:"tariff_code"`
|
||||||
|
// Comment Комментарий к заказу
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
// DeveloperKey Ключ разработчика
|
||||||
|
DeveloperKey string `json:"developer_key,omitempty"`
|
||||||
|
// ShipmentPoint Код ПВЗ СДЭК, на который будет производиться самостоятельный привоз клиентом
|
||||||
|
ShipmentPoint string `json:"shipment_point,omitempty"`
|
||||||
|
// DeliveryPoint Код офиса СДЭК (ПВЗ/постамат), на который будет доставлена посылка
|
||||||
|
DeliveryPoint string `json:"delivery_point,omitempty"`
|
||||||
|
// DateInvoice Дата инвойса. Только для международных заказов. date (yyyy-MM-dd)
|
||||||
|
DateInvoice string `json:"date_invoice,omitempty"`
|
||||||
|
// ShipperName Грузоотправитель. Только для международных заказов
|
||||||
|
ShipperName string `json:"shipper_name,omitempty"`
|
||||||
|
// ShipperAddress Адрес грузоотправителя. Только для международных заказов
|
||||||
|
ShipperAddress string `json:"shipper_address,omitempty"`
|
||||||
|
// DeliveryRecipientCost Доп. сбор за доставку, которую ИМ берет с получателя.
|
||||||
|
DeliveryRecipientCost Payment `json:"delivery_recipient_cost,omitempty"`
|
||||||
|
// DeliveryRecipientCostAdv Доп. сбор за доставку (которую ИМ берет с получателя), в зависимости от суммы заказа
|
||||||
|
DeliveryRecipientCostAdv []Cost `json:"delivery_recipient_cost_adv,omitempty"`
|
||||||
|
// Sender Отправитель
|
||||||
|
Sender RecipientSender `json:"sender"`
|
||||||
|
// Seller Реквизиты истинного продавца
|
||||||
|
Seller Seller `json:"seller,omitempty"`
|
||||||
|
// Recipient Получатель
|
||||||
|
Recipient RecipientSender `json:"recipient,omitempty"`
|
||||||
|
// FromLocation Адрес отправления. Не может использоваться одновременно с shipment_point
|
||||||
|
FromLocation Location `json:"from_location"`
|
||||||
|
// ToLocation Адрес получения. Не может использоваться одновременно с delivery_point
|
||||||
|
ToLocation Location `json:"to_location"`
|
||||||
|
// ItemsCostCurrency TODO
|
||||||
|
ItemsCostCurrency string `json:"items_cost_currency"`
|
||||||
|
// RecipientCurrency TODO
|
||||||
|
RecipientCurrency string `json:"recipient_currency"`
|
||||||
|
// Services Дополнительные услуги
|
||||||
|
Services []Service `json:"services,omitempty"`
|
||||||
|
// Packages Список информации по местам (упаковкам)
|
||||||
|
Packages []Package `json:"packages"`
|
||||||
|
// DeliveryProblem Проблемы доставки, с которыми столкнулся курьер при доставке заказа "до двери"
|
||||||
|
DeliveryProblem []OrderStatusDeliveryProblem `json:"delivery_problem,omitempty"`
|
||||||
|
// DeliveryDetail Информация о вручении
|
||||||
|
DeliveryDetail OrderStatusDeliveryDetail `json:"delivery_detail,omitempty"`
|
||||||
|
// TransactedPayment Признак того, что по заказу была получена информация о переводе наложенного платежа интернет-магазину
|
||||||
|
TransactedPayment bool `json:"transacted_payment,omitempty"`
|
||||||
|
// Statuses Список статусов по заказу, отсортированных по дате и времени
|
||||||
|
Statuses []OrderStatusInfo `json:"statuses"`
|
||||||
|
// Calls Информация о прозвонах получателя
|
||||||
|
Calls []OrderStatusCall `json:"calls,omitempty"`
|
||||||
|
// @todo ticket SD-735298 - this is not documented but exists in example response https://api-docs.cdek.ru/29923975.html
|
||||||
|
DeliveryDate string `json:"delivery_date,omitempty"`
|
||||||
|
ShopSellerName string `json:"shop_seller_name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) OrderStatus(ctx context.Context, uuid string) (*Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
c.buildUri(fmt.Sprintf("/v2/orders/%s", uuid), nil),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
accessToken, err := c.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
return jsonReq[Response](req)
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderUpdateRequest struct {
|
||||||
|
// UUID Идентификатор заказа в ИС СДЭК, который нужно изменить (да, если не заполнен cdek_number)
|
||||||
|
UUID string `json:"uuid,omitempty"`
|
||||||
|
// CdekNumber Номер заказа СДЭК, который нужно изменить (да, если не заполнен uuid)
|
||||||
|
CdekNumber string `json:"cdek_number,omitempty"`
|
||||||
|
// Код тарифа (режимы старого и нового тарифа должны совпадать)
|
||||||
|
TariffCode int `json:"tariff_code,omitempty"`
|
||||||
|
// Comment Комментарий к заказу
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
// ShipmentPoint Код ПВЗ СДЭК, на который будет производится забор отправления либо самостоятельный привоз клиентом. Не может использоваться одновременно с from_location
|
||||||
|
ShipmentPoint string `json:"shipment_point,omitempty"`
|
||||||
|
// DeliveryPoint Код ПВЗ СДЭК, на который будет доставлена посылка. Не может использоваться одновременно с to_location
|
||||||
|
DeliveryPoint string `json:"delivery_point,omitempty"`
|
||||||
|
// OrderDeliveryRecipientCost Доп. сбор за доставку, которую ИМ берет с получателя. Валюта сбора должна совпадать с валютой наложенного платежа
|
||||||
|
DeliveryRecipientCost Payment `json:"delivery_recipient_cost"`
|
||||||
|
// DeliveryRecipientCostAdv Доп. сбор за доставку (которую ИМ берет с получателя) в зависимости от суммы заказа. Только для заказов "интернет-магазин". Возможно указать несколько порогов.
|
||||||
|
DeliveryRecipientCostAdv Cost `json:"delivery_recipient_cost_adv"`
|
||||||
|
// Sender Отправитель. Обязателен если:
|
||||||
|
// нет, если заказ типа "интернет-магазин"
|
||||||
|
// да, если заказ типа "доставка"
|
||||||
|
Sender RecipientSender `json:"sender,omitempty"`
|
||||||
|
// Seller Реквизиты истинного продавца
|
||||||
|
Seller Seller `json:"seller,omitempty"`
|
||||||
|
// Recipient Получатель
|
||||||
|
Recipient RecipientSender `json:"recipient,omitempty"`
|
||||||
|
// ToLocation Адрес получения. Не может использоваться одновременно с delivery_point
|
||||||
|
ToLocation Location `json:"to_location"`
|
||||||
|
// FromLocation Адрес отправления. Не может использоваться одновременно с shipment_point
|
||||||
|
FromLocation Location `json:"from_location"`
|
||||||
|
// Services Дополнительные услуги
|
||||||
|
Services []Service `json:"services,omitempty"`
|
||||||
|
// Packages Список информации по местам (упаковкам)
|
||||||
|
Packages []Package `json:"packages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderUpdateResponse struct {
|
||||||
|
Entity ResponseEntity `json:"entity,omitempty"`
|
||||||
|
Requests []ResponseRequests `json:"requests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) OrderUpdate(ctx context.Context, input *OrderUpdateRequest) (*OrderUpdateResponse, error) {
|
||||||
|
payload, err := json.Marshal(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodPost,
|
||||||
|
c.buildUri("/v2/orders", nil),
|
||||||
|
bytes.NewReader(payload),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
accessToken, err := c.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
resp, err := jsonReq[OrderUpdateResponse](req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateResponse(resp.Requests); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_OrderUpdate(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := c.OrderRegister(timedCtx, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, resp)
|
||||||
|
|
||||||
|
registerReq := &OrderRegisterRequest{
|
||||||
|
Type: 0,
|
||||||
|
Number: uuid.NewString(),
|
||||||
|
Comment: "test",
|
||||||
|
TariffCode: 62,
|
||||||
|
FromLocation: Location{Code: 44, Address: "qwe"},
|
||||||
|
ToLocation: Location{Code: 287, Address: "qwe"},
|
||||||
|
Sender: RecipientSender{
|
||||||
|
Name: "test",
|
||||||
|
Company: "test",
|
||||||
|
Email: "test@test.com",
|
||||||
|
},
|
||||||
|
Recipient: RecipientSender{
|
||||||
|
Name: "test",
|
||||||
|
Phones: []Phone{
|
||||||
|
{Number: "123"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Packages: []Package{
|
||||||
|
{
|
||||||
|
Number: "test",
|
||||||
|
Weight: 1,
|
||||||
|
Comment: "test",
|
||||||
|
Items: []PackageItem{
|
||||||
|
{
|
||||||
|
Name: "test",
|
||||||
|
WareKey: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err = c.OrderRegister(timedCtx, registerReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
require.Greater(t, len(resp.Requests), 0)
|
||||||
|
|
||||||
|
updateResp, err := c.OrderUpdate(ctx, &OrderUpdateRequest{
|
||||||
|
UUID: resp.Entity.Uuid,
|
||||||
|
Comment: "updated",
|
||||||
|
ToLocation: registerReq.ToLocation,
|
||||||
|
Recipient: registerReq.Recipient,
|
||||||
|
TariffCode: registerReq.TariffCode,
|
||||||
|
Packages: registerReq.Packages,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
statusResp, err := c.OrderStatus(ctx, updateResp.Entity.Uuid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, statusResp.Entity.Comment, "updated")
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
CDEK changelog:
|
||||||
|
|
||||||
|
https://api-docs.cdek.ru/36967918.html
|
|
@ -0,0 +1,48 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegionsRequest struct {
|
||||||
|
// CountryCodes Массив кодов стран в формате ISO_3166-1_alpha-2
|
||||||
|
CountryCodes []string `url:"country_codes,omitempty"`
|
||||||
|
// Size Ограничение выборки результата. По умолчанию 1000
|
||||||
|
Size int `url:"size,omitempty"`
|
||||||
|
// Page Номер страницы выборки результата. По умолчанию 0
|
||||||
|
Page int `url:"page,omitempty"`
|
||||||
|
// Lang Локализация офиса. По умолчанию "rus"
|
||||||
|
Lang string `url:"lang,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegionsResponse []Region
|
||||||
|
|
||||||
|
type Region struct {
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
RegionCode int `json:"region_code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientImpl) Regions(ctx context.Context, input *RegionsRequest) (*RegionsResponse, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
c.buildUri("/v2/location/regions", input),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := c.getAccessToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||||
|
|
||||||
|
return jsonReq[RegionsResponse](req)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientImpl_Regions(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := createTestClient()
|
||||||
|
|
||||||
|
resp, err := c.Regions(timedCtx, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
|
||||||
|
resp, err = c.Regions(timedCtx, &RegionsRequest{Page: 1000, Size: 1000})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
}
|
|
@ -0,0 +1,233 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
// ResponseEntity Информация о заказе
|
||||||
|
type ResponseEntity struct {
|
||||||
|
// Uuid Идентификатор заказа в ИС СДЭК
|
||||||
|
Uuid string `json:"uuid,omitempty"`
|
||||||
|
// Comment комментарий
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseErr struct {
|
||||||
|
// Message Описание ошибки
|
||||||
|
Message string `json:"message"`
|
||||||
|
// Code string Код ошибки
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseRequests Информация о запросе над заказом
|
||||||
|
type ResponseRequests struct {
|
||||||
|
// RequestUuid Идентификатор запроса в ИС СДЭК
|
||||||
|
RequestUuid string `json:"request_uuid,omitempty"`
|
||||||
|
// Type Тип запроса. Может принимать значения: CREATE, UPDATE, DELETE, AUTH, GET
|
||||||
|
Type string `json:"type"`
|
||||||
|
// State Текущее состояние запроса. Может принимать значения:
|
||||||
|
// ACCEPTED - пройдена предварительная валидация и запрос принят
|
||||||
|
// WAITING - запрос ожидает обработки (зависит от выполнения другого запроса)
|
||||||
|
// SUCCESSFUL - запрос обработан успешно
|
||||||
|
// INVALID - запрос обработался с ошибкой
|
||||||
|
State string `json:"state"`
|
||||||
|
// DateTime Дата и время установки текущего состояния запроса (формат yyyy-MM-dd'T'HH:mm:ssZ)
|
||||||
|
DateTime string `json:"date_time"`
|
||||||
|
// Errors Ошибки, возникшие в ходе выполнения запроса
|
||||||
|
Errors []ResponseErr `json:"errors,omitempty"`
|
||||||
|
// Warnings Предупреждения, возникшие в ходе выполнения запроса
|
||||||
|
Warnings []ResponseErr `json:"warnings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseRelatedEntities Связанные сущности (если в запросе был передан корректный print)
|
||||||
|
type ResponseRelatedEntities struct {
|
||||||
|
// Type Тип связанной сущности. Может принимать значения: waybill - квитанция к заказу, barcode - ШК места к заказу
|
||||||
|
Type string `json:"type"`
|
||||||
|
// Uuid Идентификатор сущности, связанной с заказом
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
// Url Ссылка на скачивание печатной формы в статусе "Сформирован", только для type = waybill, barcode
|
||||||
|
Url string `json:"url,omitempty"`
|
||||||
|
// CdekNumber Номер заказа СДЭК. Может возвращаться для return_order, direct_order, reverse_order
|
||||||
|
CdekNumber string `json:"cdek_number,omitempty"`
|
||||||
|
// Date Дата доставки, согласованная с получателем. Только для типа delivery
|
||||||
|
Date string `json:"date,omitempty"`
|
||||||
|
// TimeFrom Время начала ожидания курьера (согласованное с получателем). Только для типа delivery
|
||||||
|
TimeFrom string `json:"time_from,omitempty"`
|
||||||
|
// Date Время окончания ожидания курьера (согласованное с получателем). Только для типа delivery
|
||||||
|
TimeTo string `json:"time_to,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Entity ResponseEntity `json:"entity,omitempty"`
|
||||||
|
Requests []ResponseRequests `json:"requests"`
|
||||||
|
RelatedEntities *ResponseRelatedEntities `json:"related_entities,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
// Code Код населенного пункта СДЭК (метод "Список населенных пунктов")
|
||||||
|
Code int `json:"code,omitempty"`
|
||||||
|
// FiasGuid Уникальный идентификатор ФИАС UUID
|
||||||
|
FiasGuid string `json:"fias_guid,omitempty"`
|
||||||
|
// PostalCode Почтовый индекс
|
||||||
|
PostalCode string `json:"postal_code,omitempty"`
|
||||||
|
// Longitude Долгота
|
||||||
|
Longitude float64 `json:"longitude,omitempty"`
|
||||||
|
// Latitude Широта
|
||||||
|
Latitude float64 `json:"latitude,omitempty"`
|
||||||
|
// CountryCode
|
||||||
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
|
// Region Название региона
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
// RegionCode Код региона СДЭК
|
||||||
|
RegionCode int `json:"region_code,omitempty"`
|
||||||
|
// SubRegion Название района региона
|
||||||
|
SubRegion string `json:"sub_region,omitempty"`
|
||||||
|
// City Название города
|
||||||
|
City string `json:"city,omitempty"`
|
||||||
|
// Address Строка адреса
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Package struct {
|
||||||
|
// Number Номер упаковки (можно использовать порядковый номер упаковки заказа или номер заказа), уникален в пределах заказа. Идентификатор заказа в ИС Клиента
|
||||||
|
Number string `json:"number"`
|
||||||
|
// Weight Общий вес (в граммах)
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
// Comment Комментарий к упаковке. Обязательно и только для заказа типа "доставка"
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
// Height Габариты упаковки. Высота (в сантиметрах). Поле обязательно если:
|
||||||
|
// если указаны остальные габариты
|
||||||
|
// если заказ до постамата
|
||||||
|
// если общий вес >=100 гр
|
||||||
|
Height int `json:"height,omitempty"`
|
||||||
|
// Length Габариты упаковки. Длина (в сантиметрах). Поле обязательно если:
|
||||||
|
// если указаны остальные габариты
|
||||||
|
// если заказ до постамата
|
||||||
|
// если общий вес >=100 гр
|
||||||
|
Length int `json:"length,omitempty"`
|
||||||
|
// Width Габариты упаковки. Ширина (в сантиметрах). Поле обязательно если:
|
||||||
|
// если указаны остальные габариты
|
||||||
|
// если заказ до постамата
|
||||||
|
// если общий вес >=100 гр
|
||||||
|
Width int `json:"width,omitempty"`
|
||||||
|
// Items Позиции товаров в упаковке. Только для заказов "интернет-магазин". Максимум 126 уникальных позиций в заказе. Общее количество товаров в заказе может быть от 1 до 10000
|
||||||
|
Items []PackageItem `json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PackageItem struct {
|
||||||
|
// Name Наименование товара (может также содержать описание товара: размер, цвет)
|
||||||
|
Name string `json:"name"`
|
||||||
|
// WareKey Идентификатор/артикул товара. Артикул товара может содержать только символы: [A-z А-я 0-9 ! @ " # № $ ; % ^ : & ? * () _ - + = ? < > , .{ } [ ] \ / , пробел]
|
||||||
|
WareKey string `json:"ware_key"`
|
||||||
|
// Marking Маркировка товара. Если для товара/вложения указана маркировка, Amount не может быть больше 1.
|
||||||
|
// Для корректного отображения маркировки товара в чеке требуется передавать НЕ РАЗОБРАННЫЙ тип маркировки, который может выглядеть следующим образом:
|
||||||
|
// 1) Код товара в формате GS1. Пример: 010468008549838921AAA0005255832GS91EE06GS92VTwGVc7wKCc2tqRncUZ1RU5LeUKSXjWbfNQOpQjKK+A
|
||||||
|
// 2) Последовательность допустимых символов общей длиной в 29 символов. Пример: 00000046198488X?io+qCABm8wAYa
|
||||||
|
// 3) Меховые изделия. Имеют собственный формат. Пример: RU-430302-AAA7582720
|
||||||
|
Marking string `json:"marking,omitempty"`
|
||||||
|
// Payment Оплата за товар при получении (за единицу товара в валюте страны получателя, значение >=0) — наложенный платеж, в случае предоплаты значение = 0
|
||||||
|
Payment Payment `json:"payment"`
|
||||||
|
// Cost Объявленная стоимость товара (за единицу товара в валюте взаиморасчетов, значение >=0). С данного значения рассчитывается страховка
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
// Amount Количество единиц товара (в штуках). Количество одного товара в заказе может быть от 1 до 999
|
||||||
|
Amount int `json:"amount"`
|
||||||
|
// NameI18N Наименование на иностранном языке. Только для международных заказов
|
||||||
|
NameI18N string `json:"name_i18n,omitempty"`
|
||||||
|
// Brand Бренд на иностранном языке. Только для международных заказов
|
||||||
|
Brand string `json:"brand,omitempty"`
|
||||||
|
// CountryCode Бренд на иностранном языке. Только для международных заказов
|
||||||
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
|
// Weight Вес (за единицу товара, в граммах)
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
// WeightGross Вес брутто. Только для международных заказов
|
||||||
|
WeightGross int `json:"weight_gross,omitempty"`
|
||||||
|
// Material Код материала (подробнее см. приложение 4). Только для международных заказов
|
||||||
|
Material string `json:"material,omitempty"`
|
||||||
|
// WifiGsm Содержит wifi/gsm. Только для международных заказов
|
||||||
|
WifiGsm bool `json:"wifi_gsm,omitempty"`
|
||||||
|
// Url Ссылка на сайт интернет-магазина с описанием товара. Только для международных заказов
|
||||||
|
Url string `json:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payment struct {
|
||||||
|
// Value Сумма наложенного платежа (в случае предоплаты = 0)
|
||||||
|
Value int `json:"value"`
|
||||||
|
// VatSum Сумма НДС
|
||||||
|
VatSum int `json:"vat_sum,omitempty"`
|
||||||
|
// VatRate Ставка НДС (значение - 0, 10, 20, null - нет НДС)
|
||||||
|
VatRate int `json:"vat_rate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cost struct {
|
||||||
|
// Sum Доп. сбор за доставку товаров, общая стоимость которых попадает в интервал
|
||||||
|
Sum int `json:"sum"`
|
||||||
|
// Threshold Порог стоимости товара (действует по условию меньше или равно) в целых единицах валюты
|
||||||
|
Threshold int `json:"threshold"`
|
||||||
|
// VatSum Сумма НДС
|
||||||
|
VatSum int `json:"vat_sum,omitempty"`
|
||||||
|
// VatRate Ставка НДС (значение - 0, 10, 20, null - нет НДС)
|
||||||
|
VatRate int `json:"vat_rate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Phone struct {
|
||||||
|
// Number Номер телефона. Должен передаваться в международном формате: код страны (для России +7) и сам номер (10 и более цифр)
|
||||||
|
// Обязателен если: нет, если заказ типа "интернет-магазин". да, если заказ типа "доставка"
|
||||||
|
Number string `json:"number,omitempty"`
|
||||||
|
// Additional Дополнительная информация (добавочный номер)
|
||||||
|
Additional string `json:"additional,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecipientSender struct {
|
||||||
|
// Name нет, если заказ типа "интернет-магазин"; да, если заказ типа "доставка"
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
// Company Название компании. нет, если заказ типа "интернет-магазин"; да, если заказ типа "доставка"
|
||||||
|
Company string `json:"company,omitempty"`
|
||||||
|
// Email Эл. адрес. нет, если заказ типа "интернет-магазин"; да, если заказ типа "доставка"
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
// PassportSeries Серия паспорта
|
||||||
|
PassportSeries string `json:"passport_series,omitempty"`
|
||||||
|
// PassportNumber Номер паспорта
|
||||||
|
PassportNumber string `json:"passport_number,omitempty"`
|
||||||
|
// PassportDateOfIssue Дата выдачи паспорта
|
||||||
|
PassportDateOfIssue string `json:"passport_date_of_issue,omitempty"`
|
||||||
|
// PassportOrganization Орган выдачи паспорта
|
||||||
|
PassportOrganization string `json:"passport_organization,omitempty"`
|
||||||
|
// Tin ИНН Может содержать 10, либо 12 символов
|
||||||
|
Tin string `json:"tin,omitempty"`
|
||||||
|
// PassportDateOfBirth Дата рождения (yyyy-MM-dd)
|
||||||
|
PassportDateOfBirth string `json:"passport_date_of_birth,omitempty"`
|
||||||
|
// PassportRequirementsSatisfied Требования по паспортным данным удовлетворены (актуально для
|
||||||
|
// международных заказов):
|
||||||
|
// true - паспортные данные собраны или не требуются
|
||||||
|
// false - паспортные данные требуются и не собраны
|
||||||
|
PassportRequirementsSatisfied bool `json:"passport_requirements_satisfied,omitempty"`
|
||||||
|
// Phones Список телефонов, Не более 10 номеров
|
||||||
|
Phones []Phone `json:"phones,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Seller struct {
|
||||||
|
// Name Наименование истинного продавца. Обязателен если заполнен inn
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
// INN ИНН истинного продавца. Может содержать 10, либо 12 символов
|
||||||
|
INN string `json:"inn,omitempty"`
|
||||||
|
// Phone Телефон истинного продавца. Обязателен если заполнен inn
|
||||||
|
Phone string `json:"phone,omitempty"`
|
||||||
|
// OwnershipForm Код формы собственности (подробнее см. приложение 2). Обязателен если заполнен inn
|
||||||
|
OwnershipForm int `json:"ownership_form,omitempty"`
|
||||||
|
// Address Адрес истинного продавца. Используется при печати инвойсов для отображения адреса настоящего
|
||||||
|
// продавца товара, либо торгового названия. Только для международных заказов "интернет-магазин".
|
||||||
|
// Обязателен если заказ - международный
|
||||||
|
Address string `json:"address,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
// Code Тип дополнительной услуги (подробнее см. приложение 3)
|
||||||
|
Code string `json:"code"`
|
||||||
|
// Parameter Параметр дополнительной услуги:
|
||||||
|
// количество для услуг
|
||||||
|
// PACKAGE_1, COURIER_PACKAGE_A2, SECURE_PACKAGE_A2, SECURE_PACKAGE_A3, SECURE_PACKAGE_A4,
|
||||||
|
// SECURE_PACKAGE_A5, CARTON_BOX_XS, CARTON_BOX_S, CARTON_BOX_M, CARTON_BOX_L, CARTON_BOX_500GR,
|
||||||
|
// CARTON_BOX_1KG, CARTON_BOX_2KG, CARTON_BOX_3KG, CARTON_BOX_5KG, CARTON_BOX_10KG, CARTON_BOX_15KG,
|
||||||
|
// CARTON_BOX_20KG, CARTON_BOX_30KG, CARTON_FILLER (для всех типов заказа)
|
||||||
|
// объявленная стоимость заказа для услуги INSURANCE (только для заказов с типом "доставка")
|
||||||
|
// длина для услуг BUBBLE_WRAP, WASTE_PAPER (для всех типов заказа)
|
||||||
|
// номер телефона для услуги SMS
|
||||||
|
// код фотопроекта для услуги PHOTO_DOCUMENT
|
||||||
|
Parameter string `json:"parameter,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/ernesto-jimenez/httplogger"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateResponse(requests []ResponseRequests) error {
|
||||||
|
var result error
|
||||||
|
for _, item := range requests {
|
||||||
|
if item.State == "INVALID" {
|
||||||
|
result = multierror.Append(result, fmt.Errorf("%+v", item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = http.Client{
|
||||||
|
Transport: httplogger.NewLoggedTransport(http.DefaultTransport, newLogger()),
|
||||||
|
}
|
||||||
|
|
||||||
|
type RespErrors struct {
|
||||||
|
Errors []struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonReq[T any](req *http.Request) (*T, error) {
|
||||||
|
response, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "http.DefaultClient.Do")
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
var s T
|
||||||
|
payload, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "ioutil.ReadAll")
|
||||||
|
}
|
||||||
|
|
||||||
|
var respErr RespErrors
|
||||||
|
if err := json.Unmarshal(payload, &respErr); err == nil && len(respErr.Errors) > 0 {
|
||||||
|
return nil, fmt.Errorf("json error: %v", respErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(payload, &s); err != nil {
|
||||||
|
return nil, fmt.Errorf("json error: %s", payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpLogger struct {
|
||||||
|
log *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogger() *httpLogger {
|
||||||
|
return &httpLogger{
|
||||||
|
log: log.New(os.Stderr, "log - ", log.LstdFlags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *httpLogger) LogRequest(req *http.Request) {
|
||||||
|
if req.Body != nil {
|
||||||
|
body, _ := ioutil.ReadAll(req.Body)
|
||||||
|
req.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||||
|
|
||||||
|
l.log.Printf(
|
||||||
|
"Request %s %s %s",
|
||||||
|
req.Method,
|
||||||
|
req.URL.String(),
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
l.log.Printf(
|
||||||
|
"Request %s %s",
|
||||||
|
req.Method,
|
||||||
|
req.URL.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *httpLogger) LogResponse(req *http.Request, res *http.Response, err error, duration time.Duration) {
|
||||||
|
duration /= time.Millisecond
|
||||||
|
if err != nil {
|
||||||
|
l.log.Println(err)
|
||||||
|
} else {
|
||||||
|
l.log.Printf(
|
||||||
|
"Response method=%s status=%d durationMs=%d %s",
|
||||||
|
req.Method,
|
||||||
|
res.StatusCode,
|
||||||
|
duration,
|
||||||
|
req.URL.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue