This commit is contained in:
2026-08-23 20:49:40 +02:00
parent 3b678ca857
commit f3d4fb56ed
31 changed files with 3608 additions and 111 deletions
@@ -0,0 +1,86 @@
package catalog
import (
"context"
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgconn"
)
// DefaultFormulaTarget is one freshly created category to seed formulas onto.
type DefaultFormulaTarget struct {
ID uuid.UUID
UniqueID string
Name string
}
// pgxExecer is the subset of pgxpool.Pool / pgx.Tx used here so the helper works
// inside the CSV import transaction as well as on the plain pool.
type pgxExecer interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}
// applyDefaultCategoryFormulasSQL fills prompt / title_template / description_template
// on newly created categories. Only empty columns are written, so a tenant that
// already authored a formula (or an A1 category carrying its Slovenian legacy
// prompt) is never overwritten.
const applyDefaultCategoryFormulasSQL = `
UPDATE categories AS c SET
prompt = CASE
WHEN c.prompt IS NULL OR c.prompt = '{}'::jsonb
THEN v.prompt::jsonb ELSE c.prompt END,
title_template = CASE
WHEN c.title_template IS NULL OR c.title_template::text IN ('null', '{}')
THEN NULLIF(v.title_template, '')::jsonb ELSE c.title_template END,
description_template = CASE
WHEN c.description_template IS NULL OR c.description_template::text IN ('null', '{}')
THEN NULLIF(v.description_template, '')::jsonb ELSE c.description_template END,
updated_at = now()
FROM (
SELECT * FROM unnest($2::uuid[], $3::text[], $4::text[], $5::text[])
AS t(id, prompt, title_template, description_template)
) AS v
WHERE c.id = v.id AND c.company_id = $1`
// ApplyDefaultCategoryFormulas gives newly created categories the platform-default
// formulas (English, same style as the A1 legacy templates) when they have none.
//
// New categories get a working title/description/meta formula out of the box
// instead of falling through to generic "1-3 paragraphs" copy. Existing rows are
// untouched — this only runs at create time. A1 categories keep their Slovenian
// prompts because seed-a1 writes them and the SQL never overwrites a set column.
func ApplyDefaultCategoryFormulas(ctx context.Context, db pgxExecer, companyID uuid.UUID, targets ...DefaultFormulaTarget) error {
if db == nil || len(targets) == 0 {
return nil
}
ids := make([]uuid.UUID, 0, len(targets))
prompts := make([]string, 0, len(targets))
titles := make([]string, 0, len(targets))
descs := make([]string, 0, len(targets))
for _, t := range targets {
if t.ID == uuid.Nil {
continue
}
def := aiprompts.DefaultCategoryFormula(t.UniqueID, t.Name)
if def.Overlay == "" {
continue
}
// "*" applies to every content language: the formula text is English while
// {{language}} still selects the output language at render time.
promptJSON, err := company.EncodeLangPromptMap(company.LangPromptMap{"*": def.Overlay})
if err != nil {
return err
}
ids = append(ids, t.ID)
prompts = append(prompts, string(promptJSON))
titles = append(titles, def.TitleTemplateJSON)
descs = append(descs, def.DescriptionTemplateJSON)
}
if len(ids) == 0 {
return nil
}
_, err := db.Exec(ctx, applyDefaultCategoryFormulasSQL, companyID, ids, prompts, titles, descs)
return err
}
+13
View File
@@ -237,6 +237,13 @@ func (s *Service) flushCategoryBatch(ctx context.Context, companyID uuid.UUID, b
if err != nil {
return err
}
nameByUID := make(map[string]string, len(insUIDs))
for i, uid := range insUIDs {
if i < len(insNames) {
nameByUID[uid] = insNames[i]
}
}
var seedFormulas []DefaultFormulaTarget
for rows.Next() {
var id uuid.UUID
var uid, path string
@@ -246,6 +253,7 @@ func (s *Service) flushCategoryBatch(ctx context.Context, companyID uuid.UUID, b
return err
}
known[uid] = categoryPathInfo{id: id, path: path, level: level}
seedFormulas = append(seedFormulas, DefaultFormulaTarget{ID: id, UniqueID: uid, Name: nameByUID[uid]})
res.Created++
}
err = rows.Err()
@@ -253,6 +261,11 @@ func (s *Service) flushCategoryBatch(ctx context.Context, companyID uuid.UUID, b
if err != nil {
return err
}
// Imported categories get the platform-default formula too, so an imported
// taxonomy enhances into formula-shaped copy without further setup.
if err := ApplyDefaultCategoryFormulas(ctx, tx, companyID, seedFormulas...); err != nil {
return err
}
pendingInserts = next
}
+8
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
@@ -167,6 +168,13 @@ func (s *Service) CreateCategory(ctx context.Context, companyID uuid.UUID, name,
if err != nil {
return nil, err
}
// New categories start with the platform-default formula so enhance produces
// formula-shaped copy instead of generic paragraphs. Non-fatal: a category
// without a stored formula still falls back to the default at enhance time.
if err := ApplyDefaultCategoryFormulas(ctx, s.Pool, companyID,
DefaultFormulaTarget{ID: id, UniqueID: uniqueID, Name: name}); err != nil {
log.Printf("catalog: default formulas company=%s category=%s err=%v", companyID, id, err)
}
return s.GetCategory(ctx, companyID, id)
}