Browse Source

Feature/add ability to add Symbol as benchmark from Market Data

- Added POST api to add benchmarks
- Added "Set as Benchmark" button to symbols in Market Data
pull/2002/head
Arghya Ghosh 2 years ago
committed by Thomas
parent
commit
db241bf6ee
  1. 2
      CHANGELOG.md
  2. 50
      apps/api/src/app/benchmark/benchmark.controller.ts
  3. 2
      apps/api/src/app/benchmark/benchmark.module.ts
  4. 84
      apps/api/src/app/benchmark/benchmark.service.spec.ts
  5. 42
      apps/api/src/app/benchmark/benchmark.service.ts
  6. 2
      apps/client/src/app/app.component.ts
  7. 40
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  8. 7
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  9. 2
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  10. 2
      apps/client/src/app/components/admin-users/admin-users.component.ts
  11. 2
      apps/client/src/app/components/home-market/home-market.component.ts
  12. 2
      apps/client/src/app/components/home-summary/home-summary.component.ts
  13. 2
      apps/client/src/app/core/http-response.interceptor.ts
  14. 2
      apps/client/src/app/pages/about/about-page.component.ts
  15. 2
      apps/client/src/app/pages/account/account-page.component.ts
  16. 2
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts
  17. 2
      apps/client/src/app/pages/admin/admin-page.component.ts
  18. 2
      apps/client/src/app/pages/blog/blog-page.component.ts
  19. 2
      apps/client/src/app/pages/demo/demo-page.component.ts
  20. 2
      apps/client/src/app/pages/features/features-page.component.ts
  21. 2
      apps/client/src/app/pages/home/home-page.component.ts
  22. 2
      apps/client/src/app/pages/landing/landing-page.component.ts
  23. 2
      apps/client/src/app/pages/open/open-page.component.ts
  24. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  25. 2
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  26. 2
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  27. 2
      apps/client/src/app/pages/portfolio/portfolio-page.component.ts
  28. 2
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  29. 4
      apps/client/src/app/pages/register/register-page.component.ts
  30. 29
      apps/client/src/app/services/data.service.ts
  31. 3
      libs/common/src/lib/exceptions/index.ts
  32. 6
      libs/common/src/lib/exceptions/not-found-error.ts
  33. 3
      libs/common/src/lib/interfaces/benchmark-property.interface.ts
  34. 2
      libs/common/src/lib/interfaces/index.ts

2
CHANGELOG.md

@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added an error message for duplicates to the preview step of the activities import - Added an error message for duplicates to the preview step of the activities import
- Added a connection timeout to the environment variable `DATABASE_URL` - Added a connection timeout to the environment variable `DATABASE_URL`
- Introduced the _Open Startup_ (`/open`) page with aggregated key metrics including uptime - Introduced the _Open Startup_ (`/open`) page with aggregated key metrics including uptime
- POST api to add benchmarks
- Ability to add symbol as benchmark from Market Data
### Changed ### Changed

50
apps/api/src/app/benchmark/benchmark.controller.ts

@ -2,23 +2,35 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { import {
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkResponse BenchmarkResponse,
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { import {
Body,
Controller, Controller,
Get, Get,
HttpException,
Inject,
Param, Param,
Post,
UseGuards, UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { BenchmarkService } from './benchmark.service'; import { BenchmarkService } from './benchmark.service';
import { REQUEST } from '@nestjs/core';
import { RequestWithUser } from '@ghostfolio/common/types';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { NotFoundError } from '@ghostfolio/common/exceptions';
@Controller('benchmark') @Controller('benchmark')
export class BenchmarkController { export class BenchmarkController {
public constructor(private readonly benchmarkService: BenchmarkService) {} public constructor(
private readonly benchmarkService: BenchmarkService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Get() @Get()
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@ -45,4 +57,36 @@ export class BenchmarkController {
symbol symbol
}); });
} }
@Post()
@UseGuards(AuthGuard('jwt'))
public async addBenchmark(@Body() benchmark: UniqueAsset) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
try {
return await this.benchmarkService.addBenchmark(benchmark);
} catch (error) {
if (error instanceof NotFoundError) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
} else {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
}
} }

2
apps/api/src/app/benchmark/benchmark.module.ts

@ -9,6 +9,7 @@ import { Module } from '@nestjs/common';
import { BenchmarkController } from './benchmark.controller'; import { BenchmarkController } from './benchmark.controller';
import { BenchmarkService } from './benchmark.service'; import { BenchmarkService } from './benchmark.service';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
@Module({ @Module({
controllers: [BenchmarkController], controllers: [BenchmarkController],
@ -17,6 +18,7 @@ import { BenchmarkService } from './benchmark.service';
ConfigurationModule, ConfigurationModule,
DataProviderModule, DataProviderModule,
MarketDataModule, MarketDataModule,
PrismaModule,
PropertyModule, PropertyModule,
RedisCacheModule, RedisCacheModule,
SymbolModule, SymbolModule,

84
apps/api/src/app/benchmark/benchmark.service.spec.ts

@ -1,10 +1,51 @@
import { BenchmarkService } from './benchmark.service'; import { BenchmarkService } from './benchmark.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { NotFoundError } from '@ghostfolio/common/exceptions';
jest.mock('@ghostfolio/api/services/property/property.service', () => {
return {
PropertyService: jest.fn().mockImplementation(() => {
return {
getByKey: jest.fn().mockImplementation((key: string) => {
return [{ symbolProfileId: 'profile-id-1' }];
}),
put: jest.fn().mockImplementation(({ key, value }) => {
return Promise.resolve();
})
};
})
};
});
jest.mock('@ghostfolio/api/services/prisma/prisma.service', () => {
return {
PrismaService: jest.fn().mockImplementation(() => {
return {
symbolProfile: {
findFirst: jest.fn()
}
};
})
};
});
describe('BenchmarkService', () => { describe('BenchmarkService', () => {
let benchmarkService: BenchmarkService; let benchmarkService: BenchmarkService;
let prismaService: PrismaService = new PrismaService();
let propertyService: PropertyService = new PropertyService(prismaService);
beforeAll(async () => { beforeAll(async () => {
benchmarkService = new BenchmarkService(null, null, null, null, null, null); benchmarkService = new BenchmarkService(
null,
null,
prismaService,
propertyService,
null,
null,
null
);
}); });
it('calculateChangeInPercentage', async () => { it('calculateChangeInPercentage', async () => {
@ -12,4 +53,45 @@ describe('BenchmarkService', () => {
expect(benchmarkService.calculateChangeInPercentage(2, 2)).toEqual(0); expect(benchmarkService.calculateChangeInPercentage(2, 2)).toEqual(0);
expect(benchmarkService.calculateChangeInPercentage(2, 1)).toEqual(-0.5); expect(benchmarkService.calculateChangeInPercentage(2, 1)).toEqual(-0.5);
}); });
it('should add new benchmark', async () => {
prismaService.symbolProfile.findFirst = jest
.fn()
.mockResolvedValueOnce(
Promise.resolve({ id: 'profile-id-2', name: 'Test Profile' })
);
const result = await benchmarkService.addBenchmark({
dataSource: 'YAHOO',
symbol: 'symbol-2'
});
expect(propertyService.put).toHaveBeenCalledWith({
key: PROPERTY_BENCHMARKS,
value: JSON.stringify([
{ symbolProfileId: 'profile-id-1' },
{ symbolProfileId: 'profile-id-2' }
])
});
expect(result).toEqual({
dataSource: 'YAHOO',
id: 'profile-id-2',
name: 'Test Profile',
symbol: 'symbol-2'
});
});
it('should throw error if symbol profile not found', async () => {
prismaService.symbolProfile.findFirst = jest
.fn()
.mockResolvedValueOnce(Promise.resolve(null));
try {
await benchmarkService.addBenchmark({
dataSource: 'YAHOO',
symbol: 'symbol-2'
});
} catch (e) {
expect(e).toEqual(new NotFoundError('Symbol profile not found'));
}
});
}); });

42
apps/api/src/app/benchmark/benchmark.service.ts

@ -11,6 +11,7 @@ import {
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkProperty,
BenchmarkResponse, BenchmarkResponse,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -19,6 +20,9 @@ import { SymbolProfile } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { format } from 'date-fns'; import { format } from 'date-fns';
import ms from 'ms'; import ms from 'ms';
import { uniqBy } from 'lodash';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { NotFoundError } from '@ghostfolio/common/exceptions';
@Injectable() @Injectable()
export class BenchmarkService { export class BenchmarkService {
@ -27,6 +31,7 @@ export class BenchmarkService {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService, private readonly redisCacheService: RedisCacheService,
private readonly symbolProfileService: SymbolProfileService, private readonly symbolProfileService: SymbolProfileService,
@ -204,6 +209,43 @@ export class BenchmarkService {
return response; return response;
} }
public async addBenchmark({
dataSource,
symbol
}: UniqueAsset): Promise<Partial<SymbolProfile>> {
const symbolProfile = await this.prismaService.symbolProfile.findFirst({
where: {
dataSource,
symbol
}
});
if (!symbolProfile) {
throw new NotFoundError('Symbol profile not found');
}
const benchmarks =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? [];
benchmarks.push({ symbolProfileId: symbolProfile.id } as BenchmarkProperty);
const newBenchmarks = uniqBy(benchmarks, 'symbolProfileId');
await this.propertyService.put({
key: PROPERTY_BENCHMARKS,
value: JSON.stringify(newBenchmarks)
});
return {
dataSource,
symbol,
id: symbolProfile.id,
name: symbolProfile.name
};
}
private getMarketCondition(aPerformanceInPercent: number) { private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET'; return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
} }

2
apps/client/src/app/app.component.ts

@ -64,7 +64,7 @@ export class AppComponent implements OnDestroy, OnInit {
const urlSegments = urlSegmentGroup.segments; const urlSegments = urlSegmentGroup.segments;
this.currentRoute = urlSegments[0].path; this.currentRoute = urlSegments[0].path;
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
if (this.deviceType === 'mobile') { if (this.deviceType === 'mobile') {
setTimeout(() => { setTimeout(() => {

40
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -14,13 +14,23 @@ import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces'; import {
Filter,
InfoItem,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { AssetSubClass, DataSource } from '@prisma/client'; import { AssetSubClass, DataSource, SymbolProfile } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; import {
distinctUntilChanged,
switchMap,
take,
takeUntil
} from 'rxjs/operators';
import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component'; import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component';
import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces'; import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces';
@ -51,6 +61,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
type: 'ASSET_SUB_CLASS' type: 'ASSET_SUB_CLASS'
}; };
}); });
public benchmarks: Partial<SymbolProfile>[];
public currentDataSource: DataSource; public currentDataSource: DataSource;
public currentSymbol: string; public currentSymbol: string;
public dataSource: MatTableDataSource<AdminMarketDataItem> = public dataSource: MatTableDataSource<AdminMarketDataItem> =
@ -116,6 +127,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.benchmarks = this.dataService.getInfo().benchmarks;
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.filters$ this.filters$
@ -143,6 +155,13 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
}); });
} }
public isBenchmark({ dataSource, symbol }: UniqueAsset) {
return this.benchmarks.some(
(benchmark) =>
benchmark.dataSource === dataSource && benchmark.symbol === symbol
);
}
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
this.adminService this.adminService
.deleteProfileData({ dataSource, symbol }) .deleteProfileData({ dataSource, symbol })
@ -186,6 +205,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
.subscribe(() => {}); .subscribe(() => {});
} }
public onSetBenchmark(benchmark: UniqueAsset) {
this.dataService
.postBenchmark(benchmark)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((_) => {
this.dataService
.fetchInfo()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((info: InfoItem) => {
this.benchmarks = info.benchmarks;
(window as any) = info;
});
});
}
public onGatherSymbol({ dataSource, symbol }: UniqueAsset) { public onGatherSymbol({ dataSource, symbol }: UniqueAsset) {
this.adminService this.adminService
.gatherSymbol({ dataSource, symbol }) .gatherSymbol({ dataSource, symbol })

7
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -155,6 +155,13 @@
> >
<ng-container i18n>Gather Profile Data</ng-container> <ng-container i18n>Gather Profile Data</ng-container>
</button> </button>
<button
mat-menu-item
[disabled]="isBenchmark({dataSource: element.dataSource, symbol: element.symbol})"
(click)="onSetBenchmark({dataSource: element.dataSource, symbol: element.symbol})"
>
<ng-container i18n>Set as Benchmark</ng-container>
</button>
<button <button
mat-menu-item mat-menu-item
[disabled]="element.activitiesCount !== 0" [disabled]="element.activitiesCount !== 0"

2
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -50,7 +50,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

2
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -35,7 +35,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
this.hasPermissionForSubscription = hasPermission( this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions, this.info?.globalPermissions,

2
apps/client/src/app/components/home-market/home-market.component.ts

@ -37,7 +37,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
this.isLoading = true; this.isLoading = true;
this.userService.stateChanged this.userService.stateChanged

2
apps/client/src/app/components/home-summary/home-summary.component.ts

@ -42,7 +42,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
this.hasPermissionForSubscription = hasPermission( this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions, this.info?.globalPermissions,

2
apps/client/src/app/core/http-response.interceptor.ts

@ -35,7 +35,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private webAuthnService: WebAuthnService private webAuthnService: WebAuthnService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
this.hasPermissionForSubscription = hasPermission( this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions, this.info?.globalPermissions,

2
apps/client/src/app/pages/about/about-page.component.ts

@ -31,7 +31,7 @@ export class AboutPageComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private userService: UserService private userService: UserService
) { ) {
const { globalPermissions, statistics } = this.dataService.fetchInfo(); const { globalPermissions, statistics } = this.dataService.getInfo();
this.hasPermissionForBlog = hasPermission( this.hasPermissionForBlog = hasPermission(
globalPermissions, globalPermissions,

2
apps/client/src/app/pages/account/account-page.component.ts

@ -85,7 +85,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
public webAuthnService: WebAuthnService public webAuthnService: WebAuthnService
) { ) {
const { baseCurrency, currencies, globalPermissions, subscriptions } = const { baseCurrency, currencies, globalPermissions, subscriptions } =
this.dataService.fetchInfo(); this.dataService.getInfo();
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.currencies = currencies; this.currencies = currencies;

2
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts

@ -30,7 +30,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
) {} ) {}
ngOnInit() { ngOnInit() {
const { currencies, platforms } = this.dataService.fetchInfo(); const { currencies, platforms } = this.dataService.getInfo();
this.currencies = currencies; this.currencies = currencies;
this.platforms = platforms; this.platforms = platforms;

2
apps/client/src/app/pages/admin/admin-page.component.ts

@ -18,7 +18,7 @@ export class AdminPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor(private dataService: DataService) { public constructor(private dataService: DataService) {
const { systemMessage } = this.dataService.fetchInfo(); const { systemMessage } = this.dataService.getInfo();
this.hasMessage = !!systemMessage; this.hasMessage = !!systemMessage;
} }

2
apps/client/src/app/pages/blog/blog-page.component.ts

@ -15,7 +15,7 @@ export class BlogPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor(private dataService: DataService) { public constructor(private dataService: DataService) {
const info = this.dataService.fetchInfo(); const info = this.dataService.getInfo();
this.hasPermissionForSubscription = hasPermission( this.hasPermissionForSubscription = hasPermission(
info?.globalPermissions, info?.globalPermissions,

2
apps/client/src/app/pages/demo/demo-page.component.ts

@ -20,7 +20,7 @@ export class DemoPageComponent implements OnDestroy {
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
} }
public ngOnInit() { public ngOnInit() {

2
apps/client/src/app/pages/features/features-page.component.ts

@ -23,7 +23,7 @@ export class FeaturesPageComponent implements OnDestroy {
private dataService: DataService, private dataService: DataService,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
} }
public ngOnInit() { public ngOnInit() {

2
apps/client/src/app/pages/home/home-page.component.ts

@ -35,7 +35,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

2
apps/client/src/app/pages/landing/landing-page.component.ts

@ -73,7 +73,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
demoAuthToken, demoAuthToken,
globalPermissions, globalPermissions,
statistics statistics
} = this.dataService.fetchInfo(); } = this.dataService.getInfo();
for (const country of countriesOfSubscribers) { for (const country of countriesOfSubscribers) {
this.countriesOfSubscribersMap[country] = { this.countriesOfSubscribersMap[country] = {

2
apps/client/src/app/pages/open/open-page.component.ts

@ -21,7 +21,7 @@ export class OpenPageComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private userService: UserService private userService: UserService
) { ) {
const { statistics } = this.dataService.fetchInfo(); const { statistics } = this.dataService.getInfo();
this.statistics = statistics; this.statistics = statistics;
} }

2
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -81,7 +81,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
} }
public ngOnInit() { public ngOnInit() {
const { globalPermissions } = this.dataService.fetchInfo(); const { globalPermissions } = this.dataService.getInfo();
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;

2
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -81,7 +81,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.locale = this.data.user?.settings?.locale; this.locale = this.data.user?.settings?.locale;
this.dateAdapter.setLocale(this.locale); this.dateAdapter.setLocale(this.locale);
const { currencies, platforms, tags } = this.dataService.fetchInfo(); const { currencies, platforms, tags } = this.dataService.getInfo();
this.currencies = currencies; this.currencies = currencies;
this.defaultDateFormat = getDateFormatString(this.locale); this.defaultDateFormat = getDateFormatString(this.locale);

2
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -73,7 +73,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
private router: Router, private router: Router,
private userService: UserService private userService: UserService
) { ) {
const { benchmarks } = this.dataService.fetchInfo(); const { benchmarks } = this.dataService.getInfo();
this.benchmarks = benchmarks; this.benchmarks = benchmarks;
route.queryParams route.queryParams

2
apps/client/src/app/pages/portfolio/portfolio-page.component.ts

@ -34,7 +34,7 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

2
apps/client/src/app/pages/pricing/pricing-page.component.ts

@ -41,7 +41,7 @@ export class PricingPageComponent implements OnDestroy, OnInit {
) {} ) {}
public ngOnInit() { public ngOnInit() {
const { baseCurrency, subscriptions } = this.dataService.fetchInfo(); const { baseCurrency, subscriptions } = this.dataService.getInfo();
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.coupon = subscriptions?.default?.coupon; this.coupon = subscriptions?.default?.coupon;

4
apps/client/src/app/pages/register/register-page.component.ts

@ -39,13 +39,13 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
private tokenStorageService: TokenStorageService, private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.getInfo();
this.tokenStorageService.signOut(); this.tokenStorageService.signOut();
} }
public ngOnInit() { public ngOnInit() {
const { demoAuthToken, globalPermissions } = this.dataService.fetchInfo(); const { demoAuthToken, globalPermissions } = this.dataService.getInfo();
this.demoAuthToken = demoAuthToken; this.demoAuthToken = demoAuthToken;
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;

29
apps/client/src/app/services/data.service.ts

@ -43,7 +43,7 @@ import { DataSource, Order as OrderModel } from '@prisma/client';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { cloneDeep, groupBy, isNumber } from 'lodash'; import { cloneDeep, groupBy, isNumber } from 'lodash';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -193,18 +193,21 @@ export class DataService {
}); });
} }
public fetchInfo(): InfoItem { public fetchInfo(): Observable<InfoItem> {
const info = cloneDeep((window as any).info);
const utmSource = <'ios' | 'trusted-web-activity'>( const utmSource = <'ios' | 'trusted-web-activity'>(
window.localStorage.getItem('utm_source') window.localStorage.getItem('utm_source')
); );
return this.http.get<InfoItem>('/api/v1/info').pipe(
take(1),
map((info: InfoItem) => {
info.globalPermissions = filterGlobalPermissions( info.globalPermissions = filterGlobalPermissions(
info.globalPermissions, info.globalPermissions,
utmSource utmSource
); );
return info; return info;
})
);
} }
public fetchInvestments({ public fetchInvestments({
@ -391,6 +394,20 @@ export class DataService {
); );
} }
public getInfo(): InfoItem {
const info = cloneDeep((window as any).info);
const utmSource = <'ios' | 'trusted-web-activity'>(
window.localStorage.getItem('utm_source')
);
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
return info;
}
public loginAnonymous(accessToken: string) { public loginAnonymous(accessToken: string) {
return this.http.post<OAuthResponse>(`/api/v1/auth/anonymous`, { return this.http.post<OAuthResponse>(`/api/v1/auth/anonymous`, {
accessToken accessToken
@ -405,6 +422,10 @@ export class DataService {
return this.http.post<OrderModel>(`/api/v1/account`, aAccount); return this.http.post<OrderModel>(`/api/v1/account`, aAccount);
} }
public postBenchmark(benchmark: UniqueAsset) {
return this.http.post(`/api/v1/benchmark`, benchmark);
}
public postOrder(aOrder: CreateOrderDto) { public postOrder(aOrder: CreateOrderDto) {
return this.http.post<OrderModel>(`/api/v1/order`, aOrder); return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
} }

3
libs/common/src/lib/exceptions/index.ts

@ -0,0 +1,3 @@
import { NotFoundError } from './not-found-error';
export { NotFoundError };

6
libs/common/src/lib/exceptions/not-found-error.ts

@ -0,0 +1,6 @@
export class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}

3
libs/common/src/lib/interfaces/benchmark-property.interface.ts

@ -0,0 +1,3 @@
export interface BenchmarkProperty {
symbolProfileId: string;
}

2
libs/common/src/lib/interfaces/index.ts

@ -8,6 +8,7 @@ import {
AdminMarketDataItem AdminMarketDataItem
} from './admin-market-data.interface'; } from './admin-market-data.interface';
import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface'; import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import { BenchmarkProperty } from './benchmark-property.interface';
import { Benchmark } from './benchmark.interface'; import { Benchmark } from './benchmark.interface';
import { Coupon } from './coupon.interface'; import { Coupon } from './coupon.interface';
import { DataProviderInfo } from './data-provider-info.interface'; import { DataProviderInfo } from './data-provider-info.interface';
@ -54,6 +55,7 @@ export {
AdminMarketDataItem, AdminMarketDataItem,
Benchmark, Benchmark,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkProperty,
BenchmarkResponse, BenchmarkResponse,
Coupon, Coupon,
DataProviderInfo, DataProviderInfo,

Loading…
Cancel
Save