import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { DEFAULT_LANGUAGE_CODE, PROPERTY_STRIPE_CONFIG } from '@ghostfolio/common/config'; import { parseDate } from '@ghostfolio/common/helper'; import { SubscriptionOffer } from '@ghostfolio/common/interfaces'; import { SubscriptionOfferKey, UserWithSettings } from '@ghostfolio/common/types'; import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type'; import { Injectable, Logger } from '@nestjs/common'; import { Subscription } from '@prisma/client'; import { addMilliseconds, isBefore } from 'date-fns'; import ms, { StringValue } from 'ms'; import Stripe from 'stripe'; @Injectable() export class SubscriptionService { private stripe: Stripe; public constructor( private readonly configurationService: ConfigurationService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService ) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { this.stripe = new Stripe( this.configurationService.get('STRIPE_SECRET_KEY'), { apiVersion: '2024-09-30.acacia' } ); } } public async createCheckoutSession({ couponId, priceId, user }: { couponId?: string; priceId: string; user: UserWithSettings; }) { const subscriptionOffers: { [offer in SubscriptionOfferKey]: SubscriptionOffer; } = ((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ?? {}; const subscriptionOffer = Object.values(subscriptionOffers).find( (subscriptionOffer) => { return subscriptionOffer.priceId === priceId; } ); const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = { cancel_url: `${this.configurationService.get('ROOT_URL')}/${ user.Settings?.settings?.language ?? DEFAULT_LANGUAGE_CODE }/account`, client_reference_id: user.id, line_items: [ { price: priceId, quantity: 1 } ], locale: (user.Settings?.settings ?.language as Stripe.Checkout.SessionCreateParams.Locale) ?? DEFAULT_LANGUAGE_CODE, metadata: subscriptionOffer ? { subscriptionOffer: JSON.stringify(subscriptionOffer) } : {}, mode: 'payment', payment_method_types: ['card'], success_url: `${this.configurationService.get( 'ROOT_URL' )}/api/v1/subscription/stripe/callback?checkoutSessionId={CHECKOUT_SESSION_ID}` }; if (couponId) { checkoutSessionCreateParams.discounts = [ { coupon: couponId } ]; } const session = await this.stripe.checkout.sessions.create( checkoutSessionCreateParams ); return { sessionId: session.id }; } public async createSubscription({ duration = '1 year', durationExtension, price, userId }: { duration?: StringValue; durationExtension?: StringValue; price: number; userId: string; }) { let expiresAt = addMilliseconds(new Date(), ms(duration)); if (durationExtension) { expiresAt = addMilliseconds(expiresAt, ms(durationExtension)); } await this.prismaService.subscription.create({ data: { expiresAt, price, User: { connect: { id: userId } } } }); } public async createSubscriptionViaStripe(aCheckoutSessionId: string) { try { let durationExtension: StringValue; const session = await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId); const subscriptionOffer: SubscriptionOffer = JSON.parse( session.metadata.subscriptionOffer ?? '{}' ); if (subscriptionOffer) { durationExtension = subscriptionOffer.durationExtension; } await this.createSubscription({ durationExtension, price: session.amount_total / 100, userId: session.client_reference_id }); return session.client_reference_id; } catch (error) { Logger.error(error, 'SubscriptionService'); } } public async getSubscription({ createdAt, subscriptions }: { createdAt: UserWithSettings['createdAt']; subscriptions: Subscription[]; }): Promise<UserWithSettings['subscription']> { if (subscriptions.length > 0) { const { expiresAt, price } = subscriptions.reduce((a, b) => { return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b; }); let offerKey: SubscriptionOfferKey = price ? 'renewal' : 'default'; if (isBefore(createdAt, parseDate('2023-01-01'))) { offerKey = 'renewal-early-bird-2023'; } else if (isBefore(createdAt, parseDate('2024-01-01'))) { offerKey = 'renewal-early-bird-2024'; } const offer = await this.getSubscriptionOffer({ key: offerKey }); return { expiresAt, offer, type: isBefore(new Date(), expiresAt) ? SubscriptionType.Premium : SubscriptionType.Basic }; } else { const offer = await this.getSubscriptionOffer({ key: 'default' }); return { offer, type: SubscriptionType.Basic }; } } public async getSubscriptionOffer({ key }: { key: SubscriptionOfferKey; }): Promise<SubscriptionOffer> { if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { return undefined; } const offers = ((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ?? {}; return offers[key]; } }