Browse Source

Feature/improve check for duplicates in import #1932

pull/1940/head
visrut 2 years ago
parent
commit
a868cd99b6
  1. 107
      apps/api/src/app/import/import.service.ts
  2. 55
      apps/api/src/app/order/create-order.dto.ts
  3. 1
      apps/api/src/app/order/interfaces/activities.interface.ts
  4. 3
      apps/api/src/app/order/order.service.ts
  5. 10
      libs/ui/src/lib/activities-table/activities-table.component.html
  6. 4
      libs/ui/src/lib/activities-table/activities-table.component.ts

107
apps/api/src/app/import/import.service.ts

@ -1,6 +1,9 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import {
CreateOrderDto,
OrderDto
} from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
@ -94,7 +97,8 @@ export class ImportService {
value, value,
assetProfile.currency, assetProfile.currency,
userCurrency userCurrency
) ),
isDuplicate: false
}; };
}); });
} catch { } catch {
@ -204,6 +208,11 @@ export class ImportService {
userId userId
}); });
const activitiesDtoWithDuplication = await this.labelDuplicateActivities({
activitiesDto,
userId
});
const accounts = (await this.accountService.getAccounts(userId)).map( const accounts = (await this.accountService.getAccounts(userId)).map(
(account) => { (account) => {
return { id: account.id, name: account.name }; return { id: account.id, name: account.name };
@ -228,8 +237,9 @@ export class ImportService {
quantity, quantity,
symbol, symbol,
type, type,
unitPrice unitPrice,
} of activitiesDto) { isDuplicate
} of activitiesDtoWithDuplication) {
const date = parseISO(<string>(<unknown>dateString)); const date = parseISO(<string>(<unknown>dateString));
const validatedAccount = accounts.find(({ id }) => { const validatedAccount = accounts.find(({ id }) => {
return id === accountId; return id === accountId;
@ -279,6 +289,10 @@ export class ImportService {
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,
@ -322,13 +336,70 @@ export class ImportService {
value, value,
currency, currency,
userCurrency userCurrency
) ),
isDuplicate
}); });
} }
console.log(activities);
return activities; return activities;
} }
private async labelDuplicateActivities({
activitiesDto,
userId
}: {
activitiesDto: Partial<CreateOrderDto>[];
userId: string;
}): Promise<Partial<OrderDto>[]> {
const existingActivities = await this.orderService.orders({
include: { SymbolProfile: true },
orderBy: { date: 'desc' },
where: { userId }
});
const activitiesDtoWithDuplication: Partial<OrderDto>[] = [];
for (const activitiesDtoEntry of activitiesDto) {
const {
currency,
dataSource,
date,
fee,
quantity,
symbol,
type,
unitPrice
} = activitiesDtoEntry;
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) {
activitiesDtoWithDuplication.push({
...activitiesDtoEntry,
isDuplicate: true
});
} else {
activitiesDtoWithDuplication.push({
...activitiesDtoEntry,
isDuplicate: false
});
}
}
return activitiesDtoWithDuplication;
}
private isUniqueAccount(accounts: AccountWithPlatform[]) { private isUniqueAccount(accounts: AccountWithPlatform[]) {
const uniqueAccountIds = new Set<string>(); const uniqueAccountIds = new Set<string>();
@ -355,33 +426,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([

55
apps/api/src/app/order/create-order.dto.ts

@ -8,6 +8,7 @@ import {
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean,
IsEnum, IsEnum,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
@ -15,6 +16,7 @@ import {
IsString IsString
} from 'class-validator'; } from 'class-validator';
import { isString } from 'lodash'; import { isString } from 'lodash';
import { CreateAccessDto } from '../access/create-access.dto';
export class CreateOrderDto { export class CreateOrderDto {
@IsOptional() @IsOptional()
@ -65,3 +67,56 @@ export class CreateOrderDto {
@IsNumber() @IsNumber()
unitPrice: number; unitPrice: number;
} }
export class OrderDto {
@IsOptional()
@IsString()
accountId?: string;
@IsOptional()
@IsEnum(AssetClass, { each: true })
assetClass?: AssetClass;
@IsOptional()
@IsEnum(AssetSubClass, { each: true })
assetSubClass?: AssetSubClass;
@IsOptional()
@IsString()
@Transform(({ value }: TransformFnParams) =>
isString(value) ? value.trim() : value
)
comment?: string;
@IsString()
currency: string;
@IsOptional()
@IsEnum(DataSource, { each: true })
dataSource?: DataSource;
@IsISO8601()
date: string;
@IsNumber()
fee: number;
@IsNumber()
quantity: number;
@IsString()
symbol: string;
@IsArray()
@IsOptional()
tags?: Tag[];
@IsEnum(Type, { each: true })
type: Type;
@IsNumber()
unitPrice: number;
@IsBoolean()
isDuplicate: boolean;
}

1
apps/api/src/app/order/interfaces/activities.interface.ts

@ -8,4 +8,5 @@ export interface Activity extends OrderWithAccount {
feeInBaseCurrency: number; feeInBaseCurrency: number;
value: number; value: number;
valueInBaseCurrency: number; valueInBaseCurrency: number;
isDuplicate: boolean;
} }

3
apps/api/src/app/order/order.service.ts

@ -310,7 +310,8 @@ export class OrderService {
value, value,
order.SymbolProfile.currency, order.SymbolProfile.currency,
userCurrency userCurrency
) ),
isDuplicate: false
}; };
}); });
} }

10
libs/ui/src/lib/activities-table/activities-table.component.html

@ -85,12 +85,22 @@
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<mat-checkbox <mat-checkbox
*ngIf="!element.isDuplicate"
class="mt-2" class="mt-2"
color="primary" color="primary"
[checked]="selectedRows.isSelected(element)" [checked]="selectedRows.isSelected(element)"
(change)="$event ? selectedRows.toggle(element) : null" (change)="$event ? selectedRows.toggle(element) : null"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
></mat-checkbox> ></mat-checkbox>
<mat-checkbox
*ngIf="element.isDuplicate"
class="mt-2"
color="primary"
[checked]="false"
[disabled]="true"
(change)="(null)"
(click)="(null)"
></mat-checkbox>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td> <td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>

4
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -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…
Cancel
Save