You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

219 lines
5.8 KiB

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
};
}
}
private 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];
}
}