|
|
|
@ -84,6 +84,7 @@ export class ImportService {
|
|
|
|
|
feeInBaseCurrency: 0,
|
|
|
|
|
id: assetProfile.id,
|
|
|
|
|
isDraft: false,
|
|
|
|
|
isDuplicate: false, // TODO: Use evaluated state
|
|
|
|
|
SymbolProfile: <SymbolProfile>(<unknown>assetProfile),
|
|
|
|
|
symbolProfileId: assetProfile.id,
|
|
|
|
|
type: 'DIVIDEND',
|
|
|
|
@ -204,9 +205,14 @@ export class ImportService {
|
|
|
|
|
userId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activitiesMarkedAsDuplicates = await this.markActivitiesAsDuplicates({
|
|
|
|
|
activitiesDto,
|
|
|
|
|
userId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const accounts = (await this.accountService.getAccounts(userId)).map(
|
|
|
|
|
(account) => {
|
|
|
|
|
return { id: account.id, name: account.name };
|
|
|
|
|
({ id, name }) => {
|
|
|
|
|
return { id, name };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
@ -221,16 +227,14 @@ export class ImportService {
|
|
|
|
|
for (const {
|
|
|
|
|
accountId,
|
|
|
|
|
comment,
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
date: dateString,
|
|
|
|
|
date,
|
|
|
|
|
fee,
|
|
|
|
|
isDuplicate,
|
|
|
|
|
quantity,
|
|
|
|
|
symbol,
|
|
|
|
|
SymbolProfile: assetProfile,
|
|
|
|
|
type,
|
|
|
|
|
unitPrice
|
|
|
|
|
} of activitiesDto) {
|
|
|
|
|
const date = parseISO(<string>(<unknown>dateString));
|
|
|
|
|
} of activitiesMarkedAsDuplicates) {
|
|
|
|
|
const validatedAccount = accounts.find(({ id }) => {
|
|
|
|
|
return id === accountId;
|
|
|
|
|
});
|
|
|
|
@ -256,29 +260,33 @@ export class ImportService {
|
|
|
|
|
id: uuidv4(),
|
|
|
|
|
isDraft: isAfter(date, endOfToday()),
|
|
|
|
|
SymbolProfile: {
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
symbol,
|
|
|
|
|
assetClass: null,
|
|
|
|
|
assetSubClass: null,
|
|
|
|
|
comment: null,
|
|
|
|
|
countries: null,
|
|
|
|
|
createdAt: undefined,
|
|
|
|
|
id: undefined,
|
|
|
|
|
isin: null,
|
|
|
|
|
name: null,
|
|
|
|
|
scraperConfiguration: null,
|
|
|
|
|
sectors: null,
|
|
|
|
|
symbolMapping: null,
|
|
|
|
|
updatedAt: undefined,
|
|
|
|
|
url: null,
|
|
|
|
|
...assetProfiles[symbol]
|
|
|
|
|
assetClass: assetProfile.assetClass,
|
|
|
|
|
assetSubClass: assetProfile.assetSubClass,
|
|
|
|
|
comment: assetProfile.comment,
|
|
|
|
|
countries: assetProfile.countries,
|
|
|
|
|
createdAt: assetProfile.createdAt,
|
|
|
|
|
currency: assetProfile.currency,
|
|
|
|
|
dataSource: assetProfile.dataSource,
|
|
|
|
|
id: assetProfile.id,
|
|
|
|
|
isin: assetProfile.isin,
|
|
|
|
|
name: assetProfile.name,
|
|
|
|
|
scraperConfiguration: assetProfile.scraperConfiguration,
|
|
|
|
|
sectors: assetProfile.sectors,
|
|
|
|
|
symbol: assetProfile.currency,
|
|
|
|
|
symbolMapping: assetProfile.symbolMapping,
|
|
|
|
|
updatedAt: assetProfile.updatedAt,
|
|
|
|
|
url: assetProfile.url,
|
|
|
|
|
...assetProfiles[assetProfile.symbol]
|
|
|
|
|
},
|
|
|
|
|
Account: validatedAccount,
|
|
|
|
|
symbolProfileId: undefined,
|
|
|
|
|
updatedAt: new Date()
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
if (isDuplicate) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
order = await this.orderService.createOrder({
|
|
|
|
|
comment,
|
|
|
|
|
date,
|
|
|
|
@ -291,14 +299,14 @@ export class ImportService {
|
|
|
|
|
SymbolProfile: {
|
|
|
|
|
connectOrCreate: {
|
|
|
|
|
create: {
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
symbol
|
|
|
|
|
currency: assetProfile.currency,
|
|
|
|
|
dataSource: assetProfile.dataSource,
|
|
|
|
|
symbol: assetProfile.symbol
|
|
|
|
|
},
|
|
|
|
|
where: {
|
|
|
|
|
dataSource_symbol: {
|
|
|
|
|
dataSource,
|
|
|
|
|
symbol
|
|
|
|
|
dataSource: assetProfile.dataSource,
|
|
|
|
|
symbol: assetProfile.symbol
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -313,15 +321,16 @@ export class ImportService {
|
|
|
|
|
//@ts-ignore
|
|
|
|
|
activities.push({
|
|
|
|
|
...order,
|
|
|
|
|
isDuplicate,
|
|
|
|
|
value,
|
|
|
|
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
|
|
|
fee,
|
|
|
|
|
currency,
|
|
|
|
|
assetProfile.currency,
|
|
|
|
|
userCurrency
|
|
|
|
|
),
|
|
|
|
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
|
|
|
value,
|
|
|
|
|
currency,
|
|
|
|
|
assetProfile.currency,
|
|
|
|
|
userCurrency
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
@ -340,6 +349,78 @@ export class ImportService {
|
|
|
|
|
return uniqueAccountIds.size === 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async markActivitiesAsDuplicates({
|
|
|
|
|
activitiesDto,
|
|
|
|
|
userId
|
|
|
|
|
}: {
|
|
|
|
|
activitiesDto: Partial<CreateOrderDto>[];
|
|
|
|
|
userId: string;
|
|
|
|
|
}): Promise<Partial<Activity>[]> {
|
|
|
|
|
const existingActivities = await this.orderService.orders({
|
|
|
|
|
include: { SymbolProfile: true },
|
|
|
|
|
orderBy: { date: 'desc' },
|
|
|
|
|
where: { userId }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return activitiesDto.map(
|
|
|
|
|
({
|
|
|
|
|
accountId,
|
|
|
|
|
comment,
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
date: dateString,
|
|
|
|
|
fee,
|
|
|
|
|
quantity,
|
|
|
|
|
symbol,
|
|
|
|
|
type,
|
|
|
|
|
unitPrice
|
|
|
|
|
}) => {
|
|
|
|
|
const date = parseISO(<string>(<unknown>dateString));
|
|
|
|
|
const isDuplicate = existingActivities.some((activity) => {
|
|
|
|
|
return (
|
|
|
|
|
activity.SymbolProfile.currency === currency &&
|
|
|
|
|
activity.SymbolProfile.dataSource === dataSource &&
|
|
|
|
|
isSameDay(activity.date, date) &&
|
|
|
|
|
activity.fee === fee &&
|
|
|
|
|
activity.quantity === quantity &&
|
|
|
|
|
activity.SymbolProfile.symbol === symbol &&
|
|
|
|
|
activity.type === type &&
|
|
|
|
|
activity.unitPrice === unitPrice
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
accountId,
|
|
|
|
|
comment,
|
|
|
|
|
date,
|
|
|
|
|
fee,
|
|
|
|
|
isDuplicate,
|
|
|
|
|
quantity,
|
|
|
|
|
type,
|
|
|
|
|
unitPrice,
|
|
|
|
|
SymbolProfile: {
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
symbol,
|
|
|
|
|
assetClass: null,
|
|
|
|
|
assetSubClass: null,
|
|
|
|
|
comment: null,
|
|
|
|
|
countries: null,
|
|
|
|
|
createdAt: undefined,
|
|
|
|
|
id: undefined,
|
|
|
|
|
isin: null,
|
|
|
|
|
name: null,
|
|
|
|
|
scraperConfiguration: null,
|
|
|
|
|
sectors: null,
|
|
|
|
|
symbolMapping: null,
|
|
|
|
|
updatedAt: undefined,
|
|
|
|
|
url: null
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async validateActivities({
|
|
|
|
|
activitiesDto,
|
|
|
|
|
maxActivitiesToImport,
|
|
|
|
@ -356,33 +437,11 @@ export class ImportService {
|
|
|
|
|
const assetProfiles: {
|
|
|
|
|
[symbol: string]: Partial<SymbolProfile>;
|
|
|
|
|
} = {};
|
|
|
|
|
const existingActivities = await this.orderService.orders({
|
|
|
|
|
include: { SymbolProfile: true },
|
|
|
|
|
orderBy: { date: 'desc' },
|
|
|
|
|
where: { userId }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const [
|
|
|
|
|
index,
|
|
|
|
|
{ currency, dataSource, date, fee, quantity, symbol, type, unitPrice }
|
|
|
|
|
{ currency, dataSource, symbol }
|
|
|
|
|
] of activitiesDto.entries()) {
|
|
|
|
|
const duplicateActivity = existingActivities.find((activity) => {
|
|
|
|
|
return (
|
|
|
|
|
activity.SymbolProfile.currency === currency &&
|
|
|
|
|
activity.SymbolProfile.dataSource === dataSource &&
|
|
|
|
|
isSameDay(activity.date, parseISO(<string>(<unknown>date))) &&
|
|
|
|
|
activity.fee === fee &&
|
|
|
|
|
activity.quantity === quantity &&
|
|
|
|
|
activity.SymbolProfile.symbol === symbol &&
|
|
|
|
|
activity.type === type &&
|
|
|
|
|
activity.unitPrice === unitPrice
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (duplicateActivity) {
|
|
|
|
|
throw new Error(`activities.${index} is a duplicate activity`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dataSource !== 'MANUAL') {
|
|
|
|
|
const assetProfile = (
|
|
|
|
|
await this.dataProviderService.getAssetProfiles([
|
|
|
|
|