features/feature-catalog-redis
Ernest Litvinenko 2024-03-15 21:27:45 +03:00
commit 54856057c2
37 changed files with 1797 additions and 0 deletions

18
.env.sample Normal file
View File

@ -0,0 +1,18 @@
# Database config
MYSQL_USER=example
MYSQL_PASSWORD=example
MYSQL_HOST=127.0.0.1
MYSQL_DATABASE=example
# Redis config
REDIS_ADDRESS=127.0.0.1:6379
REDIS_PASSWORD=
REDIS_DATABASE=0
# EXTERANL BLOCK
BITRIX_API_EP=https://bitrix.address/rest/auth/string
YOOKASSA_ACCOUNT_ID=1234
YOOKASSA_ACCOUNT_SECRET=YOUR_PWD

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
server
./build/
.env
.idea/

367
external/bitrix/api.go vendored Normal file
View File

@ -0,0 +1,367 @@
package bitrix
import (
"bytes"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/sirupsen/logrus"
"net/http"
"os"
"strings"
"time"
)
var EP = os.Getenv("BITRIX_API_EP")
type ClientsResource struct {
EntityId string `json:"entityId"`
}
type OrderResource struct {
ID string `json:"id,omitempty"`
Clients []ClientsResource `json:"clients"`
Lid string `json:"lid,omitempty"`
DateInsert time.Time `json:"dateInsert,omitempty"`
DateUpdate time.Time `json:"dateUpdate,omitempty"`
PersonTypeID string `json:"personTypeId,omitempty"`
StatusID string `json:"statusId,omitempty"`
DateStatus time.Time `json:"dateStatus,omitempty"`
EmpStatusID any `json:"empStatusId,omitempty"`
Marked string `json:"marked,omitempty"`
DateMarked any `json:"dateMarked,omitempty"`
EmpMarkedID any `json:"empMarkedId,omitempty"`
ReasonMarked any `json:"reasonMarked,omitempty"`
Price float64 `json:"price,omitempty"`
DiscountValue string `json:"discountValue,omitempty"`
TaxValue string `json:"taxValue,omitempty"`
UserDescription string `json:"userDescription,omitempty"`
AdditionalInfo any `json:"additionalInfo,omitempty"`
Comments string `json:"comments,omitempty"`
CompanyID string `json:"companyId,omitempty"`
ResponsibleID any `json:"responsibleId,omitempty"`
StatGid any `json:"statGid,omitempty"`
DatePayBefore any `json:"datePayBefore,omitempty"`
DateBill any `json:"dateBill,omitempty"`
RecurringID any `json:"recurringId,omitempty"`
LockedBy string `json:"lockedBy,omitempty"`
DateLock time.Time `json:"dateLock,omitempty"`
RecountFlag string `json:"recountFlag,omitempty"`
AffiliateID any `json:"affiliateId,omitempty"`
DeliveryDocNum any `json:"deliveryDocNum,omitempty"`
DeliveryDocDate any `json:"deliveryDocDate,omitempty"`
Updated1C string `json:"updated1c,omitempty"`
OrderTopic any `json:"orderTopic,omitempty"`
XMLID any `json:"xmlId,omitempty"`
ID1C any `json:"id1c,omitempty"`
Version1C any `json:"version1c,omitempty"`
Version string `json:"version,omitempty"`
ExternalOrder string `json:"externalOrder,omitempty"`
StoreID any `json:"storeId,omitempty"`
Canceled string `json:"canceled,omitempty"`
EmpCanceledID any `json:"empCanceledId,omitempty"`
DateCanceled any `json:"dateCanceled,omitempty"`
ReasonCanceled any `json:"reasonCanceled,omitempty"`
BasketItems []BasketResource `json:"basketItems,omitempty"`
Properties []propertyValueResource `json:"properties,omitempty"`
Payments []PaymentResource `json:"payments,omitempty"`
Shipments []any `json:"shipments,omitempty"`
}
type propertyValueResource struct {
Id string `json:"id"`
Name string `json:"name"`
Value []string `json:"value"`
Code string `json:"code"`
OrderPropsId string `json:"orderPropsId"`
}
type PaymentResource struct {
Id string `json:"id"`
OrderId string `json:"orderId"`
Paid string `json:"paid"`
DatePaid interface{} `json:"datePaid"`
EmpPaidId interface{} `json:"empPaidId"`
PaySystemId string `json:"paySystemId"`
PsStatus interface{} `json:"psStatus"`
PsStatusCode interface{} `json:"psStatusCode"`
PsInvoiceId interface{} `json:"psInvoiceId"`
PsStatusDescription interface{} `json:"psStatusDescription"`
PsStatusMessage interface{} `json:"psStatusMessage"`
PsSum interface{} `json:"psSum"`
PsCurrency interface{} `json:"psCurrency"`
PsResponseDate interface{} `json:"psResponseDate"`
PayVoucherNum string `json:"payVoucherNum"`
PayVoucherDate interface{} `json:"payVoucherDate"`
DatePayBefore interface{} `json:"datePayBefore"`
DateBill time.Time `json:"dateBill"`
XmlId interface{} `json:"xmlId"`
Sum string `json:"sum"`
PriceCod string `json:"priceCod"`
Currency string `json:"currency"`
PaySystemName string `json:"paySystemName"`
ResponsibleId interface{} `json:"responsibleId"`
EmpResponsibleId interface{} `json:"empResponsibleId"`
DateResponsibleId interface{} `json:"dateResponsibleId"`
Comments interface{} `json:"comments"`
CompanyId string `json:"companyId"`
PayReturnNum string `json:"payReturnNum"`
PayReturnDate interface{} `json:"payReturnDate"`
EmpReturnId interface{} `json:"empReturnId"`
PayReturnComment string `json:"payReturnComment"`
IsReturn string `json:"isReturn"`
Marked string `json:"marked"`
DateMarked interface{} `json:"dateMarked"`
EmpMarkedId interface{} `json:"empMarkedId"`
ReasonMarked interface{} `json:"reasonMarked"`
Updated1C string `json:"updated1c"`
Id1C interface{} `json:"id1c"`
Version1C interface{} `json:"version1c"`
ExternalPayment string `json:"externalPayment"`
}
type BasketResource struct {
Module string `json:"module"`
ProductId string `json:"productId"`
Id string `json:"id"`
Lid string `json:"lid"`
Quantity string `json:"quantity"`
Weight string `json:"weight"`
Price float64 `json:"price"`
CustomPrice string `json:"customPrice"`
BasePrice string `json:"basePrice"`
ProductPriceId string `json:"productPriceId"`
PriceTypeId string `json:"priceTypeId"`
Currency string `json:"currency"`
BarcodeMulti string `json:"barcodeMulti"`
Name string `json:"name"`
CatalogXmlId string `json:"catalogXmlId"`
VatRate string `json:"vatRate"`
Notes string `json:"notes"`
DiscountPrice string `json:"discountPrice"`
ProductProviderClass string `json:"productProviderClass"`
Dimensions string `json:"dimensions"`
Type interface{} `json:"type"`
SetParentId interface{} `json:"setParentId"`
DetailPageUrl string `json:"detailPageUrl"`
MeasureCode string `json:"measureCode"`
MeasureName string `json:"measureName"`
OrderId string `json:"orderId"`
ProductXmlId string `json:"productXmlId"`
Subscribe string `json:"subscribe"`
Recommendation interface{} `json:"recommendation"`
VatIncluded string `json:"vatIncluded"`
Sort string `json:"sort"`
DiscountName interface{} `json:"discountName"`
DiscountValue interface{} `json:"discountValue"`
DiscountCoupon interface{} `json:"discountCoupon"`
Properties []struct {
Id string `json:"id"`
BasketId string `json:"basketId"`
Name string `json:"name"`
Value string `json:"value"`
Code string `json:"code"`
Sort string `json:"sort"`
} `json:"properties"`
}
type bitrix struct{}
type Bitrix interface {
CreateAnonymousUser() (int, error)
CreateOrder(userId int) (int, error)
ApprovePayment(paymentId int, paySystemId int) error
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
UpdateContact(contactId int, email string, name string, phone string) error
}
type createAnonymousUserRequest struct {
Email string `json:"EMAIL"`
Name string `json:"NAME"`
Password string `json:"PASSWORD"`
ConfirmPassword string `json:"CONFIRM_PASSWORD"`
UFDepartment []int `json:"UF_DEPARTMENT"`
}
type createAnonymousUserResponse struct {
Result int `json:"result"`
}
func (_ bitrix) CreateAnonymousUser() (int, error) {
uid, _ := uuid.NewUUID()
req := createAnonymousUserRequest{
Email: fmt.Sprintf("anonymous%s@anonym.ru", uid.String()),
Name: "Анонимный пользователь",
Password: uid.String(),
ConfirmPassword: uid.String(),
UFDepartment: []int{0},
}
query, _ := json.Marshal(req)
resp, err := http.Post(EP+"/user.add", "application/json", bytes.NewBuffer(query))
result := createAnonymousUserResponse{}
err = json.NewDecoder(resp.Body).Decode(&result)
return result.Result, err
}
type createOrderRequestWrapper struct {
Fields createOrderRequest `json:"fields"`
}
type createOrderRequest struct {
Lid string `json:"lid"`
PersonTypeId int `json:"personTypeId"`
Currency string `json:"currency"`
UserId int `json:"userId"`
}
type createOrderResponse struct {
Result struct {
Order struct {
Id int `json:"id"`
} `json:"order"`
} `json:"result"`
}
func (_ bitrix) CreateOrder(userId int) (int, error) {
req := createOrderRequestWrapper{createOrderRequest{
Lid: "s2",
PersonTypeId: 5,
Currency: "RUB",
UserId: userId,
}}
query, _ := json.Marshal(req)
resp, _ := http.Post(EP+"/sale.order.add", "application/json", bytes.NewBuffer(query))
result := createOrderResponse{}
err := json.NewDecoder(resp.Body).Decode(&result)
return result.Result.Order.Id, err
}
func (_ bitrix) ApprovePayment(paymentId int, paySystemId int) error {
req := map[string]interface{}{
"id": paymentId,
"fields": map[string]interface{}{"paid": "Y", "paySystemId": paySystemId},
}
query, _ := json.Marshal(req)
_, err := http.Post(EP+"/sale.payment.update", "application/json", bytes.NewBuffer(query))
return err
}
func (_ bitrix) CancelOrder(orderId int) error {
req := map[string]interface{}{
"id": orderId,
"fields": map[string]interface{}{"canceled": "Y"},
}
query, _ := json.Marshal(req)
_, err := http.Post(EP+"/sale.order.update", "application/json", bytes.NewBuffer(query))
return err
}
func (_ 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))
result := struct {
Result struct {
Order map[string]interface{} `json:"order"`
} `json:"result"`
}{}
err = json.NewDecoder(response.Body).Decode(&result)
if err != nil {
logrus.Errorf("Error: %s", err.Error())
}
resultParsed := OrderResource{}
mapstructure.Decode(result.Result.Order, &resultParsed)
return &resultParsed, err
}
func (_ bitrix) CreatePayment(orderId int, sum float64) error {
req := map[string]interface{}{
"fields": map[string]interface{}{
"orderId": orderId,
"sum": sum,
"paid": "N",
"paySystemId": 8,
},
}
query, _ := json.Marshal(req)
_, err := http.Post(EP+"/sale.payment.add", "application/json", bytes.NewBuffer(query))
return err
}
func (_ bitrix) AddProductToOrder(orderId int, productId int, price float64, quantity int) error {
req := map[string]interface{}{
"fields": map[string]interface{}{
"orderId": orderId,
"productId": productId,
"quantity": quantity,
"currency": "RUB",
"price": price,
},
}
query, _ := json.Marshal(req)
_, err := http.Post(EP+"/sale.basketitem.addCatalogProduct", "application/json", bytes.NewBuffer(query))
return err
}
func (_ bitrix) UpdateContact(contactId int, email string, name string, phone string) error {
req := map[string]interface{}{
"id": contactId,
"fields": map[string]interface{}{
"EMAIL": append([]map[string]interface{}{}, map[string]interface{}{
"VALUE_TYPE": "other",
"VALUE": email,
"TYPE_ID": "EMAIL",
}),
"NAME": strings.Split(name, " ")[1],
"LAST_NAME": strings.Split(name, " ")[0],
"SECOND_NAME": strings.Split(name, " ")[2],
"PHONE": append([]map[string]interface{}{}, map[string]interface{}{
"VALUE_TYPE": "other",
"VALUE": phone,
"TYPE_ID": "PHONE",
}),
},
}
query, _ := json.Marshal(req)
_, err := http.Post(EP+"/crm.contact.update", "application/json", bytes.NewBuffer(query))
return err
}
func Initialize() Bitrix {
return &bitrix{}
}

