222 lines
6.4 KiB
Go
222 lines
6.4 KiB
Go
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
|
|
// (role-sectioned title/description/meta/attributes USER overlay).
|
|
//
|
|
// 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/meta 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()
|
|
}
|