Añade envío de email SMTP del presupuesto y del enlace al formulario
- mailer.ts: transport nodemailer perezoso desde env; enviarPresupuestoEmail (adjunta el PDF) y enviarEnlaceFormulario. Best-effort: sin SMTP configurado o ante error devuelven false sin lanzar. - COPY-GUIDE §6.b: copy literal de ambos emails al cliente final. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
93
mvp/b2c/src/lib/email/mailer.ts
Normal file
93
mvp/b2c/src/lib/email/mailer.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import nodemailer, { type Transporter } from 'nodemailer';
|
||||
import { env, emailConfigurado } from '@/lib/env';
|
||||
|
||||
let _transport: Transporter | null = null;
|
||||
|
||||
// Transport perezoso: solo se crea cuando hay SMTP configurado y se va a enviar de verdad.
|
||||
function getTransport(): Transporter | null {
|
||||
if (!emailConfigurado()) return null;
|
||||
if (_transport) return _transport;
|
||||
const port = Number(env.SMTP_PORT ?? 587);
|
||||
_transport = nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port,
|
||||
secure: port === 465, // 465 = TLS implícito; 587/1025 = STARTTLS/none
|
||||
auth: env.SMTP_USER ? { user: env.SMTP_USER, pass: env.SMTP_PASS } : undefined,
|
||||
});
|
||||
return _transport;
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/[&<>"]/g, (c) =>
|
||||
c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : '"',
|
||||
);
|
||||
}
|
||||
|
||||
// Email de entrega del presupuesto con el PDF adjunto (COPY-GUIDE §6.b). Best-effort: si no hay
|
||||
// SMTP configurado o el envío falla devuelve false sin lanzar, para no romper el pipeline.
|
||||
export async function enviarPresupuestoEmail(opts: {
|
||||
to: string;
|
||||
nombre: string;
|
||||
empresa: string;
|
||||
pdf: Buffer;
|
||||
filename: string;
|
||||
}): Promise<boolean> {
|
||||
const transport = getTransport();
|
||||
if (!transport) return false;
|
||||
|
||||
const nombre = escapeHtml(opts.nombre.split(' ')[0] ?? opts.nombre);
|
||||
const empresa = escapeHtml(opts.empresa);
|
||||
const html = `<p>Hola ${nombre},</p>
|
||||
<p>Aquí tienes tu <strong>presupuesto orientativo de reforma</strong>, preparado por ${empresa}. Lo encontrarás adjunto en PDF, con el render de cómo quedaría tu espacio y el desglose por partidas.</p>
|
||||
<p>⚠️ Es una <strong>estimación</strong>. El precio definitivo lo confirma ${empresa} en una visita gratuita en tu casa, donde mide todo con detalle y lo ajusta.</p>
|
||||
<p>Si te encaja, responde a este email o escríbenos por WhatsApp y concertamos la visita. Sin compromiso.</p>
|
||||
<p>—<br/>${empresa}</p>`;
|
||||
|
||||
try {
|
||||
await transport.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
to: opts.to,
|
||||
subject: `Tu presupuesto de reforma con ${opts.empresa} ya está listo`,
|
||||
html,
|
||||
attachments: [{ filename: opts.filename, content: opts.pdf, contentType: 'application/pdf' }],
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('enviarPresupuestoEmail error:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Email con el enlace al formulario para subir imágenes (COPY-GUIDE §6.b), usado en el canal
|
||||
// llamada. Best-effort, mismas garantías que el anterior.
|
||||
export async function enviarEnlaceFormulario(opts: {
|
||||
to: string;
|
||||
nombre: string;
|
||||
empresa: string;
|
||||
url: string;
|
||||
}): Promise<boolean> {
|
||||
const transport = getTransport();
|
||||
if (!transport) return false;
|
||||
|
||||
const nombre = escapeHtml(opts.nombre.split(' ')[0] ?? opts.nombre);
|
||||
const empresa = escapeHtml(opts.empresa);
|
||||
const url = encodeURI(opts.url);
|
||||
const html = `<p>Hola ${nombre},</p>
|
||||
<p>Para preparar tu render y tu presupuesto, ${empresa} necesita ver el espacio. Sube unas fotos de cada zona desde este enlace, cuando te venga bien:</p>
|
||||
<p>👉 <a href="${url}">Subir mis fotos</a></p>
|
||||
<p>Tardas un minuto y puedes añadir las que quieras. En cuanto las tengamos, seguimos con tu presupuesto.</p>
|
||||
<p>—<br/>${empresa}</p>`;
|
||||
|
||||
try {
|
||||
await transport.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
to: opts.to,
|
||||
subject: `Sube las fotos de tu reforma para ${opts.empresa}`,
|
||||
html,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('enviarEnlaceFormulario error:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user