initial commit

This commit is contained in:
Gitea
2024-01-24 16:12:16 +03:00
commit 5b5dc2165c
23 changed files with 1846 additions and 0 deletions

View File

@ -0,0 +1,5 @@
package config
type Badger struct {
Path string `json:"path"`
}

64
internal/core/base.go Normal file
View File

@ -0,0 +1,64 @@
package core
import (
"context"
"fmt"
"time"
"github.com/rs/zerolog"
)
type BaseAction interface{}
type Action[Q, R any] interface {
BaseAction
Do(context.Context, Q) (R, error)
}
type ActionDecorator[Q, R any, A Action[Q, R]] interface {
Action[Q, R]
}
type baseAction struct {
env *Env
}
func newBaseAction(env *Env) baseAction {
return baseAction{
env: env,
}
}
func applyDecorators[Q, R any, A Action[Q, R]](action A) ActionDecorator[Q, R, A] {
return logActionDecorator[Q, R, A]{
action: action,
}
}
type logActionDecorator[Q, R any, A Action[Q, R]] struct {
action Action[Q, R]
}
func (d logActionDecorator[Q, R, A]) Do(ctx context.Context, params Q) (result R, err error) {
actionName := getTypeName[A]()
start := time.Now()
log := zerolog.Ctx(ctx).With().Str("action_name", actionName).Logger()
ctx = log.WithContext(ctx)
result, err = d.action.Do(ctx, params)
elapsed := time.Since(start)
if err != nil {
log.Warn().Err(err).Dur("elapsed", elapsed).Msg("action failed")
}
log.Info().Dur("elapsed", elapsed).Msg("action successed")
return result, err
}
func getTypeName[T any]() string {
var t T
out := fmt.Sprintf("%T", t)
return out
}

View File

@ -0,0 +1,45 @@
package core
import (
"context"
"fmt"
"git.loyso.art/frx/eway/internal/entity"
)
type CreateGoodsItemParams struct {
GoodsItems []entity.GoodsItem
}
type CreateGoodsItemResult struct {
GoodsItems []entity.GoodsItem
}
type createGoodsItemAction struct {
baseAction
}
type CreateGoodsItemAction Action[CreateGoodsItemParams, CreateGoodsItemResult]
func NewCreateGoodsItemAction(
env *Env,
) ActionDecorator[CreateGoodsItemParams, CreateGoodsItemResult, CreateGoodsItemAction] {
ba := newBaseAction(env)
action := &createGoodsItemAction{
baseAction: ba,
}
return applyDecorators(action)
}
func (a *createGoodsItemAction) Do(
ctx context.Context,
params CreateGoodsItemParams,
) (result CreateGoodsItemResult, err error) {
result.GoodsItems, err = a.env.repository.GoodsItem().UpsertMany(ctx, params.GoodsItems...)
if err != nil {
return result, fmt.Errorf("upserting items: %w", err)
}
return result, nil
}

11
internal/core/env.go Normal file
View File

@ -0,0 +1,11 @@
package core
import (
"git.loyso.art/frx/eway/internal/entity"
"git.loyso.art/frx/eway/internal/storage"
)
type Env struct {
repository storage.Repository
mapper entity.Mapper
}

View File

@ -0,0 +1,49 @@
package core
import (
"context"
"fmt"
"git.loyso.art/frx/eway/internal/entity"
)
type GetCategoryParams struct {
ID int64
Name string
}
type GetCategoryResult struct {
Category entity.Category
}
type GetCategoryAction Action[GetCategoryParams, GetCategoryResult]
type getCategoryAction struct {
baseAction
}
func NewGetCategoryAction(env *Env) ActionDecorator[GetCategoryParams, GetCategoryResult, GetCategoryAction] {
ba := newBaseAction(env)
action := &getCategoryAction{
baseAction: ba,
}
return applyDecorators(action)
}
func (a *getCategoryAction) Do(ctx context.Context, params GetCategoryParams) (result GetCategoryResult, err error) {
id := params.ID
if params.Name != "" {
id, err = a.env.mapper.CategoryNameToID(ctx, params.Name)
if err != nil {
return result, fmt.Errorf("resolving category id: %w", err)
}
}
result.Category, err = a.env.repository.Category().Get(ctx, id)
if err != nil {
return result, fmt.Errorf("getting category: %w", err)
}
return result, nil
}

