|
|
@ -19,8 +19,7 @@ import { |
|
|
AdminData, |
|
|
AdminData, |
|
|
AdminUserResponse, |
|
|
AdminUserResponse, |
|
|
AdminUsersResponse, |
|
|
AdminUsersResponse, |
|
|
AssetProfileIdentifier, |
|
|
AssetProfileIdentifier |
|
|
Holding |
|
|
|
|
|
} from '@ghostfolio/common/interfaces'; |
|
|
} from '@ghostfolio/common/interfaces'; |
|
|
import { PropertyKey } from '@ghostfolio/common/types'; |
|
|
import { PropertyKey } from '@ghostfolio/common/types'; |
|
|
|
|
|
|
|
|
@ -40,6 +39,7 @@ import { |
|
|
} from '@prisma/client'; |
|
|
} from '@prisma/client'; |
|
|
import { differenceInDays } from 'date-fns'; |
|
|
import { differenceInDays } from 'date-fns'; |
|
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
|
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
|
|
|
|
|
import { randomUUID } from 'node:crypto'; |
|
|
|
|
|
|
|
|
@Injectable() |
|
|
@Injectable() |
|
|
export class AdminService { |
|
|
export class AdminService { |
|
|
@ -241,16 +241,26 @@ export class AdminService { |
|
|
url |
|
|
url |
|
|
}: Prisma.SymbolProfileUpdateInput |
|
|
}: Prisma.SymbolProfileUpdateInput |
|
|
) { |
|
|
) { |
|
|
|
|
|
const isConversionToManualDataSource = |
|
|
|
|
|
newDataSource === DataSource.MANUAL && dataSource !== DataSource.MANUAL; |
|
|
|
|
|
|
|
|
|
|
|
if (isConversionToManualDataSource && !newSymbol) { |
|
|
|
|
|
// The generated symbol avoids collisions in the MANUAL namespace
|
|
|
|
|
|
newSymbol = randomUUID(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if ( |
|
|
if ( |
|
|
newDataSource && |
|
|
newDataSource && |
|
|
newSymbol && |
|
|
newSymbol && |
|
|
(newDataSource !== dataSource || newSymbol !== symbol) |
|
|
(newDataSource !== dataSource || newSymbol !== symbol) |
|
|
) { |
|
|
) { |
|
|
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ |
|
|
const newAssetProfileIdentifier: AssetProfileIdentifier = { |
|
|
{ |
|
|
dataSource: newDataSource as DataSource, |
|
|
dataSource: DataSource[newDataSource.toString()], |
|
|
|
|
|
symbol: newSymbol as string |
|
|
symbol: newSymbol as string |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ |
|
|
|
|
|
newAssetProfileIdentifier |
|
|
]); |
|
|
]); |
|
|
|
|
|
|
|
|
if (assetProfile) { |
|
|
if (assetProfile) { |
|
|
@ -260,74 +270,86 @@ export class AdminService { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const operations: Prisma.PrismaPromise<unknown>[] = [ |
|
|
await Promise.all([ |
|
|
|
|
|
this.symbolProfileService.updateAssetProfileIdentifier( |
|
|
this.symbolProfileService.updateAssetProfileIdentifier( |
|
|
{ |
|
|
{ |
|
|
dataSource, |
|
|
dataSource, |
|
|
symbol |
|
|
symbol |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
newAssetProfileIdentifier |
|
|
dataSource: DataSource[newDataSource.toString()], |
|
|
|
|
|
symbol: newSymbol as string |
|
|
|
|
|
} |
|
|
|
|
|
), |
|
|
), |
|
|
this.marketDataService.updateAssetProfileIdentifier( |
|
|
this.marketDataService.updateAssetProfileIdentifier( |
|
|
{ |
|
|
{ |
|
|
dataSource, |
|
|
dataSource, |
|
|
symbol |
|
|
symbol |
|
|
}, |
|
|
}, |
|
|
|
|
|
newAssetProfileIdentifier |
|
|
|
|
|
) |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
if (isConversionToManualDataSource) { |
|
|
|
|
|
const [currentAssetProfile] = |
|
|
|
|
|
await this.symbolProfileService.getSymbolProfiles([ |
|
|
{ |
|
|
{ |
|
|
dataSource: DataSource[newDataSource.toString()], |
|
|
dataSource, |
|
|
symbol: newSymbol as string |
|
|
symbol |
|
|
} |
|
|
} |
|
|
) |
|
|
|
|
|
]); |
|
|
]); |
|
|
|
|
|
|
|
|
if (DataSource[newDataSource.toString()] === DataSource.MANUAL) { |
|
|
if (!currentAssetProfile) { |
|
|
await Promise.all([ |
|
|
throw new HttpException( |
|
|
this.symbolProfileService.deleteAssetProfileOverrides({ |
|
|
getReasonPhrase(StatusCodes.NOT_FOUND), |
|
|
dataSource: DataSource.MANUAL, |
|
|
StatusCodes.NOT_FOUND |
|
|
symbol: newSymbol as string |
|
|
); |
|
|
}), |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
operations.push( |
|
|
|
|
|
// The overrides are not read for MANUAL asset profiles
|
|
|
|
|
|
this.symbolProfileService.deleteAssetProfileOverrides( |
|
|
|
|
|
newAssetProfileIdentifier |
|
|
|
|
|
), |
|
|
|
|
|
// Persist the current values, merged with the overrides
|
|
|
this.symbolProfileService.updateSymbolProfile( |
|
|
this.symbolProfileService.updateSymbolProfile( |
|
|
|
|
|
newAssetProfileIdentifier, |
|
|
{ |
|
|
{ |
|
|
dataSource: DataSource.MANUAL, |
|
|
assetClass: currentAssetProfile.assetClass, |
|
|
symbol: newSymbol as string |
|
|
assetSubClass: currentAssetProfile.assetSubClass, |
|
|
}, |
|
|
countries: currentAssetProfile.countries?.map( |
|
|
{ |
|
|
({ code, weight }) => { |
|
|
assetClass, |
|
|
return { code, weight }; |
|
|
assetSubClass, |
|
|
} |
|
|
countries, |
|
|
), |
|
|
name, |
|
|
holdings: currentAssetProfile.holdings?.map((holding) => { |
|
|
sectors, |
|
|
|
|
|
url, |
|
|
|
|
|
holdings: (holdings as unknown as Holding[])?.map((holding) => { |
|
|
|
|
|
return { |
|
|
return { |
|
|
name: holding.name, |
|
|
name: holding.name, |
|
|
weight: holding.allocationInPercentage |
|
|
weight: holding.allocationInPercentage |
|
|
}; |
|
|
}; |
|
|
}) |
|
|
}), |
|
|
|
|
|
name: currentAssetProfile.name, |
|
|
|
|
|
sectors: currentAssetProfile.sectors?.map((sector) => { |
|
|
|
|
|
return { name: sector.name, weight: sector.weight }; |
|
|
|
|
|
}), |
|
|
|
|
|
url: currentAssetProfile.url |
|
|
} |
|
|
} |
|
|
) |
|
|
) |
|
|
]); |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const [updatedAssetProfile] = |
|
|
|
|
|
await this.symbolProfileService.getSymbolProfiles([ |
|
|
|
|
|
{ |
|
|
|
|
|
dataSource: DataSource[newDataSource.toString()], |
|
|
|
|
|
symbol: newSymbol as string |
|
|
|
|
|
} |
|
|
} |
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
return updatedAssetProfile; |
|
|
try { |
|
|
|
|
|
await this.prismaService.$transaction(operations); |
|
|
} catch { |
|
|
} catch { |
|
|
throw new HttpException( |
|
|
throw new HttpException( |
|
|
getReasonPhrase(StatusCodes.BAD_REQUEST), |
|
|
getReasonPhrase(StatusCodes.BAD_REQUEST), |
|
|
StatusCodes.BAD_REQUEST |
|
|
StatusCodes.BAD_REQUEST |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const [updatedAssetProfile] = |
|
|
|
|
|
await this.symbolProfileService.getSymbolProfiles([ |
|
|
|
|
|
newAssetProfileIdentifier |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
return updatedAssetProfile; |
|
|
} else { |
|
|
} else { |
|
|
const assetProfileOverrides = { |
|
|
const assetProfileOverrides = { |
|
|
assetClass: assetClass as AssetClass, |
|
|
assetClass: assetClass as AssetClass, |
|
|
|