Browse Source

Feature/move subscription offer from info to user service (#4533)

* Move subscription offer from info to user service

* Update changelog
pull/4542/head
Thomas Kaul 6 days ago
committed by GitHub
parent
commit
94e53c7d4a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 2
      apps/api/src/app/info/info.module.ts
  3. 29
      apps/api/src/app/info/info.service.ts
  4. 36
      apps/api/src/app/subscription/subscription.service.ts
  5. 8
      apps/api/src/app/user/user.service.ts
  6. 12
      apps/client/src/app/app.component.ts
  7. 19
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  8. 29
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  9. 4
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  10. 4
      libs/common/src/lib/interfaces/info-item.interface.ts
  11. 4
      libs/common/src/lib/interfaces/user.interface.ts
  12. 2
      libs/common/src/lib/types/index.ts
  13. 7
      libs/common/src/lib/types/user-with-settings.type.ts

1
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`

2
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

29
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<number> {
{
try {

36
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<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 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<SubscriptionOffer> {
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
return undefined;
}
const offers =
((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
{};
return offers[key];
}
}

8
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;
}
}

12
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);

19
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();
}

29
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();
}

4
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,

4
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;
}

4
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 })[];

2
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
};

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

Loading…
Cancel
Save