workspace-v1/shared/libs/payments/_reference/payment.service.reference.ts
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

296 lines
8.4 KiB
TypeScript

/**
* PAYMENT SERVICE - REFERENCE IMPLEMENTATION
*
* @description Servicio de pagos con integración Stripe.
* Soporta pagos únicos, suscripciones y webhooks.
*
* @usage
* ```typescript
* // Crear checkout session
* const session = await this.paymentService.createCheckoutSession({
* userId: req.user.id,
* priceId: 'price_xxx',
* successUrl: 'https://app.com/success',
* cancelUrl: 'https://app.com/cancel',
* });
* // Redirigir a session.url
* ```
*
* @origin Patrón base para proyectos con pagos
*/
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import Stripe from 'stripe';
// Adaptar imports según proyecto
// import { Payment, Subscription, Customer } from '../entities';
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
private readonly stripe: Stripe;
constructor(
private readonly configService: ConfigService,
@InjectRepository(Payment, 'payments')
private readonly paymentRepo: Repository<Payment>,
@InjectRepository(Subscription, 'payments')
private readonly subscriptionRepo: Repository<Subscription>,
@InjectRepository(Customer, 'payments')
private readonly customerRepo: Repository<Customer>,
) {
this.stripe = new Stripe(this.configService.get<string>('STRIPE_SECRET_KEY'), {
apiVersion: '2023-10-16',
});
}
/**
* Crear o recuperar customer de Stripe
*/
async getOrCreateCustomer(userId: string, email: string): Promise<string> {
// Buscar customer existente
const existing = await this.customerRepo.findOne({ where: { user_id: userId } });
if (existing) return existing.stripe_customer_id;
// Crear en Stripe
const stripeCustomer = await this.stripe.customers.create({
email,
metadata: { userId },
});
// Guardar en BD
const customer = this.customerRepo.create({
user_id: userId,
stripe_customer_id: stripeCustomer.id,
email,
});
await this.customerRepo.save(customer);
return stripeCustomer.id;
}
/**
* Crear sesión de checkout
*/
async createCheckoutSession(dto: CreateCheckoutDto): Promise<{ sessionId: string; url: string }> {
const customerId = await this.getOrCreateCustomer(dto.userId, dto.email);
const session = await this.stripe.checkout.sessions.create({
customer: customerId,
payment_method_types: ['card'],
line_items: [
{
price: dto.priceId,
quantity: dto.quantity || 1,
},
],
mode: dto.mode || 'payment',
success_url: dto.successUrl,
cancel_url: dto.cancelUrl,
metadata: {
userId: dto.userId,
...dto.metadata,
},
});
return {
sessionId: session.id,
url: session.url,
};
}
/**
* Crear suscripción directa
*/
async createSubscription(dto: CreateSubscriptionDto): Promise<Subscription> {
const customerId = await this.getOrCreateCustomer(dto.userId, dto.email);
const stripeSubscription = await this.stripe.subscriptions.create({
customer: customerId,
items: [{ price: dto.priceId }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
const subscription = this.subscriptionRepo.create({
user_id: dto.userId,
stripe_subscription_id: stripeSubscription.id,
stripe_customer_id: customerId,
status: stripeSubscription.status,
current_period_start: new Date(stripeSubscription.current_period_start * 1000),
current_period_end: new Date(stripeSubscription.current_period_end * 1000),
});
return this.subscriptionRepo.save(subscription);
}
/**
* Cancelar suscripción
*/
async cancelSubscription(subscriptionId: string, userId: string): Promise<void> {
const subscription = await this.subscriptionRepo.findOne({
where: { id: subscriptionId, user_id: userId },
});
if (!subscription) {
throw new BadRequestException('Subscription not found');
}
await this.stripe.subscriptions.update(subscription.stripe_subscription_id, {
cancel_at_period_end: true,
});
subscription.cancel_at_period_end = true;
await this.subscriptionRepo.save(subscription);
}
/**
* Obtener suscripción activa del usuario
*/
async getActiveSubscription(userId: string): Promise<Subscription | null> {
return this.subscriptionRepo.findOne({
where: { user_id: userId, status: 'active' },
});
}
/**
* Procesar webhook de Stripe
*/
async handleWebhook(signature: string, payload: Buffer): Promise<void> {
const webhookSecret = this.configService.get<string>('STRIPE_WEBHOOK_SECRET');
let event: Stripe.Event;
try {
event = this.stripe.webhooks.constructEvent(payload, signature, webhookSecret);
} catch (err) {
this.logger.error(`Webhook signature verification failed: ${err.message}`);
throw new BadRequestException('Invalid webhook signature');
}
this.logger.log(`Processing webhook event: ${event.type}`);
switch (event.type) {
case 'checkout.session.completed':
await this.handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session);
break;
case 'invoice.paid':
await this.handleInvoicePaid(event.data.object as Stripe.Invoice);
break;
case 'invoice.payment_failed':
await this.handlePaymentFailed(event.data.object as Stripe.Invoice);
break;
case 'customer.subscription.updated':
case 'customer.subscription.deleted':
await this.handleSubscriptionUpdate(event.data.object as Stripe.Subscription);
break;
default:
this.logger.debug(`Unhandled webhook event: ${event.type}`);
}
}
// ============ WEBHOOK HANDLERS ============
private async handleCheckoutCompleted(session: Stripe.Checkout.Session) {
const userId = session.metadata?.userId;
if (!userId) return;
const payment = this.paymentRepo.create({
user_id: userId,
stripe_payment_id: session.payment_intent as string,
amount: session.amount_total,
currency: session.currency,
status: 'completed',
});
await this.paymentRepo.save(payment);
this.logger.log(`Payment completed for user: ${userId}`);
}
private async handleInvoicePaid(invoice: Stripe.Invoice) {
const subscriptionId = invoice.subscription as string;
if (!subscriptionId) return;
await this.subscriptionRepo.update(
{ stripe_subscription_id: subscriptionId },
{ status: 'active' },
);
}
private async handlePaymentFailed(invoice: Stripe.Invoice) {
const subscriptionId = invoice.subscription as string;
if (!subscriptionId) return;
await this.subscriptionRepo.update(
{ stripe_subscription_id: subscriptionId },
{ status: 'past_due' },
);
}
private async handleSubscriptionUpdate(stripeSubscription: Stripe.Subscription) {
await this.subscriptionRepo.update(
{ stripe_subscription_id: stripeSubscription.id },
{
status: stripeSubscription.status,
current_period_end: new Date(stripeSubscription.current_period_end * 1000),
cancel_at_period_end: stripeSubscription.cancel_at_period_end,
},
);
}
}
// ============ TIPOS ============
interface CreateCheckoutDto {
userId: string;
email: string;
priceId: string;
quantity?: number;
mode?: 'payment' | 'subscription';
successUrl: string;
cancelUrl: string;
metadata?: Record<string, string>;
}
interface CreateSubscriptionDto {
userId: string;
email: string;
priceId: string;
}
interface Payment {
id: string;
user_id: string;
stripe_payment_id: string;
amount: number;
currency: string;
status: string;
}
interface Subscription {
id: string;
user_id: string;
stripe_subscription_id: string;
stripe_customer_id: string;
status: string;
current_period_start: Date;
current_period_end: Date;
cancel_at_period_end?: boolean;
}
interface Customer {
id: string;
user_id: string;
stripe_customer_id: string;
email: string;
}