Browse Source

Add logic for importing accounts

pull/1635/head
yksolanki9 3 years ago
parent
commit
046edf7171
  1. 8
      apps/api/src/app/account/create-account.dto.ts
  2. 9
      apps/api/src/app/import/import-data.dto.ts
  3. 11
      apps/api/src/app/import/import.controller.ts
  4. 73
      apps/api/src/app/import/import.service.ts
  5. 19
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  6. 43
      apps/client/src/app/services/import-activities.service.ts

8
apps/api/src/app/account/create-account.dto.ts

@ -8,6 +8,14 @@ import {
} from 'class-validator'; } from 'class-validator';
export class CreateAccountDto { export class CreateAccountDto {
@IsOptional()
@IsString()
id: string;
@IsOptional()
@IsBoolean()
isDefault: boolean;
@IsString() @IsString()
accountType: AccountType; accountType: AccountType;

9
apps/api/src/app/import/import-data.dto.ts

@ -1,8 +1,15 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsArray, ValidateNested } from 'class-validator'; import { IsArray, ValidateNested, IsOptional } from 'class-validator';
import { CreateAccountDto } from '../account/create-account.dto';
export class ImportDataDto { export class ImportDataDto {
@IsOptional()
@IsArray()
@Type(() => CreateAccountDto)
@ValidateNested({ each: true })
accounts: CreateAccountDto[];
@IsArray() @IsArray()
@Type(() => CreateOrderDto) @Type(() => CreateOrderDto)
@ValidateNested({ each: true }) @ValidateNested({ each: true })

11
apps/api/src/app/import/import.controller.ts

@ -2,6 +2,7 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ImportResponse } from '@ghostfolio/common/interfaces'; import { ImportResponse } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
@ -39,7 +40,14 @@ export class ImportController {
@Body() importData: ImportDataDto, @Body() importData: ImportDataDto,
@Query('dryRun') isDryRun?: boolean @Query('dryRun') isDryRun?: boolean
): Promise<ImportResponse> { ): Promise<ImportResponse> {
if (!this.configurationService.get('ENABLE_FEATURE_IMPORT')) { if (
!this.configurationService.get('ENABLE_FEATURE_IMPORT') ||
(importData.accounts?.length > 0 &&
!hasPermission(
this.request.user.permissions,
permissions.createAccount
))
) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN StatusCodes.FORBIDDEN
@ -65,6 +73,7 @@ export class ImportController {
isDryRun, isDryRun,
userCurrency, userCurrency,
activitiesDto: importData.activities, activitiesDto: importData.activities,
accountsDto: importData.accounts || [],
userId: this.request.user.id userId: this.request.user.id
}); });

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

@ -17,6 +17,7 @@ import { SymbolProfile } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns'; import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { CreateAccountDto } from '../account/create-account.dto';
@Injectable() @Injectable()
export class ImportService { export class ImportService {
@ -101,17 +102,84 @@ export class ImportService {
public async import({ public async import({
activitiesDto, activitiesDto,
accountsDto,
isDryRun = false, isDryRun = false,
maxActivitiesToImport, maxActivitiesToImport,
userCurrency, userCurrency,
userId userId
}: { }: {
activitiesDto: Partial<CreateOrderDto>[]; activitiesDto: Partial<CreateOrderDto>[];
accountsDto: Partial<CreateAccountDto>[];
isDryRun?: boolean; isDryRun?: boolean;
maxActivitiesToImport: number; maxActivitiesToImport: number;
userCurrency: string; userCurrency: string;
userId: string; userId: string;
}): Promise<Activity[]> { }): Promise<Activity[]> {
const accountMappings = {};
//Validate accounts
if (accountsDto?.length) {
for (let account of accountsDto) {
const existingAccounts = await this.accountService.accounts({
where: { id: account.id }
});
const oldAccountId = account.id;
const platformId = account.platformId;
delete account.id;
delete account.platformId;
delete account.isDefault;
//If account id does not exist, then create a new one
if (existingAccounts.length === 0) {
const newAccountConfig = {
...account,
User: { connect: { id: userId } }
};
if (platformId) {
Object.assign(newAccountConfig, {
Platform: { connect: { id: platformId } }
});
}
const newAccount = await this.accountService.createAccount(
newAccountConfig,
userId
);
accountMappings[oldAccountId] = newAccount.id;
continue;
}
//If account id is used, then check if it belongs to the same user
//Yes -> Merge the accounts and don't create a new one
if (existingAccounts[0].userId === userId) {
continue;
}
//No -> Replace the account id with a new account id as well as update all the activities when looping
const newAccountConfig = {
...account,
User: { connect: { id: userId } }
};
if (platformId) {
Object.assign(newAccountConfig, {
Platform: { connect: { id: platformId } }
});
}
const newAccount = await this.accountService.createAccount(
newAccountConfig,
userId
);
accountMappings[oldAccountId] = newAccount.id;
}
}
for (const activity of activitiesDto) { for (const activity of activitiesDto) {
if (!activity.dataSource) { if (!activity.dataSource) {
if (activity.type === 'ITEM') { if (activity.type === 'ITEM') {
@ -120,6 +188,11 @@ export class ImportService {
activity.dataSource = this.dataProviderService.getPrimaryDataSource(); activity.dataSource = this.dataProviderService.getPrimaryDataSource();
} }
} }
//If we updated the account id, then update the account id in the activity as well
if (Object.keys(accountMappings).includes(activity.accountId)) {
activity.accountId = accountMappings[activity.accountId];
}
} }
const assetProfiles = await this.validateActivities({ const assetProfiles = await this.validateActivities({

19
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -8,6 +8,7 @@ import {
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service'; import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
@ -25,6 +26,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
templateUrl: 'import-activities-dialog.html' templateUrl: 'import-activities-dialog.html'
}) })
export class ImportActivitiesDialog implements OnDestroy { export class ImportActivitiesDialog implements OnDestroy {
public accounts: CreateAccountDto[] = [];
public activities: Activity[] = []; public activities: Activity[] = [];
public details: any[] = []; public details: any[] = [];
public errorMessages: string[] = []; public errorMessages: string[] = [];
@ -88,9 +90,10 @@ export class ImportActivitiesDialog implements OnDestroy {
try { try {
this.snackBar.open('⏳ ' + $localize`Importing data...`); this.snackBar.open('⏳ ' + $localize`Importing data...`);
await this.importActivitiesService.importSelectedActivities( await this.importActivitiesService.importSelectedActivities({
this.selectedActivities accounts: this.accounts,
); activities: this.selectedActivities
});
this.snackBar.open( this.snackBar.open(
'✅ ' + $localize`Import has been completed`, '✅ ' + $localize`Import has been completed`,
@ -177,10 +180,13 @@ export class ImportActivitiesDialog implements OnDestroy {
} }
try { try {
this.activities = await this.importActivitiesService.importJson({ const data = await this.importActivitiesService.importJson({
content: content.activities, activities: content.activities,
accounts: content.accounts,
isDryRun: true isDryRun: true
}); });
this.activities = data.activities;
this.accounts = data.accounts;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
this.handleImportError({ error, activities: content.activities }); this.handleImportError({ error, activities: content.activities });
@ -189,11 +195,12 @@ export class ImportActivitiesDialog implements OnDestroy {
return; return;
} else if (file.name.endsWith('.csv')) { } else if (file.name.endsWith('.csv')) {
try { try {
this.activities = await this.importActivitiesService.importCsv({ const data = await this.importActivitiesService.importCsv({
fileContent, fileContent,
isDryRun: true, isDryRun: true,
userAccounts: this.data.user.accounts userAccounts: this.data.user.accounts
}); });
this.activities = data.activities;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
this.handleImportError({ this.handleImportError({

43
apps/client/src/app/services/import-activities.service.ts

@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } 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 { Account, DataSource, Type } from '@prisma/client'; import { Account, DataSource, Type } from '@prisma/client';
@ -33,7 +34,10 @@ export class ImportActivitiesService {
fileContent: string; fileContent: string;
isDryRun?: boolean; isDryRun?: boolean;
userAccounts: Account[]; userAccounts: Account[];
}): Promise<Activity[]> { }): Promise<{
accounts?: CreateAccountDto[];
activities: Activity[];
}> {
const content = csvToJson(fileContent, { const content = csvToJson(fileContent, {
dynamicTyping: true, dynamicTyping: true,
header: true, header: true,
@ -55,20 +59,26 @@ export class ImportActivitiesService {
}); });
} }
return await this.importJson({ isDryRun, content: activities }); return await this.importJson({ isDryRun, activities });
} }
public importJson({ public importJson({
content, activities,
accounts,
isDryRun = false isDryRun = false
}: { }: {
content: CreateOrderDto[]; activities: CreateOrderDto[];
accounts?: CreateAccountDto[];
isDryRun?: boolean; isDryRun?: boolean;
}): Promise<Activity[]> { }): Promise<{
accounts?: CreateAccountDto[];
activities: Activity[];
}> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.postImport( this.postImport(
{ {
activities: content activities,
accounts
}, },
isDryRun isDryRun
) )
@ -80,22 +90,29 @@ export class ImportActivitiesService {
) )
.subscribe({ .subscribe({
next: (data) => { next: (data) => {
resolve(data.activities); resolve(data);
} }
}); });
}); });
} }
public importSelectedActivities( public importSelectedActivities({
selectedActivities: Activity[] accounts,
): Promise<Activity[]> { activities
}: {
accounts?: CreateAccountDto[];
activities: Activity[];
}): Promise<{
accounts?: CreateAccountDto[];
activities: Activity[];
}> {
const importData: CreateOrderDto[] = []; const importData: CreateOrderDto[] = [];
for (const activity of selectedActivities) { for (const activity of activities) {
importData.push(this.convertToCreateOrderDto(activity)); importData.push(this.convertToCreateOrderDto(activity));
} }
return this.importJson({ content: importData }); return this.importJson({ activities: importData, accounts });
} }
private convertToCreateOrderDto({ private convertToCreateOrderDto({
@ -347,7 +364,7 @@ export class ImportActivitiesService {
} }
private postImport( private postImport(
aImportData: { activities: CreateOrderDto[] }, aImportData: { activities: CreateOrderDto[]; accounts: CreateAccountDto[] },
aIsDryRun = false aIsDryRun = false
) { ) {
return this.http.post<{ activities: Activity[] }>( return this.http.post<{ activities: Activity[] }>(

Loading…
Cancel
Save