|
|
@ -14,7 +14,11 @@ import { |
|
|
ghostfolioPrefix, |
|
|
ghostfolioPrefix, |
|
|
TAG_ID_EXCLUDE_FROM_ANALYSIS |
|
|
TAG_ID_EXCLUDE_FROM_ANALYSIS |
|
|
} from '@ghostfolio/common/config'; |
|
|
} from '@ghostfolio/common/config'; |
|
|
import { CreateAssetProfileDto, CreateOrderDto } from '@ghostfolio/common/dtos'; |
|
|
import { |
|
|
|
|
|
CreateAccountWithBalancesDto, |
|
|
|
|
|
CreateAssetProfileDto, |
|
|
|
|
|
CreateOrderDto |
|
|
|
|
|
} from '@ghostfolio/common/dtos'; |
|
|
import { |
|
|
import { |
|
|
getAssetProfileIdentifier, |
|
|
getAssetProfileIdentifier, |
|
|
isValidCustomAssetProfileSymbol, |
|
|
isValidCustomAssetProfileSymbol, |
|
|
@ -33,7 +37,7 @@ import { |
|
|
} from '@ghostfolio/common/types'; |
|
|
} from '@ghostfolio/common/types'; |
|
|
|
|
|
|
|
|
import { Injectable } from '@nestjs/common'; |
|
|
import { Injectable } from '@nestjs/common'; |
|
|
import { DataSource, Prisma } from '@prisma/client'; |
|
|
import { Account, DataSource, Prisma } from '@prisma/client'; |
|
|
import { Big } from 'big.js'; |
|
|
import { Big } from 'big.js'; |
|
|
import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; |
|
|
import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; |
|
|
import { omit, uniqBy } from 'lodash'; |
|
|
import { omit, uniqBy } from 'lodash'; |
|
|
@ -116,6 +120,8 @@ export class ImportService { |
|
|
|
|
|
|
|
|
return await Promise.all( |
|
|
return await Promise.all( |
|
|
Object.entries(dividends).map(([dateString, { marketPrice }]) => { |
|
|
Object.entries(dividends).map(([dateString, { marketPrice }]) => { |
|
|
|
|
|
const date = parseDate(dateString); |
|
|
|
|
|
|
|
|
const quantity = |
|
|
const quantity = |
|
|
historicalData.find((historicalDataItem) => { |
|
|
historicalData.find((historicalDataItem) => { |
|
|
return historicalDataItem.date === dateString; |
|
|
return historicalDataItem.date === dateString; |
|
|
@ -123,10 +129,8 @@ export class ImportService { |
|
|
|
|
|
|
|
|
const value = new Big(quantity).mul(marketPrice).toNumber(); |
|
|
const value = new Big(quantity).mul(marketPrice).toNumber(); |
|
|
|
|
|
|
|
|
const date = parseDate(dateString); |
|
|
|
|
|
const isDuplicate = activities.some((activity) => { |
|
|
const isDuplicate = activities.some((activity) => { |
|
|
return ( |
|
|
return ( |
|
|
activity.accountId === account?.id && |
|
|
|
|
|
activity.assetProfile.currency === assetProfile.currency && |
|
|
activity.assetProfile.currency === assetProfile.currency && |
|
|
activity.assetProfile.dataSource === assetProfile.dataSource && |
|
|
activity.assetProfile.dataSource === assetProfile.dataSource && |
|
|
isSameSecond(activity.date, date) && |
|
|
isSameSecond(activity.date, date) && |
|
|
@ -355,28 +359,35 @@ export class ImportService { |
|
|
return existingAccount.id === accountWithBalances.id; |
|
|
return existingAccount.id === accountWithBalances.id; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Skip the account if it already belongs to the user
|
|
|
|
|
|
if (accountWithSameId && accountWithSameId.userId === user.id) { |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// If there is no account or if the account belongs to a different
|
|
|
// If there is no account or if the account belongs to a different
|
|
|
// user, then reuse an existing account of the user with the same name
|
|
|
// user, then reuse an existing account of the user with the same name
|
|
|
// or create a new account
|
|
|
// or create a new account
|
|
|
if (!accountWithSameId || accountWithSameId.userId !== user.id) { |
|
|
const accountToReuse = this.getAccountToReuse({ |
|
|
// Check if the user has an account with the same name
|
|
|
accountWithBalances, |
|
|
const accountWithSameNameOfUser = existingAccountsOfUser.find( |
|
|
accountsWithBalancesDto, |
|
|
({ name }) => { |
|
|
existingAccountsOfUser |
|
|
return name === accountWithBalances.name; |
|
|
}); |
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (accountWithSameNameOfUser) { |
|
|
if (accountToReuse) { |
|
|
// Reuse the account of the user instead of creating a duplicate
|
|
|
// Reuse the account of the user instead of creating a duplicate. The
|
|
|
|
|
|
// balances, the platform and the tags of the import are deliberately
|
|
|
|
|
|
// not applied to leave the existing account of the user untouched.
|
|
|
if ( |
|
|
if ( |
|
|
accountWithBalances.id && |
|
|
accountWithBalances.id && |
|
|
accountWithBalances.id !== accountWithSameNameOfUser.id |
|
|
accountWithBalances.id !== accountToReuse.id |
|
|
) { |
|
|
) { |
|
|
// Store the new to old account ID mappings for updating activities
|
|
|
// Store the new to old account ID mappings for updating activities
|
|
|
accountIdMapping[accountWithBalances.id] = |
|
|
accountIdMapping[accountWithBalances.id] = accountToReuse.id; |
|
|
accountWithSameNameOfUser.id; |
|
|
|
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const account = omit(accountWithBalances, [ |
|
|
const account = omit(accountWithBalances, [ |
|
|
'balances', |
|
|
'balances', |
|
|
'isExcluded', |
|
|
'isExcluded', |
|
|
@ -443,8 +454,6 @@ export class ImportService { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (assetProfilesWithMarketDataDto?.length) { |
|
|
if (assetProfilesWithMarketDataDto?.length) { |
|
|
const customAssetProfileNames = assetProfilesWithMarketDataDto |
|
|
const customAssetProfileNames = assetProfilesWithMarketDataDto |
|
|
@ -851,7 +860,7 @@ export class ImportService { |
|
|
|
|
|
|
|
|
const isDuplicate = existingActivities.some((activity) => { |
|
|
const isDuplicate = existingActivities.some((activity) => { |
|
|
return ( |
|
|
return ( |
|
|
(activity.comment ?? null) === (comment ?? null) && |
|
|
(activity.comment || null) === (comment || null) && |
|
|
(activity.currency === currency || |
|
|
(activity.currency === currency || |
|
|
activity.assetProfile.currency === currency) && |
|
|
activity.assetProfile.currency === currency) && |
|
|
activity.assetProfile.dataSource === dataSource && |
|
|
activity.assetProfile.dataSource === dataSource && |
|
|
@ -899,6 +908,44 @@ export class ImportService { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Returns the account of the user to reuse for the given account of the |
|
|
|
|
|
* import, based on the name. The name is only considered if it is |
|
|
|
|
|
* unambiguous, both in the accounts of the user and in the accounts of the |
|
|
|
|
|
* import, since it is not unique. Otherwise, distinct accounts would be |
|
|
|
|
|
* merged into a single one. |
|
|
|
|
|
*/ |
|
|
|
|
|
private getAccountToReuse({ |
|
|
|
|
|
accountWithBalances, |
|
|
|
|
|
accountsWithBalancesDto, |
|
|
|
|
|
existingAccountsOfUser |
|
|
|
|
|
}: { |
|
|
|
|
|
accountWithBalances: CreateAccountWithBalancesDto; |
|
|
|
|
|
accountsWithBalancesDto: ImportDataDto['accounts']; |
|
|
|
|
|
existingAccountsOfUser: Account[]; |
|
|
|
|
|
}): Account { |
|
|
|
|
|
const accountsWithSameNameOfUser = existingAccountsOfUser.filter( |
|
|
|
|
|
({ name }) => { |
|
|
|
|
|
return name === accountWithBalances.name; |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const accountsWithSameNameToImport = accountsWithBalancesDto.filter( |
|
|
|
|
|
({ name }) => { |
|
|
|
|
|
return name === accountWithBalances.name; |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
accountsWithSameNameOfUser.length !== 1 || |
|
|
|
|
|
accountsWithSameNameToImport.length !== 1 |
|
|
|
|
|
) { |
|
|
|
|
|
return undefined; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return accountsWithSameNameOfUser[0]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private isUniqueAccount(accounts: AccountWithValue[]) { |
|
|
private isUniqueAccount(accounts: AccountWithValue[]) { |
|
|
const uniqueAccountIds = new Set<string>(); |
|
|
const uniqueAccountIds = new Set<string>(); |
|
|
|
|
|
|
|
|
|