13
external/kassa/AgentType/enums.go vendored Normal file
View File

@ -0,0 +1,13 @@
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" // Агент
)

30
external/kassa/Measure/enums.go vendored Normal file
View File

@ -0,0 +1,30 @@
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" // Другое
)

13
external/kassa/PaymentMode/enums.go vendored Normal file
View File

@ -0,0 +1,13 @@
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" // Выплата по кредиту
)

25
external/kassa/PaymentSubject/enums.go vendored Normal file
View File

@ -0,0 +1,25 @@
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" // Другое
)

10
external/kassa/Settlements/enums.go vendored Normal file
View File

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

12
external/kassa/TaxSystemCode/enums.go vendored Normal file
View File

@ -0,0 +1,12 @@
package TaxSystemCode
type TaxSystemCode int
const (
GENERAL = iota + 1
USN_INCOME
USN_INCOME_MINUS_EXPENCES
ENVD
ESN
PATENT
)

12
external/kassa/VatCodes/enums.go vendored Normal file
View File

@ -0,0 +1,12 @@
package VatCodes
type VatCode int
const (
WithoutNDS = iota + 1
NDS_0
NDS_10
NDS_20
NDS_10_FOR_110
NDS_20_FOR_120
)

9
external/kassa/general.go vendored Normal file
View File

