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