diff --git a/CDEK/.gitignore b/CDEK/.gitignore new file mode 100644 index 0000000..3c66e46 --- /dev/null +++ b/CDEK/.gitignore @@ -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 diff --git a/CDEK/CONTRIBUTING.md b/CDEK/CONTRIBUTING.md new file mode 100644 index 0000000..91aa1d7 --- /dev/null +++ b/CDEK/CONTRIBUTING.md @@ -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. diff --git a/CDEK/LICENSE b/CDEK/LICENSE new file mode 100644 index 0000000..b242a53 --- /dev/null +++ b/CDEK/LICENSE @@ -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. diff --git a/CDEK/README.md b/CDEK/README.md new file mode 100644 index 0000000..c520ea5 --- /dev/null +++ b/CDEK/README.md @@ -0,0 +1,44 @@ +# GO SDK for CDEK API v2 +[![GoDoc reference](https://godoc.org/github.com/vseinstrumentiru/CDEK?status.svg)](https://godoc.org/github.com/vseinstrumentiru/CDEK) +[![Build Status](https://travis-ci.com/vseinstrumentiru/CDEK.svg?branch=master)](https://travis-ci.com/vseinstrumentiru/CDEK) +[![Coverage Status](https://coveralls.io/repos/github/vseinstrumentiru/CDEK/badge.svg?branch=travis)](https://coveralls.io/github/vseinstrumentiru/CDEK?branch=travis) +[![Go Report Card](https://goreportcard.com/badge/github.com/vseinstrumentiru/CDEK)](https://goreportcard.com/report/github.com/vseinstrumentiru/CDEK) +[![GitHub release](https://img.shields.io/github/release/vseinstrumentiru/cdek.svg)](https://github.com/vseinstrumentiru/CDEK/releases) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2990/badge)](https://bestpractices.coreinfrastructure.org/projects/2990) + +The Go language implementation of SDK for [integration with CDEK](https://www.cdek.ru/clients/integrator.html) + +---- +Installation +------------ + +To install this package, you need to install Go and setup your Go workspace on +your computer. The simplest way to install the library is to run: + +``` +$ go get github.com/vseinstrumentiru/cdek +``` +With Go module support (Go 1.11+), simply `import "github.com/vseinstrumentiru/cdek"` in +your source code and `go [build|run|test]` will automatically download the +necessary dependencies ([Go modules +ref](https://github.com/golang/go/wiki/Modules)). + +Documentation +------------- +- See [godoc](https://godoc.org/github.com/vseinstrumentiru/CDEK) for package and API + descriptions and examples. + +Example +------------- +You cat get test `clientAccount` and `clientSecurePassword` from [the official CDEK documentation](https://confluence.cdek.ru/pages/viewpage.action?pageId=20264477#DataExchangeProtocol(v1.5)-TestAccount) +``` +import "github.com/vseinstrumentiru/cdek" +... + +client := cdek.NewClient("https://integration.edu.cdek.ru/"). + SetAuth(clientAccount, clientSecurePassword) + +cities, err := client.GetCities(map[cdek.CityFilter]string{ + cdek.CityFilterPage: "1", +}) +``` diff --git a/CDEK/ROADMAP.md b/CDEK/ROADMAP.md new file mode 100644 index 0000000..2250b27 --- /dev/null +++ b/CDEK/ROADMAP.md @@ -0,0 +1,19 @@ +- [ ] расчёт тарифов и обращения к справочникам + - [X] расчёт стоимости доставки по тарифам с приоритетом + - [ ] расчёт стоимости по тарифам без приоритета + - [X] получение списка пунктов выдачи заказов (ПВЗ) с фильтрацией + - [X] получение списка регионов-субъектов РФ + - [X] получение списка городов +- [ ] управление заказами + - [X] формирование новых заказов от ИМ + - [ ] оформление заказов на доставку + - [ ] получение квитанции в PDF + - [ ] получение почтовых этикеток в PDF + - [X] удаление заказов + - [X] изменение заказов + - [ ] получение информации по заказам (отчёт «Информация по заказам») + - [ ] трекинг заказов (отчёт «Статусы заказов») + - [ ] прозвон получателя + - [ ] вызов курьера + - [ ] создание преалерта + - [X] выбор базового URL интерфейса diff --git a/CDEK/v2/auth.go b/CDEK/v2/auth.go new file mode 100644 index 0000000..b7b642f --- /dev/null +++ b/CDEK/v2/auth.go @@ -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) +} diff --git a/CDEK/v2/auth_test.go b/CDEK/v2/auth_test.go new file mode 100644 index 0000000..e91ee0a --- /dev/null +++ b/CDEK/v2/auth_test.go @@ -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) +} diff --git a/CDEK/v2/calculator.go b/CDEK/v2/calculator.go new file mode 100644 index 0000000..cb3c27b --- /dev/null +++ b/CDEK/v2/calculator.go @@ -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) +} diff --git a/CDEK/v2/calculator_test.go b/CDEK/v2/calculator_test.go new file mode 100644 index 0000000..7aa3ace --- /dev/null +++ b/CDEK/v2/calculator_test.go @@ -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) +} diff --git a/CDEK/v2/cities.go b/CDEK/v2/cities.go new file mode 100644 index 0000000..429a4d4 --- /dev/null +++ b/CDEK/v2/cities.go @@ -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) +} diff --git a/CDEK/v2/cities_test.go b/CDEK/v2/cities_test.go new file mode 100644 index 0000000..6d8813a --- /dev/null +++ b/CDEK/v2/cities_test.go @@ -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) +} diff --git a/CDEK/v2/client.go b/CDEK/v2/client.go new file mode 100644 index 0000000..558fde1 --- /dev/null +++ b/CDEK/v2/client.go @@ -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(), + ), "?") +} diff --git a/CDEK/v2/client_test.go b/CDEK/v2/client_test.go new file mode 100644 index 0000000..333bd24 --- /dev/null +++ b/CDEK/v2/client_test.go @@ -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, + }, + }) +} diff --git a/CDEK/v2/consts.go b/CDEK/v2/consts.go new file mode 100644 index 0000000..78a0fe8 --- /dev/null +++ b/CDEK/v2/consts.go @@ -0,0 +1,4 @@ +package v2 + +const EndpointTest = "https://api.edu.cdek.ru" +const EndpointProd = "https://api.cdek.ru" diff --git a/CDEK/v2/deliveryPoints.go b/CDEK/v2/deliveryPoints.go new file mode 100644 index 0000000..aee60a3 --- /dev/null +++ b/CDEK/v2/deliveryPoints.go @@ -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) +} diff --git a/CDEK/v2/deliveryPoints_test.go b/CDEK/v2/deliveryPoints_test.go new file mode 100644 index 0000000..2ff8cb2 --- /dev/null +++ b/CDEK/v2/deliveryPoints_test.go @@ -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) +} diff --git a/CDEK/v2/helper.go b/CDEK/v2/helper.go new file mode 100644 index 0000000..2f62a2a --- /dev/null +++ b/CDEK/v2/helper.go @@ -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 +} diff --git a/CDEK/v2/helper_test.go b/CDEK/v2/helper_test.go new file mode 100644 index 0000000..aabe538 --- /dev/null +++ b/CDEK/v2/helper_test.go @@ -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) +} diff --git a/CDEK/v2/orderDelete.go b/CDEK/v2/orderDelete.go new file mode 100644 index 0000000..7f8b0f3 --- /dev/null +++ b/CDEK/v2/orderDelete.go @@ -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 +} diff --git a/CDEK/v2/orderDelete_test.go b/CDEK/v2/orderDelete_test.go new file mode 100644 index 0000000..2f5abc0 --- /dev/null +++ b/CDEK/v2/orderDelete_test.go @@ -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) +} diff --git a/CDEK/v2/orderRegister.go b/CDEK/v2/orderRegister.go new file mode 100644 index 0000000..a30ef6a --- /dev/null +++ b/CDEK/v2/orderRegister.go @@ -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 +} diff --git a/CDEK/v2/orderRegister_test.go b/CDEK/v2/orderRegister_test.go new file mode 100644 index 0000000..b40437d --- /dev/null +++ b/CDEK/v2/orderRegister_test.go @@ -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") +} diff --git a/CDEK/v2/orderStatus.go b/CDEK/v2/orderStatus.go new file mode 100644 index 0000000..d74053d --- /dev/null +++ b/CDEK/v2/orderStatus.go @@ -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) +} diff --git a/CDEK/v2/orderUpdate.go b/CDEK/v2/orderUpdate.go new file mode 100644 index 0000000..004ca94 --- /dev/null +++ b/CDEK/v2/orderUpdate.go @@ -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 +} diff --git a/CDEK/v2/orderUpdate_test.go b/CDEK/v2/orderUpdate_test.go new file mode 100644 index 0000000..54e4671 --- /dev/null +++ b/CDEK/v2/orderUpdate_test.go @@ -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") +} diff --git a/CDEK/v2/readme.md b/CDEK/v2/readme.md new file mode 100644 index 0000000..830452a --- /dev/null +++ b/CDEK/v2/readme.md @@ -0,0 +1,3 @@ +CDEK changelog: + +https://api-docs.cdek.ru/36967918.html diff --git a/CDEK/v2/regions.go b/CDEK/v2/regions.go new file mode 100644 index 0000000..1bfdf62 --- /dev/null +++ b/CDEK/v2/regions.go @@ -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) +} diff --git a/CDEK/v2/regions_test.go b/CDEK/v2/regions_test.go new file mode 100644 index 0000000..e90b097 --- /dev/null +++ b/CDEK/v2/regions_test.go @@ -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) +} diff --git a/CDEK/v2/types.go b/CDEK/v2/types.go new file mode 100644 index 0000000..ef48f53 --- /dev/null +++ b/CDEK/v2/types.go @@ -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"` +} diff --git a/CDEK/v2/utils.go b/CDEK/v2/utils.go new file mode 100644 index 0000000..617b46e --- /dev/null +++ b/CDEK/v2/utils.go @@ -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(), + ) + } +} diff --git a/external/bitrix/api.go b/external/bitrix/api.go index 086bc82..82ebc1c 100644 --- a/external/bitrix/api.go +++ b/external/bitrix/api.go @@ -7,14 +7,14 @@ import ( "github.com/google/uuid" "github.com/mitchellh/mapstructure" "github.com/sirupsen/logrus" + "io" + "log" "net/http" "os" "strings" "time" ) -var EP = os.Getenv("BITRIX_API_EP") - type ClientsResource struct { EntityId string `json:"entityId"` } @@ -166,7 +166,10 @@ type BasketResource struct { } `json:"properties"` } -type bitrix struct{} +type bitrix struct { + EP string + EPCustom string +} type Bitrix interface { CreateAnonymousUser() (int, error) @@ -175,8 +178,9 @@ type Bitrix interface { CancelOrder(orderId int) error GetOrderInfo(orderId int) (*OrderResource, 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 + GetTotalForProduct(fuserId int, coupon *string) (*GetTotalOrderResponse, error) } type createAnonymousUserRequest struct { @@ -191,7 +195,7 @@ type createAnonymousUserResponse struct { Result int `json:"result"` } -func (_ bitrix) CreateAnonymousUser() (int, error) { +func (b bitrix) CreateAnonymousUser() (int, error) { uid, _ := uuid.NewUUID() req := createAnonymousUserRequest{ Email: fmt.Sprintf("anonymous%s@anonym.ru", uid.String()), @@ -203,7 +207,7 @@ func (_ bitrix) CreateAnonymousUser() (int, error) { 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{} @@ -231,7 +235,12 @@ type createOrderResponse struct { } `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{ Lid: "s2", PersonTypeId: 5, @@ -241,7 +250,7 @@ func (_ bitrix) CreateOrder(userId int) (int, error) { 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{} err := json.NewDecoder(resp.Body).Decode(&result) @@ -249,7 +258,7 @@ func (_ bitrix) CreateOrder(userId int) (int, error) { 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{}{ "id": paymentId, "fields": map[string]interface{}{"paid": "Y", "paySystemId": paySystemId}, @@ -257,12 +266,15 @@ func (_ bitrix) ApprovePayment(paymentId int, paySystemId int) error { 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 } -func (_ bitrix) CancelOrder(orderId int) error { +func (b bitrix) CancelOrder(orderId int) error { req := map[string]interface{}{ "id": orderId, "fields": map[string]interface{}{"canceled": "Y"}, @@ -270,19 +282,19 @@ func (_ bitrix) CancelOrder(orderId int) error { 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 } -func (_ bitrix) GetOrderInfo(orderId int) (*OrderResource, error) { +func (b bitrix) GetOrderInfo(orderId int) (*OrderResource, error) { req := map[string]interface{}{ "id": orderId, } 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 { @@ -300,7 +312,7 @@ func (_ bitrix) GetOrderInfo(orderId int) (*OrderResource, error) { return &resultParsed, err } -func (_ bitrix) CreatePayment(orderId int, sum float64) error { +func (b bitrix) CreatePayment(orderId int, sum float64) error { req := map[string]interface{}{ "fields": map[string]interface{}{ "orderId": orderId, @@ -312,30 +324,36 @@ func (_ bitrix) CreatePayment(orderId int, sum float64) error { 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 } -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{}{ "fields": map[string]interface{}{ - "orderId": orderId, - "productId": productId, - "quantity": quantity, - "currency": "RUB", - "price": price, + "name": productName, + "orderId": orderId, + "module": "catalog", + "productId": productId, + "quantity": quantity, + "currency": "RUB", + "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) - _, 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 } -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{}{ "id": contactId, "fields": map[string]interface{}{ @@ -344,9 +362,8 @@ func (_ bitrix) UpdateContact(contactId int, email string, name string, phone st "VALUE": email, "TYPE_ID": "EMAIL", }), - "NAME": strings.Split(name, " ")[1], - "LAST_NAME": strings.Split(name, " ")[0], - "SECOND_NAME": strings.Split(name, " ")[2], + "NAME": strings.Split(name, " ")[1], + "LAST_NAME": strings.Split(name, " ")[0], "PHONE": append([]map[string]interface{}{}, map[string]interface{}{ "VALUE_TYPE": "other", "VALUE": phone, @@ -357,11 +374,35 @@ func (_ bitrix) UpdateContact(contactId int, email string, name string, phone st 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 } -func Initialize() Bitrix { - return &bitrix{} +func (b bitrix) GetTotalForProduct(fuserId int, coupon *string) (*GetTotalOrderResponse, error) { + 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"), + } } diff --git a/external/kassa/AgentType/enums.go b/external/kassa/AgentType/enums.go index 0241765..b23a852 100644 --- a/external/kassa/AgentType/enums.go +++ b/external/kassa/AgentType/enums.go @@ -3,11 +3,11 @@ package AgentType type AgentType string const ( - BANKING_PAYMENT_AGENT = "banking_payment_agent" // Банковский платежный агент - BANKING_PAYMENT_SUBAGENT = "banking_payment_subagent" // Банковский платежный субагент - PAYMENT_AGENT = "payment_agent" // Платежный агент - PAYMENT_SUBAGENT = "payment_subagent" // Платежный субагент - ATTORNEY = "attorney" // Поверенный - COMMISSIONER = "commissioner" // Комиссионер - AGENT = "agent" // Агент + BANKING_PAYMENT_AGENT AgentType = "banking_payment_agent" // Банковский платежный агент + BANKING_PAYMENT_SUBAGENT AgentType = "banking_payment_subagent" // Банковский платежный субагент + PAYMENT_AGENT AgentType = "payment_agent" // Платежный агент + PAYMENT_SUBAGENT AgentType = "payment_subagent" // Платежный субагент + ATTORNEY AgentType = "attorney" // Поверенный + COMMISSIONER AgentType = "commissioner" // Комиссионер + AGENT AgentType = "agent" // Агент ) diff --git a/external/kassa/Measure/enums.go b/external/kassa/Measure/enums.go index ab2da6d..8838472 100644 --- a/external/kassa/Measure/enums.go +++ b/external/kassa/Measure/enums.go @@ -3,28 +3,28 @@ package Measure type Measure string const ( - PIECE = "piece" // Штука, единица товара - GRAM = "gram" // Грамм - KILOGRAM = "kilogram" // Килограмм - TON = "ton" // Тонна - CENTIMETER = "centimeter" // Сантиметр - DECIMETER = "decimeter" // Дециметр - METER = "meter" // Метр - SQUARE_CENTIMETER = "square_centimeter" // Квадратный сантиметр - SQUARE_DECIMETER = "square_decimeter" // Квадратный дециметр - SQUARE_METER = "square_meter" // Квадратный метр - MILLILITER = "milliliter" // Миллилитр - LITER = "liter" // Литр - CUBIC_METER = "cubic_meter" // Кубический метр - KILOWATT_HOUR = "kilowatt_hour" // Килловат-час - GIGACALORIE = "gigacalorie" // Гигакалория - DAY = "day" // Сутки - HOUR = "hour" // Час - MINUTE = "minute" // Минута - SECOND = "second" // Секунда - KILOBYTE = "kilobyte" // Килобайт - MEGABYTE = "megabyte" // Мегабайт - GIGABYTE = "gigabyte" // Гигабайт - TERABYTE = "terabyte" // Терабайт - ANOTHER = "another" // Другое + PIECE Measure = "piece" // Штука, единица товара + GRAM Measure = "gram" // Грамм + KILOGRAM Measure = "kilogram" // Килограмм + TON Measure = "ton" // Тонна + CENTIMETER Measure = "centimeter" // Сантиметр + DECIMETER Measure = "decimeter" // Дециметр + METER Measure = "meter" // Метр + SQUARE_CENTIMETER Measure = "square_centimeter" // Квадратный сантиметр + SQUARE_DECIMETER Measure = "square_decimeter" // Квадратный дециметр + SQUARE_METER Measure = "square_meter" // Квадратный метр + MILLILITER Measure = "milliliter" // Миллилитр + LITER Measure = "liter" // Литр + CUBIC_METER Measure = "cubic_meter" // Кубический метр + KILOWATT_HOUR Measure = "kilowatt_hour" // Килловат-час + GIGACALORIE Measure = "gigacalorie" // Гигакалория + DAY Measure = "day" // Сутки + HOUR Measure = "hour" // Час + MINUTE Measure = "minute" // Минута + SECOND Measure = "second" // Секунда + KILOBYTE Measure = "kilobyte" // Килобайт + MEGABYTE Measure = "megabyte" // Мегабайт + GIGABYTE Measure = "gigabyte" // Гигабайт + TERABYTE Measure = "terabyte" // Терабайт + ANOTHER Measure = "another" // Другое ) diff --git a/external/kassa/PaymentMode/enums.go b/external/kassa/PaymentMode/enums.go index 1c9ac0c..3a1d64e 100644 --- a/external/kassa/PaymentMode/enums.go +++ b/external/kassa/PaymentMode/enums.go @@ -3,11 +3,11 @@ package PaymentMode type PaymentMode string const ( - FULL_PREPAYMENT = "full_prepayment" // Полная предоплата - PARTIAL_PREPAYMENT = "partial_prepayment" // Частичная предоплата - ADVANCE = "advance" // Аванс - FULL_PAYMENT = "full_payment" // Полный расчет - PARTIAL_PAYMENT = "partial_payment" // Частичный расчет и кредит - CREDIT = "credit" // Кредит - CREDIT_PAYMENT = "credit_payment" // Выплата по кредиту + FULL_PREPAYMENT PaymentMode = "full_prepayment" // Полная предоплата + PARTIAL_PREPAYMENT PaymentMode = "partial_prepayment" // Частичная предоплата + ADVANCE PaymentMode = "advance" // Аванс + FULL_PAYMENT PaymentMode = "full_payment" // Полный расчет + PARTIAL_PAYMENT PaymentMode = "partial_payment" // Частичный расчет и кредит + CREDIT PaymentMode = "credit" // Кредит + CREDIT_PAYMENT PaymentMode = "credit_payment" // Выплата по кредиту ) diff --git a/external/kassa/PaymentSubject/enums.go b/external/kassa/PaymentSubject/enums.go index e639f21..3bd75d5 100644 --- a/external/kassa/PaymentSubject/enums.go +++ b/external/kassa/PaymentSubject/enums.go @@ -3,23 +3,23 @@ package PaymentSubject type PaymentSubject string const ( - COMMODITY = "commodity" //Товар Товар - EXCISE = "excise" //Подакцизный товар - JOB = "job" //Работа - SERVICE = "service" //Услуга Услуга - PAYMENT = "payment" //Платеж Платеж - CASINO = "casino" // Платеж казино - GAMBLING_BET = "gambling_bet" //Ставка в азартной игре - GAMBLING_PRIZE = "gambling_prize" // Выигрыш азартной игры - LOTTERY = "lottery" // Лотерейный билет - LOTTERY_PRIZE = "lottery_prize" // Выигрыш в лотерею - INTELLECTUAL_ACTIVITY = "intellectual_activity" //Результаты интеллектуальной деятельности - AGENT_COMMISSION = "agent_commission" //Агентское вознаграждение - PROPERTY_RIGHT = "property_right" //Имущественное право - NON_OPERATING_GAIN = "non_operating_gain" //Внереализационный доход - INSURANCE_PREMIUM = "insurance_premium" //Страховой сбор - SALES_TAX = "sales_tax" //Торговый сбор - RESORT_FEE = "resort_fee" // Курортный сбор - COMPOSITE = "composite" // Несколько вариантов - ANOTHER = "another" // Другое + COMMODITY PaymentSubject = "commodity" //Товар Товар + EXCISE PaymentSubject = "excise" //Подакцизный товар + JOB PaymentSubject = "job" //Работа + SERVICE PaymentSubject = "service" //Услуга Услуга + PAYMENT PaymentSubject = "payment" //Платеж Платеж + CASINO PaymentSubject = "casino" // Платеж казино + GAMBLING_BET PaymentSubject = "gambling_bet" //Ставка в азартной игре + GAMBLING_PRIZE PaymentSubject = "gambling_prize" // Выигрыш азартной игры + LOTTERY PaymentSubject = "lottery" // Лотерейный билет + LOTTERY_PRIZE PaymentSubject = "lottery_prize" // Выигрыш в лотерею + INTELLECTUAL_ACTIVITY PaymentSubject = "intellectual_activity" //Результаты интеллектуальной деятельности + AGENT_COMMISSION PaymentSubject = "agent_commission" //Агентское вознаграждение + PROPERTY_RIGHT PaymentSubject = "property_right" //Имущественное право + NON_OPERATING_GAIN PaymentSubject = "non_operating_gain" //Внереализационный доход + INSURANCE_PREMIUM PaymentSubject = "insurance_premium" //Страховой сбор + SALES_TAX PaymentSubject = "sales_tax" //Торговый сбор + RESORT_FEE PaymentSubject = "resort_fee" // Курортный сбор + COMPOSITE PaymentSubject = "composite" // Несколько вариантов + ANOTHER PaymentSubject = "another" // Другое ) diff --git a/external/kassa/Settlements/enums.go b/external/kassa/Settlements/enums.go index c76c5a7..89f7709 100644 --- a/external/kassa/Settlements/enums.go +++ b/external/kassa/Settlements/enums.go @@ -3,8 +3,8 @@ package Settlements type Settlements string const ( - CASHLESS = "cashless" // Безналичный расчет - PREPAYMENT = "prepayment" // Предоплата (аванс) - POSTPAYMENT = "postpayment" // Постоплата (кредит) - CONSIDERATION = "consideration" // Встречное предоставление + CASHLESS Settlements = "cashless" // Безналичный расчет + PREPAYMENT Settlements = "prepayment" // Предоплата (аванс) + POSTPAYMENT Settlements = "postpayment" // Постоплата (кредит) + CONSIDERATION Settlements = "consideration" // Встречное предоставление ) diff --git a/external/kassa/TaxSystemCode/enums.go b/external/kassa/TaxSystemCode/enums.go index e0c61b0..590e412 100644 --- a/external/kassa/TaxSystemCode/enums.go +++ b/external/kassa/TaxSystemCode/enums.go @@ -3,7 +3,7 @@ package TaxSystemCode type TaxSystemCode int const ( - GENERAL = iota + 1 + GENERAL TaxSystemCode = iota + 1 USN_INCOME USN_INCOME_MINUS_EXPENCES ENVD diff --git a/external/kassa/kassa.go b/external/kassa/kassa.go index 2d0564b..7c41596 100644 --- a/external/kassa/kassa.go +++ b/external/kassa/kassa.go @@ -7,17 +7,15 @@ import ( "fmt" "github.com/google/uuid" "net/http" + "os" "relynolli-server/external/kassa/Measure" "relynolli-server/external/kassa/PaymentMode" "relynolli-server/external/kassa/PaymentSubject" "relynolli-server/external/kassa/TaxSystemCode" "relynolli-server/external/kassa/VatCodes" - "sync" "time" ) -var once sync.Once - type KassaAmount struct { Value string `json:"value"` Currency string `json:"currency"` @@ -80,7 +78,7 @@ func basicAuth(username, password string) string { 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{ Amount: KassaAmount{Value: fmt.Sprintf("%f", sum), Currency: "RUB"}, Description: fmt.Sprintf("Заказ №%d", orderId), @@ -105,13 +103,14 @@ func CreatePayment(orderId int, sum float64, fullName string, email string, phon client := http.Client{} 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("Content-Type", "application/json") response, err := client.Do(request) - result := map[string]interface{}{} + result := new(KassaResult) json.NewDecoder(response.Body).Decode(&result) if err != nil { @@ -121,3 +120,21 @@ func CreatePayment(orderId int, sum float64, fullName string, email string, phon 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 +} diff --git a/external/yaGeo/init.go b/external/yaGeo/init.go new file mode 100644 index 0000000..73a11e3 --- /dev/null +++ b/external/yaGeo/init.go @@ -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 +} diff --git a/go.mod b/go.mod index cf914c4..05dd588 100644 --- a/go.mod +++ b/go.mod @@ -2,43 +2,67 @@ module relynolli-server 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 ( github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect github.com/bytedance/sonic v1.11.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // 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/fatih/color v1.16.0 // 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-gonic/gin v1.9.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/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/gomodule/redigo v1.8.9 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // 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/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // 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/reflect2 v1.0.2 // 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/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/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/crypto v0.20.0 // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 33f3489..85df1fd 100644 --- a/go.sum +++ b/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/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.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= 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.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= 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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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/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/go.mod h1:2KkFL8PSnPF3Tt5E2Jpc3HWuBAUKqGZnClCFMm0tXQI= 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-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 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/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/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/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/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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/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/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-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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +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/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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/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/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/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug= 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/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/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/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/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/rvinnie/yookassa-sdk-go v0.0.0-20230904104101-ff7e5be5530c h1:m6dxe045lJQ1tkJeCBwseulCwppUDcdZk+RIxzBjQXQ= -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 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +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/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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.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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 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/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/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.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= 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/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/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-20220811171246-fbc7d0a398ab/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.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/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/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= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= 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 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/address/endpoints/ep.go b/handlers/address/endpoints/ep.go new file mode 100644 index 0000000..24b5216 --- /dev/null +++ b/handlers/address/endpoints/ep.go @@ -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) +} diff --git a/handlers/address/routes.go b/handlers/address/routes.go new file mode 100644 index 0000000..fea3a5d --- /dev/null +++ b/handlers/address/routes.go @@ -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) + } +} diff --git a/handlers/article/endpoints/article.go b/handlers/article/endpoints/article.go new file mode 100644 index 0000000..3f6e6dc --- /dev/null +++ b/handlers/article/endpoints/article.go @@ -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, + }) +} diff --git a/handlers/article/routes.go b/handlers/article/routes.go new file mode 100644 index 0000000..57df3b6 --- /dev/null +++ b/handlers/article/routes.go @@ -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) + } + +} diff --git a/handlers/cart/endpoints/cart.go b/handlers/cart/endpoints/cart.go index 28d5de4..a086d65 100644 --- a/handlers/cart/endpoints/cart.go +++ b/handlers/cart/endpoints/cart.go @@ -1,34 +1,75 @@ package endpoints import ( - "github.com/gin-gonic/gin" + "context" + "fmt" "relynolli-server/models" - "relynolli-server/services" - "strconv" + "relynolli-server/status" + "relynolli-server/storage" + "time" + + "github.com/gin-gonic/gin" ) +type getCartItemsRequest struct { + FuserId int64 `form:"fuserId"` +} + func (h *handlers) GetCartItems(c *gin.Context) { + ctx := context.Background() + query := new(getCartItemsRequest) + meta := models.Meta{ + RequestStarted: time.Now().Unix(), + } - fuserId := c.Query("fuserId") - if fuserId == "" { - c.JSON(400, models.Response{Status: 400, Info: "\"fuserId\" should be provided"}) + err := c.ShouldBindQuery(query) + if err != nil || query.FuserId == 0 { + 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 } - idx, err := strconv.Atoi(fuserId) + s := storage.NewStorageCart() - if err != nil { - c.JSON(400, models.Response{Status: 400, Info: "\"fuserId should be an integer number\""}) - return - } + items, _ := s.GetCartItems(ctx, query.FuserId) - 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) { - lastInsertId := services.CreateFuser() + s := storage.NewStorageCart() + ctx := context.Background() + meta := models.Meta{ + RequestStarted: time.Now().Unix(), + } - c.JSON(201, gin.H{ - "fuserId": lastInsertId, + fuserId, fuser, err := s.CreateFuser(ctx) + 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, }) } diff --git a/handlers/cart/endpoints/ep.go b/handlers/cart/endpoints/ep.go index 5833d09..37e6f30 100644 --- a/handlers/cart/endpoints/ep.go +++ b/handlers/cart/endpoints/ep.go @@ -7,7 +7,6 @@ type handlers struct{} type Handlers interface { GetCartItems(c *gin.Context) CreateFUser(c *gin.Context) - CreateCartItem(c *gin.Context) UpdateCartItem(c *gin.Context) DeleteCartItem(c *gin.Context) diff --git a/handlers/cart/endpoints/item.go b/handlers/cart/endpoints/item.go index 6f610cc..6923e56 100644 --- a/handlers/cart/endpoints/item.go +++ b/handlers/cart/endpoints/item.go @@ -1,11 +1,13 @@ package endpoints import ( + "context" "fmt" "github.com/gin-gonic/gin" - "net/http" "relynolli-server/models" - "relynolli-server/services" + "relynolli-server/status" + "relynolli-server/storage" + "time" ) type createCartItemRequest struct { @@ -27,49 +29,157 @@ type deleteCartRequest struct { } func (h *handlers) CreateCartItem(c *gin.Context) { - req := createCartItemRequest{} - err := c.ShouldBindJSON(&req) + meta := models.Meta{ + 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 { - 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 } - 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) { - req := updateCartRequest{} - err := c.ShouldBindJSON(&req) + meta := models.Meta{ + 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 { - 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 } - 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}) - + meta.RequestFinished = time.Now().Unix() + response.Info = fmt.Sprintf("Item has updated in cart") + response.Status = status.STATUS_OK + c.JSON(200, response) } 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{} - err := c.ShouldBindJSON(&req) + query := new(deleteCartRequest) + err := c.ShouldBindJSON(query) 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 } - services.DeleteCartItem(req.FuserId, req.ProductId) - - c.JSON(http.StatusNoContent, models.Response{Status: http.StatusNoContent}) + meta.RequestFinished = time.Now().Unix() + response.Info = fmt.Sprintf("Item has dropped from cart") + 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}) +//} diff --git a/handlers/cart/routes.go b/handlers/cart/routes.go index ebe00c8..4fb5f11 100644 --- a/handlers/cart/routes.go +++ b/handlers/cart/routes.go @@ -13,7 +13,6 @@ func HandleRoutes(parent *gin.RouterGroup) { cart.GET("", h.GetCartItems) cart.POST("", h.CreateFUser) } - { itemRouter.POST("", h.CreateCartItem) itemRouter.PATCH("", h.UpdateCartItem) diff --git a/handlers/catalog/endpoints/catalog.go b/handlers/catalog/endpoints/catalog.go index 1988a83..b63256d 100644 --- a/handlers/catalog/endpoints/catalog.go +++ b/handlers/catalog/endpoints/catalog.go @@ -1,41 +1,117 @@ package endpoints import ( + "context" "fmt" - "github.com/gin-gonic/gin" "relynolli-server/models" - "relynolli-server/services" - "strconv" + "relynolli-server/status" + "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")) - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - offset := (page - 1) * limit - if c.DefaultQuery("isFilter", "0") == "0" { - c.JSON(200, services.GetCatalogItems(limit, offset)) +func (h *handlers) GetCatalogItems(c *gin.Context) { + queries := cmap.New[[]string]() + for key, val := range c.Request.URL.Query() { + queries.Set(key, val) + } + + 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 } - 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) { - code := c.Param("code") - if code == "" { - c.JSON(400, models.Response{Status: 400, Info: "product \"Code\" should be provided"}) - return + ctx := context.Background() + meta := models.Meta{ + RequestStarted: time.Now().Unix(), + } + 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 { - 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 } - c.JSON(200, resp) -} + data, err := s.GetCatalogItemByCode(ctx, path.Code) -func (h *handlers) Count(c *gin.Context) { - c.JSON(200, models.Response{Status: 200, Info: fmt.Sprintf("%d", services.GetCatalogItemsCount())}) + response.Data = data + meta.RequestFinished = time.Now().Unix() + if data == nil { + statusCode = 404 + response.Status = status.STATUS_NOT_FOUND + } + + c.JSON(statusCode, response) } diff --git a/handlers/catalog/endpoints/ep.go b/handlers/catalog/endpoints/ep.go index 8348297..8cb2605 100644 --- a/handlers/catalog/endpoints/ep.go +++ b/handlers/catalog/endpoints/ep.go @@ -8,7 +8,6 @@ type Handlers interface { GetFilters(c *gin.Context) GetCatalogItems(c *gin.Context) GetCatalogItem(c *gin.Context) - Count(c *gin.Context) } func GetHandlers() Handlers { diff --git a/handlers/catalog/endpoints/filters.go b/handlers/catalog/endpoints/filters.go index 9f44366..7598675 100644 --- a/handlers/catalog/endpoints/filters.go +++ b/handlers/catalog/endpoints/filters.go @@ -1,38 +1,41 @@ package endpoints import ( - "encoding/json" + "context" + "fmt" "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) { - stmt := "select * from api_filter;" - var responseData []filterStruct - db := internal.InitDatabase() - rows := db.Query(stmt) - 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) + meta := models.Meta{ + RequestStarted: time.Now().Unix(), } - 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, + }) + } diff --git a/handlers/catalog/routes.go b/handlers/catalog/routes.go index 1623ed7..b051292 100644 --- a/handlers/catalog/routes.go +++ b/handlers/catalog/routes.go @@ -1,11 +1,17 @@ package catalog import ( + "os" "relynolli-server/handlers/catalog/endpoints" "relynolli-server/internal" "time" "github.com/gin-contrib/cache" + + // "relynolli-server/internal" + // "time" + + // "github.com/gin-contrib/cache" "github.com/gin-gonic/gin" ) @@ -13,8 +19,18 @@ func HandleRoutes(parent *gin.RouterGroup) { h := endpoints.GetHandlers() cacheStore := internal.InitCacheStore() catalog := parent.Group("/catalog") - catalog.GET("/filters", cache.CachePage(cacheStore, 15, h.GetFilters)) - catalog.GET("/count", cache.CachePage(cacheStore, 15 * time.Minute, h.Count)) - catalog.GET("", cache.CachePage(cacheStore, 15 * time.Minute, h.GetCatalogItems)) - catalog.GET("/:code", cache.CachePage(cacheStore, 15 * time.Minute, h.GetCatalogItem)) + if os.Getenv("IS_PROD") == "1" { + // Caching for production usage + catalog.GET("", cache.CachePage(cacheStore, 15*time.Minute, h.GetCatalogItems)) + 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)) } diff --git a/handlers/cdek/endpoints/ep.go b/handlers/cdek/endpoints/ep.go new file mode 100644 index 0000000..4241396 --- /dev/null +++ b/handlers/cdek/endpoints/ep.go @@ -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) +} diff --git a/handlers/cdek/routes.go b/handlers/cdek/routes.go new file mode 100644 index 0000000..7ae1b5f --- /dev/null +++ b/handlers/cdek/routes.go @@ -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) + } +} diff --git a/handlers/news/endpoints/news.go b/handlers/news/endpoints/news.go new file mode 100644 index 0000000..c6c7ee2 --- /dev/null +++ b/handlers/news/endpoints/news.go @@ -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, + }) +} diff --git a/handlers/news/routes.go b/handlers/news/routes.go new file mode 100644 index 0000000..b044016 --- /dev/null +++ b/handlers/news/routes.go @@ -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) + } + +} diff --git a/handlers/order/endpoints/ep.go b/handlers/order/endpoints/ep.go index f97c39b..bc1c46d 100644 --- a/handlers/order/endpoints/ep.go +++ b/handlers/order/endpoints/ep.go @@ -1,18 +1,25 @@ package endpoints import ( + "context" "fmt" - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" "net/http" + "regexp" "relynolli-server/models" "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 getTotalRequest struct { - FuserId int `json:"fuserId"` + FuserId int `json:"fuserId"` + Coupon *string `json:"coupon,omitempty"` } type getTotalResponse struct { @@ -31,21 +38,55 @@ type makeOrderRequest struct { func (h handlers) GetTotal(c *gin.Context) { req := getTotalRequest{} 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 { - 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 } - 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) { + ctx := context.Background() // VALIDATION validate := validator.New(validator.WithRequiredStructEnabled()) req := makeOrderRequest{} err := c.ShouldBindJSON(&req) + meta := models.Meta{ + RequestStarted: time.Now().Unix(), + } + resp := models.Response{ + Status: status.STATUS_OK, + Meta: &meta, + } 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 } @@ -53,18 +94,35 @@ func (h handlers) MakeOrder(c *gin.Context) { if validationErr != nil { 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 } - 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 { - 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 } - c.JSON(http.StatusOK, kassaResult) + resp.Data = kassaResult + meta.RequestFinished = time.Now().Unix() + + c.JSON(http.StatusOK, resp) } type Handlers interface { diff --git a/handlers/order/routes.go b/handlers/order/routes.go index cc10a66..faa9589 100644 --- a/handlers/order/routes.go +++ b/handlers/order/routes.go @@ -1,8 +1,9 @@ package order import ( - "github.com/gin-gonic/gin" "relynolli-server/handlers/order/endpoints" + + "github.com/gin-gonic/gin" ) func HandleRoutes(parent *gin.RouterGroup) { diff --git a/handlers/routers.go b/handlers/routers.go index c6778ad..31fc9f4 100644 --- a/handlers/routers.go +++ b/handlers/routers.go @@ -1,17 +1,28 @@ package handlers import ( - "github.com/gin-gonic/gin" + "relynolli-server/handlers/address" + "relynolli-server/handlers/article" "relynolli-server/handlers/cart" - "relynolli-server/handlers/catalog" + "relynolli-server/handlers/cdek" + "relynolli-server/handlers/news" "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) { APIV1Router := router.Group("/api/v1") catalog.HandleRoutes(APIV1Router) cart.HandleRoutes(APIV1Router) + news.HandleRoutes(APIV1Router) order.HandleRoutes(APIV1Router) - validate.HandleRoutes(APIV1Router) + article.HandleRoutes(APIV1Router) + address.HandleRoutes(APIV1Router) + cdek.HandleRoutes(APIV1Router) } diff --git a/handlers/validate/endpoints/validate.go b/handlers/validate/endpoints/validate.go index a85f124..139f98a 100644 --- a/handlers/validate/endpoints/validate.go +++ b/handlers/validate/endpoints/validate.go @@ -1,11 +1,10 @@ package endpoints import ( - "fmt" - "github.com/gin-gonic/gin" "net/http" - "relynolli-server/models" "relynolli-server/services" + + "github.com/gin-gonic/gin" ) type handlers struct{} @@ -23,8 +22,7 @@ func (_ handlers) Validate(c *gin.Context) { req := ValidateReq{} err := c.ShouldBindJSON(&req) if err != nil { - c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, - Info: fmt.Sprintf("Error: %s", err.Error())}) + c.JSON(http.StatusBadRequest) } services.YookassaValidate(req.Object.Id, req.Object.Status) diff --git a/internal/database.go b/internal/database.go index a3cdb4f..525b62d 100644 --- a/internal/database.go +++ b/internal/database.go @@ -3,24 +3,22 @@ package internal import ( "database/sql" "fmt" - _ "github.com/go-sql-driver/mysql" - "github.com/jmoiron/sqlx" + "github.com/uptrace/bun/extra/bundebug" "log" "os" "sync" - "time" + + _ "github.com/go-sql-driver/mysql" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/mysqldialect" ) type database struct { - instance *sqlx.DB + instance *bun.DB } type Database interface { - GetInstance() *sqlx.DB - Close() - Query(stmt string) *sqlx.Rows - Execute(stmt string) sql.Result - FetchRows(stmt string, dest interface{}) + GetInstance() *bun.DB } var ( @@ -29,27 +27,31 @@ var ( ) func initialize() { - db, err := sqlx.Open("mysql", fmt.Sprintf( + db, err := sql.Open("mysql", fmt.Sprintf( "%s:%s@tcp(%s)/%s", os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_DATABASE"))) - db.SetConnMaxLifetime(time.Minute * 3) - db.SetMaxOpenConns(3) - db.SetMaxIdleConns(3) - if err != nil { panic(err) } - if db.Ping() != nil { - panic(err) + // Resolve instances of bun + + 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") - instance = &database{instance: db} + instance = &database{instance: ormDb} } func InitDatabase() Database { @@ -59,39 +61,6 @@ func InitDatabase() Database { return instance } -func (db *database) GetInstance() *sqlx.DB { - return db.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) - } +func (d *database) GetInstance() *bun.DB { + return d.instance } diff --git a/internal/redis.go b/internal/redis.go index 2884ab8..286be02 100644 --- a/internal/redis.go +++ b/internal/redis.go @@ -13,20 +13,20 @@ import ( var ( redisInstance *redis.Client = nil - cacheStore *persistence.RedisStore + cacheStore *persistence.RedisStore ) type Cache interface { } -func InitRedis() (*redis.Client) { +func InitRedis() *redis.Client { if redisInstance == nil { redis_db_num, err := strconv.Atoi(os.Getenv("REDIS_DATABASE")) if err != nil { 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() if conError != nil { @@ -36,9 +36,9 @@ func InitRedis() (*redis.Client) { return redisInstance } -func InitCacheStore() *persistence.RedisStore{ +func InitCacheStore() *persistence.RedisStore { 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 } diff --git a/main.go b/main.go index 447076f..ee44487 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "os/signal" "relynolli-server/handlers" "relynolli-server/internal" + "relynolli-server/services" "syscall" "github.com/gin-contrib/cors" @@ -28,13 +29,14 @@ func main() { rdb := internal.InitRedis() handlers.InitializeRouter(server) - defer db.Close() + defer db.GetInstance().Close() defer rdb.Close() gracefullyShutDown := make(chan os.Signal, 1) signal.Notify(gracefullyShutDown, syscall.SIGINT, syscall.SIGTERM) go server.Run("0.0.0.0:8000") + go services.PaymentValidation() <-gracefullyShutDown diff --git a/models/article/db.go b/models/article/db.go new file mode 100644 index 0000000..0a6b6d6 --- /dev/null +++ b/models/article/db.go @@ -0,0 +1 @@ +package article diff --git a/models/cart/db.go b/models/cart/db.go new file mode 100644 index 0000000..19a3f4f --- /dev/null +++ b/models/cart/db.go @@ -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"` +} diff --git a/models/catalog.go b/models/catalog.go index 0acd42c..06eba4a 100644 --- a/models/catalog.go +++ b/models/catalog.go @@ -1,47 +1,47 @@ package models -type CatalogStruct struct { - Id int - Code string - Name string - IsActive int `json:"is_active" db:"is_active"` - Properties []byte - DetailText string `json:"detailText" db:"detailText"` - Price []byte - AvailableQuantity int `json:"availableQuantity,omitempty" db:"available_quantity"` -} +// type CatalogStruct struct { +// Id int +// Code string +// Name string +// IsActive int `json:"is_active" db:"is_active"` +// Properties []byte +// DetailText string `json:"detailText" db:"detailText"` +// Price []byte +// AvailableQuantity int `json:"availableQuantity,omitempty" db:"available_quantity"` +// } -type CatalogStructWeb struct { - Id int `json:"id"` - Code string `json:"code"` - Name string `json:"name"` - IsActive int `json:"is_active" db:"is_active"` - Properties map[string]interface{} `json:"properties"` - DetailText string `json:"detailText" db:"detailText"` - Price map[string]interface{} `json:"price"` - AvailableQuantity int `json:"availableQuantity,omitempty" db:"available_quantity"` -} +// type CatalogStructWeb struct { +// Id int `json:"id"` +// Code string `json:"code"` +// Name string `json:"name"` +// IsActive int `json:"is_active" db:"is_active"` +// Properties map[string]interface{} `json:"properties"` +// DetailText string `json:"detailText" db:"detailText"` +// Price map[string]interface{} `json:"price"` +// AvailableQuantity int `json:"availableQuantity,omitempty" db:"available_quantity"` +// } -type CatalogWithQuantityWeb struct { - Id int `json:"id"` - Code string `json:"code"` - Name string `json:"name"` - IsActive int `json:"is_active"` - Properties map[string]interface{} `json:"properties"` - DetailText string `json:"detailText"` - Price map[string]interface{} `json:"price"` - Quantity int `json:"quantity"` - AvailableQuantity int `json:"available_quantity" db:"available_quantity"` -} +// type CatalogWithQuantityWeb struct { +// Id int `json:"id"` +// Code string `json:"code"` +// Name string `json:"name"` +// IsActive int `json:"is_active"` +// Properties map[string]interface{} `json:"properties"` +// DetailText string `json:"detailText"` +// Price map[string]interface{} `json:"price"` +// Quantity int `json:"quantity"` +// AvailableQuantity int `json:"available_quantity" db:"available_quantity"` +// } -type CatalogWithQuantity struct { - Id int - Code string - Name string - IsActive int `json:"is_active" db:"is_active"` - Properties []byte - DetailText string `json:"detailText" db:"detailText"` - Price []byte - Quantity int `json:"quantity"` - AvailableQuantity int `json:"available_quantity" db:"available_quantity"` -} +// type CatalogWithQuantity struct { +// Id int +// Code string +// Name string +// IsActive int `json:"is_active" db:"is_active"` +// Properties []byte +// DetailText string `json:"detailText" db:"detailText"` +// Price []byte +// Quantity int `json:"quantity"` +// AvailableQuantity int `json:"available_quantity" db:"available_quantity"` +// } diff --git a/models/catalog/db.go b/models/catalog/db.go new file mode 100644 index 0000000..2a73c84 --- /dev/null +++ b/models/catalog/db.go @@ -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"` +} diff --git a/models/catalog/domain.go b/models/catalog/domain.go new file mode 100644 index 0000000..e571e24 --- /dev/null +++ b/models/catalog/domain.go @@ -0,0 +1 @@ +package catalog diff --git a/models/catalog/requests.go b/models/catalog/requests.go new file mode 100644 index 0000000..e571e24 --- /dev/null +++ b/models/catalog/requests.go @@ -0,0 +1 @@ +package catalog diff --git a/models/discount/db.go b/models/discount/db.go new file mode 100644 index 0000000..1033eac --- /dev/null +++ b/models/discount/db.go @@ -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"` +} diff --git a/models/filters/db.go b/models/filters/db.go new file mode 100644 index 0000000..bcd7fcd --- /dev/null +++ b/models/filters/db.go @@ -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"` +} diff --git a/models/news/db.go b/models/news/db.go new file mode 100644 index 0000000..585e6d0 --- /dev/null +++ b/models/news/db.go @@ -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"` +} diff --git a/models/order/db.go b/models/order/db.go new file mode 100644 index 0000000..1e6edf7 --- /dev/null +++ b/models/order/db.go @@ -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"` +} diff --git a/models/response.go b/models/response.go index 8148836..a6346c9 100644 --- a/models/response.go +++ b/models/response.go @@ -1,7 +1,19 @@ package models +import "relynolli-server/status" + type Response struct { - Status int `json:"status"` - Info string `json:"info,omitempty"` - Data interface{} `json:"data,omitempty"` + Status status.Status `json:"status"` + Info string `json:"info,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"` } diff --git a/services/cart.go b/services/cart.go index a918cd9..5e568ea 100644 --- a/services/cart.go +++ b/services/cart.go @@ -1,109 +1 @@ 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() -} diff --git a/services/catalog.go b/services/catalog.go deleted file mode 100644 index 6f1522e..0000000 --- a/services/catalog.go +++ /dev/null @@ -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;")) -} diff --git a/services/order.go b/services/order.go index 352469b..7f08638 100644 --- a/services/order.go +++ b/services/order.go @@ -2,7 +2,6 @@ package services import ( "context" - "encoding/json" "fmt" "relynolli-server/external/bitrix" "relynolli-server/external/kassa" @@ -11,51 +10,21 @@ import ( "relynolli-server/external/kassa/PaymentSubject" "relynolli-server/external/kassa/VatCodes" "relynolli-server/internal" - "relynolli-server/models" + "relynolli-server/storage" "strconv" "strings" ) -func GetTotal(fuserId int) float64 { - 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) +func addProductsToOrder(ctx context.Context, storage storage.StorageCart, api bitrix.Bitrix, fuserId, orderId int) error { + // //Получаем данные из корзины + // + items, err := storage.GetCartItems(ctx, int64(fuserId)) + if err != nil { + return err } - sum := float64(0) - - 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) + for _, product := range *items { + err = api.AddProductToOrder(orderId, int(product.ProductId), product.Product.Price.BASE, int(product.Quantity), product.Product.Name) if err != nil { 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 + s := storage.NewStorageCart() api := bitrix.Initialize() @@ -79,14 +51,19 @@ func MakeOrder(fuserId int, email string, fullName string, phone string) (map[st // --- обновляем контакт пользователя order, orderErr := api.GetOrderInfo(orderId) + if orderErr != nil { return nil, orderErr } + 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. Добавляем элементы в корзину - addProductErr := addProductsToOrder(api, fuserId, orderId) + addProductErr := addProductsToOrder(ctx, s, api, fuserId, orderId) if addProductErr != nil { return nil, addProductErr } @@ -104,21 +81,19 @@ func MakeOrder(fuserId int, email string, fullName string, phone string) (map[st // 6. Получаем ресурс оплаты и url для нее paymentData, _ := kassa.CreatePayment(orderId, order.Price, fullName, email, phone, getItemsForPayment(order)) - insPaymentDataStmt := fmt.Sprintf(` - insert into api_youkassa_payment (payment_id, order_id, link, status) - values ('%s', - '%s', - '%s', - '%s'); - `, paymentData["id"].(string), + db := internal.InitDatabase().GetInstance() + + db.NewRaw(` + insert into api_youkassa_payment (payment_id, order_id, link, status) + values (?, ?, ?,? );`, + paymentData.Id, orderId, - paymentData["confirmation"].(map[string]interface{})["confirmation_url"].(string), - paymentData["status"].(string), - ) + paymentData.Confirmation.ConfirmationUrl, + paymentData.Status).Exec(ctx) - db := internal.InitDatabase() - - db.Execute(insPaymentDataStmt) + if err != nil { + return nil, err + } return paymentData, nil } diff --git a/services/validate.go b/services/validate.go index a7acee5..8312c50 100644 --- a/services/validate.go +++ b/services/validate.go @@ -1,32 +1,73 @@ package services import ( - "fmt" + "context" + "log" "relynolli-server/external/bitrix" - "relynolli-server/internal" + "relynolli-server/external/kassa" + "relynolli-server/storage" + "time" ) -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) - +func PaymentValidation() { + ctx := context.Background() + s := storage.NewStorageOrder() api := bitrix.Initialize() - if status == "succeeded" { - api.ApprovePayment(paymentIdBitrix, 8) - return + + for { + payments, err := s.GetPayments(ctx) + if err != nil { + panic(err.Error()) + } + for _, payment := range *payments { + result, _ := kassa.CheckPayment(payment.ID) + + 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) } - if status == "canceled" { - api.CancelOrder(orderId) - return - } - 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 +//} diff --git a/status/status.go b/status/status.go new file mode 100644 index 0000000..cee4ee3 --- /dev/null +++ b/status/status.go @@ -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" +) diff --git a/storage/article.go b/storage/article.go new file mode 100644 index 0000000..c022900 --- /dev/null +++ b/storage/article.go @@ -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 +} diff --git a/storage/cart.go b/storage/cart.go new file mode 100644 index 0000000..58f0b6b --- /dev/null +++ b/storage/cart.go @@ -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 +} diff --git a/storage/catalog.go b/storage/catalog.go new file mode 100644 index 0000000..16b0068 --- /dev/null +++ b/storage/catalog.go @@ -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 +} diff --git a/storage/news.go b/storage/news.go new file mode 100644 index 0000000..a197ace --- /dev/null +++ b/storage/news.go @@ -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 +} diff --git a/storage/order.go b/storage/order.go new file mode 100644 index 0000000..ad4f91b --- /dev/null +++ b/storage/order.go @@ -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 +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..947b75b --- /dev/null +++ b/storage/storage.go @@ -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 diff --git a/test.php b/test.php new file mode 100644 index 0000000..e8553a6 --- /dev/null +++ b/test.php @@ -0,0 +1,9 @@ +