@ -0,0 +1,9 @@
package kassa
import "os"
var (
ACCOUNT_ID = os.Getenv("YOOKASSA_ACCOUNT_ID")
PASSWORD = os.Getenv("YOOKASSA_ACCOUNT_SECRET")
BASE_URL = "https://api.yookassa.ru/v3/payments"
)

123
external/kassa/kassa.go vendored Normal file
View File

@ -0,0 +1,123 @@
package kassa
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/google/uuid"
"net/http"
"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"`
}
type KassaReceiptItems struct {
Description string `json:"description"`
Amount KassaAmount `json:"amount"`
VatCode VatCodes.VatCode `json:"vat_code"`
Quantity string `json:"quantity"`
Measure Measure.Measure `json:"measure"`
PaymentSubject PaymentSubject.PaymentSubject `json:"payment_subject"`
PaymentMode PaymentMode.PaymentMode `json:"payment_mode"`
}
type KassaCustomer struct {
FullName string `json:"full_name"`
Email string `json:"email"`
Phone string `json:"phone"`
}
type KassaReceipt struct {
Customer KassaCustomer `json:"customer"`
Items []KassaReceiptItems `json:"items"`
TaxSystemCode TaxSystemCode.TaxSystemCode `json:"tax_system_code"`
}
type KassaConfirmation struct {
Type string `json:"type"`
ReturnUrl string `json:"return_url"`
}
type KassaPaymentReq struct {
Amount KassaAmount `json:"amount"`
Description string `json:"description"`
Receipt KassaReceipt `json:"receipt"`
Confirmation KassaConfirmation `json:"confirmation"`
Capture bool `json:"capture"`
}
type KassaResult struct {
Id string `json:"id"`
Status string `json:"status"`
Amount KassaAmount `json:"amount"`
Description string `json:"description"`
Recipient struct {
AccountId string `json:"account_id"`
GatewayId string `json:"gateway_id"`
} `json:"recipient"`
CreatedAt time.Time `json:"created_at"`
Confirmation struct {
Type string `json:"type"`
ConfirmationUrl string `json:"confirmation_url"`
} `json:"confirmation"`
Paid bool `json:"paid"`
}
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
func CreatePayment(orderId int, sum float64, fullName string, email string, phone string, items []KassaReceiptItems) (map[string]interface{}, error) {
req := KassaPaymentReq{
Amount: KassaAmount{Value: fmt.Sprintf("%f", sum), Currency: "RUB"},
Description: fmt.Sprintf("Заказ №%d", orderId),
Confirmation: KassaConfirmation{
Type: "redirect",
ReturnUrl: "https://relynolli.ru",
},
Capture: true,
Receipt: KassaReceipt{
Customer: KassaCustomer{
FullName: fullName,
Email: email,
Phone: phone,
},
Items: items,
TaxSystemCode: TaxSystemCode.GENERAL,
},
}
query, _ := json.Marshal(req)
uid, err := uuid.NewUUID()
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("Idempotence-Key", uid.String())
request.Header.Set("Content-Type", "application/json")
response, err := client.Do(request)
result := map[string]interface{}{}
json.NewDecoder(response.Body).Decode(&result)
if err != nil {
return nil, err
}
return result, nil
}

1
external/kassa/types.go vendored Normal file
View File

@ -0,0 +1 @@
package kassa

42
go.mod Normal file
View File

@ -0,0 +1,42 @@
module relynolli-server
go 1.21
require (
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // 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/google/uuid v1.6.0 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/sirupsen/logrus v1.9.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.20.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
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

111
go.sum Normal file
View File

@ -0,0 +1,111 @@
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=
github.com/bytedance/sonic v1.11.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
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/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
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/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/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/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=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
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/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-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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
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/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/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=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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=
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/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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/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/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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -0,0 +1,34 @@
package endpoints
import (
"github.com/gin-gonic/gin"
"relynolli-server/models"
"relynolli-server/services"
"strconv"
)
func (h *handlers) GetCartItems(c *gin.Context) {
fuserId := c.Query("fuserId")
if fuserId == "" {
c.JSON(400, models.Response{Status: 400, Info: "\"fuserId\" should be provided"})
return
}
idx, err := strconv.Atoi(fuserId)
if err != nil {
c.JSON(400, models.Response{Status: 400, Info: "\"fuserId should be an integer number\""})
return
}
c.JSON(200, services.GetCartItems(idx))
}
func (h *handlers) CreateFUser(c *gin.Context) {
lastInsertId := services.CreateFuser()
c.JSON(201, gin.H{
"fuserId": lastInsertId,
})
}

View File

@ -0,0 +1,18 @@
package endpoints
import "github.com/gin-gonic/gin"
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)
}
func GetHandlers() Handlers {
return &handlers{}
}

View File

@ -0,0 +1,75 @@
package endpoints
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"relynolli-server/models"
"relynolli-server/services"
)
type createCartItemRequest struct {
FuserId int `json:"fuserId"`
PriceTypeId int `json:"priceTypeId,omitempty"`
ProductId int `json:"productId"`
Quantity int `json:"quantity,omitempty"`
}
type updateCartRequest struct {
FuserId int `json:"fuserId"`
ProductId int `json:"productId"`
Quantity int `json:"quantity"`
}
type deleteCartRequest struct {
FuserId int `json:"fuserId"`
ProductId int `json:"productId"`
}
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})
}

22
handlers/cart/routes.go Normal file
View File

@ -0,0 +1,22 @@
package cart
import (
"github.com/gin-gonic/gin"
"relynolli-server/handlers/cart/endpoints"
)
func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers()
cart := parent.Group("/cart")
itemRouter := cart.Group("/item")
{
cart.GET("", h.GetCartItems)
cart.POST("", h.CreateFUser)
}
{
itemRouter.POST("", h.CreateCartItem)
itemRouter.PATCH("", h.UpdateCartItem)
itemRouter.DELETE("", h.DeleteCartItem)
}
}

View File

@ -0,0 +1,33 @@
package endpoints
import (
"github.com/gin-gonic/gin"
"relynolli-server/models"
"relynolli-server/services"
"strconv"
)
func (h *handlers) GetCatalogItems(c *gin.Context) {
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
offset := (page - 1) * limit
c.JSON(200, services.GetCatalogItems(limit, offset))
}
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
}
resp, err := services.GetCatalogItem(code)
if err != nil {
c.JSON(404, models.Response{Status: 404, Info: err.Error()})
return
}
c.JSON(200, resp)
}

View File

@ -0,0 +1,15 @@
package endpoints
import "github.com/gin-gonic/gin"
type handlers struct{}
type Handlers interface {
GetFilters(c *gin.Context)
GetCatalogItems(c *gin.Context)
GetCatalogItem(c *gin.Context)
}
func GetHandlers() Handlers {
return &handlers{}
}

View File

@ -0,0 +1,38 @@
package endpoints
import (
"encoding/json"
"github.com/gin-gonic/gin"
"relynolli-server/internal"
)
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)
}
c.JSON(200, responseData)
}

View File

@ -0,0 +1,14 @@
package catalog
import (
"github.com/gin-gonic/gin"
"relynolli-server/handlers/catalog/endpoints"
)
func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers()
catalog := parent.Group("/catalog")
catalog.GET("/filters", h.GetFilters)
catalog.GET("/", h.GetCatalogItems)
catalog.GET("/:code", h.GetCatalogItem)
}

View File

@ -0,0 +1,77 @@
package endpoints
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"net/http"
"relynolli-server/models"
"relynolli-server/services"
)
type handlers struct{}
type getTotalRequest struct {
FuserId int `json:"fuserId"`
}
type getTotalResponse struct {
TotalProductPrice float64 `json:"total_product_price"`
}
type makeOrderRequest struct {
FuserId int `json:"fuserId" validate:"required"`
PhoneNumber string `json:"phoneNumber" validate:"required"`
FullName string `json:"fullName" validate:"required"`
Email string `json:"email" validate:"required,email"`
DeliveryType string `json:"deliveryType,omitempty"`
Address string `json:"address,omitempty"`
}
func (h handlers) GetTotal(c *gin.Context) {
req := getTotalRequest{}
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(http.StatusBadRequest, models.Response{Status: http.StatusBadRequest, Info: "fuserId is not provided"})
return
}
total := services.GetTotal(req.FuserId)
c.JSON(http.StatusOK, getTotalResponse{TotalProductPrice: total})
}
func (h handlers) MakeOrder(c *gin.Context) {
// VALIDATION
validate := validator.New(validator.WithRequiredStructEnabled())
req := makeOrderRequest{}
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(400, models.Response{Status: http.StatusBadRequest, Info: fmt.Sprintf("ERROR: %s", err.Error())})
return
}
validationErr := validate.Struct(req)
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())})
return
}
kassaResult, serviceErr := services.MakeOrder(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())})
return
}
c.JSON(http.StatusOK, kassaResult)
}
type Handlers interface {
GetTotal(c *gin.Context)
MakeOrder(c *gin.Context)
}
func GetHandlers() Handlers {
return &handlers{}
}

13
handlers/order/routes.go Normal file
View File

@ -0,0 +1,13 @@
package order
import (
"github.com/gin-gonic/gin"
"relynolli-server/handlers/order/endpoints"
)
func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers()
order := parent.Group("/order")
order.POST("/make", h.MakeOrder)
order.POST("/total", h.GetTotal)
}

17
handlers/routers.go Normal file
View File

@ -0,0 +1,17 @@
package handlers
import (
"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)
order.HandleRoutes(APIV1Router)
validate.HandleRoutes(APIV1Router)
}

View File

@ -0,0 +1,40 @@
package endpoints
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"relynolli-server/models"
"relynolli-server/services"
)
type handlers struct{}
type ValidateReq struct {
Type string `json:"type"`
Event string `json:"event"`
Object struct {
Id string `json:"id"`
Status string `json:"status"`
}
}
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())})
}
services.YookassaValidate(req.Object.Id, req.Object.Status)
}
type Handlers interface {
Validate(c *gin.Context)
}
func GetHandlers() Handlers {
return &handlers{}
}

View File

@ -0,0 +1,12 @@
package validate
import (
"github.com/gin-gonic/gin"
"relynolli-server/handlers/validate/endpoints"
)
func HandleRoutes(parent *gin.RouterGroup) {
h := endpoints.GetHandlers()
validate := parent.Group("/yookassa-validate")
validate.POST("", h.Validate)
}

97
internal/database.go Normal file
View File

@ -0,0 +1,97 @@
package internal
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"log"
"os"
"sync"
"time"
)
type database struct {
instance *sqlx.DB
}
type Database interface {
GetInstance() *sqlx.DB
Close()
Query(stmt string) *sqlx.Rows
Execute(stmt string) sql.Result
FetchRows(stmt string, dest interface{})
}
var (
instance *database = nil
once sync.Once
)
func initialize() {
db, err := sqlx.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)
}
log.Println("Connection to db succeded")
instance = &database{instance: db}
}
func InitDatabase() Database {
if instance == nil {
once.Do(initialize)
}
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)
}
}

31
internal/redis.go Normal file
View File

@ -0,0 +1,31 @@
package internal
import (
"context"
"github.com/redis/go-redis/v9"
"log"
"os"
"strconv"
)
var (
redisInstance *redis.Client = nil
)
type Cache interface {
}
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})
_, conError := redisInstance.Ping(context.Background()).Result()
if conError != nil {
log.Fatalln("Cannot connect to redis host")
}
}
return redisInstance
}

46
main.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"log"
"os"
"os/signal"
"relynolli-server/handlers"
"relynolli-server/internal"
"syscall"
)
func main() {
loadEnvironment()
server := gin.Default()
crs := cors.New(cors.Config{
AllowHeaders: []string{"*"},
AllowAllOrigins: true,
AllowMethods: []string{"GET, POST, PATCH, DELETE"},
})
server.Use(crs)
db := internal.InitDatabase()
rdb := internal.InitRedis()
handlers.InitializeRouter(server)
defer db.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")
<-gracefullyShutDown
}
func loadEnvironment() {
err := godotenv.Load(".env")
if err != nil {
log.Fatal("Error loading .env file")
}
}

47
models/catalog.go Normal file
View File

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

7
models/response.go Normal file
View File

@ -0,0 +1,7 @@
package models
type Response struct {
Status int `json:"status"`
Info string `json:"info,omitempty"`
Data interface{} `json:"data,omitempty"`
}

135
services/cart.go Normal file
View File

@ -0,0 +1,135 @@
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
//cartList := []models.CatalogWithQuantity{}
//returnedList := []models.CatalogWithQuantityWeb{}
//
//stmt := fmt.Sprintf(`select t1.*, t2.quantity, t3.QUANTITY as available_quantity from api_catalog t1
// join api_cart t2 on t1.id = t2.product_id
// left join b_catalog_product t3 on t1.id = t3.ID where t2.fuser_id = %d;`, fuserId)
//
//retrieveItems(stmt, &cartList)
//
//for _, item := range cartList {
// itemProd := models.CatalogWithQuantityWeb{
// Id: item.Id,
// Code: item.Code,
// Name: item.Name,
// IsActive: item.IsActive,
// DetailText: item.DetailText,
// Quantity: item.Quantity,
// AvailableQuantity: item.AvailableQuantity,
// }
// json.Unmarshal(item.Price, &itemProd.Price)
// json.Unmarshal(item.Properties, &itemProd.Properties)
// returnedList = append(returnedList, itemProd)
//}
//
//return returnedList
}
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()
}

