michangarrito/docs/97-adr/ADR-0011-email-multi-provider.md
rckrdmrd 2c916e75e5 [SIMCO-V4] feat: Agregar documentación SaaS, ADRs e integraciones
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>
2026-01-13 01:43:15 -06:00

296 lines
5.8 KiB
Markdown

---
id: ADR-0011
type: ADR
title: "Email Multi-Provider"
status: Accepted
decision_date: 2026-01-10
updated_at: 2026-01-10
simco_version: "4.0.1"
stakeholders:
- "Equipo MiChangarrito"
tags:
- email
- notifications
- sendgrid
- ses
- multi-provider
---
# 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:
1. Alta deliverability
2. Fallback si un proveedor falla
3. Flexibilidad para cambiar proveedor
4. Costos optimizados
---
## Decision
**Implementamos Factory Pattern con soporte para SendGrid, AWS SES y SMTP generico, con fallback automatico.**
Orden de prioridad:
1. SendGrid (principal)
2. AWS SES (fallback)
3. 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
1. **Disponibilidad:** Fallback automatico
2. **Flexibilidad:** Cambiar provider por tenant
3. **Economia:** Usar el mas economico como default
4. **Deliverability:** SendGrid tiene buena reputacion
### Negativas
1. **Complejidad:** Tres implementaciones
2. **Configuracion:** Multiples sets de credenciales
---
## Implementacion
### Factory
```typescript
@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
```typescript
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
```typescript
@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
```typescript
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
```typescript
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
```typescript
// 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
- [SendGrid API](https://docs.sendgrid.com/api-reference)
- [AWS SES](https://docs.aws.amazon.com/ses/)
- [Nodemailer](https://nodemailer.com/)
- [INT-010: Email Providers](../02-integraciones/INT-010-email-providers.md)
- [MCH-029: Infraestructura SaaS](../01-epicas/MCH-029-infraestructura-saas.md)
---
**Fecha decision:** 2026-01-10
**Autores:** Architecture Team