276 lines
6.1 KiB
Go
276 lines
6.1 KiB
Go
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()
|
|
|
|
const (
|
|
reasonNoSize = "no_size"
|
|
reasonTooLarge = "too_large"
|
|
)
|
|
|
|
skippedByReasons := map[string]int{
|
|
reasonNoSize: 0,
|
|
reasonTooLarge: 0,
|
|
}
|
|
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("reason", reasonNoSize).Msg("skipping item")
|
|
skippedByReasons[reasonNoSize]++
|
|
|
|
continue
|
|
}
|
|
|
|
const maximumAllowedSizes = 160
|
|
if sum := item.Sizes.GetSum(entity.DimensionKindCentimeter); sum > maximumAllowedSizes {
|
|
sublog.Warn().Str("reason", reasonTooLarge).Msg("skipping item")
|
|
skippedByReasons[reasonTooLarge]++
|
|
|
|
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("<!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 {
|
|
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)
|
|
}
|