220 lines
5.8 KiB
TypeScript
220 lines
5.8 KiB
TypeScript
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];
|
|
}
|
|
}
|