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:
@@ -536,6 +536,55 @@ Documento que se adjunta en la entrega por WhatsApp y se descarga desde el panel
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 6.b Emails al cliente final (funnel B2C)
|
||||||
|
|
||||||
|
Emails que se envían al cliente desde la marca del reformista. Tono cercano, honesto y orientativo,
|
||||||
|
igual que el resto del funnel. `[Reformista]` = nombre de la empresa; se usa como remitente.
|
||||||
|
|
||||||
|
### Email de entrega del presupuesto (PDF adjunto)
|
||||||
|
|
||||||
|
Se envía siempre al terminar el perfil, con el PDF del presupuesto adjunto.
|
||||||
|
|
||||||
|
- **Asunto:** *Tu presupuesto de reforma con [Reformista] ya está listo*
|
||||||
|
- **Cuerpo:**
|
||||||
|
|
||||||
|
> Hola [Nombre],
|
||||||
|
>
|
||||||
|
> Aquí tienes tu **presupuesto orientativo de reforma**, preparado por [Reformista]. Lo encontrarás
|
||||||
|
> adjunto en PDF, con el render de cómo quedaría tu espacio y el desglose por partidas.
|
||||||
|
>
|
||||||
|
> ⚠️ Es una **estimación**. El precio definitivo lo confirma [Reformista] en una visita gratuita en
|
||||||
|
> tu casa, donde mide todo con detalle y lo ajusta.
|
||||||
|
>
|
||||||
|
> Si te encaja, responde a este email o escríbenos por WhatsApp y concertamos la visita. Sin
|
||||||
|
> compromiso.
|
||||||
|
>
|
||||||
|
> —
|
||||||
|
> [Reformista]
|
||||||
|
|
||||||
|
### Email con enlace al formulario (subir imágenes)
|
||||||
|
|
||||||
|
Se envía cuando el cliente eligió continuar por llamada y necesita un sitio donde subir las fotos
|
||||||
|
del espacio. `[url]` apunta a su formulario personal del funnel.
|
||||||
|
|
||||||
|
- **Asunto:** *Sube las fotos de tu reforma para [Reformista]*
|
||||||
|
- **Cuerpo:**
|
||||||
|
|
||||||
|
> Hola [Nombre],
|
||||||
|
>
|
||||||
|
> Para preparar tu render y tu presupuesto, [Reformista] necesita ver el espacio. Sube unas fotos
|
||||||
|
> de cada zona desde este enlace, cuando te venga bien:
|
||||||
|
>
|
||||||
|
> 👉 [Subir mis fotos]([url])
|
||||||
|
>
|
||||||
|
> Tardas un minuto y puedes añadir las que quieras. En cuanto las tengamos, seguimos con tu
|
||||||
|
> presupuesto.
|
||||||
|
>
|
||||||
|
> —
|
||||||
|
> [Reformista]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 7. Microcopy del panel del reformista
|
## 7. Microcopy del panel del reformista
|
||||||
|
|
||||||
| Elemento | Texto |
|
| Elemento | Texto |
|
||||||
|
|||||||
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