Adds CRUD and preview APIs, RBAC permissions, seeded system templates, and migrates OTP email/SMS to template rendering. Co-authored-by: Cursor <cursoragent@cursor.com>
231 lines
6.5 KiB
Go
231 lines
6.5 KiB
Go
package repository
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"Yimaru-Backend/internal/ports"
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func NewEmailTemplateStore(s *Store) ports.EmailTemplateStore { return s }
|
|
|
|
func emailTemplateToDomain(
|
|
id int64,
|
|
slug string,
|
|
name string,
|
|
subject string,
|
|
bodyText string,
|
|
bodyHTML string,
|
|
variables []byte,
|
|
isSystem bool,
|
|
status string,
|
|
createdAt pgtype.Timestamptz,
|
|
updatedAt pgtype.Timestamptz,
|
|
) (domain.EmailTemplate, error) {
|
|
var vars []string
|
|
if len(variables) > 0 {
|
|
if err := json.Unmarshal(variables, &vars); err != nil {
|
|
return domain.EmailTemplate{}, err
|
|
}
|
|
}
|
|
if vars == nil {
|
|
vars = []string{}
|
|
}
|
|
|
|
return domain.EmailTemplate{
|
|
ID: id,
|
|
Slug: slug,
|
|
Name: name,
|
|
Subject: subject,
|
|
BodyText: bodyText,
|
|
BodyHTML: bodyHTML,
|
|
Variables: vars,
|
|
IsSystem: isSystem,
|
|
Status: status,
|
|
CreatedAt: createdAt.Time,
|
|
UpdatedAt: timePtr(updatedAt),
|
|
}, nil
|
|
}
|
|
|
|
func marshalEmailTemplateVariables(vars []string) ([]byte, error) {
|
|
if vars == nil {
|
|
vars = []string{}
|
|
}
|
|
return json.Marshal(vars)
|
|
}
|
|
|
|
func (s *Store) CreateEmailTemplate(ctx context.Context, input domain.CreateEmailTemplateInput) (domain.EmailTemplate, error) {
|
|
status := domain.EmailTemplateStatusActive
|
|
if input.Status != nil {
|
|
status = *input.Status
|
|
}
|
|
|
|
variablesJSON, err := marshalEmailTemplateVariables(input.Variables)
|
|
if err != nil {
|
|
return domain.EmailTemplate{}, err
|
|
}
|
|
|
|
row := s.conn.QueryRow(ctx, `
|
|
INSERT INTO email_templates (slug, name, subject, body_text, body_html, variables, is_system, status)
|
|
VALUES ($1, $2, $3, $4, $5, $6::jsonb, FALSE, $7)
|
|
RETURNING id, slug, name, subject, body_text, body_html, variables, is_system, status, created_at, updated_at
|
|
`, input.Slug, input.Name, input.Subject, input.BodyText, input.BodyHTML, variablesJSON, status)
|
|
|
|
return scanEmailTemplateRow(row)
|
|
}
|
|
|
|
func (s *Store) UpdateEmailTemplate(ctx context.Context, id int64, input domain.UpdateEmailTemplateInput) (domain.EmailTemplate, error) {
|
|
variablesSet := input.Variables != nil
|
|
variablesJSON, err := marshalEmailTemplateVariables(input.Variables)
|
|
if err != nil {
|
|
return domain.EmailTemplate{}, err
|
|
}
|
|
|
|
row := s.conn.QueryRow(ctx, `
|
|
UPDATE email_templates
|
|
SET name = COALESCE($2, name),
|
|
subject = COALESCE($3, subject),
|
|
body_text = COALESCE($4, body_text),
|
|
body_html = COALESCE($5, body_html),
|
|
variables = CASE WHEN $6::boolean THEN $7::jsonb ELSE variables END,
|
|
status = COALESCE($8, status),
|
|
updated_at = NOW()
|
|
WHERE id = $1
|
|
RETURNING id, slug, name, subject, body_text, body_html, variables, is_system, status, created_at, updated_at
|
|
`, id, input.Name, input.Subject, input.BodyText, input.BodyHTML, variablesSet, variablesJSON, input.Status)
|
|
|
|
return scanEmailTemplateRow(row)
|
|
}
|
|
|
|
func (s *Store) GetEmailTemplateByID(ctx context.Context, id int64, includeInactive bool) (domain.EmailTemplate, error) {
|
|
row := s.conn.QueryRow(ctx, `
|
|
SELECT id, slug, name, subject, body_text, body_html, variables, is_system, status, created_at, updated_at
|
|
FROM email_templates
|
|
WHERE id = $1
|
|
AND ($2::boolean = TRUE OR status = 'ACTIVE')
|
|
`, id, includeInactive)
|
|
|
|
return scanEmailTemplateRow(row)
|
|
}
|
|
|
|
func (s *Store) GetEmailTemplateBySlug(ctx context.Context, slug string, includeInactive bool) (domain.EmailTemplate, error) {
|
|
row := s.conn.QueryRow(ctx, `
|
|
SELECT id, slug, name, subject, body_text, body_html, variables, is_system, status, created_at, updated_at
|
|
FROM email_templates
|
|
WHERE slug = $1
|
|
AND ($2::boolean = TRUE OR status = 'ACTIVE')
|
|
`, slug, includeInactive)
|
|
|
|
return scanEmailTemplateRow(row)
|
|
}
|
|
|
|
func (s *Store) ListEmailTemplates(ctx context.Context, status *string, query *string, limit int32, offset int32) ([]domain.EmailTemplate, int64, error) {
|
|
rows, err := s.conn.Query(ctx, `
|
|
SELECT id, slug, name, subject, body_text, body_html, variables, is_system, status, created_at, updated_at
|
|
FROM email_templates
|
|
WHERE ($1::text IS NULL OR status = $1)
|
|
AND (
|
|
$2::text IS NULL
|
|
OR slug ILIKE '%' || $2 || '%'
|
|
OR name ILIKE '%' || $2 || '%'
|
|
OR subject ILIKE '%' || $2 || '%'
|
|
)
|
|
ORDER BY is_system DESC, name ASC, id ASC
|
|
LIMIT $3 OFFSET $4
|
|
`, toPgText(status), toPgText(query), limit, offset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
templates := make([]domain.EmailTemplate, 0)
|
|
for rows.Next() {
|
|
tmpl, err := scanEmailTemplateRows(rows)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
templates = append(templates, tmpl)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var totalCount int64
|
|
if err := s.conn.QueryRow(ctx, `
|
|
SELECT COUNT(*)
|
|
FROM email_templates
|
|
WHERE ($1::text IS NULL OR status = $1)
|
|
AND (
|
|
$2::text IS NULL
|
|
OR slug ILIKE '%' || $2 || '%'
|
|
OR name ILIKE '%' || $2 || '%'
|
|
OR subject ILIKE '%' || $2 || '%'
|
|
)
|
|
`, toPgText(status), toPgText(query)).Scan(&totalCount); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return templates, totalCount, nil
|
|
}
|
|
|
|
func (s *Store) DeleteEmailTemplate(ctx context.Context, id int64) error {
|
|
cmd, err := s.conn.Exec(ctx, `
|
|
DELETE FROM email_templates
|
|
WHERE id = $1 AND is_system = FALSE
|
|
`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cmd.RowsAffected() == 0 {
|
|
return pgx.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type emailTemplateScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanEmailTemplateRow(row emailTemplateScanner) (domain.EmailTemplate, error) {
|
|
var (
|
|
id int64
|
|
slug string
|
|
name string
|
|
subject string
|
|
bodyText string
|
|
bodyHTML string
|
|
variables []byte
|
|
isSystem bool
|
|
status string
|
|
createdAt pgtype.Timestamptz
|
|
updatedAt pgtype.Timestamptz
|
|
)
|
|
if err := row.Scan(&id, &slug, &name, &subject, &bodyText, &bodyHTML, &variables, &isSystem, &status, &createdAt, &updatedAt); err != nil {
|
|
return domain.EmailTemplate{}, err
|
|
}
|
|
return emailTemplateToDomain(id, slug, name, subject, bodyText, bodyHTML, variables, isSystem, status, createdAt, updatedAt)
|
|
}
|
|
|
|
func scanEmailTemplateRows(rows pgx.Rows) (domain.EmailTemplate, error) {
|
|
var (
|
|
id int64
|
|
slug string
|
|
name string
|
|
subject string
|
|
bodyText string
|
|
bodyHTML string
|
|
variables []byte
|
|
isSystem bool
|
|
status string
|
|
createdAt pgtype.Timestamptz
|
|
updatedAt pgtype.Timestamptz
|
|
)
|
|
if err := rows.Scan(&id, &slug, &name, &subject, &bodyText, &bodyHTML, &variables, &isSystem, &status, &createdAt, &updatedAt); err != nil {
|
|
return domain.EmailTemplate{}, err
|
|
}
|
|
return emailTemplateToDomain(id, slug, name, subject, bodyText, bodyHTML, variables, isSystem, status, createdAt, updatedAt)
|
|
}
|