workspace-v1/shared/catalog/payments/_reference/payment.service.reference.ts
rckrdmrd cb4c0681d3 feat(workspace): Add new projects and update architecture
New projects created:
- michangarrito (marketplace mobile)
- template-saas (SaaS template)
- clinica-dental (dental ERP)
- clinica-veterinaria (veterinary ERP)

Architecture updates:
- Move catalog from core/ to shared/
- Add MCP servers structure and templates
- Add git management scripts
- Update SUBREPOSITORIOS.md with 15 new repos
- Update .gitignore for new projects

Repository infrastructure:
- 4 main repositories
- 11 subrepositorios
- Gitea remotes configured

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 04:43:28 -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;
}