import type { SupabaseClient } from '@supabase/supabase-js'; import type { Database } from '@kit/supabase/database'; import type { CreateNewsletterInput, UpdateNewsletterInput, } from '../schema/newsletter.schema'; /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Template variable substitution. * Replaces {{variable}} placeholders with actual values. */ function substituteVariables( template: string, variables: Record, ): string { let result = template; for (const [key, value] of Object.entries(variables)) { result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value); } return result; } export function createNewsletterApi(client: SupabaseClient) { const db = client; return { // --- Templates --- async listTemplates( accountId: string, opts?: { search?: string; page?: number; pageSize?: number }, ) { const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 25; let query = client .from('newsletter_templates') .select('*', { count: 'exact' }) .eq('account_id', accountId) .order('name'); if (opts?.search) { query = query.ilike('name', `%${opts.search}%`); } query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize, totalPages: Math.ceil((count ?? 0) / pageSize), }; }, async createTemplate(input: { accountId: string; name: string; subject: string; bodyHtml: string; bodyText?: string; variables?: string[]; }) { const { data, error } = await client .from('newsletter_templates') .insert({ account_id: input.accountId, name: input.name, subject: input.subject, body_html: input.bodyHtml, body_text: input.bodyText, variables: input.variables ?? [], }) .select() .single(); if (error) throw error; return data; }, // --- Newsletters --- async listNewsletters( accountId: string, opts?: { search?: string; status?: string; page?: number; pageSize?: number; }, ) { const page = opts?.page ?? 1; const pageSize = opts?.pageSize ?? 25; let query = client .from('newsletters') .select('*', { count: 'exact' }) .eq('account_id', accountId) .order('created_at', { ascending: false }); if (opts?.search) { query = query.ilike('subject', `%${opts.search}%`); } if (opts?.status) { query = query.eq('status', opts.status); } query = query.range((page - 1) * pageSize, page * pageSize - 1); const { data, error, count } = await query; if (error) throw error; return { data: data ?? [], total: count ?? 0, page, pageSize, totalPages: Math.ceil((count ?? 0) / pageSize), }; }, async createNewsletter(input: CreateNewsletterInput, userId: string) { const { data, error } = await client .from('newsletters') .insert({ account_id: input.accountId, template_id: input.templateId, subject: input.subject, body_html: input.bodyHtml, body_text: input.bodyText, status: input.scheduledAt ? 'scheduled' : 'draft', scheduled_at: input.scheduledAt, created_by: userId, }) .select() .single(); if (error) throw error; return data; }, async updateNewsletter(input: UpdateNewsletterInput) { const update: Record = {}; if (input.subject !== undefined) update.subject = input.subject; if (input.bodyHtml !== undefined) update.body_html = input.bodyHtml; if (input.bodyText !== undefined) update.body_text = input.bodyText; if (input.templateId !== undefined) update.template_id = input.templateId || null; if (input.scheduledAt !== undefined) update.scheduled_at = input.scheduledAt || null; const { data, error } = await client .from('newsletters') .update(update) .eq('id', input.newsletterId) .select() .single(); if (error) throw error; return data; }, async getNewsletter(newsletterId: string) { const { data, error } = await client .from('newsletters') .select('*') .eq('id', newsletterId) .single(); if (error) throw error; return data; }, // --- Recipients --- async addRecipientsFromMembers( newsletterId: string, accountId: string, filter?: { status?: string[]; hasEmail?: boolean }, ) { let query = client .from('members') .select('id, first_name, last_name, email') .eq('account_id', accountId) .not('email', 'is', null) .neq('email', ''); if (filter?.status && filter.status.length > 0) { query = query.in( 'status', filter.status as Database['public']['Enums']['membership_status'][], ); } const { data: members, error } = await query; if (error) throw error; const recipients = (members ?? []).map((m: any) => ({ newsletter_id: newsletterId, member_id: m.id, email: m.email, name: `${m.first_name} ${m.last_name}`, status: 'pending', })); if (recipients.length > 0) { const { error: insertError } = await client .from('newsletter_recipients') .insert(recipients); if (insertError) throw insertError; } // Update newsletter total await client .from('newsletters') .update({ total_recipients: recipients.length }) .eq('id', newsletterId); return recipients.length; }, async getRecipients(newsletterId: string) { const { data, error } = await client .from('newsletter_recipients') .select('*') .eq('newsletter_id', newsletterId) .order('name'); if (error) throw error; return data ?? []; }, /** * Dispatch newsletter — sends to all pending recipients. * Uses @kit/mailers under the hood. Rate-limited. * This is a preview implementation; actual dispatch needs the mailer service. */ async dispatch(newsletterId: string) { const newsletter = await this.getNewsletter(newsletterId); const recipients = await this.getRecipients(newsletterId); const pending = recipients.filter((r: any) => r.status === 'pending'); // Mark as sending await client .from('newsletters') .update({ status: 'sending' }) .eq('id', newsletterId); let sentCount = 0; let failedCount = 0; for (const recipient of pending) { try { // Substitute variables in the body const personalizedHtml = substituteVariables(newsletter.body_html, { first_name: (recipient.name ?? '').split(' ')[0] ?? '', name: recipient.name ?? '', email: recipient.email ?? '', }); // TODO: Use @kit/mailers to actually send // await mailer.send({ to: recipient.email, subject: newsletter.subject, html: personalizedHtml }); await client .from('newsletter_recipients') .update({ status: 'sent', sent_at: new Date().toISOString(), }) .eq('id', recipient.id); sentCount++; } catch (err) { await client .from('newsletter_recipients') .update({ status: 'failed', error_message: err instanceof Error ? err.message : 'Unknown error', }) .eq('id', recipient.id); failedCount++; } } // Update newsletter totals await client .from('newsletters') .update({ status: failedCount === pending.length ? 'failed' : 'sent', sent_at: new Date().toISOString(), sent_count: sentCount, failed_count: failedCount, }) .eq('id', newsletterId); return { sentCount, failedCount }; }, // Utility substituteVariables, }; }