package catalog import ( "context" "encoding/json" "fmt" "strings" "github.com/descrybe/descrybe-v2/apps/api/internal/company" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) const enhanceInputHashKey = "enhance_input_hash" // RepairWeakEnhanceHashesResult is the detailed report for weak-hash clearing. type RepairWeakEnhanceHashesResult struct { Cleared int64 Scanned int ClearedRawIDs []uuid.UUID } // RepairWeakEnhanceHashes clears enhance_input_hash from field_sources and // localized_content for processed products whose descriptions are weak / // title-echo / filler / invent-synthesize (so the next enhance cannot hash-skip // thin or heuristic priors). // Returns the number of products updated. func RepairWeakEnhanceHashes(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (int64, error) { res, err := RepairWeakEnhanceHashesDetailed(ctx, pool, companyID) return res.Cleared, err } // RepairWeakEnhanceHashesDetailed is RepairWeakEnhanceHashes plus scan/raw-id detail // for admin Fix A1 hygiene reporting. func RepairWeakEnhanceHashesDetailed(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (RepairWeakEnhanceHashesResult, error) { var out RepairWeakEnhanceHashesResult if pool == nil { return out, fmt.Errorf("catalog pool not configured") } if companyID == uuid.Nil { return out, ClientMsg("company_id is required") } tx, err := pool.Begin(ctx) if err != nil { return out, err } defer tx.Rollback(ctx) out, err = repairWeakEnhanceHashesTx(ctx, tx, companyID) if err != nil { return out, err } if err := tx.Commit(ctx); err != nil { return out, err } return out, nil } // RepairWeakEnhanceHashes clears weak enhance skip hashes for the company. func (s *Service) RepairWeakEnhanceHashes(ctx context.Context, companyID uuid.UUID) (int64, error) { if s == nil || s.Pool == nil { return 0, fmt.Errorf("catalog service not configured") } return RepairWeakEnhanceHashes(ctx, s.Pool, companyID) } func repairWeakEnhanceHashesTx(ctx context.Context, tx pgx.Tx, companyID uuid.UUID) (RepairWeakEnhanceHashesResult, error) { var out RepairWeakEnhanceHashesResult rows, err := tx.Query(ctx, ` SELECT id, raw_product_id, COALESCE(name, ''), COALESCE(description, ''), COALESCE(processed_name, ''), COALESCE(processed_description, ''), COALESCE(field_sources, '{}'::jsonb), COALESCE(localized_content, '{}'::jsonb) FROM processed_products WHERE company_id = $1`, companyID) if err != nil { return out, fmt.Errorf("list processed products: %w", err) } defer rows.Close() type pending struct { id uuid.UUID rawID uuid.UUID fs []byte loc []byte } var updates []pending for rows.Next() { out.Scanned++ var ( id, rawID uuid.UUID name, desc, processedName, processedDesc string fsRaw, locRaw []byte ) if err := rows.Scan(&id, &rawID, &name, &desc, &processedName, &processedDesc, &fsRaw, &locRaw); err != nil { return out, err } fs := map[string]any{} if len(fsRaw) > 0 { if err := json.Unmarshal(fsRaw, &fs); err != nil { return out, err } } if fs == nil { fs = map[string]any{} } loc, err := company.DecodeLocalizedContent(locRaw) if err != nil { return out, err } changed := false primaryDesc := strings.TrimSpace(processedDesc) if primaryDesc == "" { primaryDesc = desc } primaryName := strings.TrimSpace(processedName) if primaryName == "" { primaryName = name } if _, has := fs[enhanceInputHashKey]; has { if company.ShouldRefuseEnhanceHashSkip(primaryDesc, primaryName, name) { delete(fs, enhanceInputHashKey) changed = true } } for lang, fields := range loc { d := strings.TrimSpace(fields.ProcessedDescription) n := strings.TrimSpace(fields.ProcessedName) if n == "" { n = primaryName } if strings.TrimSpace(fields.EnhanceInputHash) == "" { continue } if company.ShouldRefuseEnhanceHashSkip(d, n, primaryName, name) { fields.EnhanceInputHash = "" loc[lang] = fields changed = true } } if !changed { continue } fsBytes, err := json.Marshal(fs) if err != nil { return out, err } locBytes, err := company.EncodeLocalizedContent(loc) if err != nil { return out, err } updates = append(updates, pending{id: id, rawID: rawID, fs: fsBytes, loc: locBytes}) } if err := rows.Err(); err != nil { return out, err } rows.Close() seenRaw := map[uuid.UUID]struct{}{} for _, u := range updates { ct, err := tx.Exec(ctx, ` UPDATE processed_products SET field_sources = $2::jsonb, localized_content = $3::jsonb, updated_at = now() WHERE id = $1 AND company_id = $4`, u.id, u.fs, u.loc, companyID) if err != nil { return out, fmt.Errorf("clear enhance hashes product=%s: %w", u.id, err) } if ct.RowsAffected() > 0 { out.Cleared++ if _, ok := seenRaw[u.rawID]; !ok { seenRaw[u.rawID] = struct{}{} out.ClearedRawIDs = append(out.ClearedRawIDs, u.rawID) } } } return out, nil }