Nuevas Épicas (MCH-029 a MCH-033): - Infraestructura SaaS multi-tenant - Auth Social (OAuth2) - Auditoría Empresarial - Feature Flags - Onboarding Wizard Nuevas Integraciones (INT-010 a INT-014): - Email Providers (SendGrid, Mailgun, SES) - Storage Cloud (S3, GCS, Azure) - OAuth Social - Redis Cache - Webhooks Outbound Nuevos ADRs (0004 a 0011): - Notifications Realtime - Feature Flags Strategy - Storage Abstraction - Webhook Retry Strategy - Audit Log Retention - Rate Limiting - OAuth Social Implementation - Email Multi-provider Actualizados: - MASTER_INVENTORY.yml - CONTEXT-MAP.yml - HERENCIA-SIMCO.md - Mapas de documentación Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
5.8 KiB
5.8 KiB
| id | type | title | status | decision_date | updated_at | simco_version | stakeholders | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ADR-0011 | ADR | Email Multi-Provider | Accepted | 2026-01-10 | 2026-01-10 | 4.0.1 |
|
|
ADR-0011: Email Multi-Provider
Metadata
| Campo | Valor |
|---|---|
| ID | ADR-0011 |
| Estado | Accepted |
| Fecha | 2026-01-10 |
| Autor | Architecture Team |
| Supersede | - |
Contexto
MiChangarrito necesita enviar emails transaccionales (bienvenida, verificacion, notificaciones, facturas). Queremos:
- Alta deliverability
- Fallback si un proveedor falla
- Flexibilidad para cambiar proveedor
- Costos optimizados
Decision
Implementamos Factory Pattern con soporte para SendGrid, AWS SES y SMTP generico, con fallback automatico.
Orden de prioridad:
- SendGrid (principal)
- AWS SES (fallback)
- SMTP (ultimo recurso)
Alternativas Consideradas
Opcion 1: Solo SendGrid
- Pros:
- Simple
- Buena API
- Cons:
- Single point of failure
- Vendor lock-in
Opcion 2: Solo AWS SES
- Pros:
- Integrado con AWS
- Muy economico
- Cons:
- Setup complejo
- Sandbox restrictivo
Opcion 3: Multi-Provider con fallback (Elegida)
- Pros:
- Alta disponibilidad
- Flexibilidad
- Optimizacion de costos
- Cons:
- Mas complejidad
- Mantener multiples integraciones
Consecuencias
Positivas
- Disponibilidad: Fallback automatico
- Flexibilidad: Cambiar provider por tenant
- Economia: Usar el mas economico como default
- Deliverability: SendGrid tiene buena reputacion
Negativas
- Complejidad: Tres implementaciones
- Configuracion: Multiples sets de credenciales
Implementacion
Factory
@Injectable()
export class EmailProviderFactory {
private providers: EmailProvider[];
constructor(
private sendgrid: SendGridProvider,
private ses: SESProvider,
private smtp: SMTPProvider,
) {
// Orden de prioridad
this.providers = [sendgrid, ses, smtp].filter(p => p.isConfigured());
}
async send(email: EmailDto): Promise<SendResult> {
for (const provider of this.providers) {
try {
return await provider.send(email);
} catch (error) {
this.logger.warn(`Provider ${provider.name} failed: ${error.message}`);
continue;
}
}
throw new Error('All email providers failed');
}
}
Interface
interface EmailProvider {
name: string;
isConfigured(): boolean;
send(email: EmailDto): Promise<SendResult>;
}
interface EmailDto {
to: string | string[];
subject: string;
html?: string;
text?: string;
template?: string;
variables?: Record<string, any>;
from?: string;
replyTo?: string;
attachments?: Attachment[];
}
SendGrid Provider
@Injectable()
export class SendGridProvider implements EmailProvider {
name = 'sendgrid';
private client: MailService;
constructor(config: ConfigService) {
if (config.get('SENDGRID_API_KEY')) {
this.client = new MailService();
this.client.setApiKey(config.get('SENDGRID_API_KEY'));
}
}
isConfigured(): boolean {
return !!this.client;
}
async send(email: EmailDto): Promise<SendResult> {
const msg = {
to: email.to,
from: email.from || process.env.EMAIL_FROM,
subject: email.subject,
html: email.html,
text: email.text,
};
const [response] = await this.client.send(msg);
return {
messageId: response.headers['x-message-id'],
provider: this.name,
};
}
}
Templates
Almacenamiento
- Templates en base de datos (tenant-specific)
- Templates por defecto en codigo (fallback)
Renderizado
async renderTemplate(
key: string,
variables: Record<string, any>,
tenantId?: string,
): Promise<{ html: string; text: string }> {
// Buscar template custom del tenant
let template = await this.findTemplate(key, tenantId);
// Fallback a template por defecto
if (!template) {
template = this.getDefaultTemplate(key);
}
// Renderizar con Handlebars
const html = Handlebars.compile(template.html)(variables);
const text = Handlebars.compile(template.text)(variables);
return { html, text };
}
Rate Limiting
Por Tenant
async checkTenantEmailLimit(tenantId: string): Promise<boolean> {
const plan = await this.getPlan(tenantId);
const key = `email:count:${tenantId}:${format(new Date(), 'yyyy-MM-dd-HH')}`;
const count = await this.redis.incr(key);
await this.redis.expire(key, 3600);
return count <= plan.emailsPerHour;
}
Limites
| Plan | Por Hora | Por Dia |
|---|---|---|
| Basic | 100 | 500 |
| Pro | 1,000 | 10,000 |
| Enterprise | Ilimitado | Ilimitado |
Tracking
Webhooks de Estado
// POST /webhooks/email/sendgrid
async handleSendGridWebhook(events: SendGridEvent[]) {
for (const event of events) {
await this.emailLogRepo.update(
{ providerMessageId: event.sg_message_id },
{
status: this.mapStatus(event.event),
[event.event + 'At']: new Date(event.timestamp * 1000),
}
);
}
}
Estados
- sent: Email enviado al proveedor
- delivered: Entregado al servidor destino
- opened: Abierto por destinatario (si tracking habilitado)
- clicked: Link clickeado
- bounced: Rebotado
- spam: Marcado como spam
Referencias
Fecha decision: 2026-01-10 Autores: Architecture Team