package commands import ( "context" "encoding/xml" "fmt" "io" "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", Flags: []cli.Flag{ &cli.StringFlag{ Name: "out", Aliases: []string{"o"}, Usage: "Output to file or stdout/stderr", Value: "yml_catalog.xml", TakesFile: true, }, &cli.IntFlag{ Name: "limit", Aliases: []string{"l"}, Usage: "limits amount of items to save", }, &cli.BoolFlag{ Name: "pretty", Aliases: []string{"p"}, Usage: "pretty-prints output", }, }, } 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, err := components.GetDimensionMatcher() if err != nil { return fmt.Errorf("getting dimension matcher: %w", err) } dimensionDispatcher := dimensionDispatcher{m: matcher} log.Info().Any("patterns", matcher.GetRegisteredPatterns()).Msg("configured patterns") const ( reasonNoSize = "no_size" reasonTooLarge = "too_large" reasonNoDescription = "no_description" reasonNoPhoto = "no_photo" ) skippedByReasons := map[string]int{ reasonNoSize: 0, reasonTooLarge: 0, reasonNoDescription: 0, reasonNoPhoto: 0, } addToSkip := func(condition bool, name string, log zerolog.Logger) bool { if !condition { return false } log.Debug().Str("reason", name).Msg("skipping item") skippedByReasons[name]++ return condition } iter := getItemsIter(ctx, r.GoodsItem()) for iter.Next() { const maximumAllowedSizes = 160 item := iter.Get() sublog := log.With().Int64("cart", item.Cart).Logger() switch { case addToSkip(item.Description == "", reasonNoDescription, sublog): continue case addToSkip(!item.Sizes.AllSizesSet(), reasonNoSize, sublog): continue case addToSkip(item.Sizes.GetSum(entity.DimensionKindCentimeter) > maximumAllowedSizes, reasonTooLarge, sublog): continue } offer := h.goodsItemAsOffer(item, categoryByNameIdx, dimensionDispatcher, sublog) if addToSkip(len(offer.PictureURLs) == 0, reasonNoPhoto, sublog) { continue } shop.Offers = append(shop.Offers, offer) } if err = iter.Err(); err != nil { return fmt.Errorf("iterating over items: %w", err) } var f io.WriteCloser switch path { case "stdout": f = os.Stdout case "stderr": f = os.Stderr default: 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)).Any("skipped", skippedByReasons).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 { if url == "" { continue } outurl := imgurl(url) if outurl == basePictureURL { continue } pictureURLs = append(pictureURLs, imgurl(url)) } params := make([]export.Param, 0, 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.Price), 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 } // check cart id 126584 func (exportHandlers) formatSizeAsDimensions(size entity.GoodsItemSize, log zerolog.Logger) string { const delimeter = "/" makeFloat := func(d entity.Dimension) string { value := d.AdjustTo(entity.DimensionKindCentimeter).Value value = float64(int(value*100)) / 100.0 return strconv.FormatFloat(value, 'f', 1, 64) } knownSizes := make([]entity.Dimension, 0, 3) dimensions := []entity.Dimension{ size.Length, size.Width, size.Height, } for i, d := range dimensions { if d.IsZero() { continue } knownSizes = append(knownSizes, dimensions[i]) } 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.Debug().Str("size", side).Msg("setting to default value") case 1: var side string unknownDefaultSize := makeFloat(entity.NewCentimeterDimensionOrEmpty(30)) switch { case !size.Length.IsZero(): side = "width" w = unknownDefaultSize case !size.Width.IsZero(): side = "length" l = unknownDefaultSize case !size.Height.IsZero(): return "" } log.Debug().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) }