package admin import ( "context" "encoding/json" "errors" "regexp" "strings" "unicode/utf8" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/yuxingu/digital-psychology/apps/api/internal/repository" ) var ( ErrInvalidFunnelDefinition = errors.New("invalid funnel definition") ErrFunnelDefinitionConflict = errors.New("funnel definition code conflict") funnelDefinitionCodeRe = regexp.MustCompile(`^[a-z][a-z0-9_]{1,62}$`) ) // FunnelDefinitionWriteBody is JSON for create/update. type FunnelDefinitionWriteBody struct { Code string `json:"code"` Title string `json:"title"` Active bool `json:"active"` } // CreateFunnelDefinition validates, inserts, audits. func (s *Service) CreateFunnelDefinition(ctx context.Context, adminID uuid.UUID, body FunnelDefinitionWriteBody) (*repository.FunnelDefinitionRow, error) { in, err := normalizeFunnelDefinitionWrite(body) if err != nil { return nil, err } meta, _ := json.Marshal(map[string]any{"code": in.Code, "active": in.Active}) row, err := s.Repo.CreateFunnelDefinitionWithAudit(ctx, adminID, in, meta) if repository.FunnelDefinitionCodeConflict(err) { return nil, ErrFunnelDefinitionConflict } return row, err } // UpdateFunnelDefinition validates, updates, audits. func (s *Service) UpdateFunnelDefinition(ctx context.Context, adminID, id uuid.UUID, body FunnelDefinitionWriteBody) (*repository.FunnelDefinitionRow, error) { in, err := normalizeFunnelDefinitionWrite(body) if err != nil { return nil, err } meta, _ := json.Marshal(map[string]any{"code": in.Code, "active": in.Active}) row, err := s.Repo.UpdateFunnelDefinitionWithAudit(ctx, adminID, id, in, meta) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrFunnelDefinitionNotFound } if repository.FunnelDefinitionCodeConflict(err) { return nil, ErrFunnelDefinitionConflict } return row, err } func normalizeFunnelDefinitionWrite(body FunnelDefinitionWriteBody) (repository.FunnelDefinitionWriteInput, error) { code := strings.TrimSpace(body.Code) title := strings.TrimSpace(body.Title) if !funnelDefinitionCodeRe.MatchString(code) { return repository.FunnelDefinitionWriteInput{}, ErrInvalidFunnelDefinition } if title == "" || utf8.RuneCountInString(title) > 128 { return repository.FunnelDefinitionWriteInput{}, ErrInvalidFunnelDefinition } return repository.FunnelDefinitionWriteInput{Code: code, Title: title, Active: body.Active}, nil }