2026-08-16 16:57:36 +02:00
|
|
|
package processing
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/catalog"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// FixCompanyCatalogOpts controls optional Fix A1 / catalog hygiene steps.
|
|
|
|
|
type FixCompanyCatalogOpts struct {
|
|
|
|
|
// BackfillCategories copies mapped_data category unique_ids onto empty
|
|
|
|
|
// processed_products.category.
|
|
|
|
|
BackfillCategories bool
|
|
|
|
|
// ReprocessSampleLimit caps recommended raw_product_ids in the response
|
|
|
|
|
// (0 = counts only, no sample ids).
|
|
|
|
|
ReprocessSampleLimit int
|
|
|
|
|
// AIPrompts optionally re-applies BuiltInDefaults product_enhance per content language.
|
|
|
|
|
AIPrompts *aiprompts.Service
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FixCompanyCatalogResult is the idempotent admin Fix A1 report.
|
|
|
|
|
type FixCompanyCatalogResult struct {
|
|
|
|
|
CompanyID uuid.UUID `json:"company_id"`
|
|
|
|
|
CompanyName string `json:"company_name"`
|
|
|
|
|
A1Cohort bool `json:"a1_cohort"`
|
|
|
|
|
|
|
|
|
|
CategoryAttributeOrphansRemoved int `json:"category_attribute_orphans_removed"`
|
|
|
|
|
CategoryAttributeLinks int `json:"category_attribute_links"`
|
|
|
|
|
|
|
|
|
|
CategoryPromptsUpdated int `json:"category_prompts_updated"`
|
|
|
|
|
CategoryPromptsAlreadyOK int `json:"category_prompts_already_ok"`
|
|
|
|
|
CategoryPromptsEmpty int `json:"category_prompts_empty_skipped"`
|
|
|
|
|
// Prompts aliases category_prompts_updated for flash.admin.fixA1Success {prompts}.
|
|
|
|
|
Prompts int `json:"prompts"`
|
|
|
|
|
|
|
|
|
|
ProductEnhanceLanguages int `json:"product_enhance_languages"`
|
|
|
|
|
|
|
|
|
|
WeakHashesCleared int `json:"weak_hashes_cleared"`
|
|
|
|
|
CategoriesBackfilled int `json:"categories_backfilled"`
|
2026-08-16 18:18:39 +02:00
|
|
|
// MappedCategoriesBackfilled is processed → mapped_data.category copies.
|
|
|
|
|
MappedCategoriesBackfilled int `json:"mapped_categories_backfilled"`
|
|
|
|
|
// Coverage after Fix (Products UI reads mapped_data.category for raw rows).
|
|
|
|
|
TaxonomyCategories int64 `json:"taxonomy_categories"`
|
|
|
|
|
MappedWithCategory int64 `json:"mapped_with_category"`
|
|
|
|
|
MappedWithoutCategory int64 `json:"mapped_without_category"`
|
|
|
|
|
ProcessedWithCategory int64 `json:"processed_with_category"`
|
|
|
|
|
ProcessedWithoutCategory int64 `json:"processed_without_category"`
|
2026-08-16 16:57:36 +02:00
|
|
|
// Hashes / Categories alias the long names for flash.admin.fixA1Success.
|
|
|
|
|
Hashes int `json:"hashes"`
|
|
|
|
|
Categories int `json:"categories"`
|
2026-08-16 18:18:39 +02:00
|
|
|
// MappedBackfilled aliases mapped_categories_backfilled for flash {mapped_backfilled}.
|
|
|
|
|
MappedBackfilled int `json:"mapped_backfilled"`
|
2026-08-16 16:57:36 +02:00
|
|
|
|
|
|
|
|
DescriptionsNormalized int `json:"descriptions_normalized"`
|
|
|
|
|
DescriptionsBackfilled int `json:"descriptions_backfilled"`
|
|
|
|
|
MappedDescriptionsFlat int `json:"mapped_descriptions_flattened"`
|
|
|
|
|
MetaBackfilled int `json:"meta_backfilled"`
|
|
|
|
|
AttributesSanitized int64 `json:"attributes_sanitized"`
|
|
|
|
|
ProductsScanned int `json:"products_scanned"`
|
|
|
|
|
|
|
|
|
|
ReprocessNeededCount int `json:"reprocess_needed_count"`
|
|
|
|
|
ReprocessSampleRawProductIDs []uuid.UUID `json:"reprocess_sample_raw_product_ids,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FixCompanyCatalog runs the admin Fix A1 / catalog hygiene sequence in place.
|
|
|
|
|
// Never clears the catalog or uses A1 as a clone destination.
|
|
|
|
|
// Step 2 applies RepairCompanyCategoryEnhancePrompts (same repaired map as
|
|
|
|
|
// RepairA1DemoCategoryEnhancePrompts / cmd/repair-category-prompts -apply).
|
|
|
|
|
// Legacy converters + weak-hash clear live in FixCatalogHygieneWithIDs.
|
|
|
|
|
func FixCompanyCatalog(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID, companyName string, a1Cohort bool, opts FixCompanyCatalogOpts) (FixCompanyCatalogResult, error) {
|
|
|
|
|
out := FixCompanyCatalogResult{
|
|
|
|
|
CompanyID: companyID,
|
|
|
|
|
CompanyName: companyName,
|
|
|
|
|
A1Cohort: a1Cohort,
|
|
|
|
|
}
|
|
|
|
|
if pool == nil {
|
|
|
|
|
return out, fmt.Errorf("nil pool")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
orphans, links, err := catalog.EnsureCategoryAttributeLinks(ctx, pool, companyID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
out.CategoryAttributeOrphansRemoved = orphans
|
|
|
|
|
out.CategoryAttributeLinks = links
|
|
|
|
|
|
|
|
|
|
updated, alreadyOK, emptySkipped, err := catalog.RepairCompanyCategoryEnhancePrompts(ctx, pool, companyID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
out.CategoryPromptsUpdated = updated
|
|
|
|
|
out.CategoryPromptsAlreadyOK = alreadyOK
|
|
|
|
|
out.CategoryPromptsEmpty = emptySkipped
|
|
|
|
|
|
|
|
|
|
if opts.AIPrompts != nil {
|
|
|
|
|
n, err := opts.AIPrompts.ApplyBuiltInProductEnhance(ctx, companyID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, fmt.Errorf("product_enhance templates: %w", err)
|
|
|
|
|
}
|
|
|
|
|
out.ProductEnhanceLanguages = n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hygiene, clearedIDs, err := FixCatalogHygieneWithIDs(ctx, pool, companyID, opts.BackfillCategories)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
out.WeakHashesCleared = hygiene.WeakHashesCleared
|
|
|
|
|
out.CategoriesBackfilled = hygiene.CategoriesBackfilled
|
|
|
|
|
out.DescriptionsNormalized = hygiene.DescriptionsNormalized
|
|
|
|
|
out.DescriptionsBackfilled = hygiene.DescriptionsBackfilled
|
|
|
|
|
out.MappedDescriptionsFlat = hygiene.MappedDescriptionsFlat
|
|
|
|
|
out.MetaBackfilled = hygiene.MetaBackfilled
|
|
|
|
|
out.ProductsScanned = hygiene.ProductsScanned
|
|
|
|
|
|
2026-08-16 18:18:39 +02:00
|
|
|
if opts.BackfillCategories {
|
|
|
|
|
mappedN, err := BackfillMappedCategoriesFromProcessed(ctx, pool, companyID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, fmt.Errorf("mapped category backfill: %w", err)
|
|
|
|
|
}
|
|
|
|
|
out.MappedCategoriesBackfilled = int(mappedN)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 16:57:36 +02:00
|
|
|
sanitized, err := BackfillCompanyProcessedAttributes(ctx, pool, companyID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, fmt.Errorf("sanitize attributes: %w", err)
|
|
|
|
|
}
|
|
|
|
|
out.AttributesSanitized = sanitized
|
|
|
|
|
|
|
|
|
|
needed, sample, err := RecommendReprocessRawProductIDs(ctx, pool, companyID, clearedIDs, opts.ReprocessSampleLimit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
out.ReprocessNeededCount = needed
|
|
|
|
|
out.ReprocessSampleRawProductIDs = sample
|
|
|
|
|
|
2026-08-16 18:18:39 +02:00
|
|
|
mWith, mWithout, taxonomy, err := CountMappedCategoryCoverage(ctx, pool, companyID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
out.MappedWithCategory = mWith
|
|
|
|
|
out.MappedWithoutCategory = mWithout
|
|
|
|
|
out.TaxonomyCategories = taxonomy
|
|
|
|
|
pWith, pWithout, err := CountProcessedCategoryCoverage(ctx, pool, companyID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
out.ProcessedWithCategory = pWith
|
|
|
|
|
out.ProcessedWithoutCategory = pWithout
|
|
|
|
|
|
|
|
|
|
// Short keys for flash.admin.fixA1Success {prompts,hashes,categories,mapped_backfilled}.
|
2026-08-16 16:57:36 +02:00
|
|
|
out.Prompts = out.CategoryPromptsUpdated
|
|
|
|
|
out.Hashes = out.WeakHashesCleared
|
|
|
|
|
out.Categories = out.CategoriesBackfilled
|
2026-08-16 18:18:39 +02:00
|
|
|
out.MappedBackfilled = out.MappedCategoriesBackfilled
|
2026-08-16 16:57:36 +02:00
|
|
|
return out, nil
|
|
|
|
|
}
|