hotfix/order-create #6

Open
nzb3 wants to merge 10 commits from hotfix/order-create into master
87 changed files with 3801 additions and 632 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(),
)
}
}

View File

@ -7,14 +7,14 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"io"
"log"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time" "time"
) )
var EP = os.Getenv("BITRIX_API_EP")
type ClientsResource struct { type ClientsResource struct {
EntityId string `json:"entityId"` EntityId string `json:"entityId"`
} }
@ -166,7 +166,10 @@ type BasketResource struct {
} `json:"properties"` } `json:"properties"`
} }
type bitrix struct{} type bitrix struct {
EP string
EPCustom string
}
type Bitrix interface { type Bitrix interface {
CreateAnonymousUser() (int, error) CreateAnonymousUser() (int, error)
@ -175,8 +178,9 @@ type Bitrix interface {
CancelOrder(orderId int) error CancelOrder(orderId int) error
GetOrderInfo(orderId int) (*OrderResource, error) GetOrderInfo(orderId int) (*OrderResource, error)
CreatePayment(orderId int, sum float64) error CreatePayment(orderId int, sum float64) error
AddProductToOrder(orderId int, productId int, price float64, quantity int) error AddProductToOrder(orderId int, productId int, price float64, quantity int, productName string) error
UpdateContact(contactId int, email string, name string, phone string) error UpdateContact(contactId int, email string, name string, phone string) error
GetTotalForProduct(fuserId int, coupon *string) (*GetTotalOrderResponse, error)
} }
type createAnonymousUserRequest struct { type createAnonymousUserRequest struct {
@ -191,7 +195,7 @@ type createAnonymousUserResponse struct {
Result int `json:"result"` Result int `json:"result"`
} }
func (_ bitrix) CreateAnonymousUser() (int, error) { func (b bitrix) CreateAnonymousUser() (int, error) {
uid, _ := uuid.NewUUID() uid, _ := uuid.NewUUID()
req := createAnonymousUserRequest{ req := createAnonymousUserRequest{
Email: fmt.Sprintf("anonymous%s@anonym.ru", uid.String()), Email: fmt.Sprintf("anonymous%s@anonym.ru", uid.String()),
@ -203,7 +207,7 @@ func (_ bitrix) CreateAnonymousUser() (int, error) {
query, _ := json.Marshal(req) query, _ := json.Marshal(req)
resp, err := http.Post(EP+"/user.add", "application/json", bytes.NewBuffer(query)) resp, err := http.Post(b.EP+"/user.add", "application/json", bytes.NewBuffer(query))
result := createAnonymousUserResponse{} result := createAnonymousUserResponse{}
@ -231,7 +235,12 @@ type createOrderResponse struct {
} `json:"result"` } `json:"result"`
} }
func (_ bitrix) CreateOrder(userId int) (int, error) { type GetTotalOrderResponse struct {
Price float64 `json:"price"`
BasePrice float64 `json:"basePrice"`
}
func (b bitrix) CreateOrder(userId int) (int, error) {
req := createOrderRequestWrapper{createOrderRequest{ req := createOrderRequestWrapper{createOrderRequest{
Lid: "s2", Lid: "s2",
PersonTypeId: 5, PersonTypeId: 5,
@ -241,7 +250,7 @@ func (_ bitrix) CreateOrder(userId int) (int, error) {
query, _ := json.Marshal(req) query, _ := json.Marshal(req)
resp, _ := http.Post(EP+"/sale.order.add", "application/json", bytes.NewBuffer(query)) resp, _ := http.Post(b.EP+"/sale.order.add", "application/json", bytes.NewBuffer(query))
result := createOrderResponse{} result := createOrderResponse{}
err := json.NewDecoder(resp.Body).Decode(&result) err := json.NewDecoder(resp.Body).Decode(&result)
@ -249,7 +258,7 @@ func (_ bitrix) CreateOrder(userId int) (int, error) {
return result.Result.Order.Id, err return result.Result.Order.Id, err
} }
func (_ bitrix) ApprovePayment(paymentId int, paySystemId int) error { func (b bitrix) ApprovePayment(paymentId int, paySystemId int) error {
req := map[string]interface{}{ req := map[string]interface{}{
"id": paymentId, "id": paymentId,
"fields": map[string]interface{}{"paid": "Y", "paySystemId": paySystemId}, "fields": map[string]interface{}{"paid": "Y", "paySystemId": paySystemId},
@ -257,12 +266,15 @@ func (_ bitrix) ApprovePayment(paymentId int, paySystemId int) error {
query, _ := json.Marshal(req) query, _ := json.Marshal(req)
_, err := http.Post(EP+"/sale.payment.update", "application/json", bytes.NewBuffer(query)) resp, err := http.Post(b.EP+"/sale.payment.update", "application/json", bytes.NewBuffer(query))
str, _ := io.ReadAll(resp.Body)
log.Println(str)
return err return err
} }
func (_ bitrix) CancelOrder(orderId int) error { func (b bitrix) CancelOrder(orderId int) error {
req := map[string]interface{}{ req := map[string]interface{}{
"id": orderId, "id": orderId,
"fields": map[string]interface{}{"canceled": "Y"}, "fields": map[string]interface{}{"canceled": "Y"},
@ -270,19 +282,19 @@ func (_ bitrix) CancelOrder(orderId int) error {
query, _ := json.Marshal(req) query, _ := json.Marshal(req)
_, err := http.Post(EP+"/sale.order.update", "application/json", bytes.NewBuffer(query)) _, err := http.Post(b.EP+"/sale.order.update", "application/json", bytes.NewBuffer(query))
return err return err
} }
func (_ bitrix) GetOrderInfo(orderId int) (*OrderResource, error) { func (b bitrix) GetOrderInfo(orderId int) (*OrderResource, error) {
req := map[string]interface{}{ req := map[string]interface{}{
"id": orderId, "id": orderId,
} }
query, _ := json.Marshal(req) query, _ := json.Marshal(req)
response, err := http.Post(EP+"/sale.order.get", "application/json", bytes.NewBuffer(query)) response, err := http.Post(b.EP+"/sale.order.get", "application/json", bytes.NewBuffer(query))
result := struct { result := struct {
Result struct { Result struct {
@ -300,7 +312,7 @@ func (_ bitrix) GetOrderInfo(orderId int) (*OrderResource, error) {
return &resultParsed, err return &resultParsed, err
} }
func (_ bitrix) CreatePayment(orderId int, sum float64) error { func (b bitrix) CreatePayment(orderId int, sum float64) error {
req := map[string]interface{}{ req := map[string]interface{}{
"fields": map[string]interface{}{ "fields": map[string]interface{}{
"orderId": orderId, "orderId": orderId,
@ -312,30 +324,36 @@ func (_ bitrix) CreatePayment(orderId int, sum float64) error {
query, _ := json.Marshal(req) query, _ := json.Marshal(req)
_, err := http.Post(EP+"/sale.payment.add", "application/json", bytes.NewBuffer(query)) _, err := http.Post(b.EP+"/sale.payment.add", "application/json", bytes.NewBuffer(query))
return err return err
} }
func (_ bitrix) AddProductToOrder(orderId int, productId int, price float64, quantity int) error { func (b bitrix) AddProductToOrder(orderId int, productId int, price float64, quantity int, productName string) error {
req := map[string]interface{}{ req := map[string]interface{}{
"fields": map[string]interface{}{ "fields": map[string]interface{}{
"name": productName,
"orderId": orderId, "orderId": orderId,
"module": "catalog",
"productId": productId, "productId": productId,
"quantity": quantity, "quantity": quantity,
"currency": "RUB", "currency": "RUB",
"price": price, "vatIncluded": "Y",
"vatRate": 0.2,
"basePrice": price,
"productXmlId": fmt.Sprintf("%d", productId),
"detailPageUrl": fmt.Sprintf("\\/CRM_PRODUCT_CATALOG\\/detail.php?ID=%d", productId),
}, },
} }
query, _ := json.Marshal(req) query, _ := json.Marshal(req)
_, err := http.Post(EP+"/sale.basketitem.addCatalogProduct", "application/json", bytes.NewBuffer(query)) _, err := http.Post(b.EP+"/sale.basketitem.add", "application/json", bytes.NewBuffer(query))
return err return err
} }
func (_ bitrix) UpdateContact(contactId int, email string, name string, phone string) error { func (b bitrix) UpdateContact(contactId int, email string, name string, phone string) error {
req := map[string]interface{}{ req := map[string]interface{}{
"id": contactId, "id": contactId,
"fields": map[string]interface{}{ "fields": map[string]interface{}{
@ -346,7 +364,6 @@ func (_ bitrix) UpdateContact(contactId int, email string, name string, phone st
}), }),
"NAME": strings.Split(name, " ")[1], "NAME": strings.Split(name, " ")[1],
"LAST_NAME": strings.Split(name, " ")[0], "LAST_NAME": strings.Split(name, " ")[0],
"SECOND_NAME": strings.Split(name, " ")[2],
"PHONE": append([]map[string]interface{}{}, map[string]interface{}{ "PHONE": append([]map[string]interface{}{}, map[string]interface{}{
"VALUE_TYPE": "other", "VALUE_TYPE": "other",
"VALUE": phone, "VALUE": phone,
@ -357,11 +374,35 @@ func (_ bitrix) UpdateContact(contactId int, email string, name string, phone st
query, _ := json.Marshal(req) query, _ := json.Marshal(req)
_, err := http.Post(EP+"/crm.contact.update", "application/json", bytes.NewBuffer(query)) _, err := http.Post(b.EP+"/crm.contact.update", "application/json", bytes.NewBuffer(query))
return err return err
} }
func Initialize() Bitrix { func (b bitrix) GetTotalForProduct(fuserId int, coupon *string) (*GetTotalOrderResponse, error) {
return &bitrix{} var result *http.Response
var err error
if coupon != nil {
result, err = http.Get(b.EPCustom + fmt.Sprintf("/order/total?fuserId=%d&coupon=%s", fuserId, *coupon))
} else {
result, err = http.Get(b.EPCustom + fmt.Sprintf("/order/total?fuserId=%d", fuserId))
}
if err != nil {
return nil, err
}
defer result.Body.Close()
returnedValue := new(GetTotalOrderResponse)
json.NewDecoder(result.Body).Decode(returnedValue)
return returnedValue, nil
}
func Initialize() Bitrix {
return &bitrix{
EP: os.Getenv("BITRIX_API_EP"),
EPCustom: os.Getenv("BTIRIX_API_CUSTOM"),
}
} }

View File

@ -3,11 +3,11 @@ package AgentType
type AgentType string type AgentType string
const ( const (
BANKING_PAYMENT_AGENT = "banking_payment_agent" // Банковский платежный агент BANKING_PAYMENT_AGENT AgentType = "banking_payment_agent" // Банковский платежный агент
BANKING_PAYMENT_SUBAGENT = "banking_payment_subagent" // Банковский платежный субагент BANKING_PAYMENT_SUBAGENT AgentType = "banking_payment_subagent" // Банковский платежный субагент
PAYMENT_AGENT = "payment_agent" // Платежный агент PAYMENT_AGENT AgentType = "payment_agent" // Платежный агент
PAYMENT_SUBAGENT = "payment_subagent" // Платежный субагент PAYMENT_SUBAGENT AgentType = "payment_subagent" // Платежный субагент
ATTORNEY = "attorney" // Поверенный ATTORNEY AgentType = "attorney" // Поверенный
COMMISSIONER = "commissioner" // Комиссионер COMMISSIONER AgentType = "commissioner" // Комиссионер
AGENT = "agent" // Агент AGENT AgentType = "agent" // Агент
) )

View File

@ -3,28 +3,28 @@ package Measure
type Measure string type Measure string
const ( const (
PIECE = "piece" // Штука, единица товара PIECE Measure = "piece" // Штука, единица товара
GRAM = "gram" // Грамм GRAM Measure = "gram" // Грамм
KILOGRAM = "kilogram" // Килограмм KILOGRAM Measure = "kilogram" // Килограмм
TON = "ton" // Тонна TON Measure = "ton" // Тонна
CENTIMETER = "centimeter" // Сантиметр CENTIMETER Measure = "centimeter" // Сантиметр
DECIMETER = "decimeter" // Дециметр DECIMETER Measure = "decimeter" // Дециметр
METER = "meter" // Метр METER Measure = "meter" // Метр
SQUARE_CENTIMETER = "square_centimeter" // Квадратный сантиметр SQUARE_CENTIMETER Measure = "square_centimeter" // Квадратный сантиметр
SQUARE_DECIMETER = "square_decimeter" // Квадратный дециметр SQUARE_DECIMETER Measure = "square_decimeter" // Квадратный дециметр
SQUARE_METER = "square_meter" // Квадратный метр SQUARE_METER Measure = "square_meter" // Квадратный метр
MILLILITER = "milliliter" // Миллилитр MILLILITER Measure = "milliliter" // Миллилитр
LITER = "liter" // Литр LITER Measure = "liter" // Литр
CUBIC_METER = "cubic_meter" // Кубический метр CUBIC_METER Measure = "cubic_meter" // Кубический метр
KILOWATT_HOUR = "kilowatt_hour" // Килловат-час KILOWATT_HOUR Measure = "kilowatt_hour" // Килловат-час
GIGACALORIE = "gigacalorie" // Гигакалория GIGACALORIE Measure = "gigacalorie" // Гигакалория
DAY = "day" // Сутки DAY Measure = "day" // Сутки
HOUR = "hour" // Час HOUR Measure = "hour" // Час
MINUTE = "minute" // Минута MINUTE Measure = "minute" // Минута
SECOND = "second" // Секунда SECOND Measure = "second" // Секунда
KILOBYTE = "kilobyte" // Килобайт KILOBYTE Measure = "kilobyte" // Килобайт
MEGABYTE = "megabyte" // Мегабайт MEGABYTE Measure = "megabyte" // Мегабайт
GIGABYTE = "gigabyte" // Гигабайт GIGABYTE Measure = "gigabyte" // Гигабайт
TERABYTE = "terabyte" // Терабайт TERABYTE Measure = "terabyte" // Терабайт
ANOTHER = "another" // Другое ANOTHER Measure = "another" // Другое
) )

View File

@ -3,11 +3,11 @@ package PaymentMode
type PaymentMode string type PaymentMode string
const ( const (
FULL_PREPAYMENT = "full_prepayment" // Полная предоплата FULL_PREPAYMENT PaymentMode = "full_prepayment" // Полная предоплата
PARTIAL_PREPAYMENT = "partial_prepayment" // Частичная предоплата PARTIAL_PREPAYMENT PaymentMode = "partial_prepayment" // Частичная предоплата
ADVANCE = "advance" // Аванс ADVANCE PaymentMode = "advance" // Аванс
FULL_PAYMENT = "full_payment" // Полный расчет FULL_PAYMENT PaymentMode = "full_payment" // Полный расчет
PARTIAL_PAYMENT = "partial_payment" // Частичный расчет и кредит PARTIAL_PAYMENT PaymentMode = "partial_payment" // Частичный расчет и кредит
CREDIT = "credit" // Кредит CREDIT PaymentMode = "credit" // Кредит
CREDIT_PAYMENT = "credit_payment" // Выплата по кредиту CREDIT_PAYMENT PaymentMode = "credit_payment" // Выплата по кредиту
) )

View File

@ -3,23 +3,23 @@ package PaymentSubject
type PaymentSubject string type PaymentSubject string
const ( const (
COMMODITY = "commodity" //Товар Товар COMMODITY PaymentSubject = "commodity" //Товар Товар
EXCISE = "excise" //Подакцизный товар EXCISE PaymentSubject = "excise" //Подакцизный товар
JOB = "job" //Работа JOB PaymentSubject = "job" //Работа
SERVICE = "service" //Услуга Услуга SERVICE PaymentSubject = "service" //Услуга Услуга
PAYMENT = "payment" //Платеж Платеж PAYMENT PaymentSubject = "payment" //Платеж Платеж
CASINO = "casino" // Платеж казино CASINO PaymentSubject = "casino" // Платеж казино
GAMBLING_BET = "gambling_bet" //Ставка в азартной игре GAMBLING_BET PaymentSubject = "gambling_bet" //Ставка в азартной игре
GAMBLING_PRIZE = "gambling_prize" // Выигрыш азартной игры GAMBLING_PRIZE PaymentSubject = "gambling_prize" // Выигрыш азартной игры
LOTTERY = "lottery" // Лотерейный билет LOTTERY PaymentSubject = "lottery" // Лотерейный билет
LOTTERY_PRIZE = "lottery_prize" // Выигрыш в лотерею LOTTERY_PRIZE PaymentSubject = "lottery_prize" // Выигрыш в лотерею
INTELLECTUAL_ACTIVITY = "intellectual_activity" //Результаты интеллектуальной деятельности INTELLECTUAL_ACTIVITY PaymentSubject = "intellectual_activity" //Результаты интеллектуальной деятельности
AGENT_COMMISSION = "agent_commission" //Агентское вознаграждение AGENT_COMMISSION PaymentSubject = "agent_commission" //Агентское вознаграждение
PROPERTY_RIGHT = "property_right" //Имущественное право PROPERTY_RIGHT PaymentSubject = "property_right" //Имущественное право
NON_OPERATING_GAIN = "non_operating_gain" //Внереализационный доход NON_OPERATING_GAIN PaymentSubject = "non_operating_gain" //Внереализационный доход
INSURANCE_PREMIUM = "insurance_premium" //Страховой сбор INSURANCE_PREMIUM PaymentSubject = "insurance_premium" //Страховой сбор
SALES_TAX = "sales_tax" //Торговый сбор SALES_TAX PaymentSubject = "sales_tax" //Торговый сбор
RESORT_FEE = "resort_fee" // Курортный сбор RESORT_FEE PaymentSubject = "resort_fee" // Курортный сбор
COMPOSITE = "composite" // Несколько вариантов COMPOSITE PaymentSubject = "composite" // Несколько вариантов
ANOTHER = "another" // Другое ANOTHER PaymentSubject = "another" // Другое
) )

View File

@ -3,8 +3,8 @@ package Settlements
type Settlements string type Settlements string
const ( const (
CASHLESS = "cashless" // Безналичный расчет CASHLESS Settlements = "cashless" // Безналичный расчет
PREPAYMENT = "prepayment" // Предоплата (аванс) PREPAYMENT Settlements = "prepayment" // Предоплата (аванс)
POSTPAYMENT = "postpayment" // Постоплата (кредит) POSTPAYMENT Settlements = "postpayment" // Постоплата (кредит)
CONSIDERATION = "consideration" // Встречное предоставление CONSIDERATION Settlements = "consideration" // Встречное предоставление
) )

View File

@ -3,7 +3,7 @@ package TaxSystemCode
type TaxSystemCode int type TaxSystemCode int
const ( const (
GENERAL = iota + 1 GENERAL TaxSystemCode = iota + 1
USN_INCOME USN_INCOME
USN_INCOME_MINUS_EXPENCES USN_INCOME_MINUS_EXPENCES
ENVD ENVD

View File

@ -7,17 +7,15 @@ import (
"fmt" "fmt"
"github.com/google/uuid" "github.com/google/uuid"
"net/http" "net/http"
"os"
"relynolli-server/external/kassa/Measure" "relynolli-server/external/kassa/Measure"
"relynolli-server/external/kassa/PaymentMode" "relynolli-server/external/kassa/PaymentMode"
"relynolli-server/external/kassa/PaymentSubject" "relynolli-server/external/kassa/PaymentSubject"
"relynolli-server/external/kassa/TaxSystemCode" "relynolli-server/external/kassa/TaxSystemCode"
"relynolli-server/external/kassa/VatCodes" "relynolli-server/external/kassa/VatCodes"
"sync"
"time" "time"
) )
var once sync.Once
type KassaAmount struct { type KassaAmount struct {
Value string `json:"value"` Value string `json:"value"`
Currency string `json:"currency"` Currency string `json:"currency"`
@ -80,7 +78,7 @@ func basicAuth(username, password string) string {
return base64.StdEncoding.EncodeToString([]byte(auth)) return base64.StdEncoding.EncodeToString([]byte(auth))
} }
func CreatePayment(orderId int, sum float64, fullName string, email string, phone string, items []KassaReceiptItems) (map[string]interface{}, error) { func CreatePayment(orderId int, sum float64, fullName string, email string, phone string, items []KassaReceiptItems) (*KassaResult, error) {
req := KassaPaymentReq{ req := KassaPaymentReq{
Amount: KassaAmount{Value: fmt.Sprintf("%f", sum), Currency: "RUB"}, Amount: KassaAmount{Value: fmt.Sprintf("%f", sum), Currency: "RUB"},
Description: fmt.Sprintf("Заказ №%d", orderId), Description: fmt.Sprintf("Заказ №%d", orderId),
@ -105,13 +103,14 @@ func CreatePayment(orderId int, sum float64, fullName string, email string, phon
client := http.Client{} client := http.Client{}
request, _ := http.NewRequest(http.MethodPost, BASE_URL, bytes.NewBuffer(query)) request, _ := http.NewRequest(http.MethodPost, BASE_URL, bytes.NewBuffer(query))
request.Header.Set("Authorization", "Basic "+basicAuth(ACCOUNT_ID, PASSWORD)) request.Header.Set("Authorization", "Basic "+basicAuth(os.Getenv("YOOKASSA_ACCOUNT_ID"),
os.Getenv("YOOKASSA_ACCOUNT_SECRET")))
request.Header.Set("Idempotence-Key", uid.String()) request.Header.Set("Idempotence-Key", uid.String())
request.Header.Set("Content-Type", "application/json") request.Header.Set("Content-Type", "application/json")
response, err := client.Do(request) response, err := client.Do(request)
result := map[string]interface{}{} result := new(KassaResult)
json.NewDecoder(response.Body).Decode(&result) json.NewDecoder(response.Body).Decode(&result)
if err != nil { if err != nil {
@ -121,3 +120,21 @@ func CreatePayment(orderId int, sum float64, fullName string, email string, phon
return result, nil return result, nil
} }
func CheckPayment(paymentId uuid.UUID) (*KassaResult, error) {
uid, err := uuid.NewUUID()
client := new(http.Client)
request, _ := http.NewRequest(http.MethodGet, BASE_URL+fmt.Sprintf("/%s", paymentId.String()), nil)
request.Header.Set("Authorization", "Basic "+basicAuth(os.Getenv("YOOKASSA_ACCOUNT_ID"),
os.Getenv("YOOKASSA_ACCOUNT_SECRET")))
request.Header.Set("Idempotence-Key", uid.String())
request.Header.Set("Content-Type", "application/json")
response, err := client.Do(request)
result := new(KassaResult)
json.NewDecoder(response.Body).Decode(&result)
if err != nil {
return nil, err
}
return result, nil
}

124
external/yaGeo/init.go vendored Normal file
View File

@ -0,0 +1,124 @@
package yaGeo
import (
"encoding/json"
"net/http"
"os"
)
type GeoObject struct {
MetaDataProperty struct {
GeocoderMetaData struct {
Precision string `json:"precision"`
Text string `json:"text"`
Kind string `json:"kind"`
Address struct {
CountryCode string `json:"country_code"`
Formatted string `json:"formatted"`
PostalCode string `json:"postal_code,omitempty"`
Components []struct {
Kind string `json:"kind"`
Name string `json:"name"`
} `json:"Components"`
} `json:"Address"`
AddressDetails struct {
Country struct {
AddressLine string `json:"AddressLine"`
CountryNameCode string `json:"CountryNameCode"`
CountryName string `json:"CountryName"`
AdministrativeArea struct {
AdministrativeAreaName string `json:"AdministrativeAreaName"`
SubAdministrativeArea struct {
SubAdministrativeAreaName string `json:"SubAdministrativeAreaName"`
Locality struct {
LocalityName string `json:"LocalityName"`
Thoroughfare struct {
ThoroughfareName string `json:"ThoroughfareName"`
Premise struct {
PremiseNumber string `json:"PremiseNumber"`
PostalCode struct {
PostalCodeNumber string `json:"PostalCodeNumber"`
} `json:"PostalCode,omitempty"`
} `json:"Premise"`
} `json:"Thoroughfare,omitempty"`
DependentLocality struct {
DependentLocalityName string `json:"DependentLocalityName"`
Thoroughfare struct {
ThoroughfareName string `json:"ThoroughfareName"`
Premise struct {
PremiseNumber string `json:"PremiseNumber"`
} `json:"Premise"`
} `json:"Thoroughfare"`
} `json:"DependentLocality,omitempty"`
} `json:"Locality"`
} `json:"SubAdministrativeArea"`
} `json:"AdministrativeArea"`
} `json:"Country"`
} `json:"AddressDetails"`
} `json:"GeocoderMetaData"`
} `json:"metaDataProperty"`
Name string `json:"name"`
Description string `json:"description"`
BoundedBy struct {
Envelope struct {
LowerCorner string `json:"lowerCorner"`
UpperCorner string `json:"upperCorner"`
} `json:"Envelope"`
} `json:"boundedBy"`
Uri string `json:"uri"`
Point struct {
Pos string `json:"pos"`
} `json:"Point"`
}
type geoResponseWrapper struct {
GeoResponse `json:"response"`
}
type geoObjectCollection struct {
FeatureMember []struct {
GeoObject `json:"GeoObject"`
} `json:"featureMember"`
}
type GeoResponse struct {
geoObjectCollection `json:"GeoObjectCollection"`
}
type yaGeo struct {
EP string
apiKey string
}
type YaGeo interface {
GeoCode(q string) (*[]GeoObject, error)
}
func Init() YaGeo {
return &yaGeo{
EP: "https://geocode-maps.yandex.ru/1.x/",
apiKey: os.Getenv("YANDEX_GEOCODER_API_KEY"),
}
}
func (y *yaGeo) GeoCode(q string) (*[]GeoObject, error) {
data := new(geoResponseWrapper)
req, _ := http.NewRequest("GET", y.EP, nil)
params := req.URL.Query()
params.Add("apikey", y.apiKey)
params.Add("geocode", q)
params.Add("lang", "ru_RU")
params.Add("format", "json")
req.URL.RawQuery = params.Encode()
resp, err := http.Get(req.URL.String())
if err != nil {
return nil, err
}
err = json.NewDecoder(resp.Body).Decode(data)
items := []GeoObject{}
for _, d := range data.GeoResponse.geoObjectCollection.FeatureMember {
items = append(items, d.GeoObject)
}
return &items, err
}

46
go.mod
View File

@ -2,43 +2,67 @@ module relynolli-server
go 1.21 go 1.21
require (
github.com/ernesto-jimenez/httplogger v0.0.0-20220128121225-117514c3f345
github.com/geotrace/geo v0.0.0-20160115125640-a9248f7f2ad1
github.com/gin-contrib/cache v1.2.0
github.com/gin-contrib/cors v1.5.0
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.19.0
github.com/go-sql-driver/mysql v1.7.1
github.com/google/go-querystring v1.1.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-multierror v1.0.0
github.com/joho/godotenv v1.5.1
github.com/mitchellh/mapstructure v1.5.0
github.com/orcaman/concurrent-map/v2 v2.0.1
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.5.1
github.com/rs/zerolog v1.31.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
github.com/uptrace/bun v1.1.17
github.com/uptrace/bun/dialect/mysqldialect v1.1.17
github.com/uptrace/bun/extra/bundebug v1.1.17
)
require ( require (
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/bytedance/sonic v1.11.1 // indirect github.com/bytedance/sonic v1.11.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/cache v1.2.0 // indirect
github.com/gin-contrib/cors v1.5.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gomodule/redigo v1.8.9 // indirect github.com/gomodule/redigo v1.8.9 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/redis/go-redis/v9 v9.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/arch v0.7.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.20.0 // indirect golang.org/x/crypto v0.20.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect

75
go.sum
View File

@ -1,5 +1,9 @@
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw= github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.1 h1:JC0+6c9FoWYYxakaoa+c5QTtJeiSZNeByOBhXtAFSn4= github.com/bytedance/sonic v1.11.1 h1:JC0+6c9FoWYYxakaoa+c5QTtJeiSZNeByOBhXtAFSn4=
@ -13,12 +17,21 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpV
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/ernesto-jimenez/httplogger v0.0.0-20220128121225-117514c3f345 h1:AZLrCR38RDhsyCQakz1UxCx72As18Ai5mObrKvT8DK8=
github.com/ernesto-jimenez/httplogger v0.0.0-20220128121225-117514c3f345/go.mod h1:pw+gaKQ52Cl/SrERU62yQAiWauPpLgKpuR1hkxwL4tM=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/geotrace/geo v0.0.0-20160115125640-a9248f7f2ad1 h1:t/FumljonSghkl+LUhgKJEhIWC3Zwu9JY7rLrU9YYuU=
github.com/geotrace/geo v0.0.0-20160115125640-a9248f7f2ad1/go.mod h1:5gbC4+PtjSPzYBiq6ANs+3D4SxiPenPozK8jRUORapU=
github.com/gin-contrib/cache v1.2.0 h1:WA+AJR4kmHDTaLLShCHo/IeWVmmGRZ3Lsr3JQ46tFlE= github.com/gin-contrib/cache v1.2.0 h1:WA+AJR4kmHDTaLLShCHo/IeWVmmGRZ3Lsr3JQ46tFlE=
github.com/gin-contrib/cache v1.2.0/go.mod h1:2KkFL8PSnPF3Tt5E2Jpc3HWuBAUKqGZnClCFMm0tXQI= github.com/gin-contrib/cache v1.2.0/go.mod h1:2KkFL8PSnPF3Tt5E2Jpc3HWuBAUKqGZnClCFMm0tXQI=
github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
@ -27,28 +40,35 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -57,12 +77,18 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4= github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug= github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@ -72,15 +98,22 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko= github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY= github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
github.com/rvinnie/yookassa-sdk-go v0.0.0-20230904104101-ff7e5be5530c h1:m6dxe045lJQ1tkJeCBwseulCwppUDcdZk+RIxzBjQXQ= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rvinnie/yookassa-sdk-go v0.0.0-20230904104101-ff7e5be5530c/go.mod h1:flatybkcu+7YLaB7mMnj9JTNKeim4jZ+ZrXNFjVA0pA= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -91,29 +124,51 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/uptrace/bun v1.1.17 h1:qxBaEIo0hC/8O3O6GrMDKxqyT+mw5/s0Pn/n6xjyGIk=
github.com/uptrace/bun v1.1.17/go.mod h1:hATAzivtTIRsSJR4B8AXR+uABqnQxr3myKDKEf5iQ9U=
github.com/uptrace/bun/dialect/mysqldialect v1.1.17 h1:CsaZu+C3hW6jH5XnbQWPeZbHOoeURRpX9wd9wNy9fYU=
github.com/uptrace/bun/dialect/mysqldialect v1.1.17/go.mod h1:PDT12yHB0yLidZWFoPjhXfEKvsu7tLyjY67+OSMQsVw=
github.com/uptrace/bun/extra/bundebug v1.1.17 h1:LcZ8DzyyGdXAmbUqmnCpBq7TPFegMp59FGy+uzEE21c=
github.com/uptrace/bun/extra/bundebug v1.1.17/go.mod h1:FOwNaBEGGChv3qBVh3pz3TPlUuikZ93qKjd/LJdl91o=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vseinstrumentiru/cdek v0.0.7 h1:73O/Zp0JH/MxPWLHXKoDfrlUAQ9WHYqSPcJlefSMFuI=
github.com/vseinstrumentiru/cdek v0.0.7/go.mod h1:9oNSNbQX0Am56kJcRDpouqlZ77ZJI9Wl4g8HB38ln3Y=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,58 @@
package endpoints
import (
"fmt"
"github.com/gin-gonic/gin"
"relynolli-server/external/yaGeo"
"relynolli-server/models"
"relynolli-server/status"
"time"
)
type handlers struct{}
type Handlers interface {
SearchAddress(c *gin.Context)
}
func GetHandlers() Handlers {
return &handlers{}
}
type searchAddressParams struct {
SearchString string `form:"q"`
}
func (h *handlers) SearchAddress(c *gin.Context) {
query := new(searchAddressParams)
err := c.ShouldBindQuery(query)
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
response := models.Response{
Status: status.STATUS_OK,
Meta: &meta,
}
if err != nil {
response.Info = fmt.Sprintf("Error: %s", err.Error())
response.Status = status.STATUS_BAD_REQUEST
meta.RequestFinished = time.Now().Unix()
c.JSON(400, response)
return
}
geo := yaGeo.Init()
results, err := geo.GeoCode(query.SearchString)
if err != nil {
response.Info = fmt.Sprintf("Error: %s", err.Error())
response.Status = status.STATUS_SERVER_ERROR
meta.RequestFinished = time.Now().Unix()
c.JSON(500, response)
return
}
response.Data = results
meta.RequestFinished = time.Now().Unix()
c.JSON(200, response)
}

View File

@ -0,0 +1,21 @@
package address
import (
"github.com/gin-contrib/cache"
"github.com/gin-gonic/gin"
"os"
"relynolli-server/handlers/address/endpoints"
"relynolli-server/internal"
"time"
)
func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers()
addr := parent.Group("/address")
if os.Getenv("IS_PROD") == "1" {
store := internal.InitCacheStore()
addr.GET("/search", cache.CachePage(store, 15*time.Minute, h.SearchAddress))
} else {
addr.GET("/search", h.SearchAddress)
}
}

View File

@ -0,0 +1,113 @@
package endpoints
import (
"context"
"fmt"
"relynolli-server/models"
"relynolli-server/status"
"relynolli-server/storage"
"time"
"github.com/gin-gonic/gin"
)
type handlers struct{}
type Handlers interface {
GetNews(c *gin.Context)
RetrieveNews(c *gin.Context)
}
func GetHandlers() Handlers {
return &handlers{}
}
type ListNewsRequest struct {
Limit int `form:"limit" `
Page int `form:"page"`
}
func (h *handlers) GetNews(c *gin.Context) {
ctx := context.Background()
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
query := ListNewsRequest{
Limit: 10,
Page: 1,
}
err := c.ShouldBindQuery(&query)
if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(400, models.Response{
Status: status.STATUS_BAD_REQUEST,
Info: fmt.Sprintf("Error: %s", err.Error()),
Meta: &meta,
})
return
}
s := storage.NewStorageArticle()
count, resp, err := s.GetArticles(ctx, int64(query.Limit), int64(query.Limit*(query.Page-1)))
if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(500, models.Response{
Status: status.STATUS_SERVER_ERROR,
Info: fmt.Sprintf("Error: %s", err.Error()),
Meta: &meta,
})
return
}
meta.Count = count
meta.Limit = query.Limit
meta.Page = query.Page
c.JSON(200, models.Response{
Status: status.STATUS_OK,
Data: resp,
Meta: &meta,
})
}
type retireveNewsReq struct {
Code string `uri:"code" binding:"required"`
}
func (h *handlers) RetrieveNews(c *gin.Context) {
ctx := context.Background()
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
query := new(retireveNewsReq)
err := c.ShouldBindUri(query)
if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(400, models.Response{
Status: status.STATUS_BAD_REQUEST,
Info: fmt.Sprintf("Error: %s", err.Error()),
Meta: &meta,
})
return
}
s := storage.NewStorageArticle()
resp, _ := s.RetrieveArticle(ctx, query.Code)
meta.RequestFinished = time.Now().Unix()
statusResult := status.STATUS_OK
responseCode := 200
if resp == nil {
statusResult = status.STATUS_NOT_FOUND
responseCode = 404
}
c.JSON(responseCode, models.Response{
Status: statusResult,
Data: resp,
Meta: &meta,
})
}

View File

@ -0,0 +1,26 @@
package article
import (
"os"
"relynolli-server/handlers/article/endpoints"
"relynolli-server/internal"
"time"
"github.com/gin-contrib/cache"
"github.com/gin-gonic/gin"
)
func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers()
cacheStore := internal.InitCacheStore()
catalog := parent.Group("/articles")
if os.Getenv("IS_PROD") == "1" {
// Caching for production usage
catalog.GET("", cache.CachePage(cacheStore, 15*time.Minute, h.GetNews))
catalog.GET("/:code", cache.CachePage(cacheStore, 15*time.Minute, h.RetrieveNews))
} else {
catalog.GET("", h.GetNews)
catalog.GET("/:code", h.RetrieveNews)
}
}

View File

@ -1,34 +1,75 @@
package endpoints package endpoints
import ( import (
"github.com/gin-gonic/gin" "context"
"fmt"
"relynolli-server/models" "relynolli-server/models"
"relynolli-server/services" "relynolli-server/status"
"strconv" "relynolli-server/storage"
"time"
"github.com/gin-gonic/gin"
) )
type getCartItemsRequest struct {
FuserId int64 `form:"fuserId"`
}
func (h *handlers) GetCartItems(c *gin.Context) { func (h *handlers) GetCartItems(c *gin.Context) {
ctx := context.Background()
query := new(getCartItemsRequest)
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
fuserId := c.Query("fuserId") err := c.ShouldBindQuery(query)
if fuserId == "" { if err != nil || query.FuserId == 0 {
c.JSON(400, models.Response{Status: 400, Info: "\"fuserId\" should be provided"}) meta.RequestFinished = time.Now().Unix()
c.JSON(400, models.Response{
Status: status.STATUS_BAD_REQUEST,
Info: "\"fuserId\" should be provided and be integer number",
Meta: &meta})
return return
} }
idx, err := strconv.Atoi(fuserId) s := storage.NewStorageCart()
if err != nil { items, _ := s.GetCartItems(ctx, query.FuserId)
c.JSON(400, models.Response{Status: 400, Info: "\"fuserId should be an integer number\""})
return
}
c.JSON(200, services.GetCartItems(idx)) meta.RequestFinished = time.Now().Unix()
c.JSON(200, models.Response{
Status: status.STATUS_OK,
Data: &items,
Meta: &meta,
})
} }
func (h *handlers) CreateFUser(c *gin.Context) { func (h *handlers) CreateFUser(c *gin.Context) {
lastInsertId := services.CreateFuser() s := storage.NewStorageCart()
ctx := context.Background()
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
c.JSON(201, gin.H{ fuserId, fuser, err := s.CreateFuser(ctx)
"fuserId": lastInsertId, if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(500, models.Response{
Status: status.STATUS_SERVER_ERROR,
Info: fmt.Sprintf("Error: %s", err.Error()),
Meta: &meta,
})
return
}
meta.RequestFinished = time.Now().Unix()
c.JSON(201, models.Response{
Status: status.STATUS_OK,
Info: "New Fuser has created",
Data: &gin.H{
"fuserId": fuserId,
"fuser": &fuser,
},
Meta: &meta,
}) })
} }

View File

@ -7,7 +7,6 @@ type handlers struct{}
type Handlers interface { type Handlers interface {
GetCartItems(c *gin.Context) GetCartItems(c *gin.Context)
CreateFUser(c *gin.Context) CreateFUser(c *gin.Context)
CreateCartItem(c *gin.Context) CreateCartItem(c *gin.Context)
UpdateCartItem(c *gin.Context) UpdateCartItem(c *gin.Context)
DeleteCartItem(c *gin.Context) DeleteCartItem(c *gin.Context)

View File

@ -1,11 +1,13 @@
package endpoints package endpoints
import ( import (
"context"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http"
"relynolli-server/models" "relynolli-server/models"
"relynolli-server/services" "relynolli-server/status"
"relynolli-server/storage"
"time"
) )
type createCartItemRequest struct { type createCartItemRequest struct {
@ -27,49 +29,157 @@ type deleteCartRequest struct {
} }
func (h *handlers) CreateCartItem(c *gin.Context) { func (h *handlers) CreateCartItem(c *gin.Context) {
req := createCartItemRequest{} meta := models.Meta{
err := c.ShouldBindJSON(&req) RequestStarted: time.Now().Unix(),
}
response := models.Response{
Status: status.STATUS_OK,
Meta: &meta,
}
s := storage.NewStorageCart()
ctx := context.Background()
query := new(createCartItemRequest)
err := c.ShouldBindJSON(query)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("Bad request. Error info: %s", err.Error())}) response.Status = status.STATUS_BAD_REQUEST
response.Info = fmt.Sprintf("Error: %s", err.Error())
meta.RequestFinished = time.Now().Unix()
c.JSON(400, response)
return
}
err = s.AddItemToCart(ctx, int64(query.FuserId), int64(query.ProductId))
if err != nil {
response.Status = status.STATUS_BAD_REQUEST
response.Info = fmt.Sprintf("Error: %s", err.Error())
meta.RequestFinished = time.Now().Unix()
c.JSON(400, response)
return return
} }
services.AddItemToCart(req.FuserId, req.ProductId)
c.JSON(http.StatusCreated, models.Response{Status: http.StatusCreated, Info: fmt.Sprintf("Item %d has added to cart", req.ProductId)}) meta.RequestFinished = time.Now().Unix()
response.Info = fmt.Sprintf("Item has added to cart")
response.Status = status.STATUS_OK
c.JSON(201, response)
} }
func (h *handlers) UpdateCartItem(c *gin.Context) { func (h *handlers) UpdateCartItem(c *gin.Context) {
req := updateCartRequest{} meta := models.Meta{
err := c.ShouldBindJSON(&req) RequestStarted: time.Now().Unix(),
}
response := models.Response{
Status: status.STATUS_OK,
Meta: &meta,
}
s := storage.NewStorageCart()
ctx := context.Background()
query := new(updateCartRequest)
err := c.ShouldBindJSON(query)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("Bad request. Error info: %s", err.Error())}) response.Status = status.STATUS_BAD_REQUEST
response.Info = fmt.Sprintf("Error: %s", err.Error())
meta.RequestFinished = time.Now().Unix()
c.JSON(400, response)
return
}
err = s.UpdateCartItem(ctx, int64(query.FuserId), int64(query.ProductId), int64(query.Quantity))
if err != nil {
response.Status = status.STATUS_BAD_REQUEST
response.Info = fmt.Sprintf("Error: %s", err.Error())
meta.RequestFinished = time.Now().Unix()
c.JSON(400, response)
return return
} }
err = services.UpdateCartItem(req.FuserId, req.ProductId, req.Quantity) meta.RequestFinished = time.Now().Unix()
response.Info = fmt.Sprintf("Item has updated in cart")
if err != nil { response.Status = status.STATUS_OK
c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("Bad request. Error info: %s", err.Error())}) c.JSON(200, response)
return
}
c.JSON(http.StatusOK, models.Response{Status: http.StatusOK})
} }
func (h *handlers) DeleteCartItem(c *gin.Context) { func (h *handlers) DeleteCartItem(c *gin.Context) {
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
response := models.Response{
Status: status.STATUS_OK,
Meta: &meta,
}
s := storage.NewStorageCart()
ctx := context.Background()
req := deleteCartRequest{} query := new(deleteCartRequest)
err := c.ShouldBindJSON(&req)
err := c.ShouldBindJSON(query)
if err != nil { if err != nil {
c.JSON(400, models.Response{Status: 400, Info: fmt.Sprintf("Bad request. Error info: %s", err.Error())}) response.Status = status.STATUS_BAD_REQUEST
response.Info = fmt.Sprintf("Error: %s", err.Error())
meta.RequestFinished = time.Now().Unix()
c.JSON(400, response)
return
}
err = s.DeleteCartItem(ctx, int64(query.FuserId), int64(query.ProductId))
if err != nil {
response.Status = status.STATUS_BAD_REQUEST
response.Info = fmt.Sprintf("Error: %s", err.Error())
meta.RequestFinished = time.Now().Unix()
c.JSON(400, response)
return return
} }
services.DeleteCartItem(req.FuserId, req.ProductId) meta.RequestFinished = time.Now().Unix()
response.Info = fmt.Sprintf("Item has dropped from cart")
c.JSON(http.StatusNoContent, models.Response{Status: http.StatusNoContent}) response.Status = status.STATUS_OK
c.JSON(204, response)
} }
//func (h *handlers) CreateCartItem(c *gin.Context) {
// req := createCartItemRequest{}
// err := c.ShouldBindJSON(&req)
//
// if err != nil {
// c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("Bad request. Error info: %s", err.Error())})
// return
// }
// services.AddItemToCart(req.FuserId, req.ProductId)
//
// c.JSON(http.StatusCreated, models.Response{Status: http.StatusCreated, Info: fmt.Sprintf("Item %d has added to cart", req.ProductId)})
//}
//
//func (h *handlers) UpdateCartItem(c *gin.Context) {
// req := updateCartRequest{}
// err := c.ShouldBindJSON(&req)
//
// if err != nil {
// c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("Bad request. Error info: %s", err.Error())})
// return
// }
//
// err = services.UpdateCartItem(req.FuserId, req.ProductId, req.Quantity)
//
// if err != nil {
// c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("Bad request. Error info: %s", err.Error())})
// return
// }
//
// c.JSON(http.StatusOK, models.Response{Status: http.StatusOK})
//
//}
//
//func (h *handlers) DeleteCartItem(c *gin.Context) {
//
// req := deleteCartRequest{}
// err := c.ShouldBindJSON(&req)
//
// if err != nil {
// c.JSON(400, models.Response{Status: 400, Info: fmt.Sprintf("Bad request. Error info: %s", err.Error())})
// return
// }
//
// services.DeleteCartItem(req.FuserId, req.ProductId)
//
// c.JSON(http.StatusNoContent, models.Response{Status: http.StatusNoContent})
//}

View File

@ -13,7 +13,6 @@ func HandleRoutes(parent *gin.RouterGroup) {
cart.GET("", h.GetCartItems) cart.GET("", h.GetCartItems)
cart.POST("", h.CreateFUser) cart.POST("", h.CreateFUser)
} }
{ {
itemRouter.POST("", h.CreateCartItem) itemRouter.POST("", h.CreateCartItem)
itemRouter.PATCH("", h.UpdateCartItem) itemRouter.PATCH("", h.UpdateCartItem)

View File

@ -1,41 +1,117 @@
package endpoints package endpoints
import ( import (
"context"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"relynolli-server/models" "relynolli-server/models"
"relynolli-server/services" "relynolli-server/status"
"strconv" "relynolli-server/storage"
"time"
cmap "github.com/orcaman/concurrent-map/v2"
"github.com/gin-gonic/gin"
// "relynolli-server/models"
// "relynolli-server/services"
// "strconv"
) )
func (h *handlers) GetCatalogItems(c *gin.Context) { type GetCatalogItemsRequest struct {
Limit int `form:"limit" `
Page int `form:"page"`
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) func (h *handlers) GetCatalogItems(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) queries := cmap.New[[]string]()
offset := (page - 1) * limit for key, val := range c.Request.URL.Query() {
if c.DefaultQuery("isFilter", "0") == "0" { queries.Set(key, val)
c.JSON(200, services.GetCatalogItems(limit, offset)) }
ctx := context.Background()
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
LPQuery := new(GetCatalogItemsRequest)
LPError := c.ShouldBindQuery(LPQuery)
if LPError != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(400, models.Response{
Status: status.STATUS_BAD_REQUEST,
Info: "Limit and page query params should be integer numbers",
Meta: &meta,
})
return return
} }
c.JSON(200, services.FilterCatalogItems(c.Request.URL.Query(), limit, offset)) if LPQuery.Page == 0 {
LPQuery.Page = 1
}
if LPQuery.Limit == 0 {
LPQuery.Limit = 10
}
s := storage.NewStorageCatalog()
count, items, err := s.GetCatalogItems(ctx, queries, LPQuery.Limit, (LPQuery.Page-1)*LPQuery.Limit)
if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(500,
models.Response{
Status: status.STATUS_SERVER_ERROR,
Info: fmt.Sprintf("Cannot resolve request. Details: %s", err.Error()),
Meta: &meta})
}
meta.Limit = LPQuery.Limit
meta.Page = LPQuery.Page
meta.RequestFinished = time.Now().Unix()
meta.Count = count
c.JSON(200, models.Response{
Status: status.STATUS_OK,
Data: items,
Meta: &meta,
})
}
type catalogItemReq struct {
Code string `uri:"code"`
} }
func (h *handlers) GetCatalogItem(c *gin.Context) { func (h *handlers) GetCatalogItem(c *gin.Context) {
code := c.Param("code") ctx := context.Background()
if code == "" { meta := models.Meta{
c.JSON(400, models.Response{Status: 400, Info: "product \"Code\" should be provided"}) RequestStarted: time.Now().Unix(),
return }
s := storage.NewStorageCatalog()
path := new(catalogItemReq)
var err error = nil
var statusCode int = 200
var response models.Response = models.Response{
Status: status.STATUS_OK,
Meta: &meta,
} }
resp, err := services.GetCatalogItem(code) err = c.ShouldBindUri(path)
if err != nil { if err != nil {
c.JSON(404, models.Response{Status: 404, Info: err.Error()}) response.Info = fmt.Sprintf("Error: %s", err.Error())
response.Status = status.STATUS_BAD_REQUEST
meta.RequestFinished = time.Now().Unix()
statusCode = 400
c.JSON(statusCode, response)
return return
} }
c.JSON(200, resp) data, err := s.GetCatalogItemByCode(ctx, path.Code)
}
func (h *handlers) Count(c *gin.Context) { response.Data = data
c.JSON(200, models.Response{Status: 200, Info: fmt.Sprintf("%d", services.GetCatalogItemsCount())}) meta.RequestFinished = time.Now().Unix()
if data == nil {
statusCode = 404
response.Status = status.STATUS_NOT_FOUND
}
c.JSON(statusCode, response)
} }

View File

@ -8,7 +8,6 @@ type Handlers interface {
GetFilters(c *gin.Context) GetFilters(c *gin.Context)
GetCatalogItems(c *gin.Context) GetCatalogItems(c *gin.Context)
GetCatalogItem(c *gin.Context) GetCatalogItem(c *gin.Context)
Count(c *gin.Context)
} }
func GetHandlers() Handlers { func GetHandlers() Handlers {

View File

@ -1,38 +1,41 @@
package endpoints package endpoints
import ( import (
"encoding/json" "context"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"relynolli-server/internal" "relynolli-server/models"
"relynolli-server/status"
"relynolli-server/storage"
"time"
) )
type filterValues struct {
Id int `json:"id"`
Value string `json:"value"`
}
type filterStruct struct {
Id int `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Values []filterValues `json:"values"`
valuesString []byte
}
func (h *handlers) GetFilters(c *gin.Context) { func (h *handlers) GetFilters(c *gin.Context) {
stmt := "select * from api_filter;"
var responseData []filterStruct
db := internal.InitDatabase() meta := models.Meta{
rows := db.Query(stmt) RequestStarted: time.Now().Unix(),
for rows.Next() {
filter := filterStruct{}
// grab data from db
rows.Scan(&filter.Id, &filter.Code, &filter.Name, &filter.valuesString)
json.Unmarshal(filter.valuesString, &filter.Values)
// parse data as json
responseData = append(responseData, filter)
} }
c.JSON(200, responseData) s := storage.NewStorageCatalog()
ctx := context.Background()
count, items, err := s.GetFilters(ctx)
if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(500, models.Response{
Status: status.STATUS_SERVER_ERROR,
Info: fmt.Sprintf("Internal Server Error: %s", err.Error()),
Meta: &meta,
})
}
meta.RequestFinished = time.Now().Unix()
meta.Count = count
c.JSON(200, models.Response{
Status: status.STATUS_OK,
Data: &items,
Meta: &meta,
})
} }

View File

@ -1,11 +1,17 @@
package catalog package catalog
import ( import (
"os"
"relynolli-server/handlers/catalog/endpoints" "relynolli-server/handlers/catalog/endpoints"
"relynolli-server/internal" "relynolli-server/internal"
"time" "time"
"github.com/gin-contrib/cache" "github.com/gin-contrib/cache"
// "relynolli-server/internal"
// "time"
// "github.com/gin-contrib/cache"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -13,8 +19,18 @@ func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers() h := endpoints.GetHandlers()
cacheStore := internal.InitCacheStore() cacheStore := internal.InitCacheStore()
catalog := parent.Group("/catalog") catalog := parent.Group("/catalog")
catalog.GET("/filters", cache.CachePage(cacheStore, 15, h.GetFilters)) if os.Getenv("IS_PROD") == "1" {
catalog.GET("/count", cache.CachePage(cacheStore, 15 * time.Minute, h.Count)) // Caching for production usage
catalog.GET("", cache.CachePage(cacheStore, 15 * time.Minute, h.GetCatalogItems)) catalog.GET("", cache.CachePage(cacheStore, 15*time.Minute, h.GetCatalogItems))
catalog.GET("/:code", cache.CachePage(cacheStore, 15 * time.Minute, h.GetCatalogItem)) catalog.GET("/filters", cache.CachePage(cacheStore, 15*time.Minute, h.GetFilters))
catalog.GET("/:code", cache.CachePage(cacheStore, 15*time.Minute, h.GetCatalogItem))
} else {
catalog.GET("", h.GetCatalogItems)
catalog.GET("/:code", h.GetCatalogItem)
catalog.GET("/filters", h.GetFilters)
}
// catalog.GET("/filters", cache.CachePage(cacheStore, 15, h.GetFilters))
// catalog.GET("/count", cache.CachePage(cacheStore, 15 * time.Minute, h.Count))
// catalog.GET("/:code", cache.CachePage(cacheStore, 15 * time.Minute, h.GetCatalogItem))
} }

View File

@ -0,0 +1,112 @@
package endpoints
import (
"context"
"encoding/json"
"fmt"
"github.com/geotrace/geo"
"github.com/gin-gonic/gin"
"os"
cdek "relynolli-server/CDEK/v2"
"relynolli-server/internal"
"relynolli-server/models"
"relynolli-server/status"
"time"
)
type handlers struct{}
type Handlers interface {
GetDeliveryPoints(c *gin.Context)
}
func GetHandlers() Handlers {
return &handlers{}
}
type CoordRequest struct {
Lat float64 `form:"lat"`
Lon float64 `form:"lon"`
}
func (h *handlers) GetDeliveryPoints(c *gin.Context) {
query := new(CoordRequest)
ctx := context.Background()
err := c.ShouldBindQuery(&query)
meta := models.Meta{
RequestStarted: time.Now().Unix(),
RequestFinished: 0,
}
resp := models.Response{
Status: status.STATUS_OK,
Info: "",
Data: nil,
Meta: &meta,
}
if err != nil {
meta.RequestFinished = time.Now().Unix()
resp.Info = err.Error()
c.JSON(400, resp)
return
}
client := cdek.NewClient(&cdek.Options{
Endpoint: cdek.EndpointProd,
Credentials: &cdek.Credentials{ClientID: os.Getenv("CDEK_ACCOUNT_ID"),
ClientSecret: os.Getenv("CDEK_API_KEY")},
})
if err != nil {
meta.RequestFinished = time.Now().Unix()
resp.Info = err.Error()
c.JSON(400, resp)
return
}
rdb := internal.InitRedis()
keys, _ := rdb.Keys(ctx, "CDEK_DP:*").Result()
preflightResult := []cdek.DeliveryPoint{}
if len(keys) == 0 {
r1, _ := client.DeliveryPoints(ctx, &cdek.DeliveryPointsRequest{})
pipe := rdb.Pipeline()
for _, d := range *r1 {
str, _ := json.Marshal(d)
pipe.Set(ctx, fmt.Sprintf("CDEK_DP:%s", d.Code), str, -1).Err()
preflightResult = append(preflightResult, d)
}
pipe.Exec(ctx)
} else {
for _, key := range keys {
item := new(cdek.DeliveryPoint)
data, _ := rdb.Get(ctx, key).Result()
json.Unmarshal([]byte(data), item)
preflightResult = append(preflightResult, *item)
}
}
pointOrigin := geo.Point{
query.Lon,
query.Lat,
}
resultedArray := []cdek.DeliveryPoint{}
for _, d := range preflightResult {
p1 := geo.Point{
d.Location.Longitude,
d.Location.Latitude,
}
if pointOrigin.Distance(p1) < 10000 {
resultedArray = append(resultedArray, d)
}
}
meta.RequestFinished = time.Now().Unix()
resp.Data = resultedArray
c.JSON(200, resp)
}

18
handlers/cdek/routes.go Normal file
View File

@ -0,0 +1,18 @@
package cdek
import (
"github.com/gin-gonic/gin"
"os"
"relynolli-server/handlers/cdek/endpoints"
)
func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers()
cdek := parent.Group("/cdek")
if os.Getenv("IS_PROD") == "1" {
// Caching for production usage
cdek.GET("/points", h.GetDeliveryPoints)
} else {
cdek.GET("/points", h.GetDeliveryPoints)
}
}

View File

@ -0,0 +1,113 @@
package endpoints
import (
"context"
"fmt"
"relynolli-server/models"
"relynolli-server/status"
"relynolli-server/storage"
"time"
"github.com/gin-gonic/gin"
)
type handlers struct{}
type Handlers interface {
GetNews(c *gin.Context)
RetrieveNews(c *gin.Context)
}
func GetHandlers() Handlers {
return &handlers{}
}
type ListNewsRequest struct {
Limit int `form:"limit" `
Page int `form:"page"`
}
func (h *handlers) GetNews(c *gin.Context) {
ctx := context.Background()
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
query := ListNewsRequest{
Limit: 10,
Page: 1,
}
err := c.ShouldBindQuery(&query)
if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(400, models.Response{
Status: status.STATUS_BAD_REQUEST,
Info: fmt.Sprintf("Error: %s", err.Error()),
Meta: &meta,
})
return
}
s := storage.NewStorageNews()
count, resp, err := s.GetNews(ctx, int64(query.Limit), int64(query.Limit * (query.Page - 1)))
if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(500, models.Response{
Status: status.STATUS_SERVER_ERROR,
Info: fmt.Sprintf("Error: %s", err.Error()),
Meta: &meta,
})
return
}
meta.Count = count
meta.Limit = query.Limit
meta.Page = query.Page
c.JSON(200, models.Response{
Status: status.STATUS_OK,
Data: resp,
Meta: &meta,
})
}
type retireveNewsReq struct {
Code string `uri:"code" binding:"required"`
}
func (h *handlers) RetrieveNews(c *gin.Context) {
ctx := context.Background()
meta := models.Meta {
RequestStarted: time.Now().Unix(),
}
query := new(retireveNewsReq)
err := c.ShouldBindUri(query)
if err != nil {
meta.RequestFinished = time.Now().Unix()
c.JSON(400, models.Response{
Status: status.STATUS_BAD_REQUEST,
Info: fmt.Sprintf("Error: %s", err.Error()),
Meta: &meta,
})
return
}
s := storage.NewStorageNews()
resp, _ := s.RetrieveNews(ctx, query.Code)
meta.RequestFinished = time.Now().Unix()
statusResult := status.STATUS_OK
responseCode := 200
if resp == nil {
statusResult = status.STATUS_NOT_FOUND
responseCode = 404
}
c.JSON(responseCode, models.Response{
Status: statusResult,
Data: resp,
Meta: &meta,
})
}

27
handlers/news/routes.go Normal file
View File

@ -0,0 +1,27 @@
package news
import (
"os"
"relynolli-server/handlers/news/endpoints"
"relynolli-server/internal"
"time"
"github.com/gin-contrib/cache"
"github.com/gin-gonic/gin"
)
func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers()
cacheStore := internal.InitCacheStore()
catalog := parent.Group("/news")
if os.Getenv("IS_PROD") == "1" {
// Caching for production usage
catalog.GET("", cache.CachePage(cacheStore, 15*time.Minute, h.GetNews))
catalog.GET("/:code", cache.CachePage(cacheStore, 15*time.Minute, h.RetrieveNews))
} else {
catalog.GET("", h.GetNews)
catalog.GET("/:code", h.RetrieveNews)
}
}

View File

@ -1,18 +1,25 @@
package endpoints package endpoints
import ( import (
"context"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"net/http" "net/http"
"regexp"
"relynolli-server/models" "relynolli-server/models"
"relynolli-server/services" "relynolli-server/services"
"relynolli-server/status"
"relynolli-server/storage"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type handlers struct{} type handlers struct{}
type getTotalRequest struct { type getTotalRequest struct {
FuserId int `json:"fuserId"` FuserId int `json:"fuserId"`
Coupon *string `json:"coupon,omitempty"`
} }
type getTotalResponse struct { type getTotalResponse struct {
@ -31,21 +38,55 @@ type makeOrderRequest struct {
func (h handlers) GetTotal(c *gin.Context) { func (h handlers) GetTotal(c *gin.Context) {
req := getTotalRequest{} req := getTotalRequest{}
err := c.ShouldBindJSON(&req) err := c.ShouldBindJSON(&req)
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
ctx := context.Background()
s := storage.NewStorageOrder()
resp := models.Response{
Status: status.STATUS_OK,
Meta: &meta,
}
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: "fuserId is not provided"}) meta.RequestFinished = time.Now().Unix()
resp.Status = status.STATUS_BAD_REQUEST
resp.Info = fmt.Sprintf("Error: %s", err.Error())
c.JSON(400, resp)
return return
} }
total := services.GetTotal(req.FuserId)
c.JSON(http.StatusOK, getTotalResponse{TotalProductPrice: total}) data, err := s.GetTotal(ctx, int64(req.FuserId), req.Coupon)
if err != nil {
resp.Status = status.STATUS_SERVER_ERROR
resp.Info = err.Error()
meta.RequestFinished = time.Now().Unix()
c.JSON(500, resp)
}
resp.Data = data
meta.RequestFinished = time.Now().Unix()
c.JSON(200, resp)
} }
func (h handlers) MakeOrder(c *gin.Context) { func (h handlers) MakeOrder(c *gin.Context) {
ctx := context.Background()
// VALIDATION // VALIDATION
validate := validator.New(validator.WithRequiredStructEnabled()) validate := validator.New(validator.WithRequiredStructEnabled())
req := makeOrderRequest{} req := makeOrderRequest{}
err := c.ShouldBindJSON(&req) err := c.ShouldBindJSON(&req)
meta := models.Meta{
RequestStarted: time.Now().Unix(),
}
resp := models.Response{
Status: status.STATUS_OK,
Meta: &meta,
}
if err != nil { if err != nil {
c.JSON(400, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("ERROR: %s", err.Error())}) meta.RequestFinished = time.Now().Unix()
resp.Info = fmt.Sprintf("ERROR: %s", err.Error())
resp.Status = status.STATUS_BAD_REQUEST
c.JSON(400, resp)
return return
} }
@ -53,18 +94,35 @@ func (h handlers) MakeOrder(c *gin.Context) {
if validationErr != nil { if validationErr != nil {
responseErr := validationErr.(validator.ValidationErrors)[0] responseErr := validationErr.(validator.ValidationErrors)[0]
c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("Validation Error: Field %s should be %s", responseErr.Field(), responseErr.Tag())}) meta.RequestFinished = time.Now().Unix()
resp.Info = responseErr.Error()
resp.Status = status.STATUS_BAD_REQUEST
c.JSON(http.StatusBadRequest, resp)
return
}
phoneMatched, err := regexp.Match("^\\d{11}$", []byte(req.PhoneNumber))
if phoneMatched == false {
meta.RequestFinished = time.Now().Unix()
resp.Info = "Phone number is not valid"
resp.Status = status.STATUS_BAD_REQUEST
c.JSON(http.StatusBadRequest, resp)
return return
} }
kassaResult, serviceErr := services.MakeOrder(req.FuserId, req.Email, req.FullName, req.PhoneNumber) kassaResult, serviceErr := services.MakeOrder(ctx, req.FuserId, req.Email, req.FullName, req.PhoneNumber)
if serviceErr != nil { if serviceErr != nil {
c.JSON(http.StatusInternalServerError, models.Response{Status: http.StatusInternalServerError, Info: fmt.Sprintf("Error: %s", serviceErr.Error())}) meta.RequestFinished = time.Now().Unix()
resp.Info = fmt.Sprintf("Error: %s", serviceErr.Error())
resp.Status = status.STATUS_SERVER_ERROR
c.JSON(http.StatusInternalServerError, resp)
return return
} }
c.JSON(http.StatusOK, kassaResult) resp.Data = kassaResult
meta.RequestFinished = time.Now().Unix()
c.JSON(http.StatusOK, resp)
} }
type Handlers interface { type Handlers interface {

View File

@ -1,8 +1,9 @@
package order package order
import ( import (
"github.com/gin-gonic/gin"
"relynolli-server/handlers/order/endpoints" "relynolli-server/handlers/order/endpoints"
"github.com/gin-gonic/gin"
) )
func HandleRoutes(parent *gin.RouterGroup) { func HandleRoutes(parent *gin.RouterGroup) {

View File

@ -1,17 +1,28 @@
package handlers package handlers
import ( import (
"github.com/gin-gonic/gin" "relynolli-server/handlers/address"
"relynolli-server/handlers/article"
"relynolli-server/handlers/cart" "relynolli-server/handlers/cart"
"relynolli-server/handlers/catalog" "relynolli-server/handlers/cdek"
"relynolli-server/handlers/news"
"relynolli-server/handlers/order" "relynolli-server/handlers/order"
"relynolli-server/handlers/validate"
"github.com/gin-gonic/gin"
// "relynolli-server/handlers/cart"
"relynolli-server/handlers/catalog"
// "relynolli-server/handlers/order"
// "relynolli-server/handlers/validate"
) )
func InitializeRouter(router *gin.Engine) { func InitializeRouter(router *gin.Engine) {
APIV1Router := router.Group("/api/v1") APIV1Router := router.Group("/api/v1")
catalog.HandleRoutes(APIV1Router) catalog.HandleRoutes(APIV1Router)
cart.HandleRoutes(APIV1Router) cart.HandleRoutes(APIV1Router)
news.HandleRoutes(APIV1Router)
order.HandleRoutes(APIV1Router) order.HandleRoutes(APIV1Router)
validate.HandleRoutes(APIV1Router) article.HandleRoutes(APIV1Router)
address.HandleRoutes(APIV1Router)
cdek.HandleRoutes(APIV1Router)
} }

View File

@ -1,11 +1,10 @@
package endpoints package endpoints
import ( import (
"fmt"
"github.com/gin-gonic/gin"
"net/http" "net/http"
"relynolli-server/models"
"relynolli-server/services" "relynolli-server/services"
"github.com/gin-gonic/gin"
) )
type handlers struct{} type handlers struct{}
@ -23,8 +22,7 @@ func (_ handlers) Validate(c *gin.Context) {
req := ValidateReq{} req := ValidateReq{}
err := c.ShouldBindJSON(&req) err := c.ShouldBindJSON(&req)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, c.JSON(http.StatusBadRequest)
Info: fmt.Sprintf("Error: %s", err.Error())})
} }
services.YookassaValidate(req.Object.Id, req.Object.Status) services.YookassaValidate(req.Object.Id, req.Object.Status)

View File

@ -3,24 +3,22 @@ package internal
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
_ "github.com/go-sql-driver/mysql" "github.com/uptrace/bun/extra/bundebug"
"github.com/jmoiron/sqlx"
"log" "log"
"os" "os"
"sync" "sync"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mysqldialect"
) )
type database struct { type database struct {
instance *sqlx.DB instance *bun.DB
} }
type Database interface { type Database interface {
GetInstance() *sqlx.DB GetInstance() *bun.DB
Close()
Query(stmt string) *sqlx.Rows
Execute(stmt string) sql.Result
FetchRows(stmt string, dest interface{})
} }
var ( var (
@ -29,27 +27,31 @@ var (
) )
func initialize() { func initialize() {
db, err := sqlx.Open("mysql", fmt.Sprintf( db, err := sql.Open("mysql", fmt.Sprintf(
"%s:%s@tcp(%s)/%s", "%s:%s@tcp(%s)/%s",
os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_USER"),
os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_PASSWORD"),
os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_HOST"),
os.Getenv("MYSQL_DATABASE"))) os.Getenv("MYSQL_DATABASE")))
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(3)
db.SetMaxIdleConns(3)
if err != nil { if err != nil {
panic(err) panic(err)
} }
if db.Ping() != nil { // Resolve instances of bun
panic(err)
ormDb := bun.NewDB(db, mysqldialect.New())
ormDb.AddQueryHook(bundebug.NewQueryHook())
// Check Connection
conErr := ormDb.Ping()
if conErr != nil {
panic(conErr)
} }
log.Println("Connection to db succeded") log.Println("Connection to db succeded")
instance = &database{instance: db} instance = &database{instance: ormDb}
} }
func InitDatabase() Database { func InitDatabase() Database {
@ -59,39 +61,6 @@ func InitDatabase() Database {
return instance return instance
} }
func (db *database) GetInstance() *sqlx.DB { func (d *database) GetInstance() *bun.DB {
return db.instance return d.instance
}
func (db *database) Close() {
defer log.Println("Connection to database was closed")
err := db.instance.Close()
if err != nil {
return
}
}
func (db *database) Query(stmt string) *sqlx.Rows {
rows, err := db.instance.Queryx(stmt)
if err != nil {
return nil
}
return rows
}
func (db *database) Execute(stmt string) sql.Result {
result, err := db.instance.Exec(stmt)
if err != nil {
log.Println(err)
}
return result
}
type FetchRowStruct []interface{}
func (db *database) FetchRows(stmt string, dest interface{}) {
err := db.instance.Select(dest, stmt)
if err != nil {
log.Println(err)
}
} }

View File

@ -19,14 +19,14 @@ var (
type Cache interface { type Cache interface {
} }
func InitRedis() (*redis.Client) { func InitRedis() *redis.Client {
if redisInstance == nil { if redisInstance == nil {
redis_db_num, err := strconv.Atoi(os.Getenv("REDIS_DATABASE")) redis_db_num, err := strconv.Atoi(os.Getenv("REDIS_DATABASE"))
if err != nil { if err != nil {
log.Fatalln("REDIS_DATABASE should be integer") log.Fatalln("REDIS_DATABASE should be integer")
} }
redisInstance = redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDRESS"), Password: os.Getenv("REDIS_PASSWORD"), DB: redis_db_num}) redisInstance = redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDRESS"), Password: os.Getenv("REDIS_PASSWORD"), DB: redis_db_num, Username: os.Getenv("REDIS_USERNAME")})
_, conError := redisInstance.Ping(context.Background()).Result() _, conError := redisInstance.Ping(context.Background()).Result()
if conError != nil { if conError != nil {
@ -36,9 +36,9 @@ func InitRedis() (*redis.Client) {
return redisInstance return redisInstance
} }
func InitCacheStore() *persistence.RedisStore{ func InitCacheStore() *persistence.RedisStore {
if cacheStore == nil { if cacheStore == nil {
cacheStore = persistence.NewRedisCache(os.Getenv("REDIS_ADDRESS"), os.Getenv("REDIS_PASSWORD"), 15 * time.Minute) cacheStore = persistence.NewRedisCache(os.Getenv("REDIS_ADDRESS"), os.Getenv("REDIS_PASSWORD"), 15*time.Minute)
} }
return cacheStore return cacheStore
} }

View File

@ -6,6 +6,7 @@ import (
"os/signal" "os/signal"
"relynolli-server/handlers" "relynolli-server/handlers"
"relynolli-server/internal" "relynolli-server/internal"
"relynolli-server/services"
"syscall" "syscall"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
@ -28,13 +29,14 @@ func main() {
rdb := internal.InitRedis() rdb := internal.InitRedis()
handlers.InitializeRouter(server) handlers.InitializeRouter(server)
defer db.Close() defer db.GetInstance().Close()
defer rdb.Close() defer rdb.Close()
gracefullyShutDown := make(chan os.Signal, 1) gracefullyShutDown := make(chan os.Signal, 1)
signal.Notify(gracefullyShutDown, syscall.SIGINT, syscall.SIGTERM) signal.Notify(gracefullyShutDown, syscall.SIGINT, syscall.SIGTERM)
go server.Run("0.0.0.0:8000") go server.Run("0.0.0.0:8000")
go services.PaymentValidation()
<-gracefullyShutDown <-gracefullyShutDown

1
models/article/db.go Normal file
View File

@ -0,0 +1 @@
package article

27
models/cart/db.go Normal file
View File

@ -0,0 +1,27 @@
package cart
import (
"github.com/uptrace/bun"
"relynolli-server/models/catalog"
"time"
)
type DBFuser struct {
bun.BaseModel `bun:"table:b_sale_fuser"`
Id int64 `bun:"ID,pk" json:"id"`
Code string `bun:"CODE,default:md5(now())" json:"code" json:"code"`
UserId int64 `bun:"USER_ID,nullzero" json:"userId"`
DateInserted time.Time `bun:"DATE_INSERT" json:"dateInserted"`
DateUpdated time.Time `bun:"DATE_UPDATE" json:"dateUpdated"`
}
type DBCart struct {
bun.BaseModel `bun:"table:api_cart"`
Id int64 `bun:"id,pk" json:"id"`
FuserId int64 `bun:"fuser_id" json:"fuserId"`
ProductId int64 `bun:"product_id" json:"productId"`
PriceTypeId int64 `bun:"price_type_id" json:"priceTypeId"`
Quantity int64 `bun:"quantity" json:"quantity"`
Fuser *DBFuser `bun:"rel:belongs-to,join:fuser_id=ID" json:"fuser"`
Product *catalog.DBCatalog `bun:"rel:belongs-to,join:product_id=id" json:"product"`
}

View File

@ -1,47 +1,47 @@
package models package models
type CatalogStruct struct { // type CatalogStruct struct {
Id int // Id int
Code string // Code string
Name string // Name string
IsActive int `json:"is_active" db:"is_active"` // IsActive int `json:"is_active" db:"is_active"`
Properties []byte // Properties []byte
DetailText string `json:"detailText" db:"detailText"` // DetailText string `json:"detailText" db:"detailText"`
Price []byte // Price []byte
AvailableQuantity int `json:"availableQuantity,omitempty" db:"available_quantity"` // AvailableQuantity int `json:"availableQuantity,omitempty" db:"available_quantity"`
} // }
type CatalogStructWeb struct { // type CatalogStructWeb struct {
Id int `json:"id"` // Id int `json:"id"`
Code string `json:"code"` // Code string `json:"code"`
Name string `json:"name"` // Name string `json:"name"`
IsActive int `json:"is_active" db:"is_active"` // IsActive int `json:"is_active" db:"is_active"`
Properties map[string]interface{} `json:"properties"` // Properties map[string]interface{} `json:"properties"`
DetailText string `json:"detailText" db:"detailText"` // DetailText string `json:"detailText" db:"detailText"`
Price map[string]interface{} `json:"price"` // Price map[string]interface{} `json:"price"`
AvailableQuantity int `json:"availableQuantity,omitempty" db:"available_quantity"` // AvailableQuantity int `json:"availableQuantity,omitempty" db:"available_quantity"`
} // }
type CatalogWithQuantityWeb struct { // type CatalogWithQuantityWeb struct {
Id int `json:"id"` // Id int `json:"id"`
Code string `json:"code"` // Code string `json:"code"`
Name string `json:"name"` // Name string `json:"name"`
IsActive int `json:"is_active"` // IsActive int `json:"is_active"`
Properties map[string]interface{} `json:"properties"` // Properties map[string]interface{} `json:"properties"`
DetailText string `json:"detailText"` // DetailText string `json:"detailText"`
Price map[string]interface{} `json:"price"` // Price map[string]interface{} `json:"price"`
Quantity int `json:"quantity"` // Quantity int `json:"quantity"`
AvailableQuantity int `json:"available_quantity" db:"available_quantity"` // AvailableQuantity int `json:"available_quantity" db:"available_quantity"`
} // }
type CatalogWithQuantity struct { // type CatalogWithQuantity struct {
Id int // Id int
Code string // Code string
Name string // Name string
IsActive int `json:"is_active" db:"is_active"` // IsActive int `json:"is_active" db:"is_active"`
Properties []byte // Properties []byte
DetailText string `json:"detailText" db:"detailText"` // DetailText string `json:"detailText" db:"detailText"`
Price []byte // Price []byte
Quantity int `json:"quantity"` // Quantity int `json:"quantity"`
AvailableQuantity int `json:"available_quantity" db:"available_quantity"` // AvailableQuantity int `json:"available_quantity" db:"available_quantity"`
} // }

52
models/catalog/db.go Normal file
View File

@ -0,0 +1,52 @@
package catalog
import "github.com/uptrace/bun"
type DBCatalog struct {
bun.BaseModel `bun:"select:api_catalog"`
Id int64 `bun:"id,pk" json:"id"`
Code string `bun:"code" json:"code"`
Name string `bun:"name" json:"name"`
IsActive bool `bun:"is_active,type:integer" json:"isActive"`
Properties *DBCatalogProperties `bun:"properties" json:"properties"`
DetailText string `bun:"detailText" json:"detailText"`
Price *DBCatalogPrice `bun:"price" json:"price"`
AvailableQuantity int64 `bun:"available_quantity" json:"availableQuantity"`
}
type DBCatalogProperties struct {
Acea string `json:"acea,omitempty"`
Width string `json:"width,omitempty"`
Height string `json:"height,omitempty"`
Length string `json:"length,omitempty"`
Volume string `json:"volume,omitempty"`
Weight string `json:"weight,omitempty"`
Mileage string `json:"mileage,omitempty"`
BoxType string `json:"box_type,omitempty"`
Category string `json:"category,omitempty"`
OilType string `json:"oil_type,omitempty"`
Documents []string `json:"documents,omitempty"`
UseAreas string `json:"use_areas,omitempty"`
Viscosity string `json:"viscosity,omitempty"`
AcidIndex string `json:"acid_index,omitempty"`
MainImage []string `json:"main_image,omitempty"`
PourPoint string `json:"pour_point,omitempty"`
FlashPoint string `json:"flash_point,omitempty"`
Subcategory string `json:"subcategory,omitempty"`
VendorCode string `json:"vendor_code,omitempty"`
ApiStandart string `json:"api_standart,omitempty"`
Requirements string `json:"requirements,omitempty"`
ViscosityIndex string `json:"viscosity_index,omitempty"`
ViscosityKinematic string `json:"viscosity_kinematic,omitempty"`
TribologicalProperties string `json:"tribological_properties,omitempty"`
}
type DBCatalogPrice struct {
BASE float64 `json:"BASE"`
OPTMAX float64 `json:"OPTMAX,omitempty"`
OPTMIN float64 `json:"OPTMIN,omitempty"`
MOC float64 `json:"Мелко-Оптовая Цена (МОЦ),omitempty"`
KOC float64 `json:"Крупно-Оптовая Цена (КОЦ),omitempty"`
MCP float64 `json:"Минимальная Цена Продаж (МЦП),omitempty"`
RRC float64 `json:"Рекомендуемая Розничная цена (РРЦ),omitempty"`
}

1
models/catalog/domain.go Normal file
View File

@ -0,0 +1 @@
package catalog

View File

@ -0,0 +1 @@
package catalog

41
models/discount/db.go Normal file
View File

@ -0,0 +1,41 @@
package discount
import "github.com/uptrace/bun"
type DBDiscount struct {
bun.BaseModel `bun:"table:b_sale_discount"`
ID int64
Name string
Actions string
}
type DomainDiscounts struct {
ID int64
Name string
Actions *DomainActions
}
type DomainActions struct {
CLASSID string `json:"CLASS_ID"`
DATA struct {
All string `json:"All"`
} `json:"DATA"`
CHILDREN []struct {
CLASSID string `json:"CLASS_ID"`
DATA struct {
Type string `json:"Type"`
Value int `json:"Value"`
Unit string `json:"Unit"`
Max int `json:"Max"`
All string `json:"All"`
True string `json:"True"`
} `json:"DATA"`
CHILDREN map[string]struct {
CLASSID string `json:"CLASS_ID"`
DATA struct {
Logic string `json:"logic"`
Value string `json:"value"`
} `json:"DATA"`
} `json:"CHILDREN"`
} `json:"CHILDREN"`
}

16
models/filters/db.go Normal file
View File

@ -0,0 +1,16 @@
package filters
import "github.com/uptrace/bun"
type DBFilter struct {
bun.BaseModel `bun:"select:api_filter"`
Id int64 `bun:"id" json:"id"`
Code string `bun:"code" json:"code"`
Name string `bun:"name" json:"name"`
Values *[]DBFilterValues `bun:"values" json:"values"`
}
type DBFilterValues struct {
Id int64 `json:"id"`
Value string `json:"value"`
}

31
models/news/db.go Normal file
View File

@ -0,0 +1,31 @@
package news
import (
"time"
"github.com/uptrace/bun"
)
type DBNews struct {
bun.BaseModel `bun:"select:api_news"`
ID int64 `bun:"id" json:"id"`
IsActive bool `bun:"is_active" json:"isActive"`
Sort int64 `bun:"sort" json:"sort"`
Name string `bun:"name" json:"name"`
Content string `bun:"content" json:"content"`
Code string `bun:"code" json:"code"`
Picture string `bun:"picture" json:"picture"`
Date time.Time `bun:"date" json:"date"`
}
type DBArticle struct {
bun.BaseModel `bun:"select:api_article"`
ID int64 `bun:"id" json:"id"`
IsActive bool `bun:"is_active" json:"isActive"`
Sort int64 `bun:"sort" json:"sort"`
Name string `bun:"name" json:"name"`
Content string `bun:"content" json:"content"`
Code string `bun:"code" json:"code"`
Picture string `bun:"picture" json:"picture"`
Date time.Time `bun:"date" json:"date"`
}

21
models/order/db.go Normal file
View File

@ -0,0 +1,21 @@
package order
import (
"github.com/google/uuid"
"github.com/uptrace/bun"
)
type DBPayment struct {
bun.BaseModel `bun:"table:api_youkassa_payment"`
ID uuid.UUID `bun:"payment_id,type:char(36),pk" json:"id"`
OrderId int64 `bun:"order_id" json:"orderId"`
Status string `bun:"status" json:"status"`
Link string `bun:"link" json:"link"`
BitrixPayment *DBOrderPayment `bun:"-"`
}
type DBOrderPayment struct {
bun.BaseModel `bun:"table:b_sale_order_payment"`
ID int `bun:"ID,pk"`
OrderId int `bun:"ORDER_ID,pk"`
}

View File

@ -1,7 +1,19 @@
package models package models
import "relynolli-server/status"
type Response struct { type Response struct {
Status int `json:"status"` Status status.Status `json:"status"`
Info string `json:"info,omitempty"` Info string `json:"info,omitempty"`
Data interface{} `json:"data,omitempty"` Data interface{} `json:"data,omitempty"`
Meta *Meta `json:"meta"`
}
type Meta struct {
RequestStarted int64 `json:"requestStarted"`
RequestFinished int64 `json:"requestFinished"`
Page int `json:"page,omitempty"`
Limit int `json:"limit,omitempty"`
Count int `json:"count,omitempty"`
} }

View File

@ -1,109 +1 @@
package services package services
import (
"context"
"encoding/json"
"fmt"
"relynolli-server/internal"
"relynolli-server/models"
)
func GetCartItems(fuserId int) []models.CatalogWithQuantityWeb {
rdb := internal.InitRedis()
keys, _ := rdb.Keys(context.Background(), fmt.Sprintf("api.api_cart.%d.*", fuserId)).Result()
result := []models.CatalogWithQuantityWeb{}
for _, key := range keys {
str, _ := rdb.Get(context.Background(), key).Result()
item := models.CatalogWithQuantityWeb{}
json.Unmarshal([]byte(str), &item)
result = append(result, item)
}
return result
}
func CreateFuser() int64 {
stmt := "insert into b_sale_fuser (DATE_INSERT, DATE_UPDATE, CODE) values (now(), now(), md5(rand()));"
db := internal.InitDatabase()
result := db.Execute(stmt)
lastInsertId, _ := result.LastInsertId()
return lastInsertId
}
func AddItemToCart(fuserId int, productId int) {
rdb := internal.InitRedis()
item, _ := GetCatalogItemById(productId)
itemWithQuantity := models.CatalogWithQuantityWeb{
Id: item.Id,
Code: item.Code,
Name: item.Name,
IsActive: item.IsActive,
Properties: item.Properties,
DetailText: item.DetailText,
Price: item.Price,
Quantity: 1,
AvailableQuantity: item.AvailableQuantity,
}
marshaled, _ := json.Marshal(itemWithQuantity)
err := rdb.Set(context.Background(), fmt.Sprintf("api.api_cart.%d.%d", fuserId, productId), string(marshaled), 0).Err()
if err != nil {
panic(err.Error())
}
}
func UpdateCartItem(fuserId int, productId int, quantity int) error {
if quantity <= 0 {
DeleteCartItem(fuserId, productId)
return nil
}
item, _ := GetCatalogItemById(productId)
if item.AvailableQuantity < quantity {
return fmt.Errorf("Available quantity is less than requested. Available %d, requested %d", item.AvailableQuantity, quantity)
}
itemWithQuantity := models.CatalogWithQuantityWeb{
Id: item.Id,
Code: item.Code,
Name: item.Name,
IsActive: item.IsActive,
Properties: item.Properties,
DetailText: item.DetailText,
Price: item.Price,
Quantity: quantity,
AvailableQuantity: item.AvailableQuantity,
}
marshaled, _ := json.Marshal(itemWithQuantity)
rdb := internal.InitRedis()
rdb.Set(context.Background(), fmt.Sprintf("api.api_cart.%d.%d", fuserId, productId), string(marshaled), 0)
return nil
//var availableQunatity int
//stmtQuantity := fmt.Sprintf("select QUANTITY as q from b_catalog_product where ID = %d;", productId)
//updateStmt := fmt.Sprintf("update api_cart set quantity = %d where product_id = %d and fuser_id = %d", quantity, productId, fuserId)
//db := internal.InitDatabase()
//rows := db.Query(stmtQuantity)
//rows.Next()
//rows.Scan(&availableQunatity)
//if quantity > availableQunatity {
// return fmt.Errorf("Available quantity is less than requested. Available %d, requested %d", availableQunatity, quantity)
//}
//db.Execute(updateStmt)
//return nil
}
func DeleteCartItem(fuserId int, productId int) {
rdb := internal.InitRedis()
rdb.Del(context.Background(), fmt.Sprintf("api.api_cart.%d.%d", fuserId, productId)).Err()
}

View File

@ -1,108 +0,0 @@
package services
import (
"encoding/json"
"fmt"
"relynolli-server/internal"
"relynolli-server/models"
"strings"
)
func retrieveItems(stmt string, structure interface{}) {
db := internal.InitDatabase()
db.FetchRows(stmt, structure)
}
func retrieveCatalogItems(stmt string) []models.CatalogStructWeb {
var catalogList []models.CatalogStruct
var returnedList []models.CatalogStructWeb
retrieveItems(stmt, &catalogList)
for _, item := range catalogList {
itemProd := models.CatalogStructWeb{
Id: item.Id,
Code: item.Code,
Name: item.Name,
IsActive: item.IsActive,
DetailText: item.DetailText,
AvailableQuantity: item.AvailableQuantity,
}
json.Unmarshal(item.Price, &itemProd.Price)
json.Unmarshal(item.Properties, &itemProd.Properties)
returnedList = append(returnedList, itemProd)
}
return returnedList
}
func GetCatalogItemsCount() int {
stmt := "select count(id) from api_catalog where available_quantity > 0 and is_active = 1;"
var count int
db := internal.InitDatabase()
rows := db.Query(stmt)
rows.Next()
rows.Scan(&count)
return count
}
func GetCatalogItems(limit int, offset int) []models.CatalogStructWeb {
stmt := fmt.Sprintf("select * from api_catalog where available_quantity > 0 and is_active = 1 order by code limit %d offset %d;", limit, offset)
return retrieveCatalogItems(stmt)
}
func GetCatalogItem(code string) (models.CatalogStructWeb, error) {
stmt := fmt.Sprintf("select * from api_catalog where code = '%s';", code)
items := retrieveCatalogItems(stmt)
if len(items) == 0 {
return models.CatalogStructWeb{}, fmt.Errorf("Not founded catalog item with given code")
}
return retrieveCatalogItems(stmt)[0], nil
}
func GetCatalogItemById(id int) (models.CatalogStructWeb, error) {
stmt := fmt.Sprintf("select * from api_catalog where id = %d;", id)
items := retrieveCatalogItems(stmt)
if len(items) == 0 {
return models.CatalogStructWeb{}, fmt.Errorf("Not founded catalog item with given code")
}
return retrieveCatalogItems(stmt)[0], nil
}
func FilterCatalogItems(filters map[string][]string, limit int, offset int) []models.CatalogStructWeb {
// Generate stmt
propertiesSubStmt := "properties->>'$.%s' = '%s'"
stmt := "select * from api_catalog where %s"
sample := "(%s)"
filterList := [][]string{}
for key, filter := range filters {
if key == "isFilter" {
continue
}
if key == "limit" {
continue
}
if key == "page" {
continue
}
subFilterArr := []string{}
values := strings.Split(filter[0], ",")
for _, val := range values {
subFilterArr = append(subFilterArr, fmt.Sprintf(propertiesSubStmt, key, val))
}
filterList = append(filterList, subFilterArr)
}
samples := []string{}
for _, arr := range filterList {
samples = append(samples, fmt.Sprintf(sample, strings.Join(arr, " or ")))
}
stmt = fmt.Sprintf(stmt, strings.Join(samples, " and "))
print("\n" + stmt + "\n")
return retrieveCatalogItems(stmt + fmt.Sprintf("and is_active = 1 and available_quantity > 0;"))
}

View File

@ -2,7 +2,6 @@ package services
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"relynolli-server/external/bitrix" "relynolli-server/external/bitrix"
"relynolli-server/external/kassa" "relynolli-server/external/kassa"
@ -11,51 +10,21 @@ import (
"relynolli-server/external/kassa/PaymentSubject" "relynolli-server/external/kassa/PaymentSubject"
"relynolli-server/external/kassa/VatCodes" "relynolli-server/external/kassa/VatCodes"
"relynolli-server/internal" "relynolli-server/internal"
"relynolli-server/models" "relynolli-server/storage"
"strconv" "strconv"
"strings" "strings"
) )
func GetTotal(fuserId int) float64 { func addProductsToOrder(ctx context.Context, storage storage.StorageCart, api bitrix.Bitrix, fuserId, orderId int) error {
rdb := internal.InitRedis() // //Получаем данные из корзины
keys, _ := rdb.Keys(context.Background(), fmt.Sprintf("api.api_cart.%d.*", fuserId)).Result() //
items, err := storage.GetCartItems(ctx, int64(fuserId))
result := []models.CatalogWithQuantityWeb{} if err != nil {
return err
for _, key := range keys {
str, _ := rdb.Get(context.Background(), key).Result()
item := models.CatalogWithQuantityWeb{}
json.Unmarshal([]byte(str), &item)
result = append(result, item)
} }
sum := float64(0) for _, product := range *items {
err = api.AddProductToOrder(orderId, int(product.ProductId), product.Product.Price.BASE, int(product.Quantity), product.Product.Name)
for _, catalogItem := range result {
sum = sum + catalogItem.Price["BASE"].(float64)*float64(catalogItem.Quantity)
}
return sum
}
type addProductsToOrderReq struct {
ProductId int `db:"product_id"`
PriceTypeId int `db:"price_type_id"`
Quantity int `db:"quantity"`
Price float64 `db:"price"`
}
func addProductsToOrder(api bitrix.Bitrix, fuserId int, orderId int) error {
//Получаем данные из корзины
cartItems := GetCartItems(fuserId)
rdb := internal.InitRedis()
rdb.Keys(context.Background(), "")
for _, product := range cartItems {
err := api.AddProductToOrder(orderId, product.Id, product.Price["BASE"].(float64), product.Quantity)
if err != nil { if err != nil {
return err return err
} }
@ -64,9 +33,12 @@ func addProductsToOrder(api bitrix.Bitrix, fuserId int, orderId int) error {
} }
func MakeOrder(fuserId int, email string, fullName string, phone string) (map[string]interface{}, error) { //
func MakeOrder(ctx context.Context, fuserId int, email string, fullName, phone string) (*kassa.KassaResult, error) {
//
// Инициализируем api // Инициализируем api
s := storage.NewStorageCart()
api := bitrix.Initialize() api := bitrix.Initialize()
@ -79,14 +51,19 @@ func MakeOrder(fuserId int, email string, fullName string, phone string) (map[st
// --- обновляем контакт пользователя // --- обновляем контакт пользователя
order, orderErr := api.GetOrderInfo(orderId) order, orderErr := api.GetOrderInfo(orderId)
if orderErr != nil { if orderErr != nil {
return nil, orderErr return nil, orderErr
} }
clientId, _ := strconv.Atoi(order.Clients[0].EntityId) clientId, _ := strconv.Atoi(order.Clients[0].EntityId)
api.UpdateContact(clientId, email, fullName, phone) err := api.UpdateContact(clientId, email, fullName, phone)
if err != nil {
return nil, err
}
// 3. Добавляем элементы в корзину // 3. Добавляем элементы в корзину
addProductErr := addProductsToOrder(api, fuserId, orderId) addProductErr := addProductsToOrder(ctx, s, api, fuserId, orderId)
if addProductErr != nil { if addProductErr != nil {
return nil, addProductErr return nil, addProductErr
} }
@ -104,21 +81,19 @@ func MakeOrder(fuserId int, email string, fullName string, phone string) (map[st
// 6. Получаем ресурс оплаты и url для нее // 6. Получаем ресурс оплаты и url для нее
paymentData, _ := kassa.CreatePayment(orderId, order.Price, fullName, email, phone, getItemsForPayment(order)) paymentData, _ := kassa.CreatePayment(orderId, order.Price, fullName, email, phone, getItemsForPayment(order))
insPaymentDataStmt := fmt.Sprintf(` db := internal.InitDatabase().GetInstance()
db.NewRaw(`
insert into api_youkassa_payment (payment_id, order_id, link, status) insert into api_youkassa_payment (payment_id, order_id, link, status)
values ('%s', values (?, ?, ?,? );`,
'%s', paymentData.Id,
'%s',
'%s');
`, paymentData["id"].(string),
orderId, orderId,
paymentData["confirmation"].(map[string]interface{})["confirmation_url"].(string), paymentData.Confirmation.ConfirmationUrl,
paymentData["status"].(string), paymentData.Status).Exec(ctx)
)
db := internal.InitDatabase() if err != nil {
return nil, err
db.Execute(insPaymentDataStmt) }
return paymentData, nil return paymentData, nil
} }

View File

@ -1,32 +1,73 @@
package services package services
import ( import (
"fmt" "context"
"log"
"relynolli-server/external/bitrix" "relynolli-server/external/bitrix"
"relynolli-server/internal" "relynolli-server/external/kassa"
"relynolli-server/storage"
"time"
) )
func YookassaValidate(paymentId string, status string) { func PaymentValidation() {
stmt := fmt.Sprintf(`select t1.order_id as order_id, t2.ID as payment_id from api_youkassa_payment t1 join b_sale_order_payment t2 on t1.order_id = t2.ORDER_ID where t1.payment_id = '%s';`, paymentId) ctx := context.Background()
db := internal.InitDatabase() s := storage.NewStorageOrder()
rows := db.Query(stmt)
var (
orderId int
paymentIdBitrix int
)
rows.Next()
rows.Scan(&orderId, &paymentIdBitrix)
api := bitrix.Initialize() api := bitrix.Initialize()
if status == "succeeded" {
api.ApprovePayment(paymentIdBitrix, 8) for {
return payments, err := s.GetPayments(ctx)
if err != nil {
panic(err.Error())
} }
if status == "canceled" { for _, payment := range *payments {
api.CancelOrder(orderId) result, _ := kassa.CheckPayment(payment.ID)
return
if result == nil {
continue
}
payment.Status = result.Status
if result.Status == "succeeded" {
err := api.ApprovePayment(int(payment.BitrixPayment.ID), 8)
if err != nil {
log.Println(err.Error())
continue
}
}
if result.Status == "canceled" {
err := api.CancelOrder(int(payment.OrderId))
if err != nil {
log.Println(err.Error())
}
}
s.UpdatePayment(ctx, &payment)
time.Sleep(1 * time.Second)
}
time.Sleep(5 * time.Second)
} }
return
} }
//
//func YookassaValidate(paymentId string, status string) {
// stmt := fmt.Sprintf(`select t1.order_id as order_id, t2.ID as payment_id from api_youkassa_payment t1 join b_sale_order_payment t2 on t1.order_id = t2.ORDER_ID where t1.payment_id = '%s';`, paymentId)
// db := internal.InitDatabase()
// rows := db.Query(stmt)
//
// var (
// orderId int
// paymentIdBitrix int
// )
//
// rows.Next()
// rows.Scan(&orderId, &paymentIdBitrix)
//
// api := bitrix.Initialize()
// if status == "succeeded" {
// api.ApprovePayment(paymentIdBitrix, 8)
// return
// }
// if status == "canceled" {
// api.CancelOrder(orderId)
// return
// }
// return
//}

10
status/status.go Normal file
View File

@ -0,0 +1,10 @@
package status
type Status string
const (
STATUS_OK Status = "OK"
STATUS_NOT_FOUND Status = "not_found"
STATUS_BAD_REQUEST Status = "bad_request"
STATUS_SERVER_ERROR Status = "internal_server_error"
)

43
storage/article.go Normal file
View File

@ -0,0 +1,43 @@
package storage
import (
"context"
"relynolli-server/internal"
"relynolli-server/models/news"
)
type StorageArticle interface {
GetArticles(ctx context.Context, limit, offset int64) (int, *[]news.DBArticle, error)
RetrieveArticle(ctx context.Context, code string) (*news.DBArticle, error)
}
func NewStorageArticle() StorageArticle {
if instance == nil {
instance = &storage{
db: internal.InitDatabase().GetInstance(),
rdb: internal.InitRedis(),
}
}
return instance
}
func (s *storage) GetArticles(ctx context.Context, limit, offset int64) (int, *[]news.DBArticle, error) {
model := new([]news.DBArticle)
stmt := s.db.NewSelect().Model(model).Where("is_active = 1").OrderExpr("sort ASC, date DESC").Limit(int(limit)).Offset(int(offset))
count, err := stmt.ScanAndCount(ctx)
if err != nil {
return 0, nil, err
}
return count, model, nil
}
func (s *storage) RetrieveArticle(ctx context.Context, code string) (*news.DBArticle, error) {
model := new(news.DBArticle)
stmt := s.db.NewSelect().Model(model).Where("code = ?", code).Where("is_active = 1")
err := stmt.Scan(ctx)
if err != nil {
return nil, err
}
return model, nil
}

126
storage/cart.go Normal file
View File

@ -0,0 +1,126 @@
package storage
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"relynolli-server/internal"
"relynolli-server/models/cart"
"time"
)
type StorageCart interface {
CreateFuser(ctx context.Context) (int64, *cart.DBFuser, error)
GetCartItems(ctx context.Context, fuserId int64) (*[]cart.DBCart, error)
GetCartItem(ctx context.Context, fuserId, productId int64) (*cart.DBCart, error)
AddItemToCart(ctx context.Context, fuserId, productId int64) error
UpdateCartItem(ctx context.Context, fuserId, productId, quantity int64) error
DeleteCartItem(ctx context.Context, fuserId, productId int64) error
}
func NewStorageCart() StorageCart {
if instance == nil {
instance = &storage{
db: internal.InitDatabase().GetInstance(),
rdb: internal.InitRedis(),
}
}
return instance
}
func (s *storage) CreateFuser(ctx context.Context) (int64, *cart.DBFuser, error) {
//stmt := "insert into b_sale_fuser (DATE_INSERT, DATE_UPDATE, CODE) values (now(), now(), md5(rand()));"
hash := md5.Sum([]byte(fmt.Sprintf("%d", time.Now().Unix())))
model := &cart.DBFuser{
Code: hex.EncodeToString(hash[:]),
DateInserted: time.Now().UTC(),
DateUpdated: time.Now().UTC(),
}
res, err := s.db.NewInsert().Model(model).Exec(ctx)
id, _ := res.LastInsertId()
s.db.NewSelect().Model(model).Where("id = ?", id).Scan(ctx)
if err != nil {
return 0, nil, err
}
return model.Id, model, nil
}
func (s *storage) GetCartItems(ctx context.Context, fuserId int64) (*[]cart.DBCart, error) {
result := new([]cart.DBCart)
err := s.db.NewSelect().Model(result).Relation("Product").Relation("Fuser").Where("fuser_id = ?", fuserId).Scan(ctx)
if err != nil {
return nil, err
}
return result, nil
}
func (s *storage) GetCartItem(ctx context.Context, fuserId, productId int64) (*cart.DBCart, error) {
result := new(cart.DBCart)
err := s.db.NewSelect().Model(result).Relation("Product").Relation("Fuser").Where("fuser_id = ?", fuserId).Where("product_id = ?", productId).Scan(ctx)
if err != nil {
return nil, err
}
return result, nil
}
func (s *storage) AddItemToCart(ctx context.Context, fuserId, productId int64) error {
item, _ := s.GetCatalogItem(ctx, &productId)
isExists, err := s.db.NewSelect().Model((*cart.DBCart)(nil)).Where("fuser_id = ?", fuserId).Where("product_id = ?", productId).Exists(ctx)
if isExists || item.AvailableQuantity < 1 {
return nil
}
if err != nil {
panic(err.Error())
}
newItem := &cart.DBCart{
FuserId: fuserId,
ProductId: productId,
PriceTypeId: 1,
Quantity: 1,
}
_, err = s.db.NewInsert().Model(newItem).Exec(ctx)
if err != nil {
return nil
}
return nil
}
func (s *storage) UpdateCartItem(ctx context.Context, fuserId, productId, quantity int64) error {
if quantity <= 0 {
return s.DeleteCartItem(ctx, fuserId, productId)
}
item, _ := s.GetCartItem(ctx, fuserId, productId)
if item.Product.AvailableQuantity < quantity {
return fmt.Errorf("Available quantity is less than requested. Available %d, requested %d", item.Product.AvailableQuantity, quantity)
}
item.Quantity = quantity
s.db.NewUpdate().Model(item).Where("id = ?", item.Id).Exec(ctx)
return nil
}
func (s *storage) DeleteCartItem(ctx context.Context, fuserId, productId int64) error {
_, err := s.db.NewDelete().Model((*cart.DBCart)(nil)).Where("fuser_id = ?", fuserId).Where("product_id = ?", productId).Exec(ctx)
if err != nil {
return err
}
return nil
}

95
storage/catalog.go Normal file
View File

@ -0,0 +1,95 @@
package storage
import (
"context"
"fmt"
"relynolli-server/internal"
"relynolli-server/models/catalog"
filters2 "relynolli-server/models/filters"
"slices"
cmap "github.com/orcaman/concurrent-map/v2"
"github.com/uptrace/bun"
)
type StorageCatalog interface {
GetCatalogItem(ctx context.Context, id *int64) (*catalog.DBCatalog, error)
GetCatalogItemByCode(ctx context.Context, code string) (*catalog.DBCatalog, error)
GetCatalogItems(ctx context.Context, filters cmap.ConcurrentMap[string, []string], limit int, offset int) (int, *[]catalog.DBCatalog, error)
GetFilters(ctx context.Context) (int, *[]filters2.DBFilter, error)
}
func NewStorageCatalog() StorageCatalog {
if instance == nil {
instance = &storage{
db: internal.InitDatabase().GetInstance(),
rdb: internal.InitRedis(),
}
}
return instance
}
func (s *storage) GetCatalogItemByCode(ctx context.Context, code string) (*catalog.DBCatalog, error) {
model := new(catalog.DBCatalog)
err := s.db.NewSelect().Model(model).Where("code = ?", code).Where("available_quantity > 0").Where("is_active = 1").Scan(ctx)
if err != nil {
return nil, err
}
return model, nil
}
func (s *storage) GetCatalogItem(ctx context.Context, id *int64) (*catalog.DBCatalog, error) {
model := new(catalog.DBCatalog)
err := s.db.NewSelect().Model(model).Where("id = ?", id).Where("available_quantity > 0").Where("is_active = 1").Scan(ctx)
if err != nil {
return nil, err
}
return model, nil
}
func (s *storage) buildFilterGroup(ctx context.Context, q *bun.SelectQuery, filters *cmap.ConcurrentMap[string, []string]) *bun.SelectQuery {
availableFilters := new([]filters2.DBFilter)
//Get filters
s.db.NewSelect().Model(availableFilters).Scan(ctx)
for _, filter := range filters.Keys() {
filter = filter[0 : len(filter)-2]
if !slices.ContainsFunc(*availableFilters, func(elem filters2.DBFilter) bool {
return elem.Code == filter
}) {
continue
}
q = q.WhereGroup(" AND ", func(query *bun.SelectQuery) *bun.SelectQuery {
values, _ := filters.Get(filter + "[]")
for _, val := range values {
query = q.WhereOr(fmt.Sprintf("properties->>'$.%s' = ?", filter), val)
}
return query
})
}
return q
}
func (s *storage) GetCatalogItems(ctx context.Context, filters cmap.ConcurrentMap[string, []string], limit int, offset int) (int, *[]catalog.DBCatalog, error) {
model := new([]catalog.DBCatalog)
filterQuery := s.db.NewSelect().Model(model).Where("is_active = 1").Where("available_quantity > 0")
count, _ := s.buildFilterGroup(ctx, filterQuery, &filters).Limit(limit).Offset(offset).Order("code").ScanAndCount(ctx)
return count, model, nil
}
func (s *storage) GetFilters(ctx context.Context) (int, *[]filters2.DBFilter, error) {
models := new([]filters2.DBFilter)
count, err := s.db.NewSelect().Model(models).ScanAndCount(ctx)
if err != nil {
return 0, nil, err
}
return count, models, nil
}

45
storage/news.go Normal file
View File

@ -0,0 +1,45 @@
package storage
import (
"context"
"relynolli-server/internal"
"relynolli-server/models/news"
)
type StorageNews interface {
GetNews (ctx context.Context, limit, offset int64) (int, *[]news.DBNews, error)
RetrieveNews(ctx context.Context, code string) (*news.DBNews, error)
}
func NewStorageNews() StorageNews {
if instance == nil {
instance = &storage{
db: internal.InitDatabase().GetInstance(),
rdb: internal.InitRedis(),
}
}
return instance
}
func (s *storage) GetNews (ctx context.Context, limit, offset int64) (int, *[]news.DBNews, error) {
model := new([]news.DBNews)
stmt := s.db.NewSelect().Model(model).Where("is_active = 1").OrderExpr("sort ASC, date DESC").Limit(int(limit)).Offset(int(offset))
count, err := stmt.ScanAndCount(ctx)
if err != nil {
return 0, nil, err
}
return count, model, nil
}
func (s *storage) RetrieveNews(ctx context.Context, code string) (*news.DBNews, error) {
model := new(news.DBNews)
stmt := s.db.NewSelect().Model(model).Where("code = ?", code).Where("is_active = 1")
err := stmt.Scan(ctx)
if err != nil {
return nil, err
}
return model, nil
}

136
storage/order.go Normal file
View File

@ -0,0 +1,136 @@
package storage
import (
"bytes"
"context"
"encoding/json"
"github.com/uptrace/bun"
"os/exec"
"relynolli-server/external/bitrix"
"relynolli-server/internal"
"relynolli-server/models/cart"
"relynolli-server/models/discount"
"relynolli-server/models/order"
)
type StorageOrder interface {
GetTotal(ctx context.Context, fuserId int64, coupon *string) (*TotalQuery, error)
UpdatePayment(ctx context.Context, payment *order.DBPayment) error
GetPayments(ctx context.Context) (*[]order.DBPayment, error)
}
func NewStorageOrder() StorageOrder {
if instance == nil {
instance = &storage{
db: internal.InitDatabase().GetInstance(),
rdb: internal.InitRedis(),
}
}
return instance
}
type DiscountQuery struct {
Name string
Value int64
}
type QueryItem struct {
Cart *cart.DBCart `json:"cart"`
Discount *DiscountQuery `json:"discount"`
}
type TotalQuery struct {
Total float64 `bun:"total" json:"total"`
BasePrice float64 `bun:"-" json:"basePrice"`
Items *[]QueryItem `bun:"-" json:"items"`
}
func (s *storage) fetchDiscounts(ctx context.Context) (*[]discount.DBDiscount, error) {
model := new([]discount.DBDiscount)
err := s.db.NewSelect().Model(model).Scan(ctx)
if err != nil {
return nil, err
}
return model, nil
}
type DiscountType struct {
CLASSID string `json:"CLASS_ID"`
DATA struct {
All string `json:"All"`
} `json:"DATA"`
CHILDREN []struct {
CLASSID string `json:"CLASS_ID"`
DATA struct {
Type string `json:"Type"`
Value int `json:"Value"`
Unit string `json:"Unit"`
Max int `json:"Max"`
All string `json:"All"`
True string `json:"True"`
} `json:"DATA"`
CHILDREN []interface{} `json:"CHILDREN"`
} `json:"CHILDREN"`
}
func (s *storage) getDiscountForCoupon(ctx context.Context, coupon string) (*DiscountType, error) {
var couponDiscountPhpQuery string
stmt := "select disc.ACTIONS from b_sale_discount_coupon coupon join b_sale_discount disc on coupon.DISCOUNT_ID = disc.ID where coupon.COUPON = ? and coupon.ACTIVE = 'Y'"
err := s.db.NewRaw(stmt, coupon).Scan(ctx, &couponDiscountPhpQuery)
if err != nil {
return nil, err
}
var out bytes.Buffer
var result DiscountType
cmd := exec.Command("php", "test.php", couponDiscountPhpQuery)
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
return nil, err
}
err = json.Unmarshal(out.Bytes(), &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (s *storage) GetTotal(ctx context.Context, fuserId int64, coupon *string) (*TotalQuery, error) {
model := new(TotalQuery)
api := bitrix.Initialize()
data, err := api.GetTotalForProduct(int(fuserId), coupon)
if err != nil {
return nil, err
}
model.Total = data.Price
model.BasePrice = data.BasePrice
return model, nil
}
func (s *storage) GetPayments(ctx context.Context) (*[]order.DBPayment, error) {
var model []order.DBPayment
err := s.db.NewSelect().Model(&model).Where("status not in (?)", bun.In([]string{"succeeded", "canceled"})).Order("order_id DESC").Scan(ctx)
if err != nil {
return nil, err
}
for idx, item := range model {
bitrixPayment := new(order.DBOrderPayment)
s.db.NewSelect().Model(bitrixPayment).Where("ORDER_ID = ?", item.OrderId).Scan(ctx)
model[idx].BitrixPayment = bitrixPayment
}
return &model, nil
}
func (s *storage) UpdatePayment(ctx context.Context, payment *order.DBPayment) error {
_, err := s.db.NewUpdate().Model(payment).WherePK().Exec(ctx)
return err
}

13
storage/storage.go Normal file
View File

@ -0,0 +1,13 @@
package storage
import (
"github.com/redis/go-redis/v9"
"github.com/uptrace/bun"
)
type storage struct {
db *bun.DB
rdb *redis.Client
}
var instance *storage = nil

9
test.php Normal file
View File

@ -0,0 +1,9 @@
<?php
$input = $argv[count($argv) - 1];
$output = unserialize($input);
echo json_encode($output) . "\n";
?>