# PAYMENTS - REFERENCE IMPLEMENTATION

**Versión:** 1.0.0 | **Fecha:** 2025-12-12 | **Nivel:** Catalog (3)

---

## ÍNDICE DE ARCHIVOS

| Archivo | Descripción | LOC | Patrón Principal |
|---------|-------------|-----|------------------|
| `payment.service.reference.ts` | Servicio completo de pagos con Stripe | 296 | Checkout, Subscriptions, Webhooks |

---

## CÓMO USAR

### Flujo de adopción recomendado

```yaml
PASO_1: Configurar cuenta Stripe
  - Crear cuenta en https://stripe.com
  - Obtener API keys (test + production)
  - Configurar webhook endpoint
  - Crear productos y precios en dashboard

PASO_2: Instalar dependencias
  - npm install stripe
  - npm install @nestjs/config (si no está instalado)

PASO_3: Configurar variables de entorno
  - STRIPE_SECRET_KEY: sk_test_... (o sk_live_...)
  - STRIPE_WEBHOOK_SECRET: whsec_... (del dashboard)
  - STRIPE_API_VERSION: 2023-10-16 (o más reciente)

PASO_4: Copiar y adaptar archivo
  - Copiar payment.service.reference.ts → payment.service.ts
  - Ajustar imports de entidades (Payment, Subscription, Customer)
  - Configurar conexión a BD correcta (@InjectRepository)

PASO_5: Implementar entidades requeridas
  - Customer: user_id, stripe_customer_id, email
  - Payment: user_id, stripe_payment_id, amount, currency, status
  - Subscription: user_id, stripe_subscription_id, status, periods

PASO_6: Configurar webhook endpoint
  - Crear endpoint POST /webhooks/stripe
  - Usar raw body (no JSON parsed)
  - Verificar firma con stripe.webhooks.constructEvent()

PASO_7: Validar integración
  - Probar checkout session en modo test
  - Simular webhooks desde Stripe CLI
  - Verificar pagos en BD y dashboard Stripe
```

---

## PATRONES IMPLEMENTADOS

### 1. Checkout Session (Pago único o suscripción)

**Flujo:**
```
1. Frontend solicita checkout session
2. Backend crea session en Stripe
3. Backend retorna URL de checkout
4. Usuario completa pago en Stripe
5. Stripe envía webhook checkout.session.completed
6. Backend guarda payment en BD
```

**Ejemplo de uso:**
```typescript
// En tu controller
@Post('create-checkout')
async createCheckout(@Body() dto: CreateCheckoutDto, @Req() req) {
  const session = await this.paymentService.createCheckoutSession({
    userId: req.user.id,
    email: req.user.email,
    priceId: dto.priceId, // Del dashboard Stripe
    successUrl: `${process.env.APP_URL}/payment/success`,
    cancelUrl: `${process.env.APP_URL}/payment/cancel`,
    mode: 'payment', // o 'subscription'
  });

  return { checkoutUrl: session.url };
}
```

### 2. Suscripciones

**Crear suscripción:**
```typescript
const subscription = await this.paymentService.createSubscription({
  userId: user.id,
  email: user.email,
  priceId: 'price_monthly_premium',
});

// Suscripción queda en estado "incomplete"
// Usuario debe completar pago (requiere payment method)
```

**Cancelar suscripción:**
```typescript
await this.paymentService.cancelSubscription(
  subscriptionId,
  userId
);
// Se cancela al final del periodo actual
```

**Obtener suscripción activa:**
```typescript
const subscription = await this.paymentService.getActiveSubscription(userId);
if (subscription) {
  // Usuario tiene plan premium
}
```

### 3. Webhooks

**Eventos soportados:**

| Evento Stripe | Handler | Acción |
|---------------|---------|--------|
| `checkout.session.completed` | `handleCheckoutCompleted` | Guarda Payment en BD |
| `invoice.paid` | `handleInvoicePaid` | Actualiza Subscription a 'active' |
| `invoice.payment_failed` | `handlePaymentFailed` | Marca Subscription como 'past_due' |
| `customer.subscription.updated` | `handleSubscriptionUpdate` | Sincroniza estado de subscription |
| `customer.subscription.deleted` | `handleSubscriptionUpdate` | Sincroniza cancelación |

**Configurar webhook controller:**
```typescript
@Controller('webhooks')
export class WebhookController {
  constructor(private readonly paymentService: PaymentService) {}

  @Post('stripe')
  async handleStripeWebhook(
    @Headers('stripe-signature') signature: string,
    @Req() req: RawBodyRequest<Request>,
  ) {
    await this.paymentService.handleWebhook(
      signature,
      req.rawBody, // Importante: usar raw body
    );
    return { received: true };
  }
}
```

---

## NOTAS DE ADAPTACIÓN

### Variables a reemplazar

```typescript
// Entidades
Payment       → Tu entidad de pagos
Subscription  → Tu entidad de suscripciones
Customer      → Tu entidad de clientes Stripe

// DTOs
CreateCheckoutDto      → Tu DTO de checkout
CreateSubscriptionDto  → Tu DTO de suscripción
```

### Configurar raw body para webhooks

En `main.ts`:
```typescript
const app = await NestFactory.create(AppModule, {
  rawBody: true, // Habilitar raw body
});

// O usar middleware específico:
app.use('/webhooks/stripe', express.raw({ type: 'application/json' }));
```

### Esquema de base de datos

```sql
-- Tabla customers
CREATE TABLE customers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID UNIQUE REFERENCES users(id) ON DELETE CASCADE,
  stripe_customer_id VARCHAR(255) UNIQUE NOT NULL,
  email VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Tabla payments
CREATE TABLE payments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  stripe_payment_id VARCHAR(255) UNIQUE NOT NULL,
  amount INTEGER NOT NULL, -- En centavos (ej: 1000 = $10.00)
  currency VARCHAR(3) DEFAULT 'usd',
  status VARCHAR(50) NOT NULL, -- completed, pending, failed
  metadata JSONB,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Tabla subscriptions
CREATE TABLE subscriptions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  stripe_subscription_id VARCHAR(255) UNIQUE NOT NULL,
  stripe_customer_id VARCHAR(255) NOT NULL,
  status VARCHAR(50) NOT NULL, -- active, past_due, canceled, incomplete
  current_period_start TIMESTAMP NOT NULL,
  current_period_end TIMESTAMP NOT NULL,
  cancel_at_period_end BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_subscriptions_user_status ON subscriptions(user_id, status);
```

---

## CASOS DE USO COMUNES

### 1. Implementar plan de suscripción mensual

```typescript
// 1. Crear precio en Stripe dashboard:
//    - Producto: "Premium Plan"
//    - Precio: $9.99/mes
//    - ID: price_premium_monthly

// 2. Endpoint de suscripción
@Post('subscribe')
async subscribe(@Req() req) {
  const subscription = await this.paymentService.createSubscription({
    userId: req.user.id,
    email: req.user.email,
    priceId: 'price_premium_monthly',
  });

  return {
    subscriptionId: subscription.id,
    status: subscription.status,
  };
}

// 3. Verificar estado en guards/middlewares
@Injectable()
export class PremiumGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();
    const subscription = await this.paymentService.getActiveSubscription(req.user.id);
    return !!subscription;
  }
}
```

### 2. Pago único de producto

```typescript
@Post('buy-course')
async buyCourse(@Body() dto: BuyCourseDto, @Req() req) {
  const session = await this.paymentService.createCheckoutSession({
    userId: req.user.id,
    email: req.user.email,
    priceId: dto.coursePriceId,
    mode: 'payment', // Pago único
    successUrl: `${process.env.APP_URL}/courses/${dto.courseId}/access`,
    cancelUrl: `${process.env.APP_URL}/courses/${dto.courseId}`,
    metadata: {
      courseId: dto.courseId,
      type: 'course_purchase',
    },
  });

  return { checkoutUrl: session.url };
}

// En webhook handler personalizado:
private async handleCheckoutCompleted(session: Stripe.Checkout.Session) {
  if (session.metadata?.type === 'course_purchase') {
    await this.coursesService.grantAccess(
      session.metadata.userId,
      session.metadata.courseId,
    );
  }
}
```

### 3. Pruebas locales con Stripe CLI

```bash
# Instalar Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Escuchar webhooks (forward a localhost)
stripe listen --forward-to http://localhost:3000/webhooks/stripe

# Copiar webhook secret que muestra (whsec_...)
# Actualizar .env: STRIPE_WEBHOOK_SECRET=whsec_...

# Simular eventos
stripe trigger checkout.session.completed
stripe trigger invoice.paid
```

---

## MANEJO DE ERRORES COMUNES

### Error: "No such price"
```typescript
// Solución: Verificar que el priceId existe en Stripe dashboard
// Usar precios de test (price_test_...) en desarrollo
```

### Error: "Webhook signature verification failed"
```typescript
// Solución: Asegurar que se usa raw body
// Verificar que STRIPE_WEBHOOK_SECRET es correcto
// En desarrollo, usar Stripe CLI para obtener secret local
```

### Error: "Customer already exists"
```typescript
// Solución: Ya manejado en getOrCreateCustomer()
// Busca customer existente antes de crear
```

### Suscripción queda en "incomplete"
```typescript
// Solución: Usuario debe completar payment method
// Usar checkout.session para suscripciones (más fácil)
// O implementar setup intent para agregar payment method
```

---

## CHECKLIST DE VALIDACIÓN

Antes de marcar como completo:

- [ ] Build pasa: `npm run build`
- [ ] Lint pasa: `npm run lint`
- [ ] Variables de entorno configuradas (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET)
- [ ] Entidades creadas en BD (Customer, Payment, Subscription)
- [ ] Checkout session funciona (modo test)
- [ ] Webhook endpoint configurado con raw body
- [ ] Webhooks verifican firma correctamente
- [ ] Eventos se guardan en BD
- [ ] Probado con Stripe CLI local
- [ ] Dashboard Stripe muestra eventos correctamente

---

## REFERENCIAS CRUZADAS

### Dependencias en @CATALOG

- **auth**: Para autenticar usuarios en endpoints de pago
- **notifications**: Notificar usuario sobre pagos/suscripciones
- **multi-tenancy**: Pagos por tenant (empresas)

### Relacionado con SIMCO

- **@OP_BACKEND**: Operaciones de backend (crear service, webhooks)
- **@SIMCO-REUTILIZAR**: Este catálogo es candidato para reutilización
- **@SIMCO-VALIDAR**: Validar con Stripe CLI antes de deploy

### Documentación adicional

- Stripe API: https://stripe.com/docs/api
- Checkout Sessions: https://stripe.com/docs/payments/checkout
- Webhooks: https://stripe.com/docs/webhooks
- Testing: https://stripe.com/docs/testing

---

## SEGURIDAD

### Mejores prácticas implementadas:

1. **Webhook signature verification**: Evita requests maliciosos
2. **Refresh token hashing**: Nunca guardar tokens planos
3. **Metadata validation**: Validar userId en webhooks
4. **API versioning**: Fijar versión de API Stripe
5. **Idempotency**: Webhooks pueden repetirse (manejar duplicados)

### Recomendaciones adicionales:

- Usar HTTPS en producción (requerido por Stripe)
- Limitar rate de endpoints de pago
- Logging detallado de eventos Stripe
- Monitorear webhooks fallidos en dashboard
- Implementar retry logic para webhooks críticos

---

**Mantenido por:** Core Team | **Origen:** Patrón base para integraciones Stripe
