From 8109fffa8078a3517aefd3dc9632a53f85e413e6 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:32:27 +0700 Subject: [PATCH] Task/improve type safety in account subscription and user services (#8361) Improve type safety --- .../src/services/account/account.service.ts | 41 ++++++++++++++----- .../subscription/subscription.service.ts | 29 +++++++++---- apps/api/src/services/user/user.service.ts | 33 +++++++++------ .../lib/types/account-with-platform.type.ts | 2 +- 4 files changed, 72 insertions(+), 33 deletions(-) diff --git a/apps/api/src/services/account/account.service.ts b/apps/api/src/services/account/account.service.ts index 4cbc7cd648..e4bb055440 100644 --- a/apps/api/src/services/account/account.service.ts +++ b/apps/api/src/services/account/account.service.ts @@ -73,9 +73,10 @@ export class AccountService { accountWhereUniqueInput: Prisma.AccountWhereUniqueInput, accountInclude: Prisma.AccountInclude ): Promise< - Account & { - activities?: Order[]; - } + | (Account & { + activities?: Order[]; + }) + | null > { return this.prismaService.account.findUnique({ include: accountInclude, @@ -97,7 +98,7 @@ export class AccountService { tags?: Pick[]; })[]; balances?: AccountBalance[]; - platform?: Platform; + platform?: Platform | null; tags?: Tag[]; })[] > { @@ -140,7 +141,15 @@ export class AccountService { const startOfUtcDateOfTomorrow = getStartOfUtcDateOfTomorrow(); return accounts.map((account) => { - const result = { + const result: AccountWithBalance & { + activities?: (Order & { + SymbolProfile?: SymbolProfile; + tags?: Pick[]; + })[]; + balances?: AccountBalance[]; + platform?: Platform | null; + tags?: Tag[]; + } = { ...account, balance: // The balances are ordered by date descending, hence the first account @@ -181,7 +190,9 @@ export class AccountService { tagIds?: string[]; userId: string; }): Promise { - await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId }); + if (tagIds?.length) { + await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId }); + } const account = await this.prismaService.account.create({ data: { @@ -248,7 +259,7 @@ export class AccountService { return accounts.map((account) => { const result = { ...account, - activitiesCount: account.activities.length + activitiesCount: account.activities?.length ?? 0 }; delete result.activities; @@ -296,7 +307,7 @@ export class AccountService { totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus( this.exchangeRateDataService.toCurrency( account.balance, - account.currency, + account.currency ?? currency, currency ) ); @@ -321,7 +332,9 @@ export class AccountService { userId: string; where: Prisma.AccountWhereUniqueInput; }): Promise { - await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId }); + if (tagIds?.length) { + await this.tagService.validateTagIdsWithoutDraftTag({ tagIds, userId }); + } const account = await this.prismaService.account.update({ data: { @@ -372,18 +385,24 @@ export class AccountService { date?: Date; userId: string; }) { - const { balance, currency: currencyOfAccount } = await this.account({ + const account = await this.account({ id_userId: { userId, id: accountId } }); + if (!account) { + throw new Error(`Account '${accountId}' has not been found`); + } + + const { balance, currency: currencyOfAccount } = account; + const amountInCurrencyOfAccount = await this.exchangeRateDataService.toCurrencyAtDate( amount, currency, - currencyOfAccount, + currencyOfAccount ?? currency, date ); diff --git a/apps/api/src/services/subscription/subscription.service.ts b/apps/api/src/services/subscription/subscription.service.ts index 958f314311..41c861575d 100644 --- a/apps/api/src/services/subscription/subscription.service.ts +++ b/apps/api/src/services/subscription/subscription.service.ts @@ -86,7 +86,7 @@ export class SubscriptionService { { allowed_payment_method_types: ['card'], cancel_url: `${this.configurationService.get('ROOT_URL')}/${ - user.settings.settings.language + user.settings?.settings?.language ?? DEFAULT_LANGUAGE_CODE }/account`, client_reference_id: user.id, line_items: [ @@ -115,6 +115,10 @@ export class SubscriptionService { stripeCheckoutSessionCreateParams ); + if (!session.url) { + throw new Error(`Stripe Checkout Session '${session.id}' is missing url`); + } + return { sessionUrl: session.url }; @@ -170,10 +174,16 @@ export class SubscriptionService { const durationExtension = subscriptionOffer?.durationExtension; + if (!session.client_reference_id) { + throw new Error( + `Stripe Checkout Session '${aCheckoutSessionId}' is missing client_reference_id` + ); + } + try { await this.createSubscription({ durationExtension, - price: session.amount_total / 100, + price: (session.amount_total ?? 0) / 100, stripeCheckoutSessionId: session.id, userId: session.client_reference_id }); @@ -214,11 +224,11 @@ export class SubscriptionService { 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'; - } else if (isBefore(createdAt, parseDate('2024-01-01'))) { + } else if (createdAt && isBefore(createdAt, parseDate('2024-01-01')!)) { 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'; } @@ -227,9 +237,9 @@ export class SubscriptionService { }); return { - offer, subscribedAt, expiresAt: isBefore(new Date(), expiresAt) ? expiresAt : undefined, + offer: offer as SubscriptionOffer, type: isBefore(new Date(), expiresAt) ? SubscriptionType.Premium : SubscriptionType.Basic @@ -240,7 +250,7 @@ export class SubscriptionService { }); return { - offer, + offer: offer as SubscriptionOffer, type: SubscriptionType.Basic }; } @@ -250,7 +260,7 @@ export class SubscriptionService { key }: { key: SubscriptionOfferKey; - }): Promise { + }): Promise { if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { return undefined; } @@ -267,7 +277,7 @@ export class SubscriptionService { } private getStripeLocale( - languageCode: string + languageCode?: string ): Stripe.Checkout.SessionCreateParams.Locale { const unsupportedLanguageCodes: Record< Exclude< @@ -286,6 +296,7 @@ export class SubscriptionService { }; if ( + languageCode && (SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(languageCode) && !(languageCode in unsupportedLanguageCodes) ) { diff --git a/apps/api/src/services/user/user.service.ts b/apps/api/src/services/user/user.service.ts index 3db52a9284..b566332f40 100644 --- a/apps/api/src/services/user/user.service.ts +++ b/apps/api/src/services/user/user.service.ts @@ -143,7 +143,7 @@ export class UserService { }), impersonationUserId ? this.user({ id: impersonationUserId }) - : Promise.resolve(null), + : Promise.resolve(null), this.tagService.getTagsForUser(impersonationUserId || user.id) ]); @@ -207,10 +207,8 @@ export class UserService { activityTypes, dateOfFirstActivity, id, - permissions, referralPartners, scopes, - subscription, systemMessage, tags, access: access.map((accessItem) => { @@ -223,13 +221,17 @@ export class UserService { }; }), accounts: accounts.sort((a, b) => { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + return (a.name ?? '') + .toLowerCase() + .localeCompare((b.name ?? '').toLowerCase()); }), + permissions: permissions ?? [], settings: { ...resolvedUserSettings, baseCurrency: resolvedUserSettings.baseCurrency ?? DEFAULT_CURRENCY, locale: resolvedUserSettings.locale ?? locale - } + }, + subscription: subscription as IUser['subscription'] }; } @@ -436,7 +438,10 @@ export class UserService { frequency = 6; } - if (frequency === 1 || analytics?.activityCount % frequency === 1) { + if ( + frequency === 1 || + (analytics?.activityCount ?? 0) % frequency === 1 + ) { currentPermissions.push(permissions.enableSubscriptionInterstitial); } @@ -551,7 +556,9 @@ export class UserService { } 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(); @@ -660,11 +667,13 @@ export class UserService { }); } catch {} - try { - await this.activitiesService.deleteActivities({ - userId: where.id - }); - } catch {} + if (where.id) { + try { + await this.activitiesService.deleteActivities({ + userId: where.id + }); + } catch {} + } try { await this.prismaService.settings.delete({ diff --git a/libs/common/src/lib/types/account-with-platform.type.ts b/libs/common/src/lib/types/account-with-platform.type.ts index cccec2e522..2f4cfe1e8d 100644 --- a/libs/common/src/lib/types/account-with-platform.type.ts +++ b/libs/common/src/lib/types/account-with-platform.type.ts @@ -1,6 +1,6 @@ import type { Account, Platform, Tag } from '@ghostfolio/prisma/browser'; export type AccountWithPlatform = Account & { - platform?: Platform; + platform?: Platform | null; tags?: Tag[]; };