package catalog import ( "context" "fmt" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // EnsureCategoryAttributeLinks removes orphan category_attributes rows (missing // category or attribute) without wiping taxonomy or valid links. Returns orphans // removed and remaining link count. func EnsureCategoryAttributeLinks(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (orphansRemoved int, links int, err error) { if pool == nil { return 0, 0, fmt.Errorf("nil pool") } ct, err := pool.Exec(ctx, ` DELETE FROM category_attributes ca WHERE ca.company_id = $1 AND ( NOT EXISTS ( SELECT 1 FROM categories c WHERE c.company_id = ca.company_id AND c.unique_id = ca.category_unique_id ) OR NOT EXISTS ( SELECT 1 FROM attributes a WHERE a.id = ca.attribute_id AND a.company_id = ca.company_id ) )`, companyID) if err != nil { return 0, 0, fmt.Errorf("purge orphan category_attributes: %w", err) } orphansRemoved = int(ct.RowsAffected()) if err := pool.QueryRow(ctx, ` SELECT count(*)::int FROM category_attributes WHERE company_id = $1`, companyID).Scan(&links); err != nil { return orphansRemoved, 0, err } return orphansRemoved, links, nil } // RepairCompanyCategoryEnhancePrompts is the company-scoped variant of // RepairA1DemoCategoryEnhancePrompts: loads wp_product_categories.sql when // available (upload bytes, SEED_A1_WP_CATEGORIES / Downloads), else // a1-category-prompts.json, splits legacy combined overlays into role sections, // and force-applies WP dump matches. Falls back to CategoryEnhanceUserTemplate. // Idempotent when unchanged. func RepairCompanyCategoryEnhancePrompts(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (updated, alreadyOK, emptySkipped int, err error) { res, err := RepairCompanyCategoryEnhancePromptsWithOptions(ctx, pool, companyID, RepairA1DemoOptions{}) if err != nil { return 0, 0, 0, err } return res.Updated, res.AlreadyOK, res.EmptySkipped, nil } // RepairCompanyCategoryEnhancePromptsWithOptions applies category enhance prompt // repair for one company. Pass WPCategoriesSQL to force-apply an uploaded dump // (Admin Sync A1); empty opts keep auto-detect fallback. func RepairCompanyCategoryEnhancePromptsWithOptions(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID, opts RepairA1DemoOptions) (RepairCategoryEnhancePromptsResult, error) { out := RepairCategoryEnhancePromptsResult{ ByCompany: map[string]int{}, } if pool == nil { return out, fmt.Errorf("nil pool") } if len(opts.WPCategoriesSQL) > 0 { if _, _, err := LoadWPCategoryPromptOverlaysFromBytes(opts.WPCategoriesSQL); err != nil { return out, fmt.Errorf("wp_product_categories upload: %w", err) } } seedByNorm, seedByUID, seedMeta := resolveCategoryPromptOverlays(opts) out.SeedPath = seedMeta.Path out.WPCategoriesPath = seedMeta.WPPath out.SeedEntries = seedMeta.Entries forceFromSeed := seedMeta.ForceFromSeed if opts.ForceFromSeed != nil { forceFromSeed = *opts.ForceFromSeed } if _, err := repairCompanyCategoryEnhancePrompts(ctx, pool, companyID, companyID.String(), seedByNorm, seedByUID, false, forceFromSeed, &out); err != nil { return out, err } return out, nil } // ResolvePlatformDemoCompanyID finds the Platform Demo sandbox (never A1 cohort). func ResolvePlatformDemoCompanyID(ctx context.Context, pool *pgxpool.Pool) (uuid.UUID, error) { var id uuid.UUID err := pool.QueryRow(ctx, ` SELECT c.id FROM companies c WHERE c.name = $1 AND COALESCE(c.legacy_company_id, '') <> $2 ORDER BY c.created_at ASC LIMIT 1`, platformDemoCompanyName, billing.A1LegacyCompanyID).Scan(&id) if err != nil { return uuid.Nil, fmt.Errorf("platform demo company not found: %w", err) } return id, nil }