workspace-v1/shared/libs/payments/_reference
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
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
2026-01-04 03:37:42 -06:00
..
payment.service.reference.ts feat: Workspace-v1 complete migration with NEXUS v3.4 2026-01-04 03:37:42 -06:00
README.md feat: Workspace-v1 complete migration with NEXUS v3.4 2026-01-04 03:37:42 -06:00

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

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:

// 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:

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:

await this.paymentService.cancelSubscription(
  subscriptionId,
  userId
);
// Se cancela al final del periodo actual

Obtener suscripción activa:

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:

@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

// 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:

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

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

// 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

@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

# 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"

// Solución: Verificar que el priceId existe en Stripe dashboard
// Usar precios de test (price_test_...) en desarrollo

Error: "Webhook signature verification failed"

// 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"

// Solución: Ya manejado en getOrCreateCustomer()
// Busca customer existente antes de crear

Suscripción queda en "incomplete"

// 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


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