57
services/catalog.go Normal file
View File

@ -0,0 +1,57 @@
package services
import (
"encoding/json"
"fmt"
"relynolli-server/internal"
"relynolli-server/models"
)
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 GetCatalogItems(limit int, offset int) []models.CatalogStructWeb {
stmt := fmt.Sprintf("select * from api_catalog order by id DESC 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
}

147
services/order.go Normal file
View File

@ -0,0 +1,147 @@
package services
import (
"context"
"encoding/json"
"fmt"
"relynolli-server/external/bitrix"
"relynolli-server/external/kassa"
"relynolli-server/external/kassa/Measure"
"relynolli-server/external/kassa/PaymentMode"
"relynolli-server/external/kassa/PaymentSubject"
"relynolli-server/external/kassa/VatCodes"
"relynolli-server/internal"
"relynolli-server/models"
"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)
}
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)
if err != nil {
return err
}
}
return nil
}
func MakeOrder(fuserId int, email string, fullName string, phone string) (map[string]interface{}, error) {
// Инициализируем api
api := bitrix.Initialize()
// 1. Создаем анонимного пользователя
userId, _ := api.CreateAnonymousUser()
// 2. Создаем заказ
orderId, _ := api.CreateOrder(userId)
// --- обновляем контакт пользователя
order, orderErr := api.GetOrderInfo(orderId)
if orderErr != nil {
return nil, orderErr
}
clientId, _ := strconv.Atoi(order.Clients[0].EntityId)
api.UpdateContact(clientId, email, fullName, phone)
// 3. Добавляем элементы в корзину
addProductErr := addProductsToOrder(api, fuserId, orderId)
if addProductErr != nil {
return nil, addProductErr
}
// 4. Получаем обновленный ресурс заказа
order, _ = api.GetOrderInfo(orderId)
// 5. Добавляем способ оплаты товара
createPaymentError := api.CreatePayment(orderId, order.Price)
if createPaymentError != nil {
return nil, createPaymentError
}
// 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),
orderId,
paymentData["confirmation"].(map[string]interface{})["confirmation_url"].(string),
paymentData["status"].(string),
)
db := internal.InitDatabase()
db.Execute(insPaymentDataStmt)
return paymentData, nil
}
func getItemsForPayment(order *bitrix.OrderResource) []kassa.KassaReceiptItems {
result := []kassa.KassaReceiptItems{}
for _, basketItem := range order.BasketItems {
quantity, _ := strconv.Atoi(strings.Split(basketItem.Quantity, ".")[0])
item := kassa.KassaReceiptItems{
Description: basketItem.Name,
Amount: kassa.KassaAmount{
Value: fmt.Sprintf("%f", basketItem.Price),
Currency: "RUB",
},
VatCode: VatCodes.NDS_20,
Quantity: fmt.Sprintf("%d", quantity),
Measure: Measure.PIECE,
PaymentSubject: PaymentSubject.COMMODITY,
PaymentMode: PaymentMode.FULL_PAYMENT,
}
result = append(result, item)
}
return result
}

32
services/validate.go Normal file
View File

@ -0,0 +1,32 @@
package services
import (
"fmt"
"relynolli-server/external/bitrix"
"relynolli-server/internal"
)
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
}