Browse Source

Add logic for importing accounts

pull/1635/head
yksolanki9 3 years ago
committed by Thomas
parent
commit
3e9df51bf7
  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';
export class CreateAccountDto {
@IsOptional()
@IsString()
id: string;
@IsOptional()
@IsBoolean()
isDefault: boolean;
@IsString()
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 { 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 {
@IsOptional()
@IsArray()
@Type(() => CreateAccountDto)
@ValidateNested({ each: true })
accounts: CreateAccountDto[];
@IsArray()
@Type(() => CreateOrderDto)
@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 { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { ImportResponse } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@ -38,7 +39,14 @@ export class ImportController {
@Body() importData: ImportDataDto,
@Query('dryRun') isDryRun?: boolean
): 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(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
@ -64,6 +72,7 @@ export class ImportController {
isDryRun,
userCurrency,
activitiesDto: importData.activities,
accountsDto: importData.accounts || [],
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 { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
import { v4 as uuidv4 } from 'uuid';
import { CreateAccountDto } from '../account/create-account.dto';
@Injectable()
export class ImportService {
@ -101,17 +102,84 @@ export class ImportService {
public async import({
activitiesDto,
accountsDto,
isDryRun = false,
maxActivitiesToImport,
userCurrency,
userId
}: {
activitiesDto: Partial<CreateOrderDto>[];
accountsDto: Partial<CreateAccountDto>[];
isDryRun?: boolean;
maxActivitiesToImport: number;
userCurrency: string;
userId: string;
}): 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) {
if (!activity.dataSource) {
if (activity.type === 'ITEM') {
@ -120,6 +188,11 @@ export class ImportService {
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({

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

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

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

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

Loading…
Cancel
Save