* Improve check for duplicates in import * Update changelog
This commit is contained in:
parent
b923cf7752
commit
7d6a74a67d
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the preview step of the activities import by unchecking duplicates
|
||||||
|
|
||||||
## 1.267.0 - 2023-05-07
|
## 1.267.0 - 2023-05-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -84,6 +84,7 @@ export class ImportService {
|
|||||||
feeInBaseCurrency: 0,
|
feeInBaseCurrency: 0,
|
||||||
id: assetProfile.id,
|
id: assetProfile.id,
|
||||||
isDraft: false,
|
isDraft: false,
|
||||||
|
isDuplicate: false, // TODO: Use evaluated state
|
||||||
SymbolProfile: <SymbolProfile>(<unknown>assetProfile),
|
SymbolProfile: <SymbolProfile>(<unknown>assetProfile),
|
||||||
symbolProfileId: assetProfile.id,
|
symbolProfileId: assetProfile.id,
|
||||||
type: 'DIVIDEND',
|
type: 'DIVIDEND',
|
||||||
@ -204,9 +205,14 @@ export class ImportService {
|
|||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activitiesMarkedAsDuplicates = await this.markActivitiesAsDuplicates({
|
||||||
|
activitiesDto,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
const accounts = (await this.accountService.getAccounts(userId)).map(
|
const accounts = (await this.accountService.getAccounts(userId)).map(
|
||||||
(account) => {
|
({ id, name }) => {
|
||||||
return { id: account.id, name: account.name };
|
return { id, name };
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -221,16 +227,14 @@ export class ImportService {
|
|||||||
for (const {
|
for (const {
|
||||||
accountId,
|
accountId,
|
||||||
comment,
|
comment,
|
||||||
currency,
|
date,
|
||||||
dataSource,
|
|
||||||
date: dateString,
|
|
||||||
fee,
|
fee,
|
||||||
|
isDuplicate,
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
SymbolProfile: assetProfile,
|
||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
} of activitiesDto) {
|
} of activitiesMarkedAsDuplicates) {
|
||||||
const date = parseISO(<string>(<unknown>dateString));
|
|
||||||
const validatedAccount = accounts.find(({ id }) => {
|
const validatedAccount = accounts.find(({ id }) => {
|
||||||
return id === accountId;
|
return id === accountId;
|
||||||
});
|
});
|
||||||
@ -256,29 +260,33 @@ export class ImportService {
|
|||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
isDraft: isAfter(date, endOfToday()),
|
isDraft: isAfter(date, endOfToday()),
|
||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
currency,
|
assetClass: assetProfile.assetClass,
|
||||||
dataSource,
|
assetSubClass: assetProfile.assetSubClass,
|
||||||
symbol,
|
comment: assetProfile.comment,
|
||||||
assetClass: null,
|
countries: assetProfile.countries,
|
||||||
assetSubClass: null,
|
createdAt: assetProfile.createdAt,
|
||||||
comment: null,
|
currency: assetProfile.currency,
|
||||||
countries: null,
|
dataSource: assetProfile.dataSource,
|
||||||
createdAt: undefined,
|
id: assetProfile.id,
|
||||||
id: undefined,
|
isin: assetProfile.isin,
|
||||||
isin: null,
|
name: assetProfile.name,
|
||||||
name: null,
|
scraperConfiguration: assetProfile.scraperConfiguration,
|
||||||
scraperConfiguration: null,
|
sectors: assetProfile.sectors,
|
||||||
sectors: null,
|
symbol: assetProfile.currency,
|
||||||
symbolMapping: null,
|
symbolMapping: assetProfile.symbolMapping,
|
||||||
updatedAt: undefined,
|
updatedAt: assetProfile.updatedAt,
|
||||||
url: null,
|
url: assetProfile.url,
|
||||||
...assetProfiles[symbol]
|
...assetProfiles[assetProfile.symbol]
|
||||||
},
|
},
|
||||||
Account: validatedAccount,
|
Account: validatedAccount,
|
||||||
symbolProfileId: undefined,
|
symbolProfileId: undefined,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
if (isDuplicate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
order = await this.orderService.createOrder({
|
order = await this.orderService.createOrder({
|
||||||
comment,
|
comment,
|
||||||
date,
|
date,
|
||||||
@ -291,14 +299,14 @@ export class ImportService {
|
|||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
connectOrCreate: {
|
connectOrCreate: {
|
||||||
create: {
|
create: {
|
||||||
currency,
|
currency: assetProfile.currency,
|
||||||
dataSource,
|
dataSource: assetProfile.dataSource,
|
||||||
symbol
|
symbol: assetProfile.symbol
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
dataSource_symbol: {
|
dataSource_symbol: {
|
||||||
dataSource,
|
dataSource: assetProfile.dataSource,
|
||||||
symbol
|
symbol: assetProfile.symbol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,15 +321,16 @@ export class ImportService {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
activities.push({
|
activities.push({
|
||||||
...order,
|
...order,
|
||||||
|
isDuplicate,
|
||||||
value,
|
value,
|
||||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
fee,
|
fee,
|
||||||
currency,
|
assetProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
value,
|
value,
|
||||||
currency,
|
assetProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@ -340,6 +349,78 @@ export class ImportService {
|
|||||||
return uniqueAccountIds.size === 1;
|
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({
|
private async validateActivities({
|
||||||
activitiesDto,
|
activitiesDto,
|
||||||
maxActivitiesToImport,
|
maxActivitiesToImport,
|
||||||
@ -356,33 +437,11 @@ export class ImportService {
|
|||||||
const assetProfiles: {
|
const assetProfiles: {
|
||||||
[symbol: string]: Partial<SymbolProfile>;
|
[symbol: string]: Partial<SymbolProfile>;
|
||||||
} = {};
|
} = {};
|
||||||
const existingActivities = await this.orderService.orders({
|
|
||||||
include: { SymbolProfile: true },
|
|
||||||
orderBy: { date: 'desc' },
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [
|
for (const [
|
||||||
index,
|
index,
|
||||||
{ currency, dataSource, date, fee, quantity, symbol, type, unitPrice }
|
{ currency, dataSource, symbol }
|
||||||
] of activitiesDto.entries()) {
|
] 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') {
|
if (dataSource !== 'MANUAL') {
|
||||||
const assetProfile = (
|
const assetProfile = (
|
||||||
await this.dataProviderService.getAssetProfiles([
|
await this.dataProviderService.getAssetProfiles([
|
||||||
|
@ -6,6 +6,7 @@ export interface Activities {
|
|||||||
|
|
||||||
export interface Activity extends OrderWithAccount {
|
export interface Activity extends OrderWithAccount {
|
||||||
feeInBaseCurrency: number;
|
feeInBaseCurrency: number;
|
||||||
|
isDuplicate: boolean;
|
||||||
updateAccountBalance?: boolean;
|
updateAccountBalance?: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
valueInBaseCurrency: number;
|
valueInBaseCurrency: number;
|
||||||
|
@ -333,6 +333,7 @@ export class OrderService {
|
|||||||
order.SymbolProfile.currency,
|
order.SymbolProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
|
isDuplicate: false,
|
||||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
value,
|
value,
|
||||||
order.SymbolProfile.currency,
|
order.SymbolProfile.currency,
|
||||||
|
@ -76,7 +76,6 @@
|
|||||||
<ng-container matColumnDef="select">
|
<ng-container matColumnDef="select">
|
||||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
class="mt-2"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
[checked]="selectedRows.hasValue() && areAllRowsSelected()"
|
[checked]="selectedRows.hasValue() && areAllRowsSelected()"
|
||||||
[indeterminate]="selectedRows.hasValue() && !areAllRowsSelected()"
|
[indeterminate]="selectedRows.hasValue() && !areAllRowsSelected()"
|
||||||
@ -85,9 +84,11 @@
|
|||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
class="mt-2"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
[checked]="selectedRows.isSelected(element)"
|
[checked]="
|
||||||
|
element.isDuplicate ? false : selectedRows.isSelected(element)
|
||||||
|
"
|
||||||
|
[disabled]="element.isDuplicate"
|
||||||
(change)="$event ? selectedRows.toggle(element) : null"
|
(change)="$event ? selectedRows.toggle(element) : null"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
></mat-checkbox>
|
></mat-checkbox>
|
||||||
|
@ -177,7 +177,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
public onClickActivity(activity: Activity) {
|
public onClickActivity(activity: Activity) {
|
||||||
if (this.showCheckbox) {
|
if (this.showCheckbox) {
|
||||||
this.selectedRows.toggle(activity);
|
if (!activity.isDuplicate) {
|
||||||
|
this.selectedRows.toggle(activity);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
this.hasPermissionToOpenDetails &&
|
this.hasPermissionToOpenDetails &&
|
||||||
!activity.isDraft &&
|
!activity.isDraft &&
|
||||||
|
Loading…
x
Reference in New Issue
Block a user