Feature/move subscription offer from info to user service (#4533)
* Move subscription offer from info to user service * Update changelog
This commit is contained in:
parent
5072ba09aa
commit
94e53c7d4a
@ -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`
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 })[];
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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…
x
Reference in New Issue
Block a user