/**
* Add Spanish translations for client-facing email templates
*
* Translates authentication, ticketing, and billing email templates to Spanish
* for client portal users.
*/
exports.up = async function(knex) {
console.log('Adding Spanish email templates...');
// Get notification subtypes
const subtypes = await knex('notification_subtypes')
.select('id', 'name')
.whereIn('name', [
'email-verification',
'password-reset',
'portal-invitation',
'tenant-recovery',
'no-account-found',
'Ticket Assigned',
'Ticket Created',
'Ticket Updated',
'Ticket Closed',
'Ticket Comment Added',
'Invoice Generated',
'Payment Received',
'Payment Overdue'
]);
const getSubtypeId = (name) => {
const subtype = subtypes.find(s => s.name === name);
if (!subtype) {
throw new Error(`Notification subtype '${name}' not found`);
}
return subtype.id;
};
// Insert Spanish templates
await knex('system_email_templates').insert([
// Authentication templates
// NOTE: email-verification template is managed in migration 20251029100000
{
name: 'password-reset',
language_code: 'es',
subject: 'Solicitud de Restablecimiento de Contraseña',
notification_subtype_id: getSubtypeId('password-reset'),
html_content: `
Solicitud de Restablecimiento de Contraseña
Hola {{userName}},
Recibimos una solicitud para restablecer la contraseña de tu cuenta asociada con {{email}}.
🔐 Verificación de Seguridad de la Cuenta
Solicitado: Hace un momento
Correo de la cuenta: {{email}}
Válido por: {{expirationTime}}
Para crear una nueva contraseña para tu cuenta, haz clic en el botón a continuación:
O copia y pega este enlace en tu navegador:
{{resetLink}}
⚠️ Información de Seguridad Importante
- Este enlace de restablecimiento expirará en {{expirationTime}}
- Por razones de seguridad, este enlace solo se puede usar una vez
- Si no solicitaste este restablecimiento, ignora este correo
- Tu contraseña no cambiará hasta que crees una nueva
¿Qué Sigue?
- Haz clic en el botón de restablecimiento arriba o usa el enlace proporcionado
- Crea una contraseña fuerte y única para tu cuenta
- Iniciarás sesión automáticamente después de restablecer
- Todas las sesiones existentes se terminarán por seguridad
- Considera habilitar la autenticación de dos factores para mayor protección
¿Necesitas Ayuda?
Si tienes problemas para restablecer tu contraseña, nuestro equipo de soporte está aquí para ayudarte.
Contactar Soporte: {{supportEmail}}
`,
text_content: `Solicitud de Restablecimiento de Contraseña
Hola {{userName}},
Recibimos una solicitud para restablecer la contraseña de tu cuenta asociada con {{email}}.
VERIFICACIÓN DE SEGURIDAD DE LA CUENTA
- Solicitado: Hace un momento
- Correo de la cuenta: {{email}}
- Válido por: {{expirationTime}}
Para crear una nueva contraseña, visita el siguiente enlace:
{{resetLink}}
INFORMACIÓN DE SEGURIDAD IMPORTANTE:
- Este enlace expirará en {{expirationTime}}
- Solo se puede usar una vez
- Si no solicitaste esto, ignora este correo
- Tu contraseña no cambiará hasta que crees una nueva
QUÉ SIGUE:
1. Usa el enlace proporcionado arriba
2. Crea una contraseña fuerte y única
3. Iniciarás sesión automáticamente
4. Todas las sesiones existentes se terminarán
5. Considera habilitar autenticación de dos factores
¿Necesitas ayuda?
Contactar Soporte: {{supportEmail}}
---
Este es un correo de seguridad automático enviado a {{email}}.
© {{currentYear}} {{clientName}}. Todos los derechos reservados.`
},
// NOTE: portal-invitation template is managed in migration 20251029100000
{
name: 'tenant-recovery',
language_code: 'es',
subject: '{{platformName}} - Tus enlaces de inicio de sesión',
notification_subtype_id: getSubtypeId('tenant-recovery'),
html_content: `
{{platformName}}
Hola,
Solicitaste acceso a tu portal{{#if isMultiple}}es{{/if}} de cliente{{#if isMultiple}}s{{/if}}.
{{#if isMultiple}}Encontramos {{tenantCount}} organizaciones asociadas con tu dirección de correo electrónico.{{else}}Aquí está tu enlace de inicio de sesión:{{/if}}
Nota de seguridad: Si no solicitaste estos enlaces de inicio de sesión, puedes ignorar este correo de forma segura. Tu cuenta permanece segura.
Si tienes preguntas o necesitas asistencia, por favor contacta al equipo de soporte de tu organización.
© {{currentYear}} {{platformName}}. Todos los derechos reservados.
Este es un mensaje automático. Por favor no respondas a este correo.
`,
text_content: `{{platformName}} - Tus enlaces de inicio de sesión
Hola,
Solicitaste acceso a tu portal{{#if isMultiple}}es{{/if}} de cliente{{#if isMultiple}}s{{/if}}.
{{#if isMultiple}}Encontramos {{tenantCount}} organizaciones asociadas con tu dirección de correo electrónico.{{else}}Aquí está tu enlace de inicio de sesión:{{/if}}
Tus enlaces de inicio de sesión:
{{tenantLinksText}}
Nota de seguridad: Si no solicitaste estos enlaces de inicio de sesión, puedes ignorar este correo de forma segura.
Si tienes preguntas o necesitas asistencia, por favor contacta al equipo de soporte de tu organización.
---
© {{currentYear}} {{platformName}}. Todos los derechos reservados.
Este es un mensaje automático. Por favor no respondas a este correo.`
},
{
name: 'no-account-found',
language_code: 'es',
subject: '{{platformName}} - Solicitud de acceso',
notification_subtype_id: getSubtypeId('no-account-found'),
html_content: `
{{platformName}}
Hola,
Recibimos una solicitud para acceder al portal del cliente usando esta dirección de correo electrónico.
Si tienes una cuenta con nosotros, deberías haber recibido un correo separado con tus enlaces de inicio de sesión.
Si no recibiste un correo de inicio de sesión, puede significar:
- Esta dirección de correo electrónico no está asociada con ninguna cuenta del portal del cliente
- Tu cuenta puede estar inactiva
- El correo puede haber sido filtrado a tu carpeta de spam
¿Necesitas ayuda?
Si crees que deberías tener acceso a un portal del cliente, por favor contacta al equipo de soporte de tu proveedor de servicios para obtener ayuda.
Nota de seguridad: Si no solicitaste acceso, puedes ignorar este correo de forma segura.
© {{currentYear}} {{platformName}}. Todos los derechos reservados.
Este es un mensaje automático. Por favor no respondas a este correo.
`,
text_content: `{{platformName}} - Solicitud de acceso
Hola,
Recibimos una solicitud para acceder al portal del cliente usando esta dirección de correo electrónico.
Si tienes una cuenta con nosotros, deberías haber recibido un correo separado con tus enlaces de inicio de sesión.
Si no recibiste un correo de inicio de sesión, puede significar:
- Esta dirección de correo electrónico no está asociada con ninguna cuenta del portal del cliente
- Tu cuenta puede estar inactiva
- El correo puede haber sido filtrado a tu carpeta de spam
¿Necesitas ayuda?
Si crees que deberías tener acceso a un portal del cliente, por favor contacta al equipo de soporte de tu proveedor de servicios para obtener ayuda.
Nota de seguridad: Si no solicitaste acceso, puedes ignorar este correo de forma segura.
---
© {{currentYear}} {{platformName}}. Todos los derechos reservados.
Este es un mensaje automático. Por favor no respondas a este correo.`
},
// Ticketing templates
{
name: 'ticket-assigned',
language_code: 'es',
subject: 'Ticket Asignado • {{ticket.title}} ({{ticket.priority}})',
notification_subtype_id: getSubtypeId('Ticket Assigned'),
html_content: `
|
Ticket Asignado
{{ticket.title}}
{{ticket.metaLine}}
|
|
Se te ha asignado un ticket para {{ticket.clientName}}. Revisa los detalles a continuación y toma acción.
| Prioridad |
{{ticket.priority}}
|
| Estado |
{{ticket.status}} |
| Asignado por |
{{ticket.assignedBy}} |
| Asignado a |
{{ticket.assignedToName}}
{{ticket.assignedToEmail}}
|
| Solicitante |
{{ticket.requesterName}}
{{ticket.requesterContact}}
|
| Tablero |
{{ticket.board}} |
| Categoría |
{{ticket.categoryDetails}} |
| Ubicación |
{{ticket.locationSummary}} |
Descripción
{{ticket.description}}
Ver Ticket
|
| Powered by Alga PSA • Manteniendo a los equipos alineados |
|
`,
text_content: `
Ticket Asignado a Ti
{{ticket.metaLine}}
Asignado por: {{ticket.assignedBy}}
Prioridad: {{ticket.priority}}
Estado: {{ticket.status}}
Asignado a: {{ticket.assignedDetails}}
Solicitante: {{ticket.requesterDetails}}
Tablero: {{ticket.board}}
Categoría: {{ticket.categoryDetails}}
Ubicación: {{ticket.locationSummary}}
Descripción:
{{ticket.description}}
Ver ticket: {{ticket.url}}
`
},
{
name: 'ticket-created',
language_code: 'es',
subject: 'Nuevo Ticket • {{ticket.title}} ({{ticket.priority}})',
notification_subtype_id: getSubtypeId('Ticket Created'),
html_content: `
|
Nuevo Ticket Creado
{{ticket.title}}
{{ticket.metaLine}}
|
|
Se ha registrado un nuevo ticket para {{ticket.clientName}}. Revisa el resumen a continuación y sigue el enlace para tomar acción.
| Prioridad |
{{ticket.priority}}
|
| Estado |
{{ticket.status}} |
| Creado |
{{ticket.createdAt}} · {{ticket.createdBy}} |
| Asignado a |
{{ticket.assignedToName}}
{{ticket.assignedToEmail}}
|
| Solicitante |
{{ticket.requesterName}}
{{ticket.requesterContact}}
|
| Tablero |
{{ticket.board}} |
| Categoría |
{{ticket.categoryDetails}} |
| Ubicación |
{{ticket.locationSummary}} |
Descripción
{{ticket.description}}
Ver Ticket
|
| Powered by Alga PSA • Manteniendo a los equipos alineados |
|
`,
text_content: `
Nuevo Ticket Creado para {{ticket.clientName}}
{{ticket.metaLine}}
Creado: {{ticket.createdAt}} · {{ticket.createdBy}}
Prioridad: {{ticket.priority}}
Estado: {{ticket.status}}
Asignado a: {{ticket.assignedDetails}}
Solicitante: {{ticket.requesterDetails}}
Tablero: {{ticket.board}}
Categoría: {{ticket.categoryDetails}}
Ubicación: {{ticket.locationSummary}}
Descripción:
{{ticket.description}}
Ver ticket: {{ticket.url}}
`
},
{
name: 'ticket-updated',
language_code: 'es',
subject: 'Ticket Actualizado • {{ticket.title}} ({{ticket.priority}})',
notification_subtype_id: getSubtypeId('Ticket Updated'),
html_content: `
|
Ticket Actualizado
{{ticket.title}}
{{ticket.metaLine}}
|
|
Se ha actualizado un ticket para {{ticket.clientName}}. Revisa los cambios a continuación.
| Prioridad |
{{ticket.priority}}
|
| Estado |
{{ticket.status}} |
| Actualizado por |
{{ticket.updatedBy}} |
| Asignado a |
{{ticket.assignedToName}}
{{ticket.assignedToEmail}}
|
| Solicitante |
{{ticket.requesterName}}
{{ticket.requesterContact}}
|
| Tablero |
{{ticket.board}} |
| Categoría |
{{ticket.categoryDetails}} |
| Ubicación |
{{ticket.locationSummary}} |
Cambios Realizados
{{ticket.changes}}
Ver Ticket
|
| Powered by Alga PSA • Manteniendo a los equipos alineados |
|
`,
text_content: `
Ticket Actualizado
{{ticket.metaLine}}
Actualizado por: {{ticket.updatedBy}}
Prioridad: {{ticket.priority}}
Estado: {{ticket.status}}
Asignado a: {{ticket.assignedDetails}}
Solicitante: {{ticket.requesterDetails}}
Tablero: {{ticket.board}}
Categoría: {{ticket.categoryDetails}}
Ubicación: {{ticket.locationSummary}}
Cambios realizados:
{{ticket.changes}}
Ver ticket: {{ticket.url}}
`
},
{
name: 'ticket-closed',
language_code: 'es',
subject: 'Ticket Cerrado • {{ticket.title}}',
notification_subtype_id: getSubtypeId('Ticket Closed'),
html_content: `
|
Ticket Cerrado
{{ticket.title}}
{{ticket.metaLine}}
|
|
Se ha resuelto y cerrado un ticket para {{ticket.clientName}}. Revisa los detalles de la resolución a continuación.
| Estado |
Cerrado
|
| Cerrado por |
{{ticket.closedBy}} |
| Asignado a |
{{ticket.assignedToName}}
{{ticket.assignedToEmail}}
|
| Solicitante |
{{ticket.requesterName}}
{{ticket.requesterContact}}
|
| Tablero |
{{ticket.board}} |
| Categoría |
{{ticket.categoryDetails}} |
| Ubicación |
{{ticket.locationSummary}} |
Resolución
{{ticket.resolution}}
Ver Ticket
|
| Powered by Alga PSA • Manteniendo a los equipos alineados |
|
`,
text_content: `
Ticket Cerrado
{{ticket.metaLine}}
Cerrado por: {{ticket.closedBy}}
Estado: Cerrado
Asignado a: {{ticket.assignedDetails}}
Solicitante: {{ticket.requesterDetails}}
Tablero: {{ticket.board}}
Categoría: {{ticket.categoryDetails}}
Ubicación: {{ticket.locationSummary}}
Resolución:
{{ticket.resolution}}
Ver ticket: {{ticket.url}}
`
},
{
name: 'ticket-comment-added',
language_code: 'es',
subject: 'Nuevo Comentario • {{ticket.title}}',
notification_subtype_id: getSubtypeId('Ticket Comment Added'),
html_content: `
|
Nuevo Comentario Agregado
{{ticket.title}}
{{ticket.metaLine}}
|
|
Se ha agregado un nuevo comentario a un ticket para {{ticket.clientName}}.
| Prioridad |
{{ticket.priority}}
|
| Estado |
{{ticket.status}} |
| Comentario de |
{{comment.author}} |
| Asignado a |
{{ticket.assignedToName}}
{{ticket.assignedToEmail}}
|
| Solicitante |
{{ticket.requesterName}}
{{ticket.requesterContact}}
|
| Tablero |
{{ticket.board}} |
| Categoría |
{{ticket.categoryDetails}} |
| Ubicación |
{{ticket.locationSummary}} |
💬 Comentario
{{comment.content}}
Ver Ticket
|
| Powered by Alga PSA • Manteniendo a los equipos alineados |
|
`,
text_content: `
Nuevo Comentario Agregado
{{ticket.metaLine}}
Comentario de: {{comment.author}}
Prioridad: {{ticket.priority}}
Estado: {{ticket.status}}
Asignado a: {{ticket.assignedDetails}}
Solicitante: {{ticket.requesterDetails}}
Tablero: {{ticket.board}}
Categoría: {{ticket.categoryDetails}}
Ubicación: {{ticket.locationSummary}}
Comentario:
{{comment.content}}
Ver ticket: {{ticket.url}}
`
},
// Billing templates
{
name: 'invoice-generated',
language_code: 'es',
subject: 'Nueva factura #{{invoice.number}}',
notification_subtype_id: getSubtypeId('Invoice Generated'),
html_content: `
Factura {{invoice.number}}
Se ha generado una nueva factura para tu revisión:
Número de factura: {{invoice.number}}
Monto: {{invoice.amount}}
Fecha de vencimiento: {{invoice.dueDate}}
Cliente: {{invoice.clientName}}
Ver la factura
`,
text_content: `
Factura {{invoice.number}}
Se ha generado una nueva factura para tu revisión:
Número de factura: {{invoice.number}}
Monto: {{invoice.amount}}
Fecha de vencimiento: {{invoice.dueDate}}
Cliente: {{invoice.clientName}}
Ver la factura: {{invoice.url}}
`
},
{
name: 'payment-received',
language_code: 'es',
subject: 'Pago recibido: Factura #{{invoice.number}}',
notification_subtype_id: getSubtypeId('Payment Received'),
html_content: `
Pago recibido
Se ha recibido el pago de la factura #{{invoice.number}}:
Número de factura: {{invoice.number}}
Monto pagado: {{invoice.amountPaid}}
Fecha de pago: {{invoice.paymentDate}}
Método de pago: {{invoice.paymentMethod}}
Ver la factura
`,
text_content: `
Pago recibido
Se ha recibido el pago de la factura #{{invoice.number}}:
Número de factura: {{invoice.number}}
Monto pagado: {{invoice.amountPaid}}
Fecha de pago: {{invoice.paymentDate}}
Método de pago: {{invoice.paymentMethod}}
Ver la factura: {{invoice.url}}
`
},
{
name: 'payment-overdue',
language_code: 'es',
subject: 'Pago vencido: Factura #{{invoice.number}}',
notification_subtype_id: getSubtypeId('Payment Overdue'),
html_content: `
Pago vencido
El pago de la factura #{{invoice.number}} está vencido:
Número de factura: {{invoice.number}}
Monto adeudado: {{invoice.amountDue}}
Fecha de vencimiento: {{invoice.dueDate}}
Días de retraso: {{invoice.daysOverdue}}
Ver la factura
`,
text_content: `
Pago vencido
El pago de la factura #{{invoice.number}} está vencido:
Número de factura: {{invoice.number}}
Monto adeudado: {{invoice.amountDue}}
Fecha de vencimiento: {{invoice.dueDate}}
Días de retraso: {{invoice.daysOverdue}}
Ver la factura: {{invoice.url}}
`
}
]).onConflict(['name', 'language_code']).merge({
subject: knex.raw('excluded.subject'),
html_content: knex.raw('excluded.html_content'),
text_content: knex.raw('excluded.text_content'),
notification_subtype_id: knex.raw('excluded.notification_subtype_id')
});
console.log('✓ Spanish email templates added (auth + notifications)');
};
exports.down = async function(knex) {
// Remove Spanish email templates
// NOTE: email-verification and portal-invitation are NOT removed as they're managed by migration 20251029100000
await knex('system_email_templates')
.where({ language_code: 'es' })
.whereIn('name', [
'password-reset',
'tenant-recovery',
'no-account-found',
'ticket-assigned',
'ticket-created',
'ticket-updated',
'ticket-closed',
'ticket-comment-added',
'invoice-generated',
'payment-received',
'payment-overdue'
])
.del();
console.log('Spanish email templates removed');
};