commit 5b5dc2165c7b81069be68c41a1a3ebdec004a7a4 Author: Gitea Date: Wed Jan 24 16:12:16 2024 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3714b60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.json +*.zst +badger/ diff --git a/assets/category.fbs b/assets/category.fbs new file mode 100644 index 0000000..0a0aa10 --- /dev/null +++ b/assets/category.fbs @@ -0,0 +1,9 @@ +namespace internal.encoding.fbs; + +table Category { + id:long; + name:string; +} + +root_type Category; + diff --git a/assets/gooditem.fbs b/assets/gooditem.fbs new file mode 100644 index 0000000..ab29fec --- /dev/null +++ b/assets/gooditem.fbs @@ -0,0 +1,23 @@ +namespace internal.encoding.fbs; + +table GoodItem { + sku:string; + photo:string; + name:string; + description:string; + category:string; + type:string; + producer:string; + pack:short; + step:short; + price:float; + tariff:float; + cart:long; + stock:short; +} + +table GoodItems { + items:[GoodItem]; +} + +root_type GoodItems; diff --git a/cmd/converter/main.go b/cmd/converter/main.go new file mode 100644 index 0000000..06ca5b3 --- /dev/null +++ b/cmd/converter/main.go @@ -0,0 +1,444 @@ +package main + +import ( + "bufio" + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/signal" + "sync" + "sync/atomic" + "time" + + "git.loyso.art/frx/eway/internal/config" + "git.loyso.art/frx/eway/internal/entity" + "git.loyso.art/frx/eway/internal/storage" + xbadger "git.loyso.art/frx/eway/internal/storage/badger" + + badger "github.com/dgraph-io/badger/v4" + "github.com/dgraph-io/badger/v4/pb" + "github.com/rodaine/table" + "github.com/rs/zerolog" + "github.com/urfave/cli" +) + +type appSettings struct { + Badger config.Badger +} + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer func() { + cancel() + }() + + var corewg sync.WaitGroup + err := runcli(ctx, &corewg) + + if err != nil { + fmt.Fprintf(os.Stderr, "unable to handle app: %v", err) + + corewg.Wait() + os.Exit(1) + } + + corewg.Wait() + os.Exit(0) +} + +func runcli(ctx context.Context, wg *sync.WaitGroup) (err error) { + tsSet := func(wr *zerolog.ConsoleWriter) { + wr.TimeFormat = time.RFC3339 + } + log := zerolog.New(zerolog.NewConsoleWriter(tsSet)).Level(zerolog.DebugLevel).With().Timestamp().Str("app", "converter").Logger() + + defer func() { + log.Info().Err(err).Msg("app finished") + }() + + ctx = log.WithContext(ctx) + + log.Info().Msg("making badger") + + db, err := xbadger.Open(ctx, "badger/", log) + if err != nil { + return fmt.Errorf("opening badger: %w", err) + } + + go func() { + var item atomic.Uint64 + err = db.Subscribe( + ctx, + func(kvlist *badger.KVList) error { + kvs := kvlist.GetKv() + for _, kv := range kvs { + count := item.Add(1) + log.Debug().Bytes("key", kv.GetKey()).Uint64("count", count).Msg("inspecting") + } + return nil + }, + []pb.Match{ + { + Prefix: []byte("!!category!!"), + }, + { + Prefix: []byte("!!goodsitem!!"), + }, + }, + ) + log.Err(err).Msg("subscribing") + + }() + + maxBatch := db.MaxBatchCount() + log.Info().Int("max_batch", int(maxBatch)).Msg("max batch settings") + client, err := xbadger.NewClient(db) + if err != nil { + return fmt.Errorf("making new client: %w", err) + } + + wg.Add(1) + defer func() { + defer wg.Done() + + println("closing client") + errClose := client.Close() + if errClose != nil { + log.Warn().Err(errClose).Msg("unable to close client") + } + + println("flushing db") + errSync := db.Sync() + if errSync != nil { + log.Warn().Err(errSync).Msg("unable to sync db") + } + + // time.Sleep(time.Second * 5) + + println("closing db") + errClose = db.Close() + if errClose != nil { + log.Warn().Err(errClose).Msg("unable to close db") + } + }() + + app := setupCLI(ctx, client, maxBatch) + + return app.Run(os.Args) +} + +func setupCLI(ctx context.Context, r storage.Repository, maxBatch int64) *cli.App { + app := cli.NewApp() + + app.Commands = cli.Commands{ + newImportCmd(ctx, r, maxBatch), + newViewCmd(ctx, r), + } + + return app +} + +func newImportCmd(ctx context.Context, r storage.Repository, maxBatch int64) cli.Command { + return cli.Command{ + Name: "import", + Usage: "category for importing data from sources", + Flags: []cli.Flag{ + // &cli.StringFlag{ + // Name: "config", + // Usage: "path to config", + // Value: "config.json", + // TakesFile: true, + // }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "set logger to debug mode", + }, + }, + Before: func(c *cli.Context) error { + if c.Bool("verbose") { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } + return nil + }, + Subcommands: cli.Commands{ + newImportFromFileCmd(ctx, r, maxBatch), + }, + } +} + +func newViewCmd(ctx context.Context, r storage.Repository) cli.Command { + return cli.Command{ + Name: "view", + Usage: "Set of commands to view the data inside db", + Subcommands: []cli.Command{ + newViewCategoriesCmd(ctx, r), + newViewItemsCmd(ctx, r), + }, + } +} + +func newViewCategoriesCmd(ctx context.Context, r storage.Repository) cli.Command { + return cli.Command{ + Name: "categories", + Usage: "Set of commands to work with categories", + Subcommands: []cli.Command{ + newViewCategoriesListCmd(ctx, r), + }, + } +} + +func newViewItemsCmd(ctx context.Context, r storage.Repository) cli.Command { + return cli.Command{ + Name: "items", + Usage: "Set of command to work with items", + Subcommands: cli.Commands{ + newViewItemsCountCmd(ctx, r), + }, + } +} + +func newViewItemsCountCmd(ctx context.Context, r storage.Repository) cli.Command { + return cli.Command{ + Name: "count", + Usage: "iterates over collection and counts number of items", + Action: viewItemsCount(ctx, r), + } +} + +func newViewCategoriesListCmd(ctx context.Context, r storage.Repository) cli.Command { + return cli.Command{ + Name: "list", + Usage: "lists stored categories stored in database", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "limit", + Usage: "limits output to selected items", + Value: 20, + }, + &cli.IntFlag{ + Name: "page", + Usage: "in case of limit, selects page", + Value: 0, + }, + &cli.BoolFlag{ + Name: "with-total", + Usage: "prints total count of categories", + }, + }, + Action: viewCategoriesListAction(ctx, r), + } +} + +func viewItemsCount(ctx context.Context, r storage.Repository) cli.ActionFunc { + f := func(c *cli.Context) error { + // itemChan, err := r.GoodsItem().ListIter(ctx, 10) + // if err != nil { + // return fmt.Errorf("getting list iter: %w", err) + // } + // + var count int + // for range itemChan { + // count++ + // } + // + items, err := r.GoodsItem().List(ctx) + if err != nil { + return fmt.Errorf("getting list: %w", err) + } + + zerolog.Ctx(ctx).Info().Int("count", count).Int("list_count", len(items)).Msg("read all items") + return nil + } + + return wrapActionFunc(ctx, f) +} + +func viewCategoriesListAction(ctx context.Context, r storage.Repository) cli.ActionFunc { + f := func(c *cli.Context) error { + categories, err := r.Category().List(ctx) + if err != nil { + return fmt.Errorf("listing categories: %w", err) + } + + limit := c.Int("limit") + page := c.Int("page") + total := len(categories) + + if page == 0 { + page = 1 + } + + if limit > 0 { + offset := (page - 1) * limit + if offset > len(categories) { + offset = len(categories) - 1 + } + + limit = offset + limit + if limit > len(categories) { + limit = len(categories) + } + + categories = categories[offset:limit] + } + + tbl := table.New("ID", "Name") + for _, category := range categories { + if category.ID == 0 && category.Name == "" { + continue + } + tbl.AddRow(category.ID, category.Name) + } + + tbl.Print() + + if c.Bool("with-total") { + zerolog.Ctx(ctx).Info().Int("count", total).Msg("total categories stats") + } + + return nil + } + + return wrapActionFunc(ctx, f) +} + +func newImportFromFileCmd(ctx context.Context, r storage.Repository, maxBatch int64) cli.Command { + return cli.Command{ + Name: "fromfile", + Usage: "imports from file into db", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "src", + Value: "", + Usage: "source of the data.", + Required: true, + }, + }, + Action: handleConvert(ctx, r, maxBatch), + } +} + +func handleConvert(ctx context.Context, r storage.Repository, maxBatch int64) cli.ActionFunc { + f := func(c *cli.Context) error { + filesrc := c.String("src") + log := zerolog.Ctx(ctx) + + log.Debug().Str("filepath", filesrc).Msg("importing data from file") + + productsFile, err := os.Open(filesrc) + if err != nil { + return fmt.Errorf("opening file: %w", err) + } + + defer func() { + errClose := productsFile.Close() + if errClose != nil { + log.Warn().Err(errClose).Msg("unable to close file") + } + }() + + var ( + goodsItem entity.GoodsItem + // goodsItems []entity.GoodsItem + failedToInsert int + ) + + seenCategories := make(map[string]struct{}) + categories, err := r.Category().List(ctx) + if err != nil { + return fmt.Errorf("listing categories: %w", err) + } + + for _, category := range categories { + seenCategories[category.Name] = struct{}{} + } + + bfile := bufio.NewReader(productsFile) + for { + line, _, err := bfile.ReadLine() + if err != nil { + if errors.Is(err, io.EOF) { + break + + } + return fmt.Errorf("reading line: %w", err) + } + + err = json.Unmarshal(line, &goodsItem) + if err != nil { + // log.Warn().Err(err).Str("line", string(line)).Msg("unable to unmarshal line into item") + failedToInsert++ + continue + } + + _, err = r.GoodsItem().UpsertMany(ctx, goodsItem) + if err != nil { + return fmt.Errorf("unable to upsert new item: %w", err) + } + + // goodsItems = append(goodsItems, goodsItem) + + if _, ok := seenCategories[goodsItem.Type]; ok { + continue + } + + _, err = r.Category().Create(ctx, goodsItem.Type) + if err != nil { + return fmt.Errorf("unable to create new category: %w", err) + } + log.Debug().Any("category", goodsItem.Type).Msg("inserted new category") + seenCategories[goodsItem.Type] = struct{}{} + } + + // log.Debug().Int("count", len(goodsItems)).Int("failed", failedToInsert).Msg("preparing to upload") + // + // start := time.Now() + // batchSize := int(maxBatch) + // for i := 0; i < len(goodsItems); i += batchSize { + // to := i + batchSize + // if to > len(goodsItems) { + // to = len(goodsItems) + // } + // + // _, err = r.GoodsItem().UpsertMany(ctx, goodsItems[i:to]...) + // if err != nil { + // return fmt.Errorf("upserting items: %w", err) + // } + // log.Debug().Int("count", to-i).Msg("inserted batch") + // time.Sleep(time.Second) + // } + // log.Debug().Dur("elapsed", time.Since(start)).Msg("upload finished") + // + + time.Sleep(time.Second * 30) + + return nil + } + + time.Sleep(time.Second * 10) + + return wrapActionFunc(ctx, f) +} + +func wrapActionFunc(ctx context.Context, next cli.ActionFunc) cli.ActionFunc { + return func(c *cli.Context) error { + var data [3]byte + _, _ = rand.Read(data[:]) + reqid := hex.EncodeToString(data[:]) + + log := zerolog.Ctx(ctx).With().Str("reqid", reqid).Logger() + ctx = log.WithContext(ctx) + start := time.Now() + defer func() { + log.Info().Dur("elapsed", time.Since(start)).Msg("command completed") + }() + + log.Info().Msg("command execution started") + return next(c) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5b6e3b9 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module git.loyso.art/frx/eway + +go 1.21.4 + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/dgraph-io/badger/v4 v4.2.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/go-resty/resty/v2 v2.10.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect + github.com/karlseguin/ccache/v3 v3.0.5 // indirect + github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 // indirect + github.com/klauspost/compress v1.12.3 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/rodaine/table v1.1.1 // indirect + github.com/rs/zerolog v1.31.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli v1.22.14 // indirect + go.opencensus.io v0.22.5 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..81af957 --- /dev/null +++ b/go.sum @@ -0,0 +1,177 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= +github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/karlseguin/ccache/v3 v3.0.5 h1:hFX25+fxzNjsRlREYsoGNa2LoVEw5mPF8wkWq/UnevQ= +github.com/karlseguin/ccache/v3 v3.0.5/go.mod h1:qxC372+Qn+IBj8Pe3KvGjHPj0sWwEF7AeZVhsNPZ6uY= +github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 h1:M8exrBzuhWcU6aoHJlHWPe4qFjVKzkMGRal78f5jRRU= +github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23/go.mod h1:kBSna6b0/RzsOcOZf515vAXwSsXYusl2U7SA0XP09yI= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rodaine/table v1.1.1 h1:zBliy3b4Oj6JRmncse2Z85WmoQvDrXOYuy0JXCt8Qz8= +github.com/rodaine/table v1.1.1/go.mod h1:iqTRptjn+EVcrVBYtNMlJ2wrJZa3MpULUmcXFpfcziA= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= +github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/config/badger.go b/internal/config/badger.go new file mode 100644 index 0000000..586c2da --- /dev/null +++ b/internal/config/badger.go @@ -0,0 +1,5 @@ +package config + +type Badger struct { + Path string `json:"path"` +} diff --git a/internal/core/base.go b/internal/core/base.go new file mode 100644 index 0000000..a4d081f --- /dev/null +++ b/internal/core/base.go @@ -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 +} diff --git a/internal/core/creategoodsitem.go b/internal/core/creategoodsitem.go new file mode 100644 index 0000000..290a352 --- /dev/null +++ b/internal/core/creategoodsitem.go @@ -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 +} diff --git a/internal/core/env.go b/internal/core/env.go new file mode 100644 index 0000000..af2b9b4 --- /dev/null +++ b/internal/core/env.go @@ -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 +} diff --git a/internal/core/getcategory.go b/internal/core/getcategory.go new file mode 100644 index 0000000..782ade1 --- /dev/null +++ b/internal/core/getcategory.go @@ -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 +} diff --git a/internal/core/listcategories.go b/internal/core/listcategories.go new file mode 100644 index 0000000..7263f5a --- /dev/null +++ b/internal/core/listcategories.go @@ -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 +} diff --git a/internal/encoding/fbs/Category.go b/internal/encoding/fbs/Category.go new file mode 100644 index 0000000..f0d0ce9 --- /dev/null +++ b/internal/encoding/fbs/Category.go @@ -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() +} diff --git a/internal/encoding/fbs/GoodItem.go b/internal/encoding/fbs/GoodItem.go new file mode 100644 index 0000000..e7ee1f9 --- /dev/null +++ b/internal/encoding/fbs/GoodItem.go @@ -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() +} diff --git a/internal/encoding/fbs/GoodsItem.go b/internal/encoding/fbs/GoodsItem.go new file mode 100644 index 0000000..db6c581 --- /dev/null +++ b/internal/encoding/fbs/GoodsItem.go @@ -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() +} diff --git a/internal/encoding/fbs/helpers.go b/internal/encoding/fbs/helpers.go new file mode 100644 index 0000000..94bc05f --- /dev/null +++ b/internal/encoding/fbs/helpers.go @@ -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) +} diff --git a/internal/entity/category.go b/internal/entity/category.go new file mode 100644 index 0000000..38b54e8 --- /dev/null +++ b/internal/entity/category.go @@ -0,0 +1,6 @@ +package entity + +type Category struct { + ID int64 + Name string +} diff --git a/internal/entity/error.go b/internal/entity/error.go new file mode 100644 index 0000000..186ca55 --- /dev/null +++ b/internal/entity/error.go @@ -0,0 +1,11 @@ +package entity + +type SimpleError string + +func (err SimpleError) Error() string { + return string(err) +} + +const ( + ErrNotFound SimpleError = "not found" +) diff --git a/internal/entity/gooditem.go b/internal/entity/gooditem.go new file mode 100644 index 0000000..5b26e18 --- /dev/null +++ b/internal/entity/gooditem.go @@ -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 +} diff --git a/internal/entity/repository.go b/internal/entity/repository.go new file mode 100644 index 0000000..1563617 --- /dev/null +++ b/internal/entity/repository.go @@ -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) +} diff --git a/internal/interconnect/eway/client.go b/internal/interconnect/eway/client.go new file mode 100644 index 0000000..ab217e2 --- /dev/null +++ b/internal/interconnect/eway/client.go @@ -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 +} diff --git a/internal/storage/inmemory/mapper.go b/internal/storage/inmemory/mapper.go new file mode 100644 index 0000000..0c17323 --- /dev/null +++ b/internal/storage/inmemory/mapper.go @@ -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 +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..1144e49 --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,8 @@ +package storage + +import "git.loyso.art/frx/eway/internal/entity" + +type Repository interface { + Category() entity.CategoryRepository + GoodsItem() entity.GoodsItemRepository +}