/** * Stripe Adapter * * Implementation of the StripePort interface using the Stripe SDK. */ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Stripe from 'stripe'; import { StripePort, CreateCheckoutSessionInput, CreateCheckoutSessionOutput, CreateCommissionCheckoutInput, CreateCommissionCheckoutOutput, CreatePortalSessionInput, CreatePortalSessionOutput, StripeSubscriptionData, StripeCheckoutSessionData, StripeWebhookEvent, } from '@domain/ports/out/stripe.port'; import { SubscriptionPlanType } from '@domain/value-objects/subscription-plan.vo'; @Injectable() export class StripeAdapter implements StripePort { private readonly logger = new Logger(StripeAdapter.name); private readonly stripe: Stripe; private readonly webhookSecret: string; private readonly priceIdMap: Map; private readonly planPriceMap: Map; constructor(private readonly configService: ConfigService) { const apiKey = this.configService.get('STRIPE_SECRET_KEY'); if (!apiKey) { this.logger.warn('STRIPE_SECRET_KEY not configured - Stripe features will be disabled'); } this.stripe = new Stripe(apiKey || 'sk_test_placeholder'); this.webhookSecret = this.configService.get('STRIPE_WEBHOOK_SECRET') || ''; // Map Stripe price IDs to plans this.priceIdMap = new Map(); this.planPriceMap = new Map(); // Configure plan price IDs from environment const silverMonthly = this.configService.get('STRIPE_SILVER_MONTHLY_PRICE_ID'); const silverYearly = this.configService.get('STRIPE_SILVER_YEARLY_PRICE_ID'); const goldMonthly = this.configService.get('STRIPE_GOLD_MONTHLY_PRICE_ID'); const goldYearly = this.configService.get('STRIPE_GOLD_YEARLY_PRICE_ID'); const platiniumMonthly = this.configService.get('STRIPE_PLATINIUM_MONTHLY_PRICE_ID'); const platiniumYearly = this.configService.get('STRIPE_PLATINIUM_YEARLY_PRICE_ID'); if (silverMonthly) this.priceIdMap.set(silverMonthly, 'SILVER'); if (silverYearly) this.priceIdMap.set(silverYearly, 'SILVER'); if (goldMonthly) this.priceIdMap.set(goldMonthly, 'GOLD'); if (goldYearly) this.priceIdMap.set(goldYearly, 'GOLD'); if (platiniumMonthly) this.priceIdMap.set(platiniumMonthly, 'PLATINIUM'); if (platiniumYearly) this.priceIdMap.set(platiniumYearly, 'PLATINIUM'); this.planPriceMap.set('SILVER', { monthly: silverMonthly || '', yearly: silverYearly || '', }); this.planPriceMap.set('GOLD', { monthly: goldMonthly || '', yearly: goldYearly || '', }); this.planPriceMap.set('PLATINIUM', { monthly: platiniumMonthly || '', yearly: platiniumYearly || '', }); } async createCheckoutSession( input: CreateCheckoutSessionInput ): Promise { const planPrices = this.planPriceMap.get(input.plan); if (!planPrices) { throw new Error(`No price configuration for plan: ${input.plan}`); } const priceId = input.billingInterval === 'yearly' ? planPrices.yearly : planPrices.monthly; if (!priceId) { throw new Error(`No ${input.billingInterval} price configured for plan: ${input.plan}`); } const sessionParams: Stripe.Checkout.SessionCreateParams = { mode: 'subscription', payment_method_types: ['card'], line_items: [ { price: priceId, quantity: 1, }, ], success_url: input.successUrl, cancel_url: input.cancelUrl, customer_email: input.customerId ? undefined : input.email, customer: input.customerId || undefined, metadata: { organizationId: input.organizationId, organizationName: input.organizationName, plan: input.plan, }, subscription_data: { metadata: { organizationId: input.organizationId, plan: input.plan, }, }, allow_promotion_codes: true, billing_address_collection: 'required', }; const session = await this.stripe.checkout.sessions.create(sessionParams); this.logger.log( `Created checkout session ${session.id} for organization ${input.organizationId}` ); return { sessionId: session.id, sessionUrl: session.url || '', }; } async createCommissionCheckout( input: CreateCommissionCheckoutInput ): Promise { const session = await this.stripe.checkout.sessions.create({ mode: 'payment', payment_method_types: ['card'], line_items: [ { price_data: { currency: input.currency, unit_amount: input.amountCents, product_data: { name: 'Commission Xpeditis', description: input.bookingDescription, }, }, quantity: 1, }, ], customer_email: input.customerEmail, success_url: input.successUrl, cancel_url: input.cancelUrl, metadata: { type: 'commission', bookingId: input.bookingId, organizationId: input.organizationId, }, }); this.logger.log( `Created commission checkout session ${session.id} for booking ${input.bookingId}` ); return { sessionId: session.id, sessionUrl: session.url || '', }; } async createPortalSession(input: CreatePortalSessionInput): Promise { const session = await this.stripe.billingPortal.sessions.create({ customer: input.customerId, return_url: input.returnUrl, }); this.logger.log(`Created portal session for customer ${input.customerId}`); return { sessionUrl: session.url, }; } async getSubscription(subscriptionId: string): Promise { try { const subscription = await this.stripe.subscriptions.retrieve(subscriptionId); // Get the price ID from the first item const priceId = subscription.items.data[0]?.price.id || ''; return { subscriptionId: subscription.id, customerId: subscription.customer as string, status: subscription.status, planId: priceId, currentPeriodStart: new Date(subscription.current_period_start * 1000), currentPeriodEnd: new Date(subscription.current_period_end * 1000), cancelAtPeriodEnd: subscription.cancel_at_period_end, }; } catch (error) { if ((error as any).code === 'resource_missing') { return null; } throw error; } } async getCheckoutSession(sessionId: string): Promise { try { const session = await this.stripe.checkout.sessions.retrieve(sessionId); return { sessionId: session.id, customerId: session.customer as string | null, subscriptionId: session.subscription as string | null, status: session.status || 'unknown', metadata: (session.metadata || {}) as Record, }; } catch (error) { if ((error as any).code === 'resource_missing') { return null; } this.logger.error(`Failed to retrieve checkout session ${sessionId}:`, error); throw error; } } async cancelSubscriptionAtPeriodEnd(subscriptionId: string): Promise { await this.stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true, }); this.logger.log(`Scheduled subscription ${subscriptionId} for cancellation at period end`); } async cancelSubscriptionImmediately(subscriptionId: string): Promise { await this.stripe.subscriptions.cancel(subscriptionId); this.logger.log(`Cancelled subscription ${subscriptionId} immediately`); } async resumeSubscription(subscriptionId: string): Promise { await this.stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: false, }); this.logger.log(`Resumed subscription ${subscriptionId}`); } async constructWebhookEvent( payload: string | Buffer, signature: string ): Promise { const event = this.stripe.webhooks.constructEvent(payload, signature, this.webhookSecret); return { type: event.type, data: { object: event.data.object as Record, }, }; } mapPriceIdToPlan(priceId: string): SubscriptionPlanType | null { return this.priceIdMap.get(priceId) || null; } }