template-saas/apps/backend/dist/modules/billing/services/stripe.service.js
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

457 lines
19 KiB
JavaScript

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var StripeService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.StripeService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const stripe_1 = __importDefault(require("stripe"));
const subscription_entity_1 = require("../entities/subscription.entity");
const invoice_entity_1 = require("../entities/invoice.entity");
const payment_method_entity_1 = require("../entities/payment-method.entity");
const stripe_webhook_dto_1 = require("../dto/stripe-webhook.dto");
let StripeService = StripeService_1 = class StripeService {
constructor(configService, subscriptionRepo, invoiceRepo, paymentMethodRepo) {
this.configService = configService;
this.subscriptionRepo = subscriptionRepo;
this.invoiceRepo = invoiceRepo;
this.paymentMethodRepo = paymentMethodRepo;
this.logger = new common_1.Logger(StripeService_1.name);
}
onModuleInit() {
const apiKey = this.configService.get('STRIPE_SECRET_KEY');
if (!apiKey) {
this.logger.warn('STRIPE_SECRET_KEY not configured - Stripe integration disabled');
return;
}
this.stripe = new stripe_1.default(apiKey, {
apiVersion: '2025-02-24.acacia',
typescript: true,
});
this.logger.log('Stripe client initialized');
}
ensureStripeConfigured() {
if (!this.stripe) {
throw new common_1.BadRequestException('Stripe is not configured');
}
}
async createCustomer(dto) {
this.ensureStripeConfigured();
const customer = await this.stripe.customers.create({
email: dto.email,
name: dto.name,
metadata: {
tenant_id: dto.tenant_id,
...dto.metadata,
},
});
this.logger.log(`Created Stripe customer ${customer.id} for tenant ${dto.tenant_id}`);
return customer;
}
async getCustomer(customerId) {
this.ensureStripeConfigured();
try {
const customer = await this.stripe.customers.retrieve(customerId);
return customer;
}
catch (error) {
if (error.code === 'resource_missing') {
return null;
}
throw error;
}
}
async findCustomerByTenantId(tenantId) {
this.ensureStripeConfigured();
const customers = await this.stripe.customers.search({
query: `metadata['tenant_id']:'${tenantId}'`,
});
return customers.data[0] || null;
}
async updateCustomer(customerId, data) {
this.ensureStripeConfigured();
return this.stripe.customers.update(customerId, data);
}
async createSubscription(dto) {
this.ensureStripeConfigured();
const subscriptionData = {
customer: dto.customer_id,
items: [{ price: dto.price_id }],
payment_behavior: 'default_incomplete',
payment_settings: {
save_default_payment_method: 'on_subscription',
},
expand: ['latest_invoice.payment_intent'],
metadata: dto.metadata,
};
if (dto.trial_period_days) {
subscriptionData.trial_period_days = dto.trial_period_days;
}
const subscription = await this.stripe.subscriptions.create(subscriptionData);
this.logger.log(`Created Stripe subscription ${subscription.id}`);
return subscription;
}
async getStripeSubscription(subscriptionId) {
this.ensureStripeConfigured();
try {
return await this.stripe.subscriptions.retrieve(subscriptionId);
}
catch (error) {
if (error.code === 'resource_missing') {
return null;
}
throw error;
}
}
async cancelStripeSubscription(subscriptionId, options) {
this.ensureStripeConfigured();
if (options?.immediately) {
return this.stripe.subscriptions.cancel(subscriptionId);
}
return this.stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
}
async updateStripeSubscription(subscriptionId, priceId) {
this.ensureStripeConfigured();
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
return this.stripe.subscriptions.update(subscriptionId, {
items: [
{
id: subscription.items.data[0].id,
price: priceId,
},
],
proration_behavior: 'create_prorations',
});
}
async createCheckoutSession(dto) {
this.ensureStripeConfigured();
let customer = await this.findCustomerByTenantId(dto.tenant_id);
if (!customer) {
throw new common_1.NotFoundException('Stripe customer not found for tenant');
}
const sessionParams = {
customer: customer.id,
mode: 'subscription',
line_items: [
{
price: dto.price_id,
quantity: 1,
},
],
success_url: dto.success_url,
cancel_url: dto.cancel_url,
subscription_data: {
metadata: {
tenant_id: dto.tenant_id,
},
...(dto.trial_period_days && { trial_period_days: dto.trial_period_days }),
},
};
const session = await this.stripe.checkout.sessions.create(sessionParams);
this.logger.log(`Created checkout session ${session.id} for tenant ${dto.tenant_id}`);
return session;
}
async createBillingPortalSession(dto) {
this.ensureStripeConfigured();
const customer = await this.findCustomerByTenantId(dto.tenant_id);
if (!customer) {
throw new common_1.NotFoundException('Stripe customer not found for tenant');
}
const session = await this.stripe.billingPortal.sessions.create({
customer: customer.id,
return_url: dto.return_url,
});
this.logger.log(`Created billing portal session for tenant ${dto.tenant_id}`);
return session;
}
async attachPaymentMethod(paymentMethodId, customerId) {
this.ensureStripeConfigured();
return this.stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId,
});
}
async detachPaymentMethod(paymentMethodId) {
this.ensureStripeConfigured();
return this.stripe.paymentMethods.detach(paymentMethodId);
}
async listPaymentMethods(customerId) {
this.ensureStripeConfigured();
const paymentMethods = await this.stripe.paymentMethods.list({
customer: customerId,
type: 'card',
});
return paymentMethods.data;
}
async setDefaultPaymentMethod(customerId, paymentMethodId) {
this.ensureStripeConfigured();
return this.stripe.customers.update(customerId, {
invoice_settings: {
default_payment_method: paymentMethodId,
},
});
}
constructWebhookEvent(payload, signature) {
this.ensureStripeConfigured();
const webhookSecret = this.configService.get('STRIPE_WEBHOOK_SECRET');
if (!webhookSecret) {
throw new common_1.BadRequestException('Webhook secret not configured');
}
return this.stripe.webhooks.constructEvent(payload, signature, webhookSecret);
}
async handleWebhookEvent(event) {
this.logger.log(`Processing webhook event: ${event.type}`);
switch (event.type) {
case stripe_webhook_dto_1.StripeWebhookEventType.SUBSCRIPTION_CREATED:
case stripe_webhook_dto_1.StripeWebhookEventType.SUBSCRIPTION_UPDATED:
await this.syncSubscription(event.data.object);
break;
case stripe_webhook_dto_1.StripeWebhookEventType.SUBSCRIPTION_DELETED:
await this.handleSubscriptionDeleted(event.data.object);
break;
case stripe_webhook_dto_1.StripeWebhookEventType.INVOICE_PAID:
await this.handleInvoicePaid(event.data.object);
break;
case stripe_webhook_dto_1.StripeWebhookEventType.INVOICE_PAYMENT_FAILED:
await this.handleInvoicePaymentFailed(event.data.object);
break;
case stripe_webhook_dto_1.StripeWebhookEventType.PAYMENT_METHOD_ATTACHED:
await this.syncPaymentMethod(event.data.object);
break;
case stripe_webhook_dto_1.StripeWebhookEventType.PAYMENT_METHOD_DETACHED:
await this.handlePaymentMethodDetached(event.data.object);
break;
case stripe_webhook_dto_1.StripeWebhookEventType.CHECKOUT_SESSION_COMPLETED:
await this.handleCheckoutCompleted(event.data.object);
break;
default:
this.logger.log(`Unhandled event type: ${event.type}`);
}
}
async syncSubscription(stripeSubscription) {
const tenantId = stripeSubscription.metadata?.tenant_id;
if (!tenantId) {
this.logger.warn(`Subscription ${stripeSubscription.id} has no tenant_id metadata`);
return;
}
let subscription = await this.subscriptionRepo.findOne({
where: { external_subscription_id: stripeSubscription.id },
});
const status = this.mapStripeStatus(stripeSubscription.status);
if (!subscription) {
subscription = this.subscriptionRepo.create({
tenant_id: tenantId,
plan_id: stripeSubscription.items.data[0]?.price?.product,
external_subscription_id: stripeSubscription.id,
payment_provider: 'stripe',
});
}
subscription.status = status;
subscription.current_period_start = new Date(stripeSubscription.current_period_start * 1000);
subscription.current_period_end = new Date(stripeSubscription.current_period_end * 1000);
if (stripeSubscription.trial_end) {
subscription.trial_end = new Date(stripeSubscription.trial_end * 1000);
}
if (stripeSubscription.canceled_at) {
subscription.cancelled_at = new Date(stripeSubscription.canceled_at * 1000);
}
subscription.metadata = {
...subscription.metadata,
stripe_customer_id: stripeSubscription.customer,
stripe_price_id: stripeSubscription.items.data[0]?.price?.id,
};
await this.subscriptionRepo.save(subscription);
this.logger.log(`Synced subscription ${stripeSubscription.id} for tenant ${tenantId}`);
}
async handleSubscriptionDeleted(stripeSubscription) {
const subscription = await this.subscriptionRepo.findOne({
where: { external_subscription_id: stripeSubscription.id },
});
if (subscription) {
subscription.status = subscription_entity_1.SubscriptionStatus.CANCELLED;
subscription.cancelled_at = new Date();
await this.subscriptionRepo.save(subscription);
this.logger.log(`Cancelled subscription ${stripeSubscription.id}`);
}
}
async handleInvoicePaid(stripeInvoice) {
const tenantId = stripeInvoice.subscription_details?.metadata?.tenant_id ||
stripeInvoice.metadata?.tenant_id;
if (!tenantId) {
this.logger.warn(`Invoice ${stripeInvoice.id} has no tenant_id`);
return;
}
let invoice = await this.invoiceRepo.findOne({
where: {
tenant_id: tenantId,
invoice_number: stripeInvoice.number || stripeInvoice.id,
},
});
if (!invoice) {
invoice = this.invoiceRepo.create({
tenant_id: tenantId,
subscription_id: stripeInvoice.subscription,
invoice_number: stripeInvoice.number || stripeInvoice.id,
subtotal: stripeInvoice.subtotal / 100,
tax: stripeInvoice.tax ? stripeInvoice.tax / 100 : 0,
total: stripeInvoice.total / 100,
due_date: stripeInvoice.due_date
? new Date(stripeInvoice.due_date * 1000)
: new Date(),
});
}
invoice.status = invoice_entity_1.InvoiceStatus.PAID;
invoice.paid_at = new Date();
invoice.external_invoice_id = stripeInvoice.id;
invoice.pdf_url = stripeInvoice.invoice_pdf || stripeInvoice.hosted_invoice_url || null;
await this.invoiceRepo.save(invoice);
this.logger.log(`Recorded paid invoice ${stripeInvoice.id} for tenant ${tenantId}`);
}
async handleInvoicePaymentFailed(stripeInvoice) {
const tenantId = stripeInvoice.subscription_details?.metadata?.tenant_id;
if (!tenantId) {
return;
}
const subscription = await this.subscriptionRepo.findOne({
where: { tenant_id: tenantId },
order: { created_at: 'DESC' },
});
if (subscription) {
subscription.status = subscription_entity_1.SubscriptionStatus.PAST_DUE;
subscription.metadata = {
...subscription.metadata,
payment_failed_at: new Date().toISOString(),
failed_invoice_id: stripeInvoice.id,
};
await this.subscriptionRepo.save(subscription);
this.logger.log(`Marked subscription as past_due for tenant ${tenantId}`);
}
}
async syncPaymentMethod(stripePaymentMethod) {
const customerId = stripePaymentMethod.customer;
if (!customerId)
return;
const customer = await this.getCustomer(customerId);
const tenantId = customer?.metadata?.tenant_id;
if (!tenantId)
return;
let paymentMethod = await this.paymentMethodRepo.findOne({
where: { external_payment_method_id: stripePaymentMethod.id },
});
if (!paymentMethod) {
paymentMethod = this.paymentMethodRepo.create({
tenant_id: tenantId,
external_payment_method_id: stripePaymentMethod.id,
});
}
const card = stripePaymentMethod.card;
if (card) {
paymentMethod.type = 'card';
paymentMethod.card_brand = card.brand;
paymentMethod.card_last_four = card.last4;
paymentMethod.card_exp_month = card.exp_month;
paymentMethod.card_exp_year = card.exp_year;
}
paymentMethod.payment_provider = 'stripe';
paymentMethod.is_active = true;
await this.paymentMethodRepo.save(paymentMethod);
this.logger.log(`Synced payment method ${stripePaymentMethod.id} for tenant ${tenantId}`);
}
async handlePaymentMethodDetached(stripePaymentMethod) {
const paymentMethod = await this.paymentMethodRepo.findOne({
where: { external_payment_method_id: stripePaymentMethod.id },
});
if (paymentMethod) {
paymentMethod.is_active = false;
await this.paymentMethodRepo.save(paymentMethod);
this.logger.log(`Deactivated payment method ${stripePaymentMethod.id}`);
}
}
async handleCheckoutCompleted(session) {
const tenantId = session.metadata?.tenant_id ||
session.subscription_data?.metadata?.tenant_id;
if (!tenantId || !session.subscription) {
return;
}
const stripeSubscription = await this.getStripeSubscription(session.subscription);
if (stripeSubscription) {
await this.syncSubscription(stripeSubscription);
}
this.logger.log(`Processed checkout completion for tenant ${tenantId}`);
}
mapStripeStatus(stripeStatus) {
const statusMap = {
trialing: subscription_entity_1.SubscriptionStatus.TRIAL,
active: subscription_entity_1.SubscriptionStatus.ACTIVE,
past_due: subscription_entity_1.SubscriptionStatus.PAST_DUE,
canceled: subscription_entity_1.SubscriptionStatus.CANCELLED,
unpaid: subscription_entity_1.SubscriptionStatus.PAST_DUE,
incomplete: subscription_entity_1.SubscriptionStatus.TRIAL,
incomplete_expired: subscription_entity_1.SubscriptionStatus.EXPIRED,
paused: subscription_entity_1.SubscriptionStatus.CANCELLED,
};
return statusMap[stripeStatus] || subscription_entity_1.SubscriptionStatus.ACTIVE;
}
async listPrices(productId) {
this.ensureStripeConfigured();
const params = {
active: true,
expand: ['data.product'],
};
if (productId) {
params.product = productId;
}
const prices = await this.stripe.prices.list(params);
return prices.data;
}
async getPrice(priceId) {
this.ensureStripeConfigured();
try {
return await this.stripe.prices.retrieve(priceId, {
expand: ['product'],
});
}
catch (error) {
if (error.code === 'resource_missing') {
return null;
}
throw error;
}
}
async createSetupIntent(customerId) {
this.ensureStripeConfigured();
return this.stripe.setupIntents.create({
customer: customerId,
payment_method_types: ['card'],
});
}
};
exports.StripeService = StripeService;
exports.StripeService = StripeService = StripeService_1 = __decorate([
(0, common_1.Injectable)(),
__param(1, (0, typeorm_1.InjectRepository)(subscription_entity_1.Subscription)),
__param(2, (0, typeorm_1.InjectRepository)(invoice_entity_1.Invoice)),
__param(3, (0, typeorm_1.InjectRepository)(payment_method_entity_1.PaymentMethod)),
__metadata("design:paramtypes", [config_1.ConfigService,
typeorm_2.Repository,
typeorm_2.Repository,
typeorm_2.Repository])
], StripeService);
//# sourceMappingURL=stripe.service.js.map