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

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
Equipo MiChangarrito
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

@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