|
|
|
@ -8,12 +8,11 @@ import { parseDate as parseDateHelper } from '@ghostfolio/common/helper'; |
|
|
|
import { Activity } from '@ghostfolio/common/interfaces'; |
|
|
|
|
|
|
|
import { HttpClient } from '@angular/common/http'; |
|
|
|
import { Injectable } from '@angular/core'; |
|
|
|
import { inject, Injectable } from '@angular/core'; |
|
|
|
import { Account, DataSource, Type as ActivityType } from '@prisma/client'; |
|
|
|
import { isFinite } from 'lodash'; |
|
|
|
import { isFinite, isNumber, isString } from 'lodash'; |
|
|
|
import { parse as csvToJson } from 'papaparse'; |
|
|
|
import { EMPTY } from 'rxjs'; |
|
|
|
import { catchError } from 'rxjs/operators'; |
|
|
|
import { firstValueFrom } from 'rxjs'; |
|
|
|
|
|
|
|
@Injectable({ |
|
|
|
providedIn: 'root' |
|
|
|
@ -35,7 +34,7 @@ export class ImportActivitiesService { |
|
|
|
'value' |
|
|
|
]; |
|
|
|
|
|
|
|
public constructor(private http: HttpClient) {} |
|
|
|
private readonly http = inject(HttpClient); |
|
|
|
|
|
|
|
public async importCsv({ |
|
|
|
fileContent, |
|
|
|
@ -49,7 +48,7 @@ export class ImportActivitiesService { |
|
|
|
activities: Activity[]; |
|
|
|
assetProfiles: CreateAssetProfileWithMarketDataDto[]; |
|
|
|
}> { |
|
|
|
const content = csvToJson(fileContent, { |
|
|
|
const content = csvToJson<Record<string, unknown>>(fileContent, { |
|
|
|
dynamicTyping: true, |
|
|
|
header: true, |
|
|
|
skipEmptyLines: true |
|
|
|
@ -83,22 +82,22 @@ export class ImportActivitiesService { |
|
|
|
assetProfiles.push({ |
|
|
|
currency, |
|
|
|
symbol, |
|
|
|
assetClass: null, |
|
|
|
assetSubClass: null, |
|
|
|
comment: null, |
|
|
|
assetClass: undefined, |
|
|
|
assetSubClass: undefined, |
|
|
|
comment: undefined, |
|
|
|
countries: [], |
|
|
|
cusip: null, |
|
|
|
cusip: undefined, |
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
figi: null, |
|
|
|
figiComposite: null, |
|
|
|
figiShareClass: null, |
|
|
|
figi: undefined, |
|
|
|
figiComposite: undefined, |
|
|
|
figiShareClass: undefined, |
|
|
|
holdings: [], |
|
|
|
isActive: true, |
|
|
|
isin: null, |
|
|
|
isin: undefined, |
|
|
|
marketData: [], |
|
|
|
name: symbol, |
|
|
|
sectors: [], |
|
|
|
url: null |
|
|
|
url: undefined |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -126,7 +125,7 @@ export class ImportActivitiesService { |
|
|
|
}): Promise<{ |
|
|
|
activities: Activity[]; |
|
|
|
}> { |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
return firstValueFrom( |
|
|
|
this.postImport( |
|
|
|
{ |
|
|
|
accounts, |
|
|
|
@ -136,18 +135,7 @@ export class ImportActivitiesService { |
|
|
|
}, |
|
|
|
isDryRun |
|
|
|
) |
|
|
|
.pipe( |
|
|
|
catchError((error) => { |
|
|
|
reject(error); |
|
|
|
return EMPTY; |
|
|
|
}) |
|
|
|
) |
|
|
|
.subscribe({ |
|
|
|
next: (data) => { |
|
|
|
resolve(data); |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
public importSelectedActivities({ |
|
|
|
@ -163,11 +151,9 @@ export class ImportActivitiesService { |
|
|
|
}): Promise<{ |
|
|
|
activities: Activity[]; |
|
|
|
}> { |
|
|
|
const importData: CreateOrderDto[] = []; |
|
|
|
|
|
|
|
for (const activity of activities) { |
|
|
|
importData.push(this.convertToCreateOrderDto(activity)); |
|
|
|
} |
|
|
|
const importData = activities.map((activity) => |
|
|
|
this.convertToCreateOrderDto(activity) |
|
|
|
); |
|
|
|
|
|
|
|
return this.importJson({ |
|
|
|
accounts, |
|
|
|
@ -191,14 +177,14 @@ export class ImportActivitiesService { |
|
|
|
updateAccountBalance |
|
|
|
}: Activity): CreateOrderDto { |
|
|
|
return { |
|
|
|
accountId, |
|
|
|
comment, |
|
|
|
fee, |
|
|
|
quantity, |
|
|
|
type, |
|
|
|
unitPrice, |
|
|
|
updateAccountBalance, |
|
|
|
currency: currency ?? SymbolProfile.currency, |
|
|
|
accountId: accountId ?? undefined, |
|
|
|
comment: comment ?? undefined, |
|
|
|
currency: currency ?? SymbolProfile.currency ?? '', |
|
|
|
dataSource: SymbolProfile.dataSource, |
|
|
|
date: date.toString(), |
|
|
|
symbol: SymbolProfile.symbol, |
|
|
|
@ -208,28 +194,32 @@ export class ImportActivitiesService { |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private lowercaseKeys(aObject: any) { |
|
|
|
return Object.keys(aObject).reduce((acc, key) => { |
|
|
|
acc[key.toLowerCase()] = aObject[key]; |
|
|
|
return acc; |
|
|
|
}, {}); |
|
|
|
private lowercaseKeys( |
|
|
|
aObject: Record<string, unknown> |
|
|
|
): Record<string, unknown> { |
|
|
|
return Object.fromEntries( |
|
|
|
Object.entries(aObject).map(([key, val]) => { |
|
|
|
return [key.toLowerCase(), val]; |
|
|
|
}) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
private parseAccount({ |
|
|
|
item, |
|
|
|
userAccounts |
|
|
|
}: { |
|
|
|
item: any; |
|
|
|
item: Record<string, unknown>; |
|
|
|
userAccounts: Account[]; |
|
|
|
}) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.ACCOUNT_KEYS) { |
|
|
|
if (item[key]) { |
|
|
|
return userAccounts.find((account) => { |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isNumber(value) || isString(value)) { |
|
|
|
return userAccounts.find(({ id, name }) => { |
|
|
|
return ( |
|
|
|
account.id === item[key] || |
|
|
|
account.name.toLowerCase() === item[key].toLowerCase() |
|
|
|
id === value || name?.toLowerCase() === String(value).toLowerCase() |
|
|
|
); |
|
|
|
})?.id; |
|
|
|
} |
|
|
|
@ -238,12 +228,14 @@ export class ImportActivitiesService { |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
|
|
|
|
private parseComment({ item }: { item: any }) { |
|
|
|
private parseComment({ item }: { item: Record<string, unknown> }) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.COMMENT_KEYS) { |
|
|
|
if (item[key]) { |
|
|
|
return item[key]; |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isNumber(value) || isString(value)) { |
|
|
|
return String(value); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -255,15 +247,17 @@ export class ImportActivitiesService { |
|
|
|
index, |
|
|
|
item |
|
|
|
}: { |
|
|
|
content: any[]; |
|
|
|
content: Record<string, unknown>[]; |
|
|
|
index: number; |
|
|
|
item: any; |
|
|
|
item: Record<string, unknown>; |
|
|
|
}) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.CURRENCY_KEYS) { |
|
|
|
if (item[key]) { |
|
|
|
return item[key]; |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isString(value)) { |
|
|
|
return value; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -273,12 +267,14 @@ export class ImportActivitiesService { |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private parseDataSource({ item }: { item: any }) { |
|
|
|
private parseDataSource({ item }: { item: Record<string, unknown> }) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.DATA_SOURCE_KEYS) { |
|
|
|
if (item[key]) { |
|
|
|
return DataSource[item[key].toUpperCase()]; |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isString(value)) { |
|
|
|
return DataSource[value.toUpperCase() as keyof typeof DataSource]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -290,16 +286,22 @@ export class ImportActivitiesService { |
|
|
|
index, |
|
|
|
item |
|
|
|
}: { |
|
|
|
content: any[]; |
|
|
|
content: Record<string, unknown>[]; |
|
|
|
index: number; |
|
|
|
item: any; |
|
|
|
item: Record<string, unknown>; |
|
|
|
}) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.DATE_KEYS) { |
|
|
|
if (item[key]) { |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isNumber(value) || isString(value)) { |
|
|
|
try { |
|
|
|
return parseDateHelper(item[key].toString()).toISOString(); |
|
|
|
const parsedDate = parseDateHelper(String(value)); |
|
|
|
|
|
|
|
if (parsedDate) { |
|
|
|
return parsedDate.toISOString(); |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -315,15 +317,17 @@ export class ImportActivitiesService { |
|
|
|
index, |
|
|
|
item |
|
|
|
}: { |
|
|
|
content: any[]; |
|
|
|
content: Record<string, unknown>[]; |
|
|
|
index: number; |
|
|
|
item: any; |
|
|
|
item: Record<string, unknown>; |
|
|
|
}) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.FEE_KEYS) { |
|
|
|
if (isFinite(item[key])) { |
|
|
|
return Math.abs(item[key]); |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isNumber(value) && isFinite(value)) { |
|
|
|
return Math.abs(value); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -338,15 +342,17 @@ export class ImportActivitiesService { |
|
|
|
index, |
|
|
|
item |
|
|
|
}: { |
|
|
|
content: any[]; |
|
|
|
content: Record<string, unknown>[]; |
|
|
|
index: number; |
|
|
|
item: any; |
|
|
|
item: Record<string, unknown>; |
|
|
|
}) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.QUANTITY_KEYS) { |
|
|
|
if (isFinite(item[key])) { |
|
|
|
return Math.abs(item[key]); |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isNumber(value) && isFinite(value)) { |
|
|
|
return Math.abs(value); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -361,15 +367,17 @@ export class ImportActivitiesService { |
|
|
|
index, |
|
|
|
item |
|
|
|
}: { |
|
|
|
content: any[]; |
|
|
|
content: Record<string, unknown>[]; |
|
|
|
index: number; |
|
|
|
item: any; |
|
|
|
item: Record<string, unknown>; |
|
|
|
}) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.SYMBOL_KEYS) { |
|
|
|
if (item[key]) { |
|
|
|
return item[key]; |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isNumber(value) || isString(value)) { |
|
|
|
return String(value); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -384,15 +392,17 @@ export class ImportActivitiesService { |
|
|
|
index, |
|
|
|
item |
|
|
|
}: { |
|
|
|
content: any[]; |
|
|
|
content: Record<string, unknown>[]; |
|
|
|
index: number; |
|
|
|
item: any; |
|
|
|
item: Record<string, unknown>; |
|
|
|
}): ActivityType { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.TYPE_KEYS) { |
|
|
|
if (item[key]) { |
|
|
|
switch (item[key].toLowerCase()) { |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isString(value)) { |
|
|
|
switch (value.toLowerCase()) { |
|
|
|
case 'buy': |
|
|
|
return 'BUY'; |
|
|
|
case 'dividend': |
|
|
|
@ -422,15 +432,17 @@ export class ImportActivitiesService { |
|
|
|
index, |
|
|
|
item |
|
|
|
}: { |
|
|
|
content: any[]; |
|
|
|
content: Record<string, unknown>[]; |
|
|
|
index: number; |
|
|
|
item: any; |
|
|
|
item: Record<string, unknown>; |
|
|
|
}) { |
|
|
|
item = this.lowercaseKeys(item); |
|
|
|
|
|
|
|
for (const key of ImportActivitiesService.UNIT_PRICE_KEYS) { |
|
|
|
if (isFinite(item[key])) { |
|
|
|
return Math.abs(item[key]); |
|
|
|
const value = item[key]; |
|
|
|
|
|
|
|
if (isNumber(value) && isFinite(value)) { |
|
|
|
return Math.abs(value); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|