From 3c3b4e46708b32ff34b53a6dabee74f9fbb8bb14 Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Tue, 13 Feb 2024 21:19:39 +0300 Subject: [PATCH] minor rework and filter dimensions --- cmd/cli/commands/categories.go | 101 ++++ cmd/cli/commands/channeliter.go | 55 ++ cmd/cli/commands/dimensiondispatcher.go | 78 +++ cmd/cli/commands/exports.go | 259 ++++++++ cmd/cli/commands/helper.go | 55 ++ cmd/cli/commands/items.go | 373 ++++++++++++ cmd/cli/dimensiondispatcher.go | 29 +- cmd/cli/main.go | 771 +----------------------- internal/encoding/fbs/helpers.go | 6 +- internal/entity/dimension.go | 16 +- internal/export/itemsmarket.go | 1 + internal/interconnect/eway/client.go | 2 +- internal/matcher/radix_test.go | 3 + 13 files changed, 969 insertions(+), 780 deletions(-) create mode 100644 cmd/cli/commands/categories.go create mode 100644 cmd/cli/commands/channeliter.go create mode 100644 cmd/cli/commands/dimensiondispatcher.go create mode 100644 cmd/cli/commands/exports.go create mode 100644 cmd/cli/commands/helper.go create mode 100644 cmd/cli/commands/items.go diff --git a/cmd/cli/commands/categories.go b/cmd/cli/commands/categories.go new file mode 100644 index 0000000..49ac539 --- /dev/null +++ b/cmd/cli/commands/categories.go @@ -0,0 +1,101 @@ +package commands + +import ( + "context" + "fmt" + + "git.loyso.art/frx/eway/cmd/cli/components" + + "github.com/rodaine/table" + "github.com/rs/zerolog" + "github.com/urfave/cli/v3" +) + +func CategoriesCommandTree() *cli.Command { + var h categoriesHandlers + + return &cli.Command{ + Name: "categories", + Usage: "Interact with stored categories", + Commands: []*cli.Command{ + newCategoriesListCommand(h), + }, + } +} + +func newCategoriesListCommand(h categoriesHandlers) *cli.Command { + cmd := cli.Command{ + Name: "list", + Usage: "List categories, stories in db", + 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", + }, + }, + } + + return cmdWithAction(cmd, h.List) +} + +type categoriesHandlers struct{} + +func (categoriesHandlers) List(ctx context.Context, c *cli.Command) error { + r, err := components.GetRepository() + if err != nil { + return fmt.Errorf("getting repository: %w", err) + } + + categories, err := r.Category().List(ctx) + if err != nil { + return fmt.Errorf("listing categories: %w", err) + } + + limit := int(c.Int("limit")) + page := int(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 +} diff --git a/cmd/cli/commands/channeliter.go b/cmd/cli/commands/channeliter.go new file mode 100644 index 0000000..4ecb56e --- /dev/null +++ b/cmd/cli/commands/channeliter.go @@ -0,0 +1,55 @@ +package commands + +import ( + "context" + "errors" + + "git.loyso.art/frx/eway/internal/entity" +) + +type chanIter[T any] struct { + in <-chan T + err error + next T +} + +var errChannelClosed = errors.New("channel closed") + +func (i *chanIter[T]) Next() (ok bool) { + if i.err != nil { + return false + } + + i.next, ok = <-i.in + if !ok { + i.err = errChannelClosed + } + + return ok +} + +func (i *chanIter[T]) Get() T { + return i.next +} + +func (i *chanIter[T]) Err() error { + if errors.Is(i.err, errChannelClosed) { + return nil + } + + return i.err +} + +func (i *chanIter[T]) Close() { + for range i.in { + } +} + +func getItemsIter(ctx context.Context, r entity.GoodsItemRepository) *chanIter[entity.GoodsItem] { + in, err := r.ListIter(ctx, 3) + + return &chanIter[entity.GoodsItem]{ + in: in, + err: err, + } +} diff --git a/cmd/cli/commands/dimensiondispatcher.go b/cmd/cli/commands/dimensiondispatcher.go new file mode 100644 index 0000000..9fdfe14 --- /dev/null +++ b/cmd/cli/commands/dimensiondispatcher.go @@ -0,0 +1,78 @@ +package commands + +import ( + "context" + "strings" + + "git.loyso.art/frx/eway/internal/entity" + "git.loyso.art/frx/eway/internal/matcher" + "github.com/rs/zerolog" +) + +type dimensionDispatcher struct { + heigth matcher.Unit + width matcher.Unit + length matcher.Unit +} + +func (d dimensionDispatcher) isDimensionParam(value string) bool { + return d.heigth.Match(value) || d.width.Match(value) || d.length.Match(value) +} + +func (d dimensionDispatcher) dispatch(ctx context.Context, key, value string, in *entity.GoodsItemSize) (updated bool) { + if !d.isDimensionParam(key) { + return false + } + + log := zerolog.Ctx(ctx).With().Str("key", key).Str("value", value).Logger() + if strings.Contains(value, "/") { + dimensionValues := strings.Split(value, "/") + for _, dv := range dimensionValues { + updated = updated || d.dispatch(ctx, key, dv, in) + } + } else { + out, err := entity.ParseDimention(value, entity.DimensionLocalRU) + if err != nil { + log.Warn().Err(err).Msg("unable to parse key, skipping") + return false + } + + out = out.AdjustTo(entity.DimensionKindCentimeter) + + updated = true + switch { + case d.heigth.Match(key): + in.Height = out + case d.width.Match(key): + in.Width = out + case d.length.Match(key): + in.Length = out + default: + log.Error().Str("key", key).Msg("unable to find proper matcher") + updated = false + } + } + + return updated +} + +func makeDefaultDimensionDispatcher() dimensionDispatcher { + h := matcher.NewRadix(matcher.RadixCaseInsensitive()) + h.Register("Высота") + h.Register("Высота/*") + + w := matcher.NewRadix(matcher.RadixCaseInsensitive()) + w.Register("Ширина") + w.Register("Ширина/*") + + l := matcher.NewRadix(matcher.RadixCaseInsensitive()) + l.Register("Длина") + l.Register("Длина/*") + l.Register("Общ. длина") + + return dimensionDispatcher{ + heigth: h, + width: w, + length: l, + } +} diff --git a/cmd/cli/commands/exports.go b/cmd/cli/commands/exports.go new file mode 100644 index 0000000..6584d4f --- /dev/null +++ b/cmd/cli/commands/exports.go @@ -0,0 +1,259 @@ +package commands + +import ( + "context" + "encoding/xml" + "fmt" + "os" + "strconv" + "strings" + "time" + + "git.loyso.art/frx/eway/cmd/cli/components" + "git.loyso.art/frx/eway/internal/entity" + "git.loyso.art/frx/eway/internal/export" + + "github.com/rs/zerolog" + "github.com/urfave/cli/v3" +) + +func ExportCommandTree() *cli.Command { + var h exportHandlers + return &cli.Command{ + Name: "export", + Usage: "Provide actions to export data from the database", + Commands: []*cli.Command{ + newExportYMLCatalogCommand(h), + }, + } +} + +func newExportYMLCatalogCommand(h exportHandlers) *cli.Command { + cmd := cli.Command{ + Name: "yml-catalog", + Usage: "Export data as yml_catalog", + } + + return cmdWithAction(cmd, h.YMLCatalog) +} + +type exportHandlers struct{} + +func (h exportHandlers) YMLCatalog(ctx context.Context, cmd *cli.Command) error { + const defaultCurrency = "RUR" + + log := zerolog.Ctx(ctx) + + path := cmd.String("out") + limit := cmd.Int("limit") + pretty := cmd.Bool("pretty") + + r, err := components.GetRepository() + if err != nil { + return fmt.Errorf("getting repository: %w", err) + } + + log.Info(). + Str("path", path). + Int64("limit", limit). + Bool("pretty", pretty). + Msg("executing with parameters") + + categories, err := r.Category().List(ctx) + if err != nil { + return fmt.Errorf("listing categories: %w", err) + } + + shop := export.Shop{ + Currencies: []export.Currency{{ + ID: defaultCurrency, + Rate: 1, + }}, + Categories: make([]export.Category, 0, len(categories)), + } + + categoryByNameIdx := make(map[string]int64, len(categories)) + for _, category := range categories { + categoryByNameIdx[category.Name] = category.ID + shop.Categories = append(shop.Categories, export.Category{ + ID: category.ID, + Name: category.Name, + }) + } + + matcher := makeDefaultDimensionDispatcher() + + var skipped int + iter := getItemsIter(ctx, r.GoodsItem()) + for iter.Next() { + item := iter.Get() + sublog := log.With().Int64("cart", item.Cart).Logger() + if item.Sizes == (entity.GoodsItemSize{}) { + sublog.Warn().Str("sku", item.Articul).Int64("cart", item.Cart).Msg("skipping item because it does not have size") + skipped++ + + continue + } + + offer := h.goodsItemAsOffer(iter.Get(), categoryByNameIdx, matcher, sublog) + shop.Offers = append(shop.Offers, offer) + } + if err = iter.Err(); err != nil { + return fmt.Errorf("iterating over items: %w", err) + } + + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("creating file: %w", err) + } + defer func() { + errClose := f.Close() + if err == nil { + err = errClose + return + } + + log.Err(errClose).Msg("file closed or not") + }() + + if limit > 0 { + shop.Offers = shop.Offers[:limit] + } + + container := export.YmlContainer{ + YmlCatalog: export.YmlCatalog{ + Shop: shop, + Date: time.Now(), + }, + } + + _, err = f.Write([]byte(xml.Header)) + if err != nil { + return fmt.Errorf("writing header: %w", err) + } + _, err = f.Write([]byte("\n")) + enc := xml.NewEncoder(f) + if pretty { + enc.Indent("", " ") + } + + log.Info().Int("processed", len(shop.Offers)).Int("skipped", skipped).Msg("completed") + + return enc.Encode(container) +} + +func (h exportHandlers) goodsItemAsOffer(in entity.GoodsItem, categoryIDByName map[string]int64, d dimensionDispatcher, log zerolog.Logger) (out export.Offer) { + const defaultType = "vendor.model" + const defaultCurrency = "RUR" + const defaultAvailable = true + const quantityParamName = "Количество на складе «Москва»" + const basePictureURL = "https://eway.elevel.ru" + + imgurl := func(path string) string { + return basePictureURL + path + } + + categoryID := categoryIDByName[in.Type] + + pictureURLs := make([]string, 0, len(in.PhotoURLs)) + for _, url := range in.PhotoURLs { + pictureURLs = append(pictureURLs, imgurl(url)) + } + params := make([]export.Param, len(in.Parameters)) + for k, v := range in.Parameters { + if k == "" || v == "" { + continue + } + + if d.isDimensionParam(k) { + continue + } + + params = append(params, export.Param{ + Name: k, + Value: v, + }) + } + params = append(params, export.Param{ + Name: quantityParamName, + Value: strconv.Itoa(in.Stock), + }) + + dimensions := h.formatSizeAsDimensions(in.Sizes, log) + out = export.Offer{ + ID: in.Cart, + VendorCode: in.Articul, + Price: int(in.TariffPrice), + Model: in.Name, + Vendor: in.Producer, + TypePrefix: in.Name, + Description: in.Description, + Dimensions: dimensions, + + CategoryID: categoryID, + PictureURLs: pictureURLs, + Params: params, + + Type: defaultType, + CurrencyID: defaultCurrency, + Available: defaultAvailable, + ManufacturerWarrany: true, + } + + return out +} + +func (exportHandlers) formatSizeAsDimensions(size entity.GoodsItemSize, log zerolog.Logger) string { + const delimeter = "/" + makeFloat := func(d entity.Dimension) string { + value := size.Length.AdjustTo(entity.DimensionKindCentimeter).Value + value = float64(int(value*1000)) / 1000.0 + + return strconv.FormatFloat(value, 'f', 8, 64) + } + + knownSizes := make([]entity.Dimension, 0, 3) + + for _, d := range []entity.Dimension{ + size.Length, + size.Width, + size.Height, + } { + if d.IsZero() { + continue + } + + knownSizes = append(knownSizes, d) + } + + l := makeFloat(size.Length) + w := makeFloat(size.Width) + h := makeFloat(size.Height) + switch len(knownSizes) { + case 3: + // go on + case 2: + var side string + unknownDefaultSize := makeFloat(entity.NewCentimeterDimensionOrEmpty(30)) + switch { + case size.Length.IsZero(): + side = "length" + l = unknownDefaultSize + case size.Width.IsZero(): + side = "width" + w = unknownDefaultSize + case size.Height.IsZero(): + side = "height" + h = unknownDefaultSize + } + log.Warn().Str("size", side).Msg("setting to default value") + default: + return "" + } + + // Output should be the following format: + // length/width/height in centimeters + return strings.Join([]string{ + l, w, h, + }, delimeter) +} diff --git a/cmd/cli/commands/helper.go b/cmd/cli/commands/helper.go new file mode 100644 index 0000000..59fcc46 --- /dev/null +++ b/cmd/cli/commands/helper.go @@ -0,0 +1,55 @@ +package commands + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "time" + + "git.loyso.art/frx/eway/cmd/cli/components" + "git.loyso.art/frx/eway/internal/entity" + + "github.com/urfave/cli/v3" +) + +type empty entity.Empty + +type action func(ctx context.Context, c *cli.Command) error + +func decorateAction(a action) cli.ActionFunc { + return func(ctx context.Context, c *cli.Command) error { + var data [3]byte + _, _ = rand.Read(data[:]) + reqid := hex.EncodeToString(data[:]) + + log, err := components.GetLogger() + if err != nil { + return fmt.Errorf("getting logger: %w", err) + } + + log = log.With().Str("reqid", reqid).Logger() + rctx := log.WithContext(ctx) + + start := time.Now() + defer func() { + log.Info().Float64("elapsed", time.Since(start).Seconds()).Msg("command completed") + }() + + log.Info().Msg("command execution started") + return a(rctx, c) + } +} + +func cmdWithAction(cmd cli.Command, a action) *cli.Command { + if a == nil { + a = notImplementedAction + } + + cmd.Action = decorateAction(a) + return &cmd +} + +func notImplementedAction(_ context.Context, _ *cli.Command) error { + return entity.ErrNotImplemented +} diff --git a/cmd/cli/commands/items.go b/cmd/cli/commands/items.go new file mode 100644 index 0000000..618c438 --- /dev/null +++ b/cmd/cli/commands/items.go @@ -0,0 +1,373 @@ +package commands + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + + "git.loyso.art/frx/eway/cmd/cli/components" + "git.loyso.art/frx/eway/internal/entity" + "git.loyso.art/frx/eway/internal/matcher" + + "github.com/rs/zerolog" + "github.com/urfave/cli/v3" +) + +func ItemsCommandTree() *cli.Command { + var h itemsHandlers + return &cli.Command{ + Name: "items", + Usage: "Interact with items stored inside db", + Commands: []*cli.Command{ + newItemsGetCommand(h), + newItemsCountCommand(h), + newItemsUniqueParamsCommand(h), + newItemsAggregateParametersCommand(h), + newItemsFillSizesCommand(h), + }, + } +} + +func newItemsGetCommand(h itemsHandlers) *cli.Command { + cmd := cli.Command{ + Name: "get", + Usage: "gets goods item by its id", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + Usage: "id of the goods item. Either id or cart-id should be set", + }, + &cli.IntFlag{ + Name: "cart-id", + Usage: "cart-id of the item. Either cart-id or id should be set", + }, + }, + } + + return cmdWithAction(cmd, h.Get) +} + +func newItemsCountCommand(h itemsHandlers) *cli.Command { + cmd := cli.Command{ + Name: "count", + Usage: "iterates over collection and counts number of items", + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "param-key-match", + Usage: "filters by parameters with AND logic", + }, + }, + } + + return cmdWithAction(cmd, h.Count) +} + +func newItemsUniqueParamsCommand(h itemsHandlers) *cli.Command { + cmd := cli.Command{ + Name: "unique-params", + Usage: "Show all stored unique param values", + Description: "This command iterates over each item and collect keys of params in a dict and then" + + " print it to the output. It's useful to find all unique parameters", + } + + return cmdWithAction(cmd, h.UniqueParameters) +} + +func newItemsAggregateParametersCommand(h itemsHandlers) *cli.Command { + cmd := cli.Command{ + Name: "aggregate-parameters", + Usage: "Show all values of requested parameters", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "case-insensitive", + Usage: "Ignores cases of keys", + }, + &cli.StringSliceFlag{ + Name: "regex", + Usage: "Registers regex to match", + }, + &cli.BoolFlag{ + Name: "keys-only", + Usage: "prints only keys", + }, + }, + } + + return cmdWithAction(cmd, h.AggregateParameters) +} + +func newItemsFillSizesCommand(h itemsHandlers) *cli.Command { + cmd := cli.Command{ + Name: "fix-sizes", + Usage: "Iterates over params and sets sizes from parameters", + } + + return cmdWithAction(cmd, h.FillSizes) +} + +type itemsHandlers struct{} + +func (itemsHandlers) Get(ctx context.Context, cmd *cli.Command) error { + r, err := components.GetRepository() + if err != nil { + return fmt.Errorf("getting repository: %w", err) + } + + id := cmd.String("id") + cartID := cmd.Int("cart-id") + if id == "" && cartID == 0 { + return cli.Exit("oneof: id or cart-id should be set", 1) + } else if id != "" && cartID != 0 { + return cli.Exit("oneof: id or cart-id should be set", 1) + } + + var item entity.GoodsItem + if id != "" { + item, err = r.GoodsItem().Get(ctx, id) + } else { + item, err = r.GoodsItem().GetByCart(ctx, cartID) + } + if err != nil { + return fmt.Errorf("getting item: %w", err) + } + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + + err = enc.Encode(item) + if err != nil { + return fmt.Errorf("encoding item: %w", err) + } + + return nil +} + +func (itemsHandlers) Count(ctx context.Context, cmd *cli.Command) error { + r, err := components.GetRepository() + if err != nil { + return fmt.Errorf("getting repository: %w", err) + } + + filters := cmd.StringSlice("param-key-match") + m := matcher.NewRadix() + patternMapped := make(map[string]empty, len(filters)) + if len(filters) == 0 { + m.Register("*") + } else { + for _, f := range filters { + m.Register(f) + } + for _, pattern := range m.Patterns() { + patternMapped[pattern] = empty{} + } + } + + var count int + items, err := r.GoodsItem().List(ctx) + if err != nil { + return fmt.Errorf("getting items: %w", err) + } + for _, item := range items { + seenPatterns := map[string]empty{} + + for k := range item.Parameters { + pattern := m.MatchByPattern(k) + if pattern == "" { + continue + } + if _, ok := seenPatterns[pattern]; ok { + continue + } + seenPatterns[pattern] = empty{} + } + + if len(seenPatterns) == len(patternMapped) { + println("Item matched", item.Articul, item.Cart) + count++ + } + } + + println(count) + + return nil +} + +func (itemsHandlers) UniqueParameters(ctx context.Context, cmd *cli.Command) error { + repository, err := components.GetRepository() + if err != nil { + return fmt.Errorf("getting repository: %w", err) + } + + knownParams := map[string]empty{} + iter, err := repository.GoodsItem().ListIter(ctx, 1) + if err != nil { + return fmt.Errorf("getting list iter: %w", err) + } + for item := range iter { + for k := range item.Parameters { + knownParams[k] = empty{} + } + } + + bw := bufio.NewWriter(cmd.Writer) + for paramName := range knownParams { + _, err = bw.WriteString(paramName + "\n") + if err != nil { + return fmt.Errorf("unable to write: %w", err) + } + } + + return bw.Flush() +} + +func (itemsHandlers) AggregateParameters(ctx context.Context, cmd *cli.Command) error { + repository, err := components.GetRepository() + if err != nil { + return fmt.Errorf("getting repository: %w", err) + } + log, err := components.GetLogger() + if err != nil { + return fmt.Errorf("getting logger: %w", err) + } + + params := cmd.Args().Slice() + opts := make([]matcher.RadixOpt, 0, 1) + if cmd.Bool("case-insensitive") { + opts = append(opts, matcher.RadixCaseInsensitive()) + } + + m := matcher.NewRadix(opts...) + for _, param := range params { + log.Debug().Str("param", param).Msg("registering param") + m.Register(param) + } + for _, regexp := range cmd.StringSlice("regex") { + log.Debug().Str("regexp", regexp).Msg("registering regexp") + m.RegisterRegexp(regexp) + } + + requestedValues := make(map[string]map[string]empty, len(params)) + requestedValuesByPattern := make(map[string]map[string]empty, len(params)) + iter := getItemsIter(ctx, repository.GoodsItem()) + for iter.Next() { + item := iter.Get() + for k, v := range item.Parameters { + matchedPattern := m.MatchByPattern(k) + if matchedPattern == "" { + continue + } + + values, ok := requestedValues[k] + if !ok { + values = make(map[string]empty) + } + values[v] = empty{} + requestedValues[k] = values + + values, ok = requestedValuesByPattern[matchedPattern] + if !ok { + values = map[string]empty{} + } + values[v] = empty{} + requestedValuesByPattern[matchedPattern] = values + } + } + + bw := bufio.NewWriter(cmd.Writer) + _, _ = bw.WriteString("Matches:\n") + + if cmd.Bool("keys-only") { + for k := range requestedValues { + _, _ = bw.WriteString(k + "\n") + } + } else { + for k, v := range requestedValues { + _, _ = bw.WriteString(k + ": ") + values := make([]string, 0, len(v)) + for item := range v { + values = append(values, strconv.Quote(item)) + } + valuesStr := "[" + strings.Join(values, ",") + "]\n" + _, _ = bw.WriteString(valuesStr) + } + } + + _, _ = bw.WriteString("\nPatterns:\n") + for _, pattern := range m.Patterns() { + _, _ = bw.WriteString(pattern + "\n") + } + + return bw.Flush() +} + +func (itemsHandlers) FillSizes(ctx context.Context, cmd *cli.Command) error { + repository, err := components.GetRepository() + if err != nil { + return fmt.Errorf("getting repository: %w", err) + } + + dimensionDispatcher := makeDefaultDimensionDispatcher() + log := zerolog.Ctx(ctx) + + toUpdate := make([]entity.GoodsItem, 0, 20_000) + bus := getItemsIter(ctx, repository.GoodsItem()) + for bus.Next() { + item := bus.Get() + + var valueBeenUpdated bool + for key, value := range item.Parameters { + trimmedValue := strings.TrimSpace(value) + trimmedKey := strings.TrimSpace(key) + + if trimmedKey != key || trimmedValue != value { + log.Warn(). + Str("old_key", key). + Str("new_key", trimmedKey). + Str("old_value", value). + Str("new_value", trimmedValue). + Msg("found mismatch") + + delete(item.Parameters, key) + + key = trimmedKey + value = trimmedValue + + item.Parameters[key] = value + valueBeenUpdated = true + } + + updateValue := strings.HasSuffix(key, ":") + if updateValue { + key = strings.TrimSuffix(key, ":") + item.Parameters[key] = value + delete(item.Parameters, key+":") + valueBeenUpdated = true + } + + if dimensionDispatcher.dispatch(ctx, key, value, &item.Sizes) { + valueBeenUpdated = true + log.Debug().Str("key", key).Any("sizes", item.Sizes).Msg("been updated") + } + } + + if valueBeenUpdated { + toUpdate = append(toUpdate, item) + } + } + if bus.Err() != nil { + return fmt.Errorf("iterating: %w", bus.Err()) + } + + _, err = repository.GoodsItem().UpsertMany(ctx, toUpdate...) + if err != nil { + return fmt.Errorf("updating items: %w", err) + } + + log.Info().Int("count", len(toUpdate)).Msg("updated items") + + return nil +} diff --git a/cmd/cli/dimensiondispatcher.go b/cmd/cli/dimensiondispatcher.go index 2d33783..42fdb71 100644 --- a/cmd/cli/dimensiondispatcher.go +++ b/cmd/cli/dimensiondispatcher.go @@ -19,45 +19,52 @@ func (d dimensionDispatcher) isDimensionParam(value string) bool { return d.heigth.Match(value) || d.width.Match(value) || d.length.Match(value) } -func (d dimensionDispatcher) dispatch(ctx context.Context, value, key string, in *entity.GoodsItemSize) { - if !d.isDimensionParam(value) { - return +func (d dimensionDispatcher) dispatch(ctx context.Context, key, value string, in *entity.GoodsItemSize) (updated bool) { + if !d.isDimensionParam(key) { + return false } + log := zerolog.Ctx(ctx).With().Str("key", key).Str("value", value).Logger() if strings.Contains(value, "/") { dimensionValues := strings.Split(value, "/") for _, dv := range dimensionValues { - d.dispatch(ctx, dv, key, in) + updated = updated || d.dispatch(ctx, key, dv, in) } } else { - out, err := entity.ParseDimention(key, entity.DimensionLocalRU) + out, err := entity.ParseDimention(value, entity.DimensionLocalRU) if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("unable to parse key, skipping") - return + log.Warn().Err(err).Msg("unable to parse key, skipping") + return false } out = out.AdjustTo(entity.DimensionKindCentimeter) + updated = true switch { - case d.heigth.Match(value): + case d.heigth.Match(key): in.Height = out - case d.width.Match(value): + case d.width.Match(key): in.Width = out - case d.width.Match(value): + case d.length.Match(key): in.Length = out default: - zerolog.Ctx(ctx).Error().Str("key", key).Msg("unable to find proper matcher") + log.Error().Str("key", key).Msg("unable to find proper matcher") + updated = false } } + + return updated } func makeDefaultDimensionDispatcher() dimensionDispatcher { h := matcher.NewRadix(matcher.RadixCaseInsensitive()) h.Register("Высота") h.Register("Высота/*") + w := matcher.NewRadix(matcher.RadixCaseInsensitive()) w.Register("Ширина") w.Register("Ширина/*") + l := matcher.NewRadix(matcher.RadixCaseInsensitive()) l.Register("Длина") l.Register("Длина/*") diff --git a/cmd/cli/main.go b/cmd/cli/main.go index a78dd7b..26aa048 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -4,30 +4,23 @@ import ( "bufio" "context" "crypto/rand" - "encoding/binary" "encoding/hex" "encoding/json" - "encoding/xml" "errors" "fmt" "io" "math/big" - "net" - "net/http" "os" "os/signal" - "strconv" - "strings" "syscall" "time" rooteway "git.loyso.art/frx/eway" + "git.loyso.art/frx/eway/cmd/cli/commands" "git.loyso.art/frx/eway/cmd/cli/components" "git.loyso.art/frx/eway/internal/crypto" "git.loyso.art/frx/eway/internal/entity" - "git.loyso.art/frx/eway/internal/export" "git.loyso.art/frx/eway/internal/interconnect/eway" - "git.loyso.art/frx/eway/internal/matcher" "github.com/rodaine/table" "github.com/rs/zerolog" @@ -126,12 +119,13 @@ func setupCLI() *cli.Command { After: releaseDI(), Commands: []*cli.Command{ - newAppCmd(), + commands.CategoriesCommandTree(), + commands.ItemsCommandTree(), + commands.ExportCommandTree(), + newCryptoCmd(), newParseCmd(), newImportCmd(), - newExportCmd(), - newViewCmd(), }, } @@ -164,52 +158,6 @@ func newCryptoEncyptCmd() *cli.Command { } } -func newAppCmd() *cli.Command { - return &cli.Command{ - Name: "app", - Usage: "commands for running different applications", - Commands: []*cli.Command{ - newAppYmlExporter(), - }, - } -} - -func newAppYmlExporter() *cli.Command { - return &cli.Command{ - Name: "ymlexporter", - Usage: "server on given port a http api for accessing yml catalog", - Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "port", - Usage: "Port for accepting connections", - Value: 9448, - Validator: func(i int64) error { - if i > 1<<16 || i == 0 { - return cli.Exit("bad port value, allowed values should not exceed 65535", 1) - } - - return nil - }, - }, - &cli.StringFlag{ - Name: "src", - TakesFile: true, - Usage: "Source to catalog. Should be a valid xml file", - Value: "yml_catalog.xml", - Validator: func(s string) error { - _, err := os.Stat(s) - if err != nil { - return err - } - - return nil - }, - }, - }, - Action: appYMLExporterAction, - } -} - func newParseCmd() *cli.Command { return &cli.Command{ Name: "parse", @@ -310,462 +258,6 @@ func newImportFromFileCmd() *cli.Command { } } -func newExportCmd() *cli.Command { - return &cli.Command{ - Name: "export", - Usage: "category for exporting stored data", - Commands: []*cli.Command{ - newExportYMLCatalogCmd(), - }, - } -} - -func newExportYMLCatalogCmd() *cli.Command { - return &cli.Command{ - Name: "yml_catalog", - Usage: "export data into as yml_catalog in xml format", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "out", - Aliases: []string{"o"}, - Usage: "destination path", - Value: "yml_catalog.xml", - TakesFile: true, - }, - &cli.IntFlag{ - Name: "limit", - Usage: "limits output offers to required value", - }, - &cli.BoolFlag{ - Name: "pretty", - Usage: "prettify output", - }, - }, - Action: decorateAction(exportYMLCatalogAction), - } -} - -func newViewCmd() *cli.Command { - return &cli.Command{ - Name: "view", - Usage: "Set of commands to view the data inside db", - Commands: []*cli.Command{ - newViewCategoriesCmd(), - newViewItemsCmd(), - }, - } -} - -func newViewCategoriesCmd() *cli.Command { - return &cli.Command{ - Name: "categories", - Usage: "Set of commands to work with categories", - Commands: []*cli.Command{ - newViewCategoriesListCmd(), - }, - } -} - -func newViewItemsCmd() *cli.Command { - return &cli.Command{ - Name: "items", - Usage: "Set of command to work with items", - Commands: []*cli.Command{ - newViewItemsGetCmd(), - newViewItemsCountCmd(), - newViewItemsUniqueParams(), - newViewItemsParamsKnownValues(), - newViewItemsFixSizesCmd(), - }, - } -} - -func newViewItemsFixSizesCmd() *cli.Command { - return &cli.Command{ - Name: "fix-sizes", - Usage: "Iterates over params and sets sizes from parameters", - Action: decorateAction(viewItemsFixSizesAction), - } -} - -func newViewItemsUniqueParams() *cli.Command { - return &cli.Command{ - Name: "unique-params", - Usage: "Show all stored unique param values", - Description: "This command iterates over each item and collect keys of params in a dict and then" + - " print it to the output. It's useful to find all unique parameters", - Action: decorateAction(viewItemsUniqueParamsAction), - } -} - -func newViewItemsParamsKnownValues() *cli.Command { - return &cli.Command{ - Name: "params-values", - Usage: "Show all values of requested parameters", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "case-insensitive", - Usage: "Ignores cases of keys", - }, - &cli.StringSliceFlag{ - Name: "regex", - Usage: "Registers regex to match", - }, - &cli.BoolFlag{ - Name: "keys-only", - Usage: "prints only keys", - }, - }, - Action: decorateAction(viewItemsParamsKnownValuesAction), - } -} - -func newViewItemsCountCmd() *cli.Command { - return &cli.Command{ - Name: "count", - Usage: "iterates over collection and counts number of items", - Flags: []cli.Flag{ - &cli.StringSliceFlag{ - Name: "param-key-match", - Usage: "filters by parameters with AND logic", - }, - }, - Action: decorateAction(viewItemsCountAction), - } -} - -func newViewItemsGetCmd() *cli.Command { - return &cli.Command{ - Name: "get", - Usage: "gets goods item by its id", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "id", - Usage: "id of the goods item. Either id or cart-id should be set", - }, - &cli.IntFlag{ - Name: "cart-id", - Usage: "cart-id of the item. Either cart-id or id should be set", - }, - }, - Action: decorateAction(viewItemsGetAction), - } -} - -func newViewCategoriesListCmd() *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: decorateAction(viewCategoriesListAction), - } -} - -func viewItemsGetAction(ctx context.Context, c *cli.Command) error { - r, err := components.GetRepository() - if err != nil { - return fmt.Errorf("getting repository: %w", err) - } - - id := c.String("id") - cartID := c.Int("cart-id") - if id == "" && cartID == 0 { - return cli.Exit("oneof: id or cart-id should be set", 1) - } else if id != "" && cartID != 0 { - return cli.Exit("oneof: id or cart-id should be set", 1) - } - - var item entity.GoodsItem - if id != "" { - item, err = r.GoodsItem().Get(ctx, id) - } else { - item, err = r.GoodsItem().GetByCart(ctx, cartID) - } - if err != nil { - return fmt.Errorf("getting item: %w", err) - } - - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - - err = enc.Encode(item) - if err != nil { - return fmt.Errorf("encoding item: %w", err) - } - - return nil -} - -func viewItemsUniqueParamsAction(ctx context.Context, c *cli.Command) error { - repository, err := components.GetRepository() - if err != nil { - return fmt.Errorf("getting repository: %w", err) - } - - knownParams := map[string]empty{} - iter, err := repository.GoodsItem().ListIter(ctx, 1) - if err != nil { - return fmt.Errorf("getting list iter: %w", err) - } - for item := range iter { - for k := range item.Parameters { - knownParams[k] = empty{} - } - } - - bw := bufio.NewWriter(c.Writer) - for paramName := range knownParams { - _, err = bw.WriteString(paramName + "\n") - if err != nil { - return fmt.Errorf("unable to write: %w", err) - } - } - - return bw.Flush() -} - -type chanIter[T any] struct { - in <-chan T - err error - next T -} - -var errChannelClosed = errors.New("channel closed") - -func (i *chanIter[T]) Next() (ok bool) { - if i.err != nil { - return false - } - - i.next, ok = <-i.in - if !ok { - i.err = errChannelClosed - } - - return ok -} - -func (i *chanIter[T]) Get() T { - return i.next -} - -func (i *chanIter[T]) Err() error { - if errors.Is(i.err, errChannelClosed) { - return nil - } - - return i.err -} - -func (i *chanIter[T]) Close() { - for range i.in { - } -} - -func getItemsIter(ctx context.Context, r entity.GoodsItemRepository) *chanIter[entity.GoodsItem] { - in, err := r.ListIter(ctx, 3) - - return &chanIter[entity.GoodsItem]{ - in: in, - err: err, - } -} - -func viewItemsParamsKnownValuesAction(ctx context.Context, c *cli.Command) error { - repository, err := components.GetRepository() - if err != nil { - return fmt.Errorf("getting repository: %w", err) - } - log, err := components.GetLogger() - if err != nil { - return fmt.Errorf("getting logger: %w", err) - } - - params := c.Args().Slice() - opts := make([]matcher.RadixOpt, 0, 1) - if c.Bool("case-insensitive") { - opts = append(opts, matcher.RadixCaseInsensitive()) - } - - m := matcher.NewRadix(opts...) - for _, param := range params { - log.Debug().Str("param", param).Msg("registering param") - m.Register(param) - } - for _, regexp := range c.StringSlice("regex") { - log.Debug().Str("regexp", regexp).Msg("registering regexp") - m.RegisterRegexp(regexp) - } - - requestedValues := make(map[string]map[string]empty, len(params)) - requestedValuesByPattern := make(map[string]map[string]empty, len(params)) - iter := getItemsIter(ctx, repository.GoodsItem()) - for iter.Next() { - item := iter.Get() - for k, v := range item.Parameters { - matchedPattern := m.MatchByPattern(k) - if matchedPattern == "" { - continue - } - - values, ok := requestedValues[k] - if !ok { - values = make(map[string]empty) - } - values[v] = empty{} - requestedValues[k] = values - - values, ok = requestedValuesByPattern[matchedPattern] - if !ok { - values = map[string]empty{} - } - values[v] = empty{} - requestedValuesByPattern[matchedPattern] = values - } - } - - bw := bufio.NewWriter(c.Writer) - _, _ = bw.WriteString("Matches:\n") - - if c.Bool("keys-only") { - for k := range requestedValues { - _, _ = bw.WriteString(k + "\n") - } - } else { - for k, v := range requestedValues { - _, _ = bw.WriteString(k + ": ") - values := make([]string, 0, len(v)) - for item := range v { - values = append(values, strconv.Quote(item)) - } - valuesStr := "[" + strings.Join(values, ",") + "]\n" - _, _ = bw.WriteString(valuesStr) - } - } - - _, _ = bw.WriteString("\nPatterns:\n") - for _, pattern := range m.Patterns() { - _, _ = bw.WriteString(pattern + "\n") - } - - return bw.Flush() -} - -func viewItemsCountAction(ctx context.Context, c *cli.Command) error { - r, err := components.GetRepository() - if err != nil { - return fmt.Errorf("getting repository: %w", err) - } - - filters := c.StringSlice("param-key-match") - m := matcher.NewRadix() - patternMapped := make(map[string]empty, len(filters)) - if len(filters) == 0 { - m.Register("*") - } else { - for _, f := range filters { - m.Register(f) - } - for _, pattern := range m.Patterns() { - patternMapped[pattern] = empty{} - } - } - - var count int - items, err := r.GoodsItem().List(ctx) - if err != nil { - return fmt.Errorf("getting items: %w", err) - } - for _, item := range items { - seenPatterns := map[string]empty{} - - for k := range item.Parameters { - pattern := m.MatchByPattern(k) - if pattern == "" { - continue - } - if _, ok := seenPatterns[pattern]; ok { - continue - } - seenPatterns[pattern] = empty{} - } - - if len(seenPatterns) == len(patternMapped) { - println("Item matched", item.Articul, item.Cart) - count++ - } - } - - println(count) - - return nil -} - -func viewCategoriesListAction(ctx context.Context, c *cli.Command) error { - r, err := components.GetRepository() - if err != nil { - return fmt.Errorf("getting repository: %w", err) - } - - categories, err := r.Category().List(ctx) - if err != nil { - return fmt.Errorf("listing categories: %w", err) - } - - limit := int(c.Int("limit")) - page := int(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 -} - func importFromFileAction(ctx context.Context, c *cli.Command) error { const maxBatch = 2000 @@ -896,7 +388,7 @@ func decorateAction(a action) cli.ActionFunc { } log = log.With().Str("reqid", reqid).Logger() - ctx = log.WithContext(ctx) + rctx := log.WithContext(ctx) start := time.Now() defer func() { @@ -904,97 +396,10 @@ func decorateAction(a action) cli.ActionFunc { }() log.Info().Msg("command execution started") - return a(ctx, c) + return a(rctx, c) } } -func exportYMLCatalogAction(ctx context.Context, c *cli.Command) error { - path := c.String("out") - limit := c.Int("limit") - pretty := c.Bool("pretty") - - log, err := components.GetLogger() - if err != nil { - return fmt.Errorf("getting logger: %w", err) - } - - r, err := components.GetRepository() - if err != nil { - return fmt.Errorf("getting repository: %w", err) - } - - categories, err := r.Category().List(ctx) - if err != nil { - return fmt.Errorf("listing categories: %w", err) - } - - shop := export.Shop{ - Currencies: []export.Currency{{ - ID: "RUR", - Rate: 1, - }}, - Categories: make([]export.Category, 0, len(categories)), - } - - categoryByNameIdx := make(map[string]int64, len(categories)) - for _, category := range categories { - categoryByNameIdx[category.Name] = category.ID - shop.Categories = append(shop.Categories, export.Category{ - ID: category.ID, - Name: category.Name, - }) - } - - itemsIter, err := r.GoodsItem().ListIter(ctx, 1) - if err != nil { - return fmt.Errorf("getting items iterator: %w", err) - } - - for item := range itemsIter { - offer := goodsItemAsOffer(item, categoryByNameIdx) - shop.Offers = append(shop.Offers, offer) - } - if err = ctx.Err(); err != nil { - return err - } - - f, err := os.Create(path) - if err != nil { - return fmt.Errorf("creating file: %w", err) - } - defer func() { - errClose := f.Close() - if err == nil { - err = errClose - return - } - - log.Err(errClose).Msg("file closed or not") - }() - - if limit > 0 { - shop.Offers = shop.Offers[:limit] - } - - container := export.YmlContainer{ - YmlCatalog: export.YmlCatalog{ - Shop: shop, - Date: time.Now(), - }, - } - - _, err = f.Write([]byte(xml.Header)) - if err != nil { - return fmt.Errorf("writing header: %w", err) - } - _, err = f.Write([]byte("\n")) - enc := xml.NewEncoder(f) - if pretty { - enc.Indent("", " ") - } - return enc.Encode(container) -} - func parseEwayGetAction(ctx context.Context, cmd *cli.Command) error { cartID := cmd.Int("cart") @@ -1124,37 +529,6 @@ func parseEwayListAction(ctx context.Context, cmd *cli.Command) error { return nil } -func viewItemsFixSizesAction(ctx context.Context, cmd *cli.Command) error { - repository, err := components.GetRepository() - if err != nil { - return fmt.Errorf("getting repository: %w", err) - } - - dimensionDispatcher := makeDefaultDimensionDispatcher() - - toUpdate := make([]entity.GoodsItem, 0, 20_000) - bus := getItemsIter(ctx, repository.GoodsItem()) - for bus.Next() { - item := bus.Get() - - for key, value := range item.Parameters { - dimensionDispatcher.dispatch(ctx, value, key, &item.Sizes) - } - - toUpdate = append(toUpdate, item) - } - if bus.Err() != nil { - return fmt.Errorf("iterating: %w", bus.Err()) - } - - _, err = repository.GoodsItem().UpsertMany(ctx, toUpdate...) - if err != nil { - return fmt.Errorf("updating items: %w", err) - } - - return nil -} - func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { client, err := components.GetEwayClient() if err != nil { @@ -1306,7 +680,7 @@ func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { left = int(((1 - progressFloat) / progressFloat) * elapsed) } - logger.Debug(). + logger.Info(). Int("from", start). Int("to", start+batchSize). Int("total", total). @@ -1347,135 +721,6 @@ func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { return nil } -func goodsItemAsOffer(in entity.GoodsItem, categoryIDByName map[string]int64) (out export.Offer) { - const defaultType = "vendor.model" - const defaultCurrency = "RUR" - const defaultAvailable = true - const quantityParamName = "Количество на складе «Москва»" - const basePictureURL = "https://eway.elevel.ru" - - imgurl := func(path string) string { - return basePictureURL + path - } - - categoryID := categoryIDByName[in.Type] - - pictureURLs := make([]string, 0, len(in.PhotoURLs)) - for _, url := range in.PhotoURLs { - pictureURLs = append(pictureURLs, imgurl(url)) - } - params := make([]export.Param, len(in.Parameters)) - for k, v := range in.Parameters { - params = append(params, export.Param{ - Name: k, - Value: v, - }) - } - params = append(params, export.Param{ - Name: quantityParamName, - Value: strconv.Itoa(in.Stock), - }) - out = export.Offer{ - ID: in.Cart, - VendorCode: in.Articul, - Price: int(in.TariffPrice), - CategoryID: categoryID, - PictureURLs: pictureURLs, - - Model: in.Name, - Vendor: in.Producer, - TypePrefix: in.Name, - Description: in.Description, - ManufacturerWarrany: true, - Params: params, - - Type: defaultType, - CurrencyID: defaultCurrency, - Available: defaultAvailable, - } - - return out -} - -func appYMLExporterAction(ctx context.Context, cmd *cli.Command) error { - port := cmd.Int("port") - src := cmd.String("src") - - log, err := components.GetLogger() - if err != nil { - return fmt.Errorf("getting logger: %w", err) - } - - mw := func(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - remoteAddr := r.RemoteAddr - xff := r.Header.Get("x-forwarded-for") - method := r.Method - ua := r.UserAgent() - xreqid := r.Header.Get("x-request-id") - if xreqid == "" { - const reqsize = 4 - var reqidRaw [reqsize]byte - _, _ = rand.Read(reqidRaw[:]) - value := binary.BigEndian.Uint32(reqidRaw[:]) - xreqid = fmt.Sprintf("%x", value) - w.Header().Set("x-request-id", xreqid) - } - - start := time.Now() - xlog := log.With().Str("request_id", xreqid).Logger() - xlog.Debug(). - Str("path", path). - Str("remote_addr", remoteAddr). - Str("xff", xff). - Str("method", method). - Str("user_agent", ua). - Msg("incoming request") - - xctx := xlog.WithContext(r.Context()) - r = r.WithContext(xctx) - next(w, r) - - elapsed := time.Since(start).Truncate(time.Millisecond).Seconds() - xlog.Info(). - Float64("elapsed", elapsed). - Msg("request completed") - } - } - - mux := http.NewServeMux() - mux.HandleFunc("/yml_catalog.xml", mw(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, src) - })) - - srv := &http.Server{ - Addr: net.JoinHostPort("0.0.0.0", strconv.Itoa(int(port))), - Handler: mux, - } - - go func() { - <-ctx.Done() - - sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*5) - defer sdcancel() - - errShutdown := srv.Shutdown(sdctx) - if errShutdown != nil { - log.Warn().Err(errShutdown).Msg("unable to shutdown server") - } - }() - - log.Info().Str("listen_addr", srv.Addr).Msg("running server") - - err = srv.ListenAndServe() - if err != nil && errors.Is(err, http.ErrServerClosed) { - return fmt.Errorf("serving http api: %w", err) - } - - return nil -} - func cryptoDeEncryptAction(encrypt bool) cli.ActionFunc { return func(ctx context.Context, c *cli.Command) (err error) { value := c.String("text") diff --git a/internal/encoding/fbs/helpers.go b/internal/encoding/fbs/helpers.go index 77b4725..d9fec09 100644 --- a/internal/encoding/fbs/helpers.go +++ b/internal/encoding/fbs/helpers.go @@ -153,9 +153,9 @@ func ParseGoodsItem(data []byte) (item entity.GoodsItem, err error) { } item.Sizes = entity.GoodsItemSize{ - Width: entity.NewMilimeterDimension(w), - Height: entity.NewMilimeterDimension(h), - Length: entity.NewMilimeterDimension(l), + Width: entity.NewCentimeterDimensionOrEmpty(w), + Height: entity.NewCentimeterDimensionOrEmpty(h), + Length: entity.NewCentimeterDimensionOrEmpty(l), } createdAt := itemFBS.CreatedAt() diff --git a/internal/entity/dimension.go b/internal/entity/dimension.go index 976d41b..6397549 100644 --- a/internal/entity/dimension.go +++ b/internal/entity/dimension.go @@ -49,7 +49,15 @@ type Dimension struct { Kind DimensionKind } +func (d Dimension) IsZero() bool { + return d.Value == 0 +} + func (d Dimension) MarshalText() ([]byte, error) { + if d.Value == 0 && d.Kind == DimensionKindUnspecified { + return nil, nil + } + value := strconv.FormatFloat(d.Value, 'f', 4, 64) + " " + d.Kind.String() return []byte(value), nil } @@ -114,10 +122,14 @@ func ParseDimention(value string, locale DimensionLocale) (Dimension, error) { return out, nil } -func NewMilimeterDimension(value float64) Dimension { +func NewCentimeterDimensionOrEmpty(value float64) Dimension { + if value == 0 { + return Dimension{} + } + return Dimension{ Value: value, - Kind: DimensionKindMilimeter, + Kind: DimensionKindCentimeter, } } diff --git a/internal/export/itemsmarket.go b/internal/export/itemsmarket.go index 82c60d7..3ecbc39 100644 --- a/internal/export/itemsmarket.go +++ b/internal/export/itemsmarket.go @@ -23,6 +23,7 @@ type Offer struct { TypePrefix string `xml:"typePrefix"` Description string `xml:"description"` ManufacturerWarrany bool `xml:"manufacturer_warranty"` + Dimensions string `xml:"dimensions"` Params []Param `xml:"param"` } diff --git a/internal/interconnect/eway/client.go b/internal/interconnect/eway/client.go index f43ea93..87a8623 100644 --- a/internal/interconnect/eway/client.go +++ b/internal/interconnect/eway/client.go @@ -385,7 +385,7 @@ func (c *client) getProductInfo(ctx context.Context, cartID int64) (pi entity.Go } cleanText := func(t string) string { - return strings.TrimSuffix(strings.TrimSpace(t), ":") + return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(t), ":")) } const parametersSelector = "body > div.page-container > div.page-content > div.content-wrapper > div.content > div.row > div.col-md-4 > div > div > div:nth-child(6)" diff --git a/internal/matcher/radix_test.go b/internal/matcher/radix_test.go index 94f9c4b..8a7641f 100644 --- a/internal/matcher/radix_test.go +++ b/internal/matcher/radix_test.go @@ -34,6 +34,9 @@ func TestRadixMatcherWithPattern(t *testing.T) { }, { name: "should not match 2", in: "whoa", + }, { + name: "should not match 3", + in: "alohaya", }} for _, tc := range tt {