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