Files
eway/cmd/cli/commands/exports.go
Aleksandr Trushkin bab74f58f4 use personal price
2024-02-27 19:46:30 +03:00

354 lines
7.8 KiB
Go

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("<!DOCTYPE yml_catalog SYSTEM \"shops.dtd\">\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)
}