View File

@ -0,0 +1,38 @@
package core
import (
"context"
"fmt"
"git.loyso.art/frx/eway/internal/entity"
)
type ListCategoriesParams struct{}
type ListCategoriesResult struct {
Categories []entity.Category
}
type listCategoriesAction struct {
baseAction
}
type ListCategoriesAction Action[ListCategoriesParams, ListCategoriesResult]
func NewListCategoriesAction(env *Env) ActionDecorator[ListCategoriesParams, ListCategoriesResult, ListCategoriesAction] {
ba := newBaseAction(env)
action := &listCategoriesAction{
baseAction: ba,
}
return applyDecorators(action)
}
func (l *listCategoriesAction) Do(ctx context.Context, params ListCategoriesParams) (result ListCategoriesResult, err error) {
result.Categories, err = l.env.repository.Category().List(ctx)
if err != nil {
return result, fmt.Errorf("listing: %w", err)
}
return result, nil
}

View File

@ -0,0 +1,75 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package fbs
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type Category struct {
_tab flatbuffers.Table
}
func GetRootAsCategory(buf []byte, offset flatbuffers.UOffsetT) *Category {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &Category{}
x.Init(buf, n+offset)
return x
}
func FinishCategoryBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsCategory(buf []byte, offset flatbuffers.UOffsetT) *Category {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &Category{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedCategoryBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *Category) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *Category) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *Category) Id() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *Category) MutateId(n int64) bool {
return rcv._tab.MutateInt64Slot(4, n)
}
func (rcv *Category) Name() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func CategoryStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func CategoryAddId(builder *flatbuffers.Builder, id int64) {
builder.PrependInt64Slot(0, id, 0)
}
func CategoryAddName(builder *flatbuffers.Builder, name flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(name), 0)
}
func CategoryEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}

View File

@ -0,0 +1,216 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package fbs
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type GoodItem struct {
_tab flatbuffers.Table
}
func GetRootAsGoodItem(buf []byte, offset flatbuffers.UOffsetT) *GoodItem {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &GoodItem{}
x.Init(buf, n+offset)
return x
}
func FinishGoodItemBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsGoodItem(buf []byte, offset flatbuffers.UOffsetT) *GoodItem {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &GoodItem{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedGoodItemBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *GoodItem) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *GoodItem) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *GoodItem) Sku() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *GoodItem) Photo() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *GoodItem) Name() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *GoodItem) Description() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *GoodItem) Category() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *GoodItem) Type() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *GoodItem) Producer() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *GoodItem) Pack() int16 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(18))
if o != 0 {
return rcv._tab.GetInt16(o + rcv._tab.Pos)
}
return 0
}
func (rcv *GoodItem) MutatePack(n int16) bool {
return rcv._tab.MutateInt16Slot(18, n)
}
func (rcv *GoodItem) Step() int16 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(20))
if o != 0 {
return rcv._tab.GetInt16(o + rcv._tab.Pos)
}
return 0
}
func (rcv *GoodItem) MutateStep(n int16) bool {
return rcv._tab.MutateInt16Slot(20, n)
}
func (rcv *GoodItem) Price() float32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(22))
if o != 0 {
return rcv._tab.GetFloat32(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *GoodItem) MutatePrice(n float32) bool {
return rcv._tab.MutateFloat32Slot(22, n)
}
func (rcv *GoodItem) Tariff() float32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(24))
if o != 0 {
return rcv._tab.GetFloat32(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *GoodItem) MutateTariff(n float32) bool {
return rcv._tab.MutateFloat32Slot(24, n)
}
func (rcv *GoodItem) Cart() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(26))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *GoodItem) MutateCart(n int64) bool {
return rcv._tab.MutateInt64Slot(26, n)
}
func (rcv *GoodItem) Stock() int16 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(28))
if o != 0 {
return rcv._tab.GetInt16(o + rcv._tab.Pos)
}
return 0
}
func (rcv *GoodItem) MutateStock(n int16) bool {
return rcv._tab.MutateInt16Slot(28, n)
}
func GoodItemStart(builder *flatbuffers.Builder) {
builder.StartObject(13)
}
func GoodItemAddSku(builder *flatbuffers.Builder, sku flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(sku), 0)
}
func GoodItemAddPhoto(builder *flatbuffers.Builder, photo flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(photo), 0)
}
func GoodItemAddName(builder *flatbuffers.Builder, name flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(name), 0)
}
func GoodItemAddDescription(builder *flatbuffers.Builder, description flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(description), 0)
}
func GoodItemAddCategory(builder *flatbuffers.Builder, category flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(category), 0)
}
func GoodItemAddType(builder *flatbuffers.Builder, type_ flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(type_), 0)
}
func GoodItemAddProducer(builder *flatbuffers.Builder, producer flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(producer), 0)
}
func GoodItemAddPack(builder *flatbuffers.Builder, pack int16) {
builder.PrependInt16Slot(7, pack, 0)
}
func GoodItemAddStep(builder *flatbuffers.Builder, step int16) {
builder.PrependInt16Slot(8, step, 0)
}
func GoodItemAddPrice(builder *flatbuffers.Builder, price float32) {
builder.PrependFloat32Slot(9, price, 0.0)
}
func GoodItemAddTariff(builder *flatbuffers.Builder, tariff float32) {
builder.PrependFloat32Slot(10, tariff, 0.0)
}
func GoodItemAddCart(builder *flatbuffers.Builder, cart int64) {
builder.PrependInt64Slot(11, cart, 0)
}
func GoodItemAddStock(builder *flatbuffers.Builder, stock int16) {
builder.PrependInt16Slot(12, stock, 0)
}
func GoodItemEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}

