"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