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
296 lines
8.4 KiB
TypeScript
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;
|
|
}
|