diff --git a/CHANGELOG.md b/CHANGELOG.md index 673233c5f..4a6fb2160 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Deactivated asset profiles automatically on delisting in the _Yahoo Finance_ service - Optimized the query of the data range functionality (`getRange()`) in the market data service +- Moved the subscription offer from the info to the user service - Upgraded `Nx` from version `20.7.1` to `20.8.0` - Upgraded `prisma` from version `6.5.0` to `6.6.0` - Upgraded `storybook` from version `8.4.7` to `8.6.12` diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts index d7a5ed641..9ded44600 100644 --- a/apps/api/src/app/info/info.module.ts +++ b/apps/api/src/app/info/info.module.ts @@ -1,5 +1,6 @@ import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; @@ -31,6 +32,7 @@ import { InfoService } from './info.service'; PlatformModule, PropertyModule, RedisCacheModule, + SubscriptionModule, SymbolProfileModule, TransformDataSourceInResponseModule, UserModule diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 780860232..9581807b9 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -1,5 +1,6 @@ import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -13,7 +14,6 @@ import { PROPERTY_DEMO_USER_ID, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SLACK_COMMUNITY_USERS, - PROPERTY_STRIPE_CONFIG, ghostfolioFearAndGreedIndexDataSource } from '@ghostfolio/common/config'; import { @@ -21,13 +21,8 @@ import { encodeDataSource, extractNumberFromString } from '@ghostfolio/common/helper'; -import { - InfoItem, - Statistics, - SubscriptionOffer -} from '@ghostfolio/common/interfaces'; +import { InfoItem, Statistics } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; -import { SubscriptionOfferKey } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; @@ -46,6 +41,7 @@ export class InfoService { private readonly platformService: PlatformService, private readonly propertyService: PropertyService, private readonly redisCacheService: RedisCacheService, + private readonly subscriptionService: SubscriptionService, private readonly userService: UserService ) {} @@ -101,7 +97,7 @@ export class InfoService { isUserSignupEnabled, platforms, statistics, - subscriptionOffers + subscriptionOffer ] = await Promise.all([ this.benchmarkService.getBenchmarkAssetProfiles(), this.getDemoAuthToken(), @@ -110,7 +106,7 @@ export class InfoService { orderBy: { name: 'asc' } }), this.getStatistics(), - this.getSubscriptionOffers() + this.subscriptionService.getSubscriptionOffer({ key: 'default' }) ]); if (isUserSignupEnabled) { @@ -125,7 +121,7 @@ export class InfoService { isReadOnlyMode, platforms, statistics, - subscriptionOffers, + subscriptionOffer, baseCurrency: DEFAULT_CURRENCY, currencies: this.exchangeRateDataService.getCurrencies() }; @@ -299,19 +295,6 @@ export class InfoService { return statistics; } - private async getSubscriptionOffers(): Promise<{ - [offer in SubscriptionOfferKey]: SubscriptionOffer; - }> { - if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { - return undefined; - } - - return ( - ((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ?? - {} - ); - } - private async getUptime(): Promise { { try { diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index ae0260d8c..dc3e5cf59 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -158,26 +158,30 @@ export class SubscriptionService { } } - public getSubscription({ + public async getSubscription({ createdAt, subscriptions }: { createdAt: UserWithSettings['createdAt']; subscriptions: Subscription[]; - }): UserWithSettings['subscription'] { + }): Promise { if (subscriptions.length > 0) { const { expiresAt, price } = subscriptions.reduce((a, b) => { return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b; }); - let offer: SubscriptionOfferKey = price ? 'renewal' : 'default'; + let offerKey: SubscriptionOfferKey = price ? 'renewal' : 'default'; if (isBefore(createdAt, parseDate('2023-01-01'))) { - offer = 'renewal-early-bird-2023'; + offerKey = 'renewal-early-bird-2023'; } else if (isBefore(createdAt, parseDate('2024-01-01'))) { - offer = 'renewal-early-bird-2024'; + offerKey = 'renewal-early-bird-2024'; } + const offer = await this.getSubscriptionOffer({ + key: offerKey + }); + return { expiresAt, offer, @@ -186,10 +190,30 @@ export class SubscriptionService { : SubscriptionType.Basic }; } else { + const offer = await this.getSubscriptionOffer({ + key: 'default' + }); + return { - offer: 'default', + offer, type: SubscriptionType.Basic }; } } + + public async getSubscriptionOffer({ + key + }: { + key: SubscriptionOfferKey; + }): Promise { + if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { + return undefined; + } + + const offers = + ((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ?? + {}; + + return offers[key]; + } } diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index e9b8078b1..31d8786df 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -339,7 +339,7 @@ export class UserService { } if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { - user.subscription = this.subscriptionService.getSubscription({ + user.subscription = await this.subscriptionService.getSubscription({ createdAt: user.createdAt, subscriptions: Subscription }); @@ -392,6 +392,12 @@ export class UserService { currentPermissions, permissions.deleteOwnUser ); + + // Reset offer + user.subscription.offer.coupon = undefined; + user.subscription.offer.couponId = undefined; + user.subscription.offer.durationExtension = undefined; + user.subscription.offer.label = undefined; } } diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 4220208c0..38e48f139 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -143,8 +143,8 @@ export class AppComponent implements OnDestroy, OnInit { ); this.hasPromotion = - !!this.info?.subscriptionOffers?.default?.coupon || - !!this.info?.subscriptionOffers?.default?.durationExtension; + !!this.info?.subscriptionOffer?.coupon || + !!this.info?.subscriptionOffer?.durationExtension; this.impersonationStorageService .onChangeHasImpersonation() @@ -242,12 +242,8 @@ export class AppComponent implements OnDestroy, OnInit { this.canCreateAccount || !!this.user?.systemMessage; this.hasPromotion = - !!this.info?.subscriptionOffers?.[ - this.user?.subscription?.offer ?? 'default' - ]?.coupon || - !!this.info?.subscriptionOffers?.[ - this.user?.subscription?.offer ?? 'default' - ]?.durationExtension; + !!this.user?.subscription?.offer?.coupon || + !!this.user?.subscription?.offer?.durationExtension; this.initializeTheme(this.user?.settings.colorScheme); diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts index 2214d91f9..8d54f737c 100644 --- a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts +++ b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts @@ -51,8 +51,7 @@ export class UserAccountMembershipComponent implements OnDestroy { private stripeService: StripeService, private userService: UserService ) { - const { baseCurrency, globalPermissions, subscriptionOffers } = - this.dataService.fetchInfo(); + const { baseCurrency, globalPermissions } = this.dataService.fetchInfo(); this.baseCurrency = baseCurrency; @@ -81,18 +80,12 @@ export class UserAccountMembershipComponent implements OnDestroy { permissions.updateUserSettings ); - this.coupon = - subscriptionOffers?.[this.user.subscription.offer]?.coupon; - this.couponId = - subscriptionOffers?.[this.user.subscription.offer]?.couponId; + this.coupon = this.user?.subscription?.offer?.coupon; + this.couponId = this.user?.subscription?.offer?.couponId; this.durationExtension = - subscriptionOffers?.[ - this.user.subscription.offer - ]?.durationExtension; - this.price = - subscriptionOffers?.[this.user.subscription.offer]?.price; - this.priceId = - subscriptionOffers?.[this.user.subscription.offer]?.priceId; + this.user?.subscription?.offer?.durationExtension; + this.price = this.user?.subscription?.offer?.price; + this.priceId = this.user?.subscription?.offer?.priceId; this.changeDetectorRef.markForCheck(); } diff --git a/apps/client/src/app/pages/pricing/pricing-page.component.ts b/apps/client/src/app/pages/pricing/pricing-page.component.ts index 62e2bec0b..cadc3a48c 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.component.ts +++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts @@ -55,13 +55,13 @@ export class PricingPageComponent implements OnDestroy, OnInit { ) {} public ngOnInit() { - const { baseCurrency, subscriptionOffers } = this.dataService.fetchInfo(); + const { baseCurrency, subscriptionOffer } = this.dataService.fetchInfo(); this.baseCurrency = baseCurrency; - this.coupon = subscriptionOffers?.default?.coupon; - this.durationExtension = subscriptionOffers?.default?.durationExtension; - this.label = subscriptionOffers?.default?.label; - this.price = subscriptionOffers?.default?.price; + this.coupon = subscriptionOffer?.coupon; + this.durationExtension = subscriptionOffer?.durationExtension; + this.label = subscriptionOffer?.label; + this.price = subscriptionOffer?.price; this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) @@ -74,20 +74,13 @@ export class PricingPageComponent implements OnDestroy, OnInit { permissions.updateUserSettings ); - this.coupon = - subscriptionOffers?.[this.user?.subscription?.offer]?.coupon; - this.couponId = - subscriptionOffers?.[this.user.subscription.offer]?.couponId; + this.coupon = this.user?.subscription?.offer?.coupon; + this.couponId = this.user?.subscription?.offer?.couponId; this.durationExtension = - subscriptionOffers?.[ - this.user?.subscription?.offer - ]?.durationExtension; - this.label = - subscriptionOffers?.[this.user?.subscription?.offer]?.label; - this.price = - subscriptionOffers?.[this.user?.subscription?.offer]?.price; - this.priceId = - subscriptionOffers?.[this.user.subscription.offer]?.priceId; + this.user?.subscription?.offer?.durationExtension; + this.label = this.user?.subscription?.offer?.label; + this.price = this.user?.subscription?.offer?.price; + this.priceId = this.user?.subscription?.offer?.priceId; this.changeDetectorRef.markForCheck(); } diff --git a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts index 6a8543e71..bee5cb642 100644 --- a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts +++ b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -33,9 +33,9 @@ export class GfProductPageComponent implements OnInit { ) {} public ngOnInit() { - const { subscriptionOffers } = this.dataService.fetchInfo(); + const { subscriptionOffer } = this.dataService.fetchInfo(); - this.price = subscriptionOffers?.default?.price; + this.price = subscriptionOffer?.price; this.product1 = { founded: 2021, diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index bd3eb1f94..fe4101197 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -1,5 +1,3 @@ -import { SubscriptionOfferKey } from '@ghostfolio/common/types'; - import { Platform, SymbolProfile } from '@prisma/client'; import { Statistics } from './statistics.interface'; @@ -18,5 +16,5 @@ export interface InfoItem { platforms: Platform[]; statistics: Statistics; stripePublicKey?: string; - subscriptionOffers: { [offer in SubscriptionOfferKey]: SubscriptionOffer }; + subscriptionOffer?: SubscriptionOffer; } diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts index 84f48d1dc..a48317fad 100644 --- a/libs/common/src/lib/interfaces/user.interface.ts +++ b/libs/common/src/lib/interfaces/user.interface.ts @@ -1,8 +1,8 @@ -import { SubscriptionOfferKey } from '@ghostfolio/common/types'; import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type'; import { Access, Account, Tag } from '@prisma/client'; +import { SubscriptionOffer } from './subscription-offer.interface'; import { SystemMessage } from './system-message.interface'; import { UserSettings } from './user-settings.interface'; @@ -18,7 +18,7 @@ export interface User { systemMessage?: SystemMessage; subscription: { expiresAt?: Date; - offer: SubscriptionOfferKey; + offer: SubscriptionOffer; type: SubscriptionType; }; tags: (Tag & { isUsed: boolean })[]; diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index 668486a94..8ffd345db 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -17,6 +17,7 @@ import type { Market } from './market.type'; import type { OrderWithAccount } from './order-with-account.type'; import type { RequestWithUser } from './request-with-user.type'; import type { SubscriptionOfferKey } from './subscription-offer-key.type'; +import type { SubscriptionType } from './subscription-type.type'; import type { UserWithSettings } from './user-with-settings.type'; import type { ViewMode } from './view-mode.type'; @@ -40,6 +41,7 @@ export type { OrderWithAccount, RequestWithUser, SubscriptionOfferKey, + SubscriptionType, UserWithSettings, ViewMode }; diff --git a/libs/common/src/lib/types/user-with-settings.type.ts b/libs/common/src/lib/types/user-with-settings.type.ts index 5f9835176..5fb25a664 100644 --- a/libs/common/src/lib/types/user-with-settings.type.ts +++ b/libs/common/src/lib/types/user-with-settings.type.ts @@ -1,6 +1,5 @@ -import { UserSettings } from '@ghostfolio/common/interfaces'; -import { SubscriptionOfferKey } from '@ghostfolio/common/types'; -import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type'; +import { SubscriptionOffer, UserSettings } from '@ghostfolio/common/interfaces'; +import { SubscriptionType } from '@ghostfolio/common/types'; import { Access, Account, Settings, User } from '@prisma/client'; @@ -14,7 +13,7 @@ export type UserWithSettings = User & { Settings: Settings & { settings: UserSettings }; subscription?: { expiresAt?: Date; - offer: SubscriptionOfferKey; + offer: SubscriptionOffer; type: SubscriptionType; }; };