import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DataSource } from 'typeorm'; import { Resend } from 'resend'; @Injectable() export class EmailService { private readonly logger = new Logger(EmailService.name); private resend: Resend | null = null; private fromAddress: string; private replyToAddress: string; constructor( private configService: ConfigService, private dataSource: DataSource, ) { const apiKey = this.configService.get('RESEND_API_KEY'); if (apiKey && !apiKey.includes('placeholder')) { this.resend = new Resend(apiKey); this.logger.log('Resend email service initialized'); } else { this.logger.warn('Resend not configured — emails will be logged only (stub mode)'); } this.fromAddress = this.configService.get('RESEND_FROM_ADDRESS') || 'noreply@hoaledgeriq.com'; this.replyToAddress = this.configService.get('RESEND_REPLY_TO') || ''; } // ─── Public API ────────────────────────────────────────────── async sendActivationEmail(email: string, businessName: string, activationUrl: string): Promise { const subject = `Activate your ${businessName} account on HOA LedgerIQ`; const html = this.buildTemplate({ preheader: 'Your HOA LedgerIQ account is ready to activate.', heading: 'Welcome to HOA LedgerIQ!', body: `

Your organization ${this.esc(businessName)} has been created and is ready to go.

Click the button below to set your password and activate your account:

`, ctaText: 'Activate My Account', ctaUrl: activationUrl, footer: 'This activation link expires in 72 hours. If you did not sign up for HOA LedgerIQ, please ignore this email.', }); await this.send(email, subject, html, 'activation', { businessName, activationUrl }); } async sendWelcomeEmail(email: string, businessName: string): Promise { const appUrl = this.configService.get('APP_URL') || 'https://app.hoaledgeriq.com'; const subject = `Welcome to HOA LedgerIQ — ${businessName}`; const html = this.buildTemplate({ preheader: `${businessName} is all set up on HOA LedgerIQ.`, heading: `You're all set!`, body: `

Your account for ${this.esc(businessName)} is now active.

Log in to start managing your HOA's finances, assessments, and investments — all in one place.

`, ctaText: 'Go to Dashboard', ctaUrl: `${appUrl}/dashboard`, footer: 'If you have any questions, just reply to this email and we\'ll help you get started.', }); await this.send(email, subject, html, 'welcome', { businessName }); } async sendPaymentFailedEmail(email: string, businessName: string): Promise { const subject = `Action required: Payment failed for ${businessName}`; const html = this.buildTemplate({ preheader: 'We were unable to process your payment.', heading: 'Payment Failed', body: `

We were unable to process the latest payment for ${this.esc(businessName)}.

Please update your payment method to avoid any interruption to your service.

`, ctaText: 'Update Payment Method', ctaUrl: `${this.configService.get('APP_URL') || 'https://app.hoaledgeriq.com'}/settings`, footer: 'If you believe this is an error, please reply to this email and we\'ll look into it.', }); await this.send(email, subject, html, 'payment_failed', { businessName }); } async sendInviteMemberEmail(email: string, orgName: string, inviteUrl: string): Promise { const subject = `You've been invited to ${orgName} on HOA LedgerIQ`; const html = this.buildTemplate({ preheader: `Join ${orgName} on HOA LedgerIQ.`, heading: 'You\'re Invited!', body: `

You've been invited to join ${this.esc(orgName)} on HOA LedgerIQ.

Click below to accept the invitation and set up your account:

`, ctaText: 'Accept Invitation', ctaUrl: inviteUrl, footer: 'This invitation link expires in 7 days. If you were not expecting this, please ignore this email.', }); await this.send(email, subject, html, 'invite_member', { orgName, inviteUrl }); } async sendTrialEndingEmail(email: string, businessName: string, daysRemaining: number, settingsUrl: string): Promise { const subject = `Your free trial ends in ${daysRemaining} days — ${businessName}`; const html = this.buildTemplate({ preheader: `Your HOA LedgerIQ trial for ${businessName} is ending soon.`, heading: `Your Trial Ends in ${daysRemaining} Days`, body: `

Your free trial for ${this.esc(businessName)} on HOA LedgerIQ ends in ${daysRemaining} days.

To continue using all features without interruption, add a payment method before your trial expires.

If you don't add a payment method, your account will become read-only and you won't be able to make changes to your data.

`, ctaText: 'Add Payment Method', ctaUrl: settingsUrl, footer: 'If you have any questions about plans or pricing, just reply to this email.', }); await this.send(email, subject, html, 'trial_ending', { businessName, daysRemaining, settingsUrl }); } async sendTrialExpiredEmail(email: string, businessName: string): Promise { const appUrl = this.configService.get('APP_URL') || 'https://app.hoaledgeriq.com'; const subject = `Your free trial has ended — ${businessName}`; const html = this.buildTemplate({ preheader: `Your HOA LedgerIQ trial for ${businessName} has ended.`, heading: 'Your Trial Has Ended', body: `

The free trial for ${this.esc(businessName)} on HOA LedgerIQ has ended.

Your data is safe and your account is preserved. Subscribe to a plan to regain full access to your HOA financial management tools.

`, ctaText: 'Choose a Plan', ctaUrl: `${appUrl}/pricing`, footer: 'Your data will be preserved. You can reactivate your account at any time by subscribing to a plan.', }); await this.send(email, subject, html, 'trial_expired', { businessName }); } async sendPasswordResetEmail(email: string, resetUrl: string): Promise { const subject = 'Reset your HOA LedgerIQ password'; const html = this.buildTemplate({ preheader: 'Password reset requested for your HOA LedgerIQ account.', heading: 'Password Reset', body: `

We received a request to reset your password. Click the button below to choose a new one:

`, ctaText: 'Reset Password', ctaUrl: resetUrl, footer: 'This link expires in 1 hour. If you did not request a password reset, please ignore this email — your password will remain unchanged.', }); await this.send(email, subject, html, 'password_reset', { resetUrl }); } // ─── Core send logic ──────────────────────────────────────── private async send( toEmail: string, subject: string, html: string, template: string, metadata: Record, ): Promise { // Always log to the database await this.log(toEmail, subject, html, template, metadata); if (!this.resend) { this.logger.log(`📧 EMAIL STUB → ${toEmail}`); this.logger.log(` Subject: ${subject}`); return; } try { const result = await this.resend.emails.send({ from: this.fromAddress, to: [toEmail], replyTo: this.replyToAddress || undefined, subject, html, }); if (result.error) { this.logger.error(`Resend error for ${toEmail}: ${JSON.stringify(result.error)}`); await this.updateLogStatus(toEmail, template, 'failed', result.error.message); } else { this.logger.log(`✅ Email sent to ${toEmail} (id: ${result.data?.id})`); await this.updateLogStatus(toEmail, template, 'sent', result.data?.id); } } catch (err: any) { this.logger.error(`Failed to send email to ${toEmail}: ${err.message}`); await this.updateLogStatus(toEmail, template, 'failed', err.message); } } // ─── Database logging ─────────────────────────────────────── private async log( toEmail: string, subject: string, body: string, template: string, metadata: Record, ): Promise { try { await this.dataSource.query( `INSERT INTO shared.email_log (to_email, subject, body, template, metadata) VALUES ($1, $2, $3, $4, $5)`, [toEmail, subject, body, template, JSON.stringify(metadata)], ); } catch (err) { this.logger.warn(`Failed to log email: ${err}`); } } private async updateLogStatus(toEmail: string, template: string, status: string, detail?: string): Promise { try { await this.dataSource.query( `UPDATE shared.email_log SET metadata = metadata || $1::jsonb WHERE to_email = $2 AND template = $3 AND created_at = ( SELECT MAX(created_at) FROM shared.email_log WHERE to_email = $2 AND template = $3 )`, [JSON.stringify({ send_status: status, send_detail: detail || '' }), toEmail, template], ); } catch { // Best effort — don't block the flow } } // ─── HTML email template ──────────────────────────────────── private esc(text: string): string { return text.replace(/&/g, '&').replace(//g, '>'); } private buildTemplate(opts: { preheader: string; heading: string; body: string; ctaText: string; ctaUrl: string; footer: string; }): string { return ` ${this.esc(opts.heading)}
${this.esc(opts.preheader)}
HOA LedgerIQ

${this.esc(opts.heading)}

${opts.body}
${this.esc(opts.ctaText)}

If the button doesn't work, copy and paste this link into your browser:
${opts.ctaUrl}

${this.esc(opts.footer)}

© ${new Date().getFullYear()} HOA LedgerIQ — Smart Financial Management for HOAs

`; } }