View File

@ -0,0 +1,75 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package fbs
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type GoodItems struct {
_tab flatbuffers.Table
}
func GetRootAsGoodItems(buf []byte, offset flatbuffers.UOffsetT) *GoodItems {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &GoodItems{}
x.Init(buf, n+offset)
return x
}
func FinishGoodItemsBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsGoodItems(buf []byte, offset flatbuffers.UOffsetT) *GoodItems {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &GoodItems{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedGoodItemsBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *GoodItems) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *GoodItems) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *GoodItems) Items(obj *GoodItem, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *GoodItems) ItemsLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func GoodItemsStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func GoodItemsAddItems(builder *flatbuffers.Builder, items flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(items), 0)
}
func GoodItemsStartItemsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func GoodItemsEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}

View File

@ -0,0 +1,140 @@
package fbs
import (
"sync"
"git.loyso.art/frx/eway/internal/entity"
flatbuffers "github.com/google/flatbuffers/go"
)
var builderPool = sync.Pool{
New: func() any {
builder := flatbuffers.NewBuilder(1024)
return builder
},
}
func getBuilder() *flatbuffers.Builder {
builder := builderPool.Get().(*flatbuffers.Builder)
return builder
}
func putBuilder(builder *flatbuffers.Builder) {
builder.Reset()
builderPool.Put(builder)
}
func MakeDomainGoodItems(in ...entity.GoodsItem) []byte {
builder := getBuilder()
defer putBuilder(builder)
offsets := make([]flatbuffers.UOffsetT, 0, len(in))
for _, item := range in {
inFB := makeDomainGoodItem(builder, item)
offsets = append(offsets, inFB)
}
GoodItemsStartItemsVector(builder, len(offsets))
for i := len(offsets) - 1; i >= 0; i-- {
builder.PrependUOffsetT(offsets[i])
}
goodItemsVec := builder.EndVector(len(offsets))
GoodItemsStart(builder)
GoodItemsAddItems(builder, goodItemsVec)
out := GoodItemsEnd(builder)
builder.Finish(out)
return builder.FinishedBytes()
}
func MakeDomainGoodItemFinished(in entity.GoodsItem) []byte {
builder := getBuilder()
defer putBuilder(builder)
item := makeDomainGoodItem(builder, in)
builder.Finish(item)
return builder.FinishedBytes()
}
func makeDomainGoodItem(builder *flatbuffers.Builder, in entity.GoodsItem) flatbuffers.UOffsetT {
sku := builder.CreateString(in.Articul)
photo := builder.CreateString(in.Photo)
name := builder.CreateString(in.Name)
desc := builder.CreateString(in.Description)
var cat flatbuffers.UOffsetT
if in.Category != "" {
cat = builder.CreateString(in.Category)
}
t := builder.CreateString(in.Type)
producer := builder.CreateString(in.Producer)
GoodItemStart(builder)
GoodItemAddSku(builder, sku)
GoodItemAddPhoto(builder, photo)
GoodItemAddName(builder, name)
GoodItemAddDescription(builder, desc)
if in.Category != "" {
GoodItemAddCategory(builder, cat)
}
GoodItemAddType(builder, t)
GoodItemAddProducer(builder, producer)
GoodItemAddPack(builder, int16(in.Pack))
GoodItemAddStep(builder, int16(in.Step))
GoodItemAddPrice(builder, float32(in.Price))
GoodItemAddTariff(builder, float32(in.TariffPrice))
GoodItemAddCart(builder, int64(in.Cart))
GoodItemAddStock(builder, int16(in.Stock))
return GoodItemEnd(builder)
}
func ParseGoodsItem(data []byte) (item entity.GoodsItem) {
itemFBS := GetRootAsGoodItem(data, 0)
item.Articul = string(itemFBS.Sku())
item.Photo = string(itemFBS.Photo())
item.Name = string(itemFBS.Name())
item.Description = string(itemFBS.Description())
if value := itemFBS.Category(); value != nil {
item.Category = string(value)
}
item.Type = string(itemFBS.Type())
item.Producer = string(itemFBS.Producer())
item.Pack = int(itemFBS.Pack())
item.Step = int(itemFBS.Step())
item.Price = float64(itemFBS.Price())
item.TariffPrice = float64(itemFBS.Tariff())
item.Cart = itemFBS.Cart()
item.Stock = int(itemFBS.Stock())
return item
}
func ParseCategory(data []byte) (category entity.Category) {
categoryFBS := GetRootAsCategory(data, 0)
category.ID = categoryFBS.Id()
category.Name = string(categoryFBS.Name())
return category
}
func MakeCategoryFinished(category entity.Category) []byte {
builder := getBuilder()
defer putBuilder(builder)
offset := makeCategory(builder, category)
builder.Finish(offset)
return builder.FinishedBytes()
}
func makeCategory(builder *flatbuffers.Builder, category entity.Category) flatbuffers.UOffsetT {
name := builder.CreateString(category.Name)
CategoryStart(builder)
CategoryAddId(builder, category.ID)
CategoryAddName(builder, name)
return CategoryEnd(builder)
}

