This commit is contained in:
2026-08-16 16:57:36 +02:00
parent 96f8c1115c
commit 532d439c41
106 changed files with 10147 additions and 494 deletions
+220
View File
@@ -0,0 +1,220 @@
package catalog
import (
"context"
"fmt"
"strings"
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// Canonical Postgres id for the migrated A1 Slovenija tenant (seed-a1 default).
const a1CompanyID = "604f23a8-b66e-4b21-8b45-0d72b68f4790"
// platformDemoCompanyName matches migrator / seed-demo standalone demo tenant.
const platformDemoCompanyName = "Platform Demo"
// RepairCategoryEnhancePromptsResult is the dry-run / apply summary for
// RepairA1DemoCategoryEnhancePrompts.
type RepairCategoryEnhancePromptsResult struct {
CompaniesScanned int `json:"companies_scanned"`
CategoriesSeen int `json:"categories_seen"`
WouldUpdate int `json:"would_update"`
Updated int `json:"updated"`
AlreadyOK int `json:"already_ok"`
EmptySkipped int `json:"empty_skipped"`
ByCompany map[string]int `json:"by_company"`
DryRun bool `json:"dry_run"`
}
// RepairA1DemoCategoryEnhancePrompts replaces legacy HTML marketing categories.prompt
// values for A1 Slovenija + Platform Demo with aiprompts.CategoryEnhanceUserTemplate.
//
// LOCAL repair only (idempotent):
// - Writes JSON-compatible user overlays under both "sl" (prompt->>'sl') and "*"
// (LangPromptAny) so language stays via {{language}}, not hardcoded-only copy.
// - Touches ONLY categories.prompt — never title_template / description_template
// (unique name/description formulas stay intact; AppendFormulaConstraints encodes
// them as plain-text instructions at enhance render time).
// - dryRun=true: count WouldUpdate only; dryRun=false: apply and set Updated.
//
// Entrypoint: go run ./cmd/repair-category-prompts (-dry-run | -apply).
func RepairA1DemoCategoryEnhancePrompts(ctx context.Context, pool *pgxpool.Pool, dryRun bool) (RepairCategoryEnhancePromptsResult, error) {
out := RepairCategoryEnhancePromptsResult{
DryRun: dryRun,
ByCompany: map[string]int{},
}
if pool == nil {
return out, fmt.Errorf("postgres pool is required")
}
a1ID, err := uuid.Parse(a1CompanyID)
if err != nil {
return out, fmt.Errorf("a1 company id: %w", err)
}
companyRows, err := pool.Query(ctx, `
SELECT id, name
FROM companies
WHERE id = $1
OR name = $2
OR COALESCE(legacy_company_id, '') = $3
ORDER BY name`, a1ID, platformDemoCompanyName, billing.A1LegacyCompanyID)
if err != nil {
return out, fmt.Errorf("list target companies: %w", err)
}
defer companyRows.Close()
type co struct {
id uuid.UUID
name string
}
companies := make([]co, 0, 2)
for companyRows.Next() {
var c co
if err := companyRows.Scan(&c.id, &c.name); err != nil {
return out, err
}
companies = append(companies, c)
}
if err := companyRows.Err(); err != nil {
return out, err
}
out.CompaniesScanned = len(companies)
if len(companies) == 0 {
return out, fmt.Errorf("no A1 / Platform Demo companies found")
}
want, err := repairedCategoryEnhancePromptMap()
if err != nil {
return out, err
}
for _, c := range companies {
n, err := repairCompanyCategoryEnhancePrompts(ctx, pool, c.id, c.name, want, dryRun, &out)
if err != nil {
return out, err
}
if n > 0 {
out.ByCompany[c.name] = n
}
}
return out, nil
}
// repairedCategoryEnhancePromptMap is the canonical stored shape: "sl" (prompt->>'sl'
// for A1/Demo) plus LangPromptAny ("*") so PromptForLanguage resolves for any content
// language. Copy stays language-agnostic via {{language}} — not hardcoded Slovenian.
func repairedCategoryEnhancePromptMap() (company.LangPromptMap, error) {
tpl := strings.TrimSpace(aiprompts.CategoryEnhanceUserTemplate)
if tpl == "" {
return nil, fmt.Errorf("CategoryEnhanceUserTemplate is empty")
}
return company.LangPromptMap{
"sl": tpl,
company.LangPromptAny: tpl,
}, nil
}
func categoryEnhancePromptMapOK(m company.LangPromptMap, want company.LangPromptMap) bool {
if !company.HasAnyPrompt(m) || !company.HasAnyPrompt(want) {
return false
}
tpl := strings.TrimSpace(want[company.LangPromptAny])
if tpl == "" {
tpl = strings.TrimSpace(want["sl"])
}
if tpl == "" {
return false
}
// Accept already-repaired maps: every non-empty value equals the shared template
// and LangPromptAny (or legacy sl-only) is present.
hasKey := false
for lang, p := range m {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if p != tpl {
return false
}
if lang == company.LangPromptAny || lang == "sl" {
hasKey = true
}
}
return hasKey
}
func repairCompanyCategoryEnhancePrompts(
ctx context.Context,
pool *pgxpool.Pool,
companyID uuid.UUID,
companyName string,
want company.LangPromptMap,
dryRun bool,
out *RepairCategoryEnhancePromptsResult,
) (int, error) {
rows, err := pool.Query(ctx, `
SELECT id, COALESCE(prompt, '{}'::jsonb)
FROM categories
WHERE company_id = $1`, companyID)
if err != nil {
return 0, fmt.Errorf("list categories for %s: %w", companyName, err)
}
defer rows.Close()
updatedHere := 0
for rows.Next() {
var id uuid.UUID
var raw []byte
if err := rows.Scan(&id, &raw); err != nil {
return updatedHere, err
}
out.CategoriesSeen++
m, err := company.DecodeLangPromptMap(raw)
if err != nil {
return updatedHere, fmt.Errorf("decode prompt category=%s company=%s: %w", id, companyName, err)
}
if !company.HasAnyPrompt(m) {
out.EmptySkipped++
continue
}
if categoryEnhancePromptMapOK(m, want) {
out.AlreadyOK++
continue
}
out.WouldUpdate++
if dryRun {
updatedHere++
continue
}
cleaned, err := company.SanitizeLangPromptMap(want, MaxCategoryPromptRunes)
if err != nil {
return updatedHere, fmt.Errorf("sanitize prompt category=%s: %w", id, err)
}
encoded, err := company.EncodeLangPromptMap(cleaned)
if err != nil {
return updatedHere, err
}
ct, err := pool.Exec(ctx, `
UPDATE categories
SET prompt = $3::jsonb, updated_at = now()
WHERE id = $1 AND company_id = $2`, id, companyID, string(encoded))
if err != nil {
return updatedHere, fmt.Errorf("update prompt category=%s: %w", id, err)
}
if ct.RowsAffected() == 0 {
return updatedHere, fmt.Errorf("category %s not updated (company mismatch?)", id)
}
out.Updated++
updatedHere++
}
return updatedHere, rows.Err()
}