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) }