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 }