View File

@ -0,0 +1,6 @@
package entity
type Category struct {
ID int64
Name string
}

11
internal/entity/error.go Normal file
View File

@ -0,0 +1,11 @@
package entity
type SimpleError string
func (err SimpleError) Error() string {
return string(err)
}
const (
ErrNotFound SimpleError = "not found"
)

122
internal/entity/gooditem.go Normal file
View File

@ -0,0 +1,122 @@
package entity
import (
"fmt"
"strconv"
"strings"
"unicode"
)
type GoodsItem struct {
Articul string `json:"sku"`
Photo string `json:"photo"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Type string `json:"type"`
Producer string `json:"producer"`
Pack int `json:"pack"`
Step int `json:"step"`
Price float64 `json:"price"`
TariffPrice float64 `json:"tariff_price"`
Cart int64 `json:"cart"`
Stock int `json:"stock"`
}
type GoodsItemRaw struct {
SKU string // or articul
Photo string
Certificate string
Configurator []string
Name []string // first element is name, second is description
Category string
Type string
Producer string
Storage string // ?
Pack string // like "20 шт."
Step string
Price string // float actually
TariffPrice string // float
SpecialOffers []any
Cart float64
Other string
}
type MappedGoodsRemnants map[int]GoodsRemnant
type GoodsRemnant [4]int32
func ExtractProductIDs(items []GoodsItem) (out []int) {
out = make([]int, 0, len(items))
for _, item := range items {
out = append(out, int(item.Cart))
}
return out
}
func MakeGoodsItem(
gi GoodsItemRaw,
remnants MappedGoodsRemnants,
) (out GoodsItem, err error) {
var name, desc string
var pack, step int
var price, tariffPrice float64
if len(gi.Name) >= 2 {
name = gi.Name[0]
desc = gi.Name[1]
}
fixSpace := func(in string) string {
return strings.ReplaceAll(in, " ", "")
}
price, err = strconv.ParseFloat(fixSpace(gi.Price), 64)
if err != nil {
return out, fmt.Errorf("parsing price: %w", err)
}
tariffPrice, err = strconv.ParseFloat(fixSpace(gi.TariffPrice), 64)
if err != nil {
return out, fmt.Errorf("parsing tariff_price: %w", err)
}
getDigits := func(in string) string {
var sb strings.Builder
sb.Grow(len(in))
for _, c := range in {
if unicode.IsDigit(c) {
sb.WriteRune(c)
continue
}
break
}
return sb.String()
}
const countTemplate = "%d"
_, err = fmt.Sscanf(getDigits(gi.Pack), countTemplate, &pack)
if err != nil {
return out, fmt.Errorf("getting pack count (%s): %w", gi.Pack, err)
}
_, err = fmt.Sscanf(getDigits(gi.Step), countTemplate, &step)
if err != nil {
return out, fmt.Errorf("getting step count (%s): %w", gi.Step, err)
}
return GoodsItem{
Articul: gi.SKU,
Photo: gi.Photo,
Name: name,
Description: desc,
Category: gi.Category,
Type: gi.Type,
Producer: gi.Producer,
Pack: pack,
Step: step,
Price: price,
TariffPrice: tariffPrice,
Cart: int64(gi.Cart),
Stock: int(remnants[int(gi.Cart)][0]),
}, nil
}

View File

@ -0,0 +1,23 @@
package entity
import "context"
type GoodsItemRepository interface {
ListIter(context.Context, int) (<-chan GoodsItem, error)
List(context.Context) ([]GoodsItem, error)
Get(context.Context, string) (GoodsItem, error)
GetByCart(context.Context, int64) (GoodsItem, error)
UpsertMany(context.Context, ...GoodsItem) ([]GoodsItem, error)
}
type CategoryRepository interface {
List(context.Context) ([]Category, error)
Get(context.Context, int64) (Category, error)
Create(ctx context.Context, name string) (Category, error)
}
type Mapper interface {
CategoryNameToID(context.Context, string) (int64, error)
}

View File

@ -0,0 +1,228 @@
package eway
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"strings"
"git.loyso.art/frx/eway/internal/entity"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog"
)
type client struct {
http *resty.Client
log zerolog.Logger
}
func NewClientWithSession(sessionid, sessionuser string, log zerolog.Logger) client {
cookies := []*http.Cookie{
{
Name: "session_id",
Value: sessionid,
Domain: "eway.elevel.ru",
HttpOnly: true,
},
{
Name: "session_user",
Value: sessionuser,
Domain: "eway.elevel.ru",
HttpOnly: true,
},
{
Name: "contract",
Value: "6101",
Domain: "eway.elevel.ru",
HttpOnly: true,
},
}
httpclient := resty.New().
SetDebug(false).
SetCookies(cookies).
SetBaseURL("https://eway.elevel.ru/api")
return client{
http: httpclient,
log: log.With().Str("client", "eway").Logger(),
}
}
type getGoodsNewOrder struct {
Column int
Dir string
}
type GetGoodsNewParams struct {
Draw int
Order getGoodsNewOrder
Start int
// 100 is max
Length int
SearchInStocks bool
RemmantsAtleast int
}
type getGoodsNewResponse struct {
Draw string `json:"draw"`
RecordsFiltered int `json:"recordsFiltered"`
RecordsTotal int `json:"recordsTotal"`
Data [][]any `json:"data"`
Replacement bool `json:"replacement"`
}
type goodRemnant [4]int
func parseGoodItem(items []any) (out entity.GoodsItemRaw) {
valueOf := reflect.ValueOf(&out).Elem()
typeOf := valueOf.Type()
numField := valueOf.NumField()
for i := 0; i < numField; i++ {
field := valueOf.Field(i)
fieldType := typeOf.Field(i)
if fieldType.Type.Kind() == reflect.Slice &&
field.Type().Elem().Kind() != reflect.String {
continue
}
itemValue := reflect.ValueOf(items[i])
if items[i] == nil ||
(itemValue.CanAddr() && itemValue.IsNil()) ||
itemValue.IsZero() {
continue
}
if field.Type().Kind() != itemValue.Type().Kind() {
continue
}
// Dirty hack that accepts only strings.
if field.Type().Kind() == reflect.Slice {
values := items[i].([]any)
elemSlice := reflect.MakeSlice(typeOf.Field(i).Type, 0, field.Len())
for _, value := range values {
valueStr, ok := value.(string)
if ok {
elemSlice = reflect.Append(elemSlice, reflect.ValueOf(valueStr))
}
}
field.Set(elemSlice)
continue
}
field.Set(itemValue)
}
return out
}
func mapResponseByOrder(response getGoodsNewResponse) (items []entity.GoodsItemRaw) {
for _, columns := range response.Data {
gi := parseGoodItem(columns)
items = append(items, gi)
}
return items
}
func (c client) GetGoodsRemnants(
ctx context.Context,
productIDs []int,
) (out entity.MappedGoodsRemnants, err error) {
if len(productIDs) == 0 {
return nil, nil
}
productsStr := make([]string, 0, len(productIDs))
for _, sku := range productIDs {
productsStr = append(productsStr, strconv.Itoa(sku))
}
resp, err := c.http.R().
SetFormData(map[string]string{
"products": strings.Join(productsStr, ","),
}).
SetDoNotParseResponse(true).
Post("/goods_remnants")
if err != nil {
return nil, fmt.Errorf("getting goods new: %w", err)
}
defer func() {
err = resp.RawBody().Close()
if err != nil {
c.log.Error().Err(err).Msg("unable to close body")
}
}()
if resp.IsError() {
return nil, errors.New("request was not successful")
}
data, err := io.ReadAll(resp.RawBody())
if err != nil {
return nil, fmt.Errorf("reading raw body: %w", err)
}
c.log.Debug().RawJSON("response", data).Msg("body prepared")
out = make(entity.MappedGoodsRemnants, len(productIDs))
err = json.NewDecoder(bytes.NewReader(data)).Decode(&out)
if err != nil {
return nil, fmt.Errorf("decoding body: %w", err)
}
return out, nil
}
func (c client) GetGoodsNew(
ctx context.Context,
params GetGoodsNewParams,
) (items []entity.GoodsItemRaw, total int, err error) {
var response getGoodsNewResponse
resp, err := c.http.R().
SetFormData(map[string]string{
"draw": strconv.Itoa(params.Draw),
"start": strconv.Itoa(params.Start),
"length": strconv.Itoa(params.Length),
"order[0][column]": "14",
"order[0][dir]": "desc",
"search[value]": "",
"search[regex]": "false",
"search_in_stocks": "on",
"remnants_atleast": "5",
}).
SetQueryParam("category_id", "0").
SetQueryParam("own", "26476"). // user id?
SetDoNotParseResponse(true).
Post("/goods_new")
if err != nil {
return nil, -1, fmt.Errorf("getting goods new: %w", err)
}
defer func() {
err = resp.RawBody().Close()
if err != nil {
c.log.Error().Err(err).Msg("unable to close body")
}
}()
if resp.IsError() {
return nil, -1, errors.New("request was not successful")
}
err = json.NewDecoder(resp.RawBody()).Decode(&response)
if err != nil {
return nil, -1, fmt.Errorf("decoding body: %w", err)
}
return mapResponseByOrder(response), response.RecordsTotal, nil
}

View File

@ -0,0 +1,40 @@
package inmemory
import (
"context"
"fmt"
"git.loyso.art/frx/eway/internal/entity"
"git.loyso.art/frx/eway/internal/storage"
)
type mapper struct {
categoryIDByName map[string]int64
base storage.Repository
}
func NewMapper(ctx context.Context, r storage.Repository) (*mapper, error) {
categories, err := r.Category().List(ctx)
if err != nil {
return nil, fmt.Errorf("listing categories: %w", err)
}
catsByName := make(map[string]int64, len(categories))
for _, category := range categories {
catsByName[category.Name] = category.ID
}
return &mapper{
categoryIDByName: catsByName,
base: r,
}, nil
}
func (m *mapper) CategoryNameToID(ctx context.Context, name string) (int64, error) {
out, ok := m.categoryIDByName[name]
if !ok {
return 0, entity.ErrNotFound
}
return out, nil
}

View File

@ -0,0 +1,8 @@
package storage
import "git.loyso.art/frx/eway/internal/entity"
type Repository interface {
Category() entity.CategoryRepository
GoodsItem() entity.GoodsItemRepository
}