Browse Source

Task/improve type safety in account subscription and user services (#8361)

Improve type safety
pull/8387/head
Kenrick Tandrian 2 days ago
committed by GitHub
parent
commit
8109fffa80
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 41
      apps/api/src/services/account/account.service.ts
  2. 29
      apps/api/src/services/subscription/subscription.service.ts
  3. 33
      apps/api/src/services/user/user.service.ts
  4. 2
      libs/common/src/lib/types/account-with-platform.type.ts

41
apps/api/src/services/account/account.service.ts

@ -73,9 +73,10 @@ export class AccountService {
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput, accountWhereUniqueInput: Prisma.AccountWhereUniqueInput,
accountInclude: Prisma.AccountInclude accountInclude: Prisma.AccountInclude
): Promise< ): Promise<
Account & { | (Account & {
activities?: Order[]; activities?: Order[];
} })
| null
> { > {
return this.prismaService.account.findUnique({ return this.prismaService.account.findUnique({
include: accountInclude, include: accountInclude,
@ -97,7 +98,7 @@ export class AccountService {
tags?: Pick<Tag, 'id'>[]; tags?: Pick<Tag, 'id'>[];
})[]; })[];
balances?: AccountBalance[]; balances?: AccountBalance[];
platform?: Platform; platform?: Platform | null;
tags?: Tag[]; tags?: Tag[];
})[] })[]
> { > {
@ -140,7 +141,15 @@ export class AccountService {
const startOfUtcDateOfTomorrow = getStartOfUtcDateOfTomorrow(); const startOfUtcDateOfTomorrow = getStartOfUtcDateOfTomorrow();
return accounts.map((account) => { return accounts.map((account) => {
const result = { const result: AccountWithBalance & {
activities?: (Order & {
SymbolProfile?: SymbolProfile;
tags?: Pick<Tag, 'id'>[];
})[];
balances?: AccountBalance[];
platform?: Platform | null;
tags?: Tag[];
} = {
...account, ...account,
balance: balance:
// The balances are ordered by date descending, hence the first account // The balances are ordered by date descending, hence the first account
@ -181,7 +190,9 @@ export class AccountService {
tagIds?: string[]; tagIds?: string[];
userId: string; userId: string;
}): Promise<Account> { }): Promise<Account> {
await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId }); if (tagIds?.length) {
await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId });
}
const account = await this.prismaService.account.create({ const account = await this.prismaService.account.create({
data: { data: {
@ -248,7 +259,7 @@ export class AccountService {
return accounts.map((account) => { return accounts.map((account) => {
const result = { const result = {
...account, ...account,
activitiesCount: account.activities.length activitiesCount: account.activities?.length ?? 0
}; };
delete result.activities; delete result.activities;
@ -296,7 +307,7 @@ export class AccountService {
totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus( totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
account.balance, account.balance,
account.currency, account.currency ?? currency,
currency currency
) )
); );
@ -321,7 +332,9 @@ export class AccountService {
userId: string; userId: string;
where: Prisma.AccountWhereUniqueInput; where: Prisma.AccountWhereUniqueInput;
}): Promise<Account> { }): Promise<Account> {
await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId }); if (tagIds?.length) {
await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId });
}
const account = await this.prismaService.account.update({ const account = await this.prismaService.account.update({
data: { data: {
@ -372,18 +385,24 @@ export class AccountService {
date?: Date; date?: Date;
userId: string; userId: string;
}) { }) {
const { balance, currency: currencyOfAccount } = await this.account({ const account = await this.account({
id_userId: { id_userId: {
userId, userId,
id: accountId id: accountId
} }
}); });
if (!account) {
throw new Error(`Account '${accountId}' has not been found`);
}
const { balance, currency: currencyOfAccount } = account;
const amountInCurrencyOfAccount = const amountInCurrencyOfAccount =
await this.exchangeRateDataService.toCurrencyAtDate( await this.exchangeRateDataService.toCurrencyAtDate(
amount, amount,
currency, currency,
currencyOfAccount, currencyOfAccount ?? currency,
date date
); );

29
apps/api/src/services/subscription/subscription.service.ts

@ -86,7 +86,7 @@ export class SubscriptionService {
{ {
allowed_payment_method_types: ['card'], allowed_payment_method_types: ['card'],
cancel_url: `${this.configurationService.get('ROOT_URL')}/${ cancel_url: `${this.configurationService.get('ROOT_URL')}/${
user.settings.settings.language user.settings?.settings?.language ?? DEFAULT_LANGUAGE_CODE
}/account`, }/account`,
client_reference_id: user.id, client_reference_id: user.id,
line_items: [ line_items: [
@ -115,6 +115,10 @@ export class SubscriptionService {
stripeCheckoutSessionCreateParams stripeCheckoutSessionCreateParams
); );
if (!session.url) {
throw new Error(`Stripe Checkout Session '${session.id}' is missing url`);
}
return { return {
sessionUrl: session.url sessionUrl: session.url
}; };
@ -170,10 +174,16 @@ export class SubscriptionService {
const durationExtension = subscriptionOffer?.durationExtension; const durationExtension = subscriptionOffer?.durationExtension;
if (!session.client_reference_id) {
throw new Error(
`Stripe Checkout Session '${aCheckoutSessionId}' is missing client_reference_id`
);
}
try { try {
await this.createSubscription({ await this.createSubscription({
durationExtension, durationExtension,
price: session.amount_total / 100, price: (session.amount_total ?? 0) / 100,
stripeCheckoutSessionId: session.id, stripeCheckoutSessionId: session.id,
userId: session.client_reference_id userId: session.client_reference_id
}); });
@ -214,11 +224,11 @@ export class SubscriptionService {
let offerKey: SubscriptionOfferKey = price ? 'renewal' : 'default'; let offerKey: SubscriptionOfferKey = price ? 'renewal' : 'default';
if (isBefore(createdAt, parseDate('2023-01-01'))) { if (createdAt && isBefore(createdAt, parseDate('2023-01-01')!)) {
offerKey = 'renewal-early-bird-2023'; offerKey = 'renewal-early-bird-2023';
} else if (isBefore(createdAt, parseDate('2024-01-01'))) { } else if (createdAt && isBefore(createdAt, parseDate('2024-01-01')!)) {
offerKey = 'renewal-early-bird-2024'; offerKey = 'renewal-early-bird-2024';
} else if (isBefore(createdAt, parseDate('2025-12-01'))) { } else if (createdAt && isBefore(createdAt, parseDate('2025-12-01')!)) {
offerKey = 'renewal-early-bird-2025'; offerKey = 'renewal-early-bird-2025';
} }
@ -227,9 +237,9 @@ export class SubscriptionService {
}); });
return { return {
offer,
subscribedAt, subscribedAt,
expiresAt: isBefore(new Date(), expiresAt) ? expiresAt : undefined, expiresAt: isBefore(new Date(), expiresAt) ? expiresAt : undefined,
offer: offer as SubscriptionOffer,
type: isBefore(new Date(), expiresAt) type: isBefore(new Date(), expiresAt)
? SubscriptionType.Premium ? SubscriptionType.Premium
: SubscriptionType.Basic : SubscriptionType.Basic
@ -240,7 +250,7 @@ export class SubscriptionService {
}); });
return { return {
offer, offer: offer as SubscriptionOffer,
type: SubscriptionType.Basic type: SubscriptionType.Basic
}; };
} }
@ -250,7 +260,7 @@ export class SubscriptionService {
key key
}: { }: {
key: SubscriptionOfferKey; key: SubscriptionOfferKey;
}): Promise<SubscriptionOffer> { }): Promise<SubscriptionOffer | undefined> {
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
return undefined; return undefined;
} }
@ -267,7 +277,7 @@ export class SubscriptionService {
} }
private getStripeLocale( private getStripeLocale(
languageCode: string languageCode?: string
): Stripe.Checkout.SessionCreateParams.Locale { ): Stripe.Checkout.SessionCreateParams.Locale {
const unsupportedLanguageCodes: Record< const unsupportedLanguageCodes: Record<
Exclude< Exclude<
@ -286,6 +296,7 @@ export class SubscriptionService {
}; };
if ( if (
languageCode &&
(SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(languageCode) && (SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(languageCode) &&
!(languageCode in unsupportedLanguageCodes) !(languageCode in unsupportedLanguageCodes)
) { ) {

33
apps/api/src/services/user/user.service.ts

@ -143,7 +143,7 @@ export class UserService {
}), }),
impersonationUserId impersonationUserId
? this.user({ id: impersonationUserId }) ? this.user({ id: impersonationUserId })
: Promise.resolve<UserWithSettings>(null), : Promise.resolve<UserWithSettings | null>(null),
this.tagService.getTagsForUser(impersonationUserId || user.id) this.tagService.getTagsForUser(impersonationUserId || user.id)
]); ]);
@ -207,10 +207,8 @@ export class UserService {
activityTypes, activityTypes,
dateOfFirstActivity, dateOfFirstActivity,
id, id,
permissions,
referralPartners, referralPartners,
scopes, scopes,
subscription,
systemMessage, systemMessage,
tags, tags,
access: access.map((accessItem) => { access: access.map((accessItem) => {
@ -223,13 +221,17 @@ export class UserService {
}; };
}), }),
accounts: accounts.sort((a, b) => { accounts: accounts.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); return (a.name ?? '')
.toLowerCase()
.localeCompare((b.name ?? '').toLowerCase());
}), }),
permissions: permissions ?? [],
settings: { settings: {
...resolvedUserSettings, ...resolvedUserSettings,
baseCurrency: resolvedUserSettings.baseCurrency ?? DEFAULT_CURRENCY, baseCurrency: resolvedUserSettings.baseCurrency ?? DEFAULT_CURRENCY,
locale: resolvedUserSettings.locale ?? locale locale: resolvedUserSettings.locale ?? locale
} },
subscription: subscription as IUser['subscription']
}; };
} }
@ -436,7 +438,10 @@ export class UserService {
frequency = 6; frequency = 6;
} }
if (frequency === 1 || analytics?.activityCount % frequency === 1) { if (
frequency === 1 ||
(analytics?.activityCount ?? 0) % frequency === 1
) {
currentPermissions.push(permissions.enableSubscriptionInterstitial); currentPermissions.push(permissions.enableSubscriptionInterstitial);
} }
@ -551,7 +556,9 @@ export class UserService {
} }
user.accounts = user.accounts.sort((a, b) => { user.accounts = user.accounts.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); return (a.name ?? '')
.toLowerCase()
.localeCompare((b.name ?? '').toLowerCase());
}); });
user.permissions = currentPermissions.sort(); user.permissions = currentPermissions.sort();
@ -660,11 +667,13 @@ export class UserService {
}); });
} catch {} } catch {}
try { if (where.id) {
await this.activitiesService.deleteActivities({ try {
userId: where.id await this.activitiesService.deleteActivities({
}); userId: where.id
} catch {} });
} catch {}
}
try { try {
await this.prismaService.settings.delete({ await this.prismaService.settings.delete({

2
libs/common/src/lib/types/account-with-platform.type.ts

@ -1,6 +1,6 @@
import type { Account, Platform, Tag } from '@ghostfolio/prisma/browser'; import type { Account, Platform, Tag } from '@ghostfolio/prisma/browser';
export type AccountWithPlatform = Account & { export type AccountWithPlatform = Account & {
platform?: Platform; platform?: Platform | null;
tags?: Tag[]; tags?: Tag[];
}; };

Loading…
Cancel
Save