- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
457 lines
19 KiB
JavaScript
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
|