179 lines
5.9 KiB
Go
179 lines
5.9 KiB
Go
package processing
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// CategoryBackfillResult is the outcome of BackfillProcessedCategoriesFromMapped.
|
|
type CategoryBackfillResult struct {
|
|
Updated int64 `json:"updated"`
|
|
}
|
|
|
|
// BackfillProcessedCategoriesFromMapped fills empty processed_products.category from
|
|
// raw_products.mapped_data when a usable unique_id is present. When the company has
|
|
// categories rows, only known unique_ids are applied (same rule as processOne filter).
|
|
// Safe for admin "Fix catalog" and migrator reuse. Does not invent categories via vector.
|
|
func BackfillProcessedCategoriesFromMapped(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (CategoryBackfillResult, error) {
|
|
var out CategoryBackfillResult
|
|
if pool == nil {
|
|
return out, fmt.Errorf("category backfill: nil pool")
|
|
}
|
|
if companyID == uuid.Nil {
|
|
return out, fmt.Errorf("category backfill: empty company id")
|
|
}
|
|
|
|
ct, err := pool.Exec(ctx, `
|
|
WITH src AS (
|
|
SELECT
|
|
p.id AS processed_id,
|
|
BTRIM(COALESCE(
|
|
CASE
|
|
WHEN jsonb_typeof(r.mapped_data->'category') IN ('string', 'number')
|
|
THEN NULLIF(BTRIM(r.mapped_data->>'category'), '')
|
|
ELSE NULL
|
|
END,
|
|
NULLIF(BTRIM(r.mapped_data->>'category_unique_id'), ''),
|
|
NULLIF(BTRIM(r.mapped_data->'category'->>'unique_id'), ''),
|
|
NULLIF(BTRIM(r.mapped_data->'category'->>'category_unique_id'), ''),
|
|
NULLIF(BTRIM(r.mapped_data->'category'->>'#text'), ''),
|
|
NULLIF(BTRIM(r.mapped_data->'category'->>'id'), ''),
|
|
NULLIF(BTRIM(r.mapped_data->'category'->>'value'), ''),
|
|
NULLIF(BTRIM(r.mapped_data->'category'->>'code'), '')
|
|
)) AS cat_raw
|
|
FROM processed_products p
|
|
INNER JOIN raw_products r ON r.id = p.raw_product_id AND r.company_id = p.company_id
|
|
WHERE p.company_id = $1
|
|
AND (
|
|
COALESCE(NULLIF(BTRIM(p.category), ''), '') = ''
|
|
OR LOWER(BTRIM(p.category)) = 'none'
|
|
)
|
|
),
|
|
mapped AS (
|
|
SELECT
|
|
processed_id,
|
|
CASE
|
|
WHEN cat_raw IS NULL OR cat_raw = '' OR LOWER(cat_raw) = 'none' THEN NULL
|
|
ELSE cat_raw
|
|
END AS cat_uid
|
|
FROM src
|
|
),
|
|
eligible AS (
|
|
SELECT m.processed_id, m.cat_uid
|
|
FROM mapped m
|
|
WHERE m.cat_uid IS NOT NULL
|
|
AND (
|
|
NOT EXISTS (SELECT 1 FROM categories c WHERE c.company_id = $1)
|
|
OR EXISTS (
|
|
SELECT 1 FROM categories c
|
|
WHERE c.company_id = $1
|
|
AND BTRIM(c.unique_id) = m.cat_uid
|
|
)
|
|
)
|
|
)
|
|
UPDATE processed_products p
|
|
SET category = e.cat_uid,
|
|
field_sources = jsonb_set(
|
|
COALESCE(p.field_sources, '{}'::jsonb),
|
|
'{category}',
|
|
'"mapped_backfill"'::jsonb,
|
|
true
|
|
),
|
|
updated_at = now()
|
|
FROM eligible e
|
|
WHERE p.id = e.processed_id`, companyID)
|
|
if err != nil {
|
|
return out, fmt.Errorf("category backfill: %w", err)
|
|
}
|
|
out.Updated = ct.RowsAffected()
|
|
return out, nil
|
|
}
|
|
|
|
// BackfillMappedCategoriesFromProcessed copies processed_products.category into
|
|
// raw_products.mapped_data.category when mapped category is empty. Legacy A1 dumps
|
|
// often store category only on processed rows; feed mappings do not map category,
|
|
// so Fix Catalog / re-seed must push processed → mapped for Products UI coverage.
|
|
// Does not invent categories.
|
|
func BackfillMappedCategoriesFromProcessed(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (int64, error) {
|
|
if pool == nil {
|
|
return 0, fmt.Errorf("mapped category backfill: nil pool")
|
|
}
|
|
if companyID == uuid.Nil {
|
|
return 0, fmt.Errorf("mapped category backfill: empty company id")
|
|
}
|
|
ct, err := pool.Exec(ctx, `
|
|
UPDATE raw_products r
|
|
SET mapped_data = jsonb_set(
|
|
COALESCE(r.mapped_data, '{}'::jsonb),
|
|
'{category}',
|
|
to_jsonb(p.category),
|
|
true
|
|
),
|
|
updated_at = now()
|
|
FROM processed_products p
|
|
WHERE p.raw_product_id = r.id
|
|
AND p.company_id = $1
|
|
AND r.company_id = $1
|
|
AND COALESCE(NULLIF(trim(p.category), ''), '') <> ''
|
|
AND lower(trim(p.category)) <> 'none'
|
|
AND COALESCE(NULLIF(trim(r.mapped_data->>'category'), ''), '') = ''`, companyID)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("mapped category backfill: %w", err)
|
|
}
|
|
return ct.RowsAffected(), nil
|
|
}
|
|
|
|
// CountMappedCategoryCoverage returns how many raw_products rows have a non-empty
|
|
// mapped_data.category vs empty, plus taxonomy row count for the company.
|
|
func CountMappedCategoryCoverage(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (withCat, withoutCat, taxonomy int64, err error) {
|
|
if pool == nil {
|
|
return 0, 0, 0, fmt.Errorf("mapped category coverage: nil pool")
|
|
}
|
|
err = pool.QueryRow(ctx, `
|
|
SELECT
|
|
COUNT(*) FILTER (
|
|
WHERE COALESCE(NULLIF(trim(mapped_data->>'category'), ''), '') <> ''
|
|
),
|
|
COUNT(*) FILTER (
|
|
WHERE COALESCE(NULLIF(trim(mapped_data->>'category'), ''), '') = ''
|
|
)
|
|
FROM raw_products
|
|
WHERE company_id = $1`, companyID).Scan(&withCat, &withoutCat)
|
|
if err != nil {
|
|
return 0, 0, 0, fmt.Errorf("mapped category coverage: %w", err)
|
|
}
|
|
err = pool.QueryRow(ctx, `
|
|
SELECT COUNT(*) FROM categories WHERE company_id = $1`, companyID).Scan(&taxonomy)
|
|
if err != nil {
|
|
return withCat, withoutCat, 0, fmt.Errorf("taxonomy count: %w", err)
|
|
}
|
|
return withCat, withoutCat, taxonomy, nil
|
|
}
|
|
|
|
// CountProcessedCategoryCoverage returns how many processed rows for companyID have
|
|
// a non-empty category vs empty/"none".
|
|
func CountProcessedCategoryCoverage(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (withCat, withoutCat int64, err error) {
|
|
if pool == nil {
|
|
return 0, 0, fmt.Errorf("category coverage: nil pool")
|
|
}
|
|
err = pool.QueryRow(ctx, `
|
|
SELECT
|
|
COUNT(*) FILTER (
|
|
WHERE COALESCE(NULLIF(BTRIM(category), ''), '') <> ''
|
|
AND LOWER(BTRIM(category)) <> 'none'
|
|
),
|
|
COUNT(*) FILTER (
|
|
WHERE COALESCE(NULLIF(BTRIM(category), ''), '') = ''
|
|
OR LOWER(BTRIM(category)) = 'none'
|
|
)
|
|
FROM processed_products
|
|
WHERE company_id = $1`, companyID).Scan(&withCat, &withoutCat)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("category coverage: %w", err)
|
|
}
|
|
return withCat, withoutCat, nil
|
|
}
|