This commit is contained in:
2026-08-16 12:00:28 +02:00
parent 52f27e30fc
commit 52fb129957
7 changed files with 105 additions and 23 deletions
+19
View File
@@ -433,6 +433,25 @@ func cloneCompanyCatalog(ctx context.Context, pool *pgxpool.Pool, src, dest uuid
}
add("company_language", ct.RowsAffected())
ct, err = tx.Exec(ctx, `
INSERT INTO company_settings (company_id, settings, updated_at)
SELECT $2, settings, now()
FROM company_settings WHERE company_id = $1
ON CONFLICT (company_id) DO UPDATE
SET settings = EXCLUDED.settings, updated_at = now()`, src, dest)
if err != nil {
return nil, fmt.Errorf("company_settings: %w", err)
}
add("company_settings", ct.RowsAffected())
// Cloned raws are ready to process on the destination (source flags may say processed).
if _, err := tx.Exec(ctx, `
UPDATE raw_products
SET is_processed = false, processing_status = 'unprocessed', updated_at = now()
WHERE company_id = $1`, dest); err != nil {
return nil, fmt.Errorf("reset raw processing flags: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
+41 -22
View File
@@ -42,6 +42,7 @@ func NormalizeGTIN(ean string) string {
}
// BuildMappedDataFromV1Item mirrors legacy buildMappedDataFromItem + image field storage.
// Empty optional fields are omitted (not null) so jsonb || merges cannot wipe catalog titles.
func BuildMappedDataFromV1Item(item V1ProcessItem) map[string]any {
mapped := map[string]any{
"ean": item.EAN,
@@ -49,23 +50,15 @@ func BuildMappedDataFromV1Item(item V1ProcessItem) map[string]any {
if item.Title != "" {
mapped["title"] = item.Title
mapped["name"] = item.Title
} else {
mapped["title"] = nil
}
if item.Description != "" {
mapped["description"] = item.Description
} else {
mapped["description"] = nil
}
if len(item.Specifications) > 0 {
mapped["specifications"] = item.Specifications
} else {
mapped["specifications"] = []any{}
}
if item.Search != "" {
mapped["search"] = item.Search
} else {
mapped["search"] = nil
}
if item.CategoryUniqueID != "" {
mapped["category"] = item.CategoryUniqueID
@@ -77,6 +70,21 @@ func BuildMappedDataFromV1Item(item V1ProcessItem) map[string]any {
return mapped
}
// v1ItemHasContent reports whether the request carries feed/product fields beyond EAN.
// EAN-only requests must resolve an existing catalog row (e.g. after admin clone).
func v1ItemHasContent(item V1ProcessItem) bool {
if strings.TrimSpace(item.Title) != "" || strings.TrimSpace(item.Description) != "" {
return true
}
if strings.TrimSpace(item.CategoryUniqueID) != "" || strings.TrimSpace(item.Search) != "" {
return true
}
if len(item.Specifications) > 0 {
return true
}
return len(mappedImageFieldsFromV1Item(item)) > 0
}
func mappedImageFieldsFromV1Item(item V1ProcessItem) map[string]any {
source := map[string]any{}
putIf := func(k, v string) {
@@ -279,6 +287,10 @@ type EnsureRawResult struct {
// EnsureRawProductsFromV1Items finds or creates/updates raw_products by EAN for the company.
// Returns successfully resolved IDs and per-item error messages (non-fatal for partial batches).
//
// EAN-only items reuse existing catalog mapped_data (e.g. after admin clone-from-company).
// Creating a brand-new row requires at least one content field; otherwise callers get empty
// "Product {ean}" stubs with no feed data.
func (s *Service) EnsureRawProductsFromV1Items(ctx context.Context, companyID uuid.UUID, items []V1ProcessItem) (ids []uuid.UUID, results []EnsureRawResult, errs []string, err error) {
if s == nil || s.Pool == nil {
return nil, nil, nil, fmt.Errorf("catalog not configured")
@@ -309,24 +321,23 @@ func (s *Service) EnsureRawProductsFromV1Items(ctx context.Context, companyID uu
if qErr == nil {
merged := map[string]any{}
_ = json.Unmarshal(existingMapped, &merged)
img := mappedImageFieldsFromV1Item(item)
if len(img) > 0 {
for k, v := range img {
merged[k] = v
changed := false
for k, v := range mapped {
if v == nil {
continue
}
if item.CategoryUniqueID != "" {
merged["category"] = item.CategoryUniqueID
merged["category_unique_id"] = item.CategoryUniqueID
if s, ok := v.(string); ok && strings.TrimSpace(s) == "" {
continue
}
mergedJSON, _ := json.Marshal(merged)
_, _ = s.Pool.Exec(ctx, `
UPDATE raw_products
SET mapped_data = $3::jsonb, updated_at = now()
WHERE id = $1 AND company_id = $2`, existingID, companyID, string(mergedJSON))
} else if item.CategoryUniqueID != "" {
_ = json.Unmarshal(existingMapped, &merged)
merged[k] = v
changed = true
}
if item.CategoryUniqueID != "" {
merged["category"] = item.CategoryUniqueID
merged["category_unique_id"] = item.CategoryUniqueID
changed = true
}
if changed {
mergedJSON, _ := json.Marshal(merged)
_, _ = s.Pool.Exec(ctx, `
UPDATE raw_products
@@ -342,6 +353,14 @@ func (s *Service) EnsureRawProductsFromV1Items(ctx context.Context, companyID uu
continue
}
if !v1ItemHasContent(item) {
errs = append(errs, fmt.Sprintf(
"No catalog product for EAN %s. Use a GTIN from the cloned/synced feed, or include title/description/images in the request.",
item.EAN,
))
continue
}
var newID uuid.UUID
insErr := s.Pool.QueryRow(ctx, `
INSERT INTO raw_products (company_id, gtin, raw_data, mapped_data, processing_status, is_processed)
+16
View File
@@ -38,3 +38,19 @@ func TestBuildMappedDataFromV1Item(t *testing.T) {
t.Fatalf("more=%s", b)
}
}
func TestBuildMappedDataFromV1ItemOmitsEmptyNulls(t *testing.T) {
mapped := BuildMappedDataFromV1Item(V1ProcessItem{EAN: "123"})
if _, ok := mapped["title"]; ok {
t.Fatalf("empty title must be omitted, got %v", mapped)
}
if _, ok := mapped["description"]; ok {
t.Fatalf("empty description must be omitted, got %v", mapped)
}
if !v1ItemHasContent(V1ProcessItem{EAN: "1", Title: "x"}) {
t.Fatal("title should count as content")
}
if v1ItemHasContent(V1ProcessItem{EAN: "1"}) {
t.Fatal("EAN-only must not count as content")
}
}