Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
406 lines
11 KiB
Markdown
406 lines
11 KiB
Markdown
# 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
|