hotfix/hotfix-mysql-error
Ernest Litvinenko 2024-05-03 19:02:41 +10:00
parent cb83eb5ff5
commit 3eea5e0579
30 changed files with 1658 additions and 0 deletions

12
CDEK/.gitignore vendored Normal file
View File

@ -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

13
CDEK/CONTRIBUTING.md Normal file
View File

@ -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.

21
CDEK/LICENSE Normal file
View File

@ -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.

44
CDEK/README.md Normal file
View File

@ -0,0 +1,44 @@
# GO SDK for CDEK API v2
[![GoDoc reference](https://godoc.org/github.com/vseinstrumentiru/CDEK?status.svg)](https://godoc.org/github.com/vseinstrumentiru/CDEK)
[![Build Status](https://travis-ci.com/vseinstrumentiru/CDEK.svg?branch=master)](https://travis-ci.com/vseinstrumentiru/CDEK)
[![Coverage Status](https://coveralls.io/repos/github/vseinstrumentiru/CDEK/badge.svg?branch=travis)](https://coveralls.io/github/vseinstrumentiru/CDEK?branch=travis)
[![Go Report Card](https://goreportcard.com/badge/github.com/vseinstrumentiru/CDEK)](https://goreportcard.com/report/github.com/vseinstrumentiru/CDEK)
[![GitHub release](https://img.shields.io/github/release/vseinstrumentiru/cdek.svg)](https://github.com/vseinstrumentiru/CDEK/releases)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2990/badge)](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",
})
```

19
CDEK/ROADMAP.md Normal file
View File

@ -0,0 +1,19 @@
- [ ] расчёт тарифов и обращения к справочникам
- [X] расчёт стоимости доставки по тарифам с приоритетом
- [ ] расчёт стоимости по тарифам без приоритета
- [X] получение списка пунктов выдачи заказов (ПВЗ) с фильтрацией
- [X] получение списка регионов-субъектов РФ
- [X] получение списка городов
- [ ] управление заказами
- [X] формирование новых заказов от ИМ
- [ ] оформление заказов на доставку
- [ ] получение квитанции в PDF
- [ ] получение почтовых этикеток в PDF
- [X] удаление заказов
- [X] изменение заказов
- [ ] получение информации по заказам (отчёт «Информация по заказам»)
- [ ] трекинг заказов (отчёт «Статусы заказов»)
- [ ] прозвон получателя
- [ ] вызов курьера
- [ ] создание преалерта
- [X] выбор базового URL интерфейса

70
CDEK/v2/auth.go Normal file
View File

@ -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)
}

20
CDEK/v2/auth_test.go Normal file
View File

@ -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)
}

67
CDEK/v2/calculator.go Normal file
View File

@ -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)
}

View File

@ -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)
}

69
CDEK/v2/cities.go Normal file
View File

@ -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)
}

24
CDEK/v2/cities_test.go Normal file
View File

@ -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)
}

45
CDEK/v2/client.go Normal file
View File

@ -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(),
), "?")
}

31
CDEK/v2/client_test.go Normal file
View File

@ -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,
},
})
}

4
CDEK/v2/consts.go Normal file
View File

@ -0,0 +1,4 @@
package v2
const EndpointTest = "https://api.edu.cdek.ru"
const EndpointProd = "https://api.cdek.ru"

122
CDEK/v2/deliveryPoints.go Normal file
View File

@ -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)
}

View File

@ -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)
}

38
CDEK/v2/helper.go Normal file
View File

@ -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
}

17
CDEK/v2/helper_test.go Normal file
View File

@ -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)
}

38
CDEK/v2/orderDelete.go Normal file
View File

@ -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
}

View File

@ -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)
}

78
CDEK/v2/orderRegister.go Normal file
View File

@ -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
}

View File

@ -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")
}

176
CDEK/v2/orderStatus.go Normal file
View File

@ -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)
}

85
CDEK/v2/orderUpdate.go Normal file
View File

@ -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
}

View File

@ -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")
}

3
CDEK/v2/readme.md Normal file
View File

@ -0,0 +1,3 @@
CDEK changelog:
https://api-docs.cdek.ru/36967918.html

48
CDEK/v2/regions.go Normal file
View File

@ -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)
}

24
CDEK/v2/regions_test.go Normal file
View File

@ -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)
}

233
CDEK/v2/types.go Normal file
View File

@ -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"`
}

107
CDEK/v2/utils.go Normal file
View File

@ -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(),
)
}
}