Browse Source

Add endpoints to manage the splits of an asset profile

pull/7251/head
Thomas Kaul 4 weeks ago
parent
commit
b3106741f1
  1. 3
      apps/api/src/app/asset/asset.controller.ts
  2. 93
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
  3. 2
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
  4. 41
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
  5. 12
      libs/common/src/lib/dtos/create-asset-profile-split.dto.ts
  6. 2
      libs/common/src/lib/dtos/index.ts
  7. 3
      libs/common/src/lib/interfaces/admin-market-data-details.interface.ts
  8. 3
      libs/common/src/lib/interfaces/responses/asset-profile-response.interface.ts
  9. 15
      libs/common/src/lib/validator-constraints/is-split-factor.ts

3
apps/api/src/app/asset/asset.controller.ts

@ -20,7 +20,7 @@ export class AssetController {
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AssetResponse> {
const { assetProfile, marketData } =
const { assetProfile, marketData, splits } =
await this.assetProfilesService.getAssetProfile({
dataSource,
symbol
@ -28,6 +28,7 @@ export class AssetController {
return {
marketData,
splits,
assetProfile: pick(assetProfile, ['dataSource', 'name', 'symbol'])
};
}

93
apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts

@ -4,7 +4,10 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos';
import {
CreateAssetProfileSplitDto,
UpdateAssetProfileDataDto
} from '@ghostfolio/common/dtos';
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { AssetProfileResponse } from '@ghostfolio/common/interfaces';
import {
@ -18,19 +21,22 @@ import { MarketDataPreset, RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
Delete,
Get,
HttpException,
Inject,
Param,
ParseIntPipe,
Patch,
Post,
Query,
UseGuards,
UseInterceptors
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource, Prisma } from '@prisma/client';
import { AssetProfileSplit, DataSource, Prisma } from '@prisma/client';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AssetProfilesService } from './asset-profiles.service';
@ -119,6 +125,40 @@ export class AssetProfilesController {
});
}
@Post(':dataSource/:symbol/splits')
@UseGuards(AuthGuard('jwt'))
public async createSplit(
@Body() data: CreateAssetProfileSplitDto,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AssetProfileSplit> {
const { id: symbolProfileId } = await this.validateCanManageSplits({
dataSource,
symbol
});
return this.assetProfilesService.createSplit({
symbolProfileId,
date: parseISO(data.date),
factor: data.factor
});
}
@Delete(':dataSource/:symbol/splits/:id')
@UseGuards(AuthGuard('jwt'))
public async deleteSplit(
@Param('dataSource') dataSource: DataSource,
@Param('id') id: string,
@Param('symbol') symbol: string
): Promise<void> {
const { id: symbolProfileId } = await this.validateCanManageSplits({
dataSource,
symbol
});
return this.assetProfilesService.deleteSplit({ id, symbolProfileId });
}
@HasPermission(permissions.accessAdminControl)
@Patch(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@ -139,4 +179,53 @@ export class AssetProfilesController {
assetProfileData
);
}
private async validateCanManageSplits({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([
{ dataSource, symbol }
]);
if (!assetProfile) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
const canManageAllAssetProfiles =
hasPermission(
this.request.user.permissions,
permissions.createMarketData
) &&
hasPermission(
this.request.user.permissions,
permissions.updateMarketData
);
const canManageOwnAssetProfile =
assetProfile.userId === this.request.user.id &&
hasPermission(
this.request.user.permissions,
permissions.createMarketDataOfOwnAssetProfile
) &&
hasPermission(
this.request.user.permissions,
permissions.updateMarketDataOfOwnAssetProfile
);
if (!canManageAllAssetProfiles && !canManageOwnAssetProfile) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return assetProfile;
}
}

2
apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts

@ -2,6 +2,7 @@ import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.modu
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { AssetProfileSplitModule } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
@ -20,6 +21,7 @@ import { AssetProfilesService } from './asset-profiles.service';
imports: [
ActivitiesModule,
ApiModule,
AssetProfileSplitModule,
BenchmarkModule,
DataProviderModule,
ExchangeRateDataModule,

41
apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts

@ -1,4 +1,5 @@
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -30,6 +31,7 @@ import { groupBy } from 'lodash';
export class AssetProfilesService {
public constructor(
private readonly activitiesService: ActivitiesService,
private readonly assetProfileSplitService: AssetProfileSplitService,
private readonly benchmarkService: BenchmarkService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
@ -38,6 +40,39 @@ export class AssetProfilesService {
private readonly symbolProfileService: SymbolProfileService
) {}
public async createSplit({
date,
factor,
symbolProfileId
}: {
date: Date;
factor: number;
symbolProfileId: string;
}) {
return this.assetProfileSplitService.upsert({
date,
factor,
symbolProfileId
});
}
public async deleteSplit({
id,
symbolProfileId
}: {
id: string;
symbolProfileId: string;
}) {
const isDeleted = await this.assetProfileSplitService.deleteById({
id,
symbolProfileId
});
if (!isDeleted) {
throw new NotFoundException();
}
}
public async getAssetProfile({
dataSource,
symbol
@ -54,7 +89,7 @@ export class AssetProfilesService {
await this.activitiesService.getStatisticsByCurrency(currency));
}
const [[assetProfile], marketData] = await Promise.all([
const [[assetProfile], marketData, splits] = await Promise.all([
this.symbolProfileService.getSymbolProfiles([
{
dataSource,
@ -69,6 +104,9 @@ export class AssetProfilesService {
dataSource,
symbol
}
}),
this.assetProfileSplitService.getSplits({
assetProfileIdentifiers: [{ dataSource, symbol }]
})
]);
@ -80,6 +118,7 @@ export class AssetProfilesService {
return {
marketData,
splits,
assetProfile: assetProfile ?? {
activitiesCount,
currency,

12
libs/common/src/lib/dtos/create-asset-profile-split.dto.ts

@ -0,0 +1,12 @@
import { IsISO8601, IsNumber, Validate } from 'class-validator';
import { IsSplitFactorConstraint } from '../validator-constraints/is-split-factor';
export class CreateAssetProfileSplitDto {
@IsISO8601()
date: string;
@IsNumber()
@Validate(IsSplitFactorConstraint)
factor: number;
}

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

@ -4,6 +4,7 @@ import { CreateAccessDto } from './create-access.dto';
import { CreateAccountBalanceDto } from './create-account-balance.dto';
import { CreateAccountWithBalancesDto } from './create-account-with-balances.dto';
import { CreateAccountDto } from './create-account.dto';
import { CreateAssetProfileSplitDto } from './create-asset-profile-split.dto';
import { CreateAssetProfileWithMarketDataDto } from './create-asset-profile-with-market-data.dto';
import { CreateAssetProfileDto } from './create-asset-profile.dto';
import { CreateOrderDto } from './create-order.dto';
@ -36,6 +37,7 @@ export {
CreateAccountDto,
CreateAccountWithBalancesDto,
CreateAssetProfileDto,
CreateAssetProfileSplitDto,
CreateAssetProfileWithMarketDataDto,
CreateOrderDto,
CreatePlatformDto,

3
libs/common/src/lib/interfaces/admin-market-data-details.interface.ts

@ -1,8 +1,9 @@
import { MarketData } from '@prisma/client';
import { AssetProfileSplit, MarketData } from '@prisma/client';
import { EnhancedAssetProfile } from './enhanced-asset-profile.interface';
export interface AdminMarketDataDetails {
assetProfile: Partial<EnhancedAssetProfile>;
marketData: MarketData[];
splits: AssetProfileSplit[];
}

3
libs/common/src/lib/interfaces/responses/asset-profile-response.interface.ts

@ -1,8 +1,9 @@
import { MarketData } from '@prisma/client';
import { AssetProfileSplit, MarketData } from '@prisma/client';
import { EnhancedAssetProfile } from '../enhanced-asset-profile.interface';
export interface AssetProfileResponse {
assetProfile: Partial<EnhancedAssetProfile>;
marketData: MarketData[];
splits: AssetProfileSplit[];
}

15
libs/common/src/lib/validator-constraints/is-split-factor.ts

@ -0,0 +1,15 @@
import {
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator';
@ValidatorConstraint({ name: 'isSplitFactor' })
export class IsSplitFactorConstraint implements ValidatorConstraintInterface {
public defaultMessage() {
return 'factor must be a positive number other than 1';
}
public validate(aFactor: number) {
return typeof aFactor === 'number' && aFactor > 0 && aFactor !== 1;
}
}
Loading…
Cancel
Save