mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
6.4 KiB
258 lines
6.4 KiB
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
|
import { environment } from '@ghostfolio/api/environments/environment';
|
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
|
import {
|
|
ExportResponse,
|
|
Filter,
|
|
UserSettings
|
|
} from '@ghostfolio/common/interfaces';
|
|
|
|
import { Injectable } from '@nestjs/common';
|
|
import { Platform, Prisma } from '@prisma/client';
|
|
import { groupBy, uniqBy } from 'lodash';
|
|
|
|
@Injectable()
|
|
export class ExportService {
|
|
public constructor(
|
|
private readonly accountService: AccountService,
|
|
private readonly marketDataService: MarketDataService,
|
|
private readonly orderService: OrderService,
|
|
private readonly tagService: TagService
|
|
) {}
|
|
|
|
public async export({
|
|
activityIds,
|
|
filters,
|
|
userId,
|
|
userSettings
|
|
}: {
|
|
activityIds?: string[];
|
|
filters?: Filter[];
|
|
userId: string;
|
|
userSettings: UserSettings;
|
|
}): Promise<ExportResponse> {
|
|
const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
|
|
return type;
|
|
});
|
|
const platformsMap: { [platformId: string]: Platform } = {};
|
|
|
|
let { activities } = await this.orderService.getOrders({
|
|
filters,
|
|
userId,
|
|
includeDrafts: true,
|
|
sortColumn: 'date',
|
|
sortDirection: 'asc',
|
|
userCurrency: userSettings?.baseCurrency,
|
|
withExcludedAccountsAndActivities: true
|
|
});
|
|
|
|
if (activityIds?.length > 0) {
|
|
activities = activities.filter(({ id }) => {
|
|
return activityIds.includes(id);
|
|
});
|
|
}
|
|
|
|
const where: Prisma.AccountWhereInput = { userId };
|
|
|
|
if (filtersByAccount?.length > 0) {
|
|
where.id = {
|
|
in: filtersByAccount.map(({ id }) => {
|
|
return id;
|
|
})
|
|
};
|
|
}
|
|
|
|
const accounts = (
|
|
await this.accountService.accounts({
|
|
where,
|
|
include: {
|
|
balances: true,
|
|
platform: true
|
|
},
|
|
orderBy: {
|
|
name: 'asc'
|
|
}
|
|
})
|
|
)
|
|
.filter(({ id }) => {
|
|
return activityIds?.length > 0
|
|
? activities.some(({ accountId }) => {
|
|
return accountId === id;
|
|
})
|
|
: true;
|
|
})
|
|
.map(
|
|
({
|
|
balance,
|
|
balances,
|
|
comment,
|
|
currency,
|
|
id,
|
|
isExcluded,
|
|
name,
|
|
platform,
|
|
platformId
|
|
}) => {
|
|
if (platformId) {
|
|
platformsMap[platformId] = platform;
|
|
}
|
|
|
|
return {
|
|
balance,
|
|
balances: balances.map(({ date, value }) => {
|
|
return { date: date.toISOString(), value };
|
|
}),
|
|
comment,
|
|
currency,
|
|
id,
|
|
isExcluded,
|
|
name,
|
|
platformId
|
|
};
|
|
}
|
|
);
|
|
|
|
const customAssetProfiles = uniqBy(
|
|
activities
|
|
.map(({ SymbolProfile }) => {
|
|
return SymbolProfile;
|
|
})
|
|
.filter(({ userId: assetProfileUserId }) => {
|
|
return assetProfileUserId === userId;
|
|
}),
|
|
({ id }) => {
|
|
return id;
|
|
}
|
|
);
|
|
|
|
const marketDataByAssetProfile = Object.fromEntries(
|
|
await Promise.all(
|
|
customAssetProfiles.map(async ({ dataSource, id, symbol }) => {
|
|
const marketData = (
|
|
await this.marketDataService.marketDataItems({
|
|
where: { dataSource, symbol }
|
|
})
|
|
).map(({ date, marketPrice }) => ({
|
|
date: date.toISOString(),
|
|
marketPrice
|
|
}));
|
|
|
|
return [id, marketData] as const;
|
|
})
|
|
)
|
|
);
|
|
|
|
const tags = (await this.tagService.getTagsForUser(userId))
|
|
.filter(({ id, isUsed }) => {
|
|
return (
|
|
isUsed &&
|
|
activities.some((activity) => {
|
|
return activity.tags.some(({ id: tagId }) => {
|
|
return tagId === id;
|
|
});
|
|
})
|
|
);
|
|
})
|
|
.map(({ id, name }) => {
|
|
return {
|
|
id,
|
|
name
|
|
};
|
|
});
|
|
|
|
return {
|
|
meta: { date: new Date().toISOString(), version: environment.version },
|
|
accounts,
|
|
assetProfiles: customAssetProfiles.map(
|
|
({
|
|
assetClass,
|
|
assetSubClass,
|
|
comment,
|
|
countries,
|
|
currency,
|
|
cusip,
|
|
dataSource,
|
|
figi,
|
|
figiComposite,
|
|
figiShareClass,
|
|
holdings,
|
|
id,
|
|
isActive,
|
|
isin,
|
|
name,
|
|
scraperConfiguration,
|
|
sectors,
|
|
symbol,
|
|
symbolMapping,
|
|
url
|
|
}) => {
|
|
return {
|
|
assetClass,
|
|
assetSubClass,
|
|
comment,
|
|
countries: countries as unknown as Prisma.JsonArray,
|
|
currency,
|
|
cusip,
|
|
dataSource,
|
|
figi,
|
|
figiComposite,
|
|
figiShareClass,
|
|
holdings: holdings as unknown as Prisma.JsonArray,
|
|
isActive,
|
|
isin,
|
|
marketData: marketDataByAssetProfile[id],
|
|
name,
|
|
scraperConfiguration:
|
|
scraperConfiguration as unknown as Prisma.JsonArray,
|
|
sectors: sectors as unknown as Prisma.JsonArray,
|
|
symbol,
|
|
symbolMapping,
|
|
url
|
|
};
|
|
}
|
|
),
|
|
platforms: Object.values(platformsMap),
|
|
tags,
|
|
activities: activities.map(
|
|
({
|
|
accountId,
|
|
comment,
|
|
currency,
|
|
date,
|
|
fee,
|
|
id,
|
|
quantity,
|
|
SymbolProfile,
|
|
tags: currentTags,
|
|
type,
|
|
unitPrice
|
|
}) => {
|
|
return {
|
|
accountId,
|
|
comment,
|
|
fee,
|
|
id,
|
|
quantity,
|
|
type,
|
|
unitPrice,
|
|
currency: currency ?? SymbolProfile.currency,
|
|
dataSource: SymbolProfile.dataSource,
|
|
date: date.toISOString(),
|
|
symbol: SymbolProfile.symbol,
|
|
tags: currentTags.map(({ id: tagId }) => {
|
|
return tagId;
|
|
})
|
|
};
|
|
}
|
|
),
|
|
user: {
|
|
settings: {
|
|
currency: userSettings?.baseCurrency,
|
|
performanceCalculationType: userSettings?.performanceCalculationType
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|