Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package catalog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ecommerceGroupDef struct {
|
||||
Key string
|
||||
Name string
|
||||
Description string
|
||||
Order int
|
||||
}
|
||||
|
||||
type ecommerceFieldDef struct {
|
||||
Key string
|
||||
Name string
|
||||
Type string
|
||||
GroupKey string
|
||||
Required bool
|
||||
Enabled bool
|
||||
Recommended bool
|
||||
Unit string
|
||||
DefaultValue string
|
||||
SortOrder int
|
||||
Hints []string
|
||||
Description string
|
||||
}
|
||||
|
||||
func ecommerceGroups() []ecommerceGroupDef {
|
||||
return []ecommerceGroupDef{
|
||||
{Key: "basic", Name: "Basic Information", Description: "Core product identifiers and content", Order: 10},
|
||||
{Key: "pricing", Name: "Pricing", Description: "Price and currency fields", Order: 20},
|
||||
{Key: "media", Name: "Media", Description: "Images and media URLs", Order: 30},
|
||||
{Key: "taxonomy", Name: "Taxonomy", Description: "Categories and classification", Order: 40},
|
||||
{Key: "inventory", Name: "Inventory", Description: "Stock and availability", Order: 50},
|
||||
{Key: "attributes", Name: "Attributes", Description: "Variant and product attributes", Order: 60},
|
||||
{Key: "shipping", Name: "Shipping", Description: "Weight and dimensions", Order: 70},
|
||||
}
|
||||
}
|
||||
|
||||
func ecommerceFields() []ecommerceFieldDef {
|
||||
return []ecommerceFieldDef{
|
||||
{Key: "gtin", Name: "GTIN/EAN", Type: "string", GroupKey: "basic", Required: true, Enabled: true, Recommended: true, SortOrder: 10, Hints: []string{"ean", "upc", "barcode", "gtin13"}, Description: "Product barcode"},
|
||||
{Key: "title", Name: "Product name", Type: "string", GroupKey: "basic", Required: true, Enabled: true, Recommended: true, SortOrder: 20, Hints: []string{"name", "product_name", "product_title"}, Description: "Primary product title"},
|
||||
{Key: "brand", Name: "Brand", Type: "string", GroupKey: "basic", Required: true, Enabled: true, Recommended: true, SortOrder: 30, Hints: []string{"manufacturer", "vendor"}, Description: "Brand or manufacturer"},
|
||||
{Key: "description", Name: "Description", Type: "string", GroupKey: "basic", Required: false, Enabled: true, Recommended: true, SortOrder: 40, Hints: []string{"desc", "body", "long_description"}, Description: "Product description"},
|
||||
{Key: "sku", Name: "SKU", Type: "string", GroupKey: "basic", Required: false, Enabled: true, Recommended: true, SortOrder: 50, Hints: []string{"item_sku", "article_number"}, Description: "Stock keeping unit"},
|
||||
{Key: "mpn", Name: "MPN", Type: "string", GroupKey: "basic", Required: false, Enabled: true, Recommended: true, SortOrder: 60, Hints: []string{"manufacturer_part_number", "part_number"}, Description: "Manufacturer part number"},
|
||||
{Key: "product_model", Name: "Product model", Type: "string", GroupKey: "basic", Required: false, Enabled: true, Recommended: true, SortOrder: 65, Hints: []string{"model", "productmodel", "product_model"}, Description: "Manufacturer model name/number"},
|
||||
{Key: "product_url", Name: "Product URL", Type: "url", GroupKey: "basic", Required: false, Enabled: true, Recommended: true, SortOrder: 70, Hints: []string{"url", "link", "product_link"}, Description: "Canonical product page URL"},
|
||||
{Key: "official_link", Name: "Official link", Type: "url", GroupKey: "basic", Required: false, Enabled: true, Recommended: true, SortOrder: 80, Hints: []string{"officiallink", "manufacturer_url"}, Description: "Manufacturer or brand product page"},
|
||||
{Key: "price", Name: "Price", Type: "number", GroupKey: "pricing", Required: false, Enabled: true, Recommended: true, SortOrder: 10, Unit: "EUR", Hints: []string{"regular_price", "list_price", "amount"}, Description: "Regular price"},
|
||||
{Key: "sale_price", Name: "Sale price", Type: "number", GroupKey: "pricing", Required: false, Enabled: true, Recommended: true, SortOrder: 20, Unit: "EUR", Hints: []string{"special_price", "discount_price"}, Description: "Promotional price"},
|
||||
{Key: "purchase_price", Name: "Purchase price", Type: "number", GroupKey: "pricing", Required: false, Enabled: true, Recommended: true, SortOrder: 25, Unit: "EUR", Hints: []string{"purchaseprice", "cost", "buy_price"}, Description: "Cost / buy price"},
|
||||
{Key: "currency", Name: "Currency", Type: "string", GroupKey: "pricing", Required: false, Enabled: true, Recommended: true, SortOrder: 30, DefaultValue: "EUR", Hints: []string{"price_currency", "curr"}, Description: "ISO currency code"},
|
||||
{Key: "image_url", Name: "Image URL", Type: "image", GroupKey: "media", Required: false, Enabled: true, Recommended: true, SortOrder: 10, Hints: []string{"image", "image_link", "thumbnail"}, Description: "Primary product image"},
|
||||
{Key: "main_image", Name: "Main image", Type: "image", GroupKey: "media", Required: true, Enabled: true, Recommended: true, SortOrder: 15, Hints: []string{"mainimage", "image", "image_url"}, Description: "Main gallery image URL"},
|
||||
{Key: "additional_image_urls", Name: "Additional images", Type: "image", GroupKey: "media", Required: false, Enabled: true, Recommended: true, SortOrder: 20, Hints: []string{"images", "gallery", "moreimages", "additional_images"}, Description: "Extra product images"},
|
||||
{Key: "video_url", Name: "Video URL", Type: "url", GroupKey: "media", Required: false, Enabled: true, Recommended: false, SortOrder: 30, Hints: []string{"videourl", "video"}, Description: "Product video URL"},
|
||||
{Key: "category", Name: "Category", Type: "string", GroupKey: "taxonomy", Required: false, Enabled: true, Recommended: true, SortOrder: 10, Hints: []string{"product_type", "google_product_category", "category_path"}, Description: "Product category"},
|
||||
{Key: "availability", Name: "Availability", Type: "string", GroupKey: "inventory", Required: false, Enabled: true, Recommended: true, SortOrder: 10, Hints: []string{"in_stock", "stock_status", "stockstatus"}, Description: "Availability status"},
|
||||
{Key: "stock", Name: "Stock", Type: "number", GroupKey: "inventory", Required: false, Enabled: true, Recommended: true, SortOrder: 20, Hints: []string{"quantity", "qty", "inventory"}, Description: "Stock quantity"},
|
||||
{Key: "color", Name: "Color", Type: "color", GroupKey: "attributes", Required: false, Enabled: true, Recommended: true, SortOrder: 10, Hints: []string{"colour", "farbe"}, Description: "Color attribute"},
|
||||
{Key: "size", Name: "Size", Type: "string", GroupKey: "attributes", Required: false, Enabled: true, Recommended: true, SortOrder: 20, Hints: []string{"groesse", "dimension_size"}, Description: "Size attribute"},
|
||||
{Key: "material", Name: "Material", Type: "string", GroupKey: "attributes", Required: false, Enabled: true, Recommended: false, SortOrder: 30, Hints: []string{"fabric", "composition"}, Description: "Material attribute"},
|
||||
{Key: "warranty", Name: "Warranty", Type: "string", GroupKey: "attributes", Required: false, Enabled: true, Recommended: false, SortOrder: 40, Hints: []string{"guarantee"}, Description: "Warranty term or text"},
|
||||
{Key: "service", Name: "Service", Type: "string", GroupKey: "attributes", Required: false, Enabled: true, Recommended: false, SortOrder: 50, Hints: []string{}, Description: "Service or support notes"},
|
||||
{Key: "specifications", Name: "Specifications", Type: "string", GroupKey: "attributes", Required: false, Enabled: true, Recommended: true, SortOrder: 60, Hints: []string{"specs", "specification"}, Description: "Technical specifications"},
|
||||
{Key: "eprel_id", Name: "EPREL ID", Type: "string", GroupKey: "attributes", Required: false, Enabled: true, Recommended: true, SortOrder: 70, Hints: []string{"eprelid", "eprel"}, Description: "EU energy label identifier"},
|
||||
{Key: "weight", Name: "Weight", Type: "weight", GroupKey: "shipping", Required: false, Enabled: true, Recommended: false, SortOrder: 10, Unit: "kg", Hints: []string{"shipping_weight", "product_weight", "netmass"}, Description: "Product weight"},
|
||||
{Key: "net_depth", Name: "Net depth", Type: "dimension", GroupKey: "shipping", Required: false, Enabled: true, Recommended: false, SortOrder: 20, Hints: []string{"netdepth", "depth"}, Description: "Net depth"},
|
||||
{Key: "net_height", Name: "Net height", Type: "dimension", GroupKey: "shipping", Required: false, Enabled: true, Recommended: false, SortOrder: 30, Hints: []string{"netheight", "height"}, Description: "Net height"},
|
||||
{Key: "net_width", Name: "Net width", Type: "dimension", GroupKey: "shipping", Required: false, Enabled: true, Recommended: false, SortOrder: 40, Hints: []string{"netwidth", "width"}, Description: "Net width"},
|
||||
{Key: "net_mass", Name: "Net mass", Type: "weight", GroupKey: "shipping", Required: false, Enabled: true, Recommended: false, SortOrder: 50, Hints: []string{"netmass", "mass"}, Description: "Net mass"},
|
||||
}
|
||||
}
|
||||
|
||||
func recommendedEcommerceKeys() []string {
|
||||
out := make([]string, 0)
|
||||
for _, f := range ecommerceFields() {
|
||||
if f.Recommended {
|
||||
out = append(out, f.Key)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Service) ensureGroupID(ctx context.Context, companyID uuid.UUID, g ecommerceGroupDef) (uuid.UUID, error) {
|
||||
var id uuid.UUID
|
||||
err := s.Pool.QueryRow(ctx, `
|
||||
SELECT id FROM field_groups WHERE company_id = $1 AND name = $2 LIMIT 1`,
|
||||
companyID, g.Name).Scan(&id)
|
||||
if err == nil {
|
||||
_, _ = s.Pool.Exec(ctx, `
|
||||
UPDATE field_groups SET description = $3, "order" = $4, is_system = true, updated_at = now()
|
||||
WHERE id = $1 AND company_id = $2`, id, companyID, g.Description, g.Order)
|
||||
return id, nil
|
||||
}
|
||||
err = s.Pool.QueryRow(ctx, `
|
||||
INSERT INTO field_groups (company_id, name, description, "order", is_system)
|
||||
VALUES ($1, $2, $3, $4, true) RETURNING id`,
|
||||
companyID, g.Name, g.Description, g.Order).Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// EnsureEcommerceCatalog upserts system field groups and standard fields for ecommerce.
|
||||
func (s *Service) EnsureEcommerceCatalog(ctx context.Context, companyID uuid.UUID) error {
|
||||
groupIDs := map[string]uuid.UUID{}
|
||||
for _, g := range ecommerceGroups() {
|
||||
id, err := s.ensureGroupID(ctx, companyID, g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groupIDs[g.Key] = id
|
||||
}
|
||||
|
||||
for _, f := range ecommerceFields() {
|
||||
gid, ok := groupIDs[f.GroupKey]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
hints, _ := json.Marshal(f.Hints)
|
||||
var defVal *string
|
||||
if f.DefaultValue != "" {
|
||||
v := f.DefaultValue
|
||||
defVal = &v
|
||||
}
|
||||
var unit *string
|
||||
if f.Unit != "" {
|
||||
u := f.Unit
|
||||
unit = &u
|
||||
}
|
||||
var desc *string
|
||||
if f.Description != "" {
|
||||
d := f.Description
|
||||
desc = &d
|
||||
}
|
||||
_, err := s.Pool.Exec(ctx, `
|
||||
INSERT INTO standard_fields (
|
||||
company_id, name, key, type, group_id, is_required, description, default_value,
|
||||
is_system, enabled, unit, sort_order, mapping_hints
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,true,$9,$10,$11,$12::jsonb)
|
||||
ON CONFLICT (company_id, key) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
type = EXCLUDED.type,
|
||||
group_id = EXCLUDED.group_id,
|
||||
is_required = standard_fields.is_required OR EXCLUDED.is_required,
|
||||
description = COALESCE(EXCLUDED.description, standard_fields.description),
|
||||
default_value = COALESCE(standard_fields.default_value, EXCLUDED.default_value),
|
||||
unit = COALESCE(standard_fields.unit, EXCLUDED.unit),
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
enabled = standard_fields.enabled OR EXCLUDED.enabled,
|
||||
mapping_hints = CASE
|
||||
WHEN standard_fields.mapping_hints IS NULL
|
||||
OR standard_fields.mapping_hints = '[]'::jsonb
|
||||
THEN EXCLUDED.mapping_hints
|
||||
ELSE standard_fields.mapping_hints
|
||||
END,
|
||||
updated_at = now()`,
|
||||
companyID, f.Name, f.Key, f.Type, gid, f.Required, desc, defVal,
|
||||
f.Enabled, unit, f.SortOrder, string(hints))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user