initial commit
This commit is contained in:
5
internal/config/badger.go
Normal file
5
internal/config/badger.go
Normal file
@ -0,0 +1,5 @@
|
||||
package config
|
||||
|
||||
type Badger struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
64
internal/core/base.go
Normal file
64
internal/core/base.go
Normal 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
|
||||
}
|
||||
45
internal/core/creategoodsitem.go
Normal file
45
internal/core/creategoodsitem.go
Normal 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
11
internal/core/env.go
Normal 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
|
||||
}
|
||||
49
internal/core/getcategory.go
Normal file
49
internal/core/getcategory.go
Normal 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
|
||||
}
|
||||
38
internal/core/listcategories.go
Normal file
38
internal/core/listcategories.go
Normal 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
|
||||
}
|
||||
75
internal/encoding/fbs/Category.go
Normal file
75
internal/encoding/fbs/Category.go
Normal 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()
|
||||
}
|
||||
216
internal/encoding/fbs/GoodItem.go
Normal file
216
internal/encoding/fbs/GoodItem.go
Normal 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()
|
||||
}
|
||||
75
internal/encoding/fbs/GoodsItem.go
Normal file
75
internal/encoding/fbs/GoodsItem.go
Normal 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()
|
||||
}
|
||||
140
internal/encoding/fbs/helpers.go
Normal file
140
internal/encoding/fbs/helpers.go
Normal 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)
|
||||
}
|
||||
6
internal/entity/category.go
Normal file
6
internal/entity/category.go
Normal file
@ -0,0 +1,6 @@
|
||||
package entity
|
||||
|
||||
type Category struct {
|
||||
ID int64
|
||||
Name string
|
||||
}
|
||||
11
internal/entity/error.go
Normal file
11
internal/entity/error.go
Normal 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
122
internal/entity/gooditem.go
Normal 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
|
||||
}
|
||||
23
internal/entity/repository.go
Normal file
23
internal/entity/repository.go
Normal 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)
|
||||
}
|
||||
228
internal/interconnect/eway/client.go
Normal file
228
internal/interconnect/eway/client.go
Normal 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
|
||||
}
|
||||
40
internal/storage/inmemory/mapper.go
Normal file
40
internal/storage/inmemory/mapper.go
Normal 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
|
||||
}
|
||||
8
internal/storage/storage.go
Normal file
8
internal/storage/storage.go
Normal file
@ -0,0 +1,8 @@
|
||||
package storage
|
||||
|
||||
import "git.loyso.art/frx/eway/internal/entity"
|
||||
|
||||
type Repository interface {
|
||||
Category() entity.CategoryRepository
|
||||
GoodsItem() entity.GoodsItemRepository
|
||||
}
|
||||
Reference in New Issue
Block a user