Browse Source

activities import extended by tags

pull/5287/head
Attila Cseh 3 weeks ago
parent
commit
bde7f8f55c
  1. 4
      apps/api/src/app/endpoints/tags/create-tag.dto.ts
  2. 7
      apps/api/src/app/import/import-data.dto.ts
  3. 1
      apps/api/src/app/import/import.controller.ts
  4. 2
      apps/api/src/app/import/import.module.ts
  5. 101
      apps/api/src/app/import/import.service.ts
  6. 10
      apps/api/src/app/order/create-order.dto.ts
  7. 1
      apps/api/src/app/order/interfaces/activities.interface.ts
  8. 6
      apps/api/src/app/order/order.controller.ts
  9. 12
      apps/api/src/app/order/order.service.ts
  10. 10
      apps/api/src/app/order/update-order.dto.ts
  11. 4
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  12. 7
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  13. 22
      apps/client/src/app/services/import-activities.service.ts

4
apps/api/src/app/endpoints/tags/create-tag.dto.ts

@ -1,6 +1,10 @@
import { IsOptional, IsString } from 'class-validator';
export class CreateTagDto {
@IsOptional()
@IsString()
id?: string;
@IsString()
name: string;

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

@ -3,6 +3,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Type } from 'class-transformer';
import { IsArray, IsOptional, ValidateNested } from 'class-validator';
import { CreateTagDto } from '../endpoints/tags/create-tag.dto';
import { CreateAccountWithBalancesDto } from './create-account-with-balances.dto';
import { CreateAssetProfileWithMarketDataDto } from './create-asset-profile-with-market-data.dto';
@ -23,4 +24,10 @@ export class ImportDataDto {
@Type(() => CreateAssetProfileWithMarketDataDto)
@ValidateNested({ each: true })
assetProfiles?: CreateAssetProfileWithMarketDataDto[];
@IsArray()
@IsOptional()
@Type(() => CreateTagDto)
@ValidateNested({ each: true })
tags?: CreateTagDto[];
}

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

@ -74,6 +74,7 @@ export class ImportController {
accountsWithBalancesDto: importData.accounts ?? [],
activitiesDto: importData.activities,
assetProfilesWithMarketDataDto: importData.assetProfiles ?? [],
tagsDto: importData.tags ?? [],
user: this.request.user
});

2
apps/api/src/app/import/import.module.ts

@ -13,6 +13,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
import { Module } from '@nestjs/common';
@ -35,6 +36,7 @@ import { ImportService } from './import.service';
PrismaModule,
RedisCacheModule,
SymbolProfileModule,
TagModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],

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

@ -13,12 +13,14 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
import {
getAssetProfileIdentifier,
parseDate
} from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import {
AccountWithPlatform,
OrderWithAccount,
@ -46,7 +48,8 @@ export class ImportService {
private readonly orderService: OrderService,
private readonly platformService: PlatformService,
private readonly portfolioService: PortfolioService,
private readonly symbolProfileService: SymbolProfileService
private readonly symbolProfileService: SymbolProfileService,
private readonly tagService: TagService
) {}
public async getDividends({
@ -152,6 +155,7 @@ export class ImportService {
accountsWithBalancesDto,
activitiesDto,
assetProfilesWithMarketDataDto,
tagsDto,
isDryRun = false,
maxActivitiesToImport,
user
@ -159,12 +163,14 @@ export class ImportService {
accountsWithBalancesDto: ImportDataDto['accounts'];
activitiesDto: ImportDataDto['activities'];
assetProfilesWithMarketDataDto: ImportDataDto['assetProfiles'];
tagsDto: ImportDataDto['tags'];
isDryRun?: boolean;
maxActivitiesToImport: number;
user: UserWithSettings;
}): Promise<Activity[]> {
const accountIdMapping: { [oldAccountId: string]: string } = {};
const assetProfileSymbolMapping: { [oldSymbol: string]: string } = {};
const tagIdMapping: { [oldTagId: string]: string } = {};
const userCurrency = user.settings.settings.baseCurrency;
if (!isDryRun && accountsWithBalancesDto?.length) {
@ -293,6 +299,57 @@ export class ImportService {
}
}
if (tagsDto?.length) {
const existingTags = await this.tagService.getTagsForUser(user.id);
for (const tag of tagsDto) {
// Check if there is any existing tag
const existingTag = existingTags.find(({ name }) => {
return name === tag.name;
});
// If there is no tag or if the tag belongs to a different user, then create a new tag
if (
!existingTag ||
(existingTag.userId && existingTag.userId !== user.id)
) {
const canCreateTag = hasPermission(
user.permissions,
permissions.createTag
);
const canCreateOwnTag = hasPermission(
user.permissions,
permissions.createOwnTag
);
if (!canCreateTag && !canCreateOwnTag) {
throw new Error('User does not have permission to create tags');
}
if (!isDryRun) {
let oldTagId: string;
if (existingTag) {
oldTagId = tag.id;
delete tag.id;
}
const tagObject: Prisma.TagCreateInput = {
...tag,
user: { connect: { id: user.id } }
};
const newTag = await this.tagService.createTag(tagObject);
// Store the new to old tag ID mappings for updating activities
if (existingTag && oldTagId) {
tagIdMapping[oldTagId] = newTag.id;
}
}
}
}
}
for (const activity of activitiesDto) {
if (!activity.dataSource) {
if (['FEE', 'INTEREST', 'LIABILITY'].includes(activity.type)) {
@ -313,6 +370,11 @@ export class ImportService {
if (assetProfileSymbolMapping[activity.symbol]) {
activity.symbol = assetProfileSymbolMapping[activity.symbol];
}
// If a new tag is created, then update the tag ID in all activities
activity.tags = (activity.tags ?? []).map((tagId) => {
return tagIdMapping[tagId] ?? tagId;
});
}
}
@ -340,6 +402,24 @@ export class ImportService {
});
}
const tags = (await this.tagService.getTagsForUser(user.id)).map(
({ id, name }) => {
return { id, name };
}
);
if (isDryRun) {
tagsDto
.filter(({ id }) => {
return !tags.some(({ id: tagId }) => {
return tagId === id;
});
})
.forEach(({ id, name }) => {
tags.push({ id, name });
});
}
const activities: Activity[] = [];
for (const activity of activitiesExtendedWithErrors) {
@ -351,6 +431,7 @@ export class ImportService {
const fee = activity.fee;
const quantity = activity.quantity;
const SymbolProfile = activity.SymbolProfile;
const tagIds = activity.tagIds ?? [];
const type = activity.type;
const unitPrice = activity.unitPrice;
@ -388,11 +469,17 @@ export class ImportService {
const validatedAccount = accounts.find(({ id }) => {
return id === accountId;
});
const validatedTags = tags.filter(({ id: tagId }) => {
return tagIds.some((activityTagId) => {
return activityTagId === tagId;
});
});
let order:
| OrderWithAccount
| (Omit<OrderWithAccount, 'Account'> & {
Account?: { id: string; name: string };
| (Omit<OrderWithAccount, 'account' | 'tags'> & {
account?: { id: string; name: string };
tags?: { id: string; name: string }[];
});
if (isDryRun) {
@ -404,7 +491,7 @@ export class ImportService {
quantity,
type,
unitPrice,
Account: validatedAccount,
account: validatedAccount,
accountId: validatedAccount?.id,
accountUserId: undefined,
createdAt: new Date(),
@ -436,6 +523,7 @@ export class ImportService {
userId: dataSource === 'MANUAL' ? user.id : undefined
},
symbolProfileId: undefined,
tags: validatedTags,
updatedAt: new Date(),
userId: user.id
};
@ -469,6 +557,9 @@ export class ImportService {
}
}
},
tags: validatedTags.map(({ id }) => {
return { id };
}),
updateAccountBalance: false,
user: { connect: { id: user.id } },
userId: user.id
@ -546,6 +637,7 @@ export class ImportService {
fee,
quantity,
symbol,
tags,
type,
unitPrice
}) => {
@ -578,6 +670,7 @@ export class ImportService {
error,
fee,
quantity,
tagIds: tags,
type,
unitPrice,
SymbolProfile: {

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

@ -1,13 +1,7 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
import {
AssetClass,
AssetSubClass,
DataSource,
Tag,
Type
} from '@prisma/client';
import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsArray,
@ -70,7 +64,7 @@ export class CreateOrderDto {
@IsArray()
@IsOptional()
tags?: Tag[];
tags?: string[];
@IsEnum(Type, { each: true })
type: Type;

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

@ -15,6 +15,7 @@ export interface Activity extends Order {
feeInBaseCurrency: number;
SymbolProfile?: EnhancedSymbolProfile;
tags?: Tag[];
tagIds?: string[];
unitPriceInAssetProfileCurrency: number;
updateAccountBalance?: boolean;
value: number;

6
apps/api/src/app/order/order.controller.ts

@ -217,6 +217,9 @@ export class OrderController {
}
}
},
tags: data.tags?.map((id) => {
return { id };
}),
user: { connect: { id: this.request.user.id } },
userId: this.request.user.id
});
@ -293,6 +296,9 @@ export class OrderController {
name: data.symbol
}
},
tags: data.tags?.map((id) => {
return { id };
}),
user: { connect: { id: this.request.user.id } }
},
where: {

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

@ -97,7 +97,7 @@ export class OrderService {
assetSubClass?: AssetSubClass;
currency?: string;
symbol?: string;
tags?: Tag[];
tags?: { id: string }[];
updateAccountBalance?: boolean;
userId: string;
}
@ -201,9 +201,7 @@ export class OrderService {
account,
isDraft,
tags: {
connect: tags.map(({ id }) => {
return { id };
})
connect: tags
}
},
include: { SymbolProfile: true }
@ -658,7 +656,7 @@ export class OrderService {
assetSubClass?: AssetSubClass;
currency?: string;
symbol?: string;
tags?: Tag[];
tags?: { id: string }[];
type?: ActivityType;
};
where: Prisma.OrderWhereUniqueInput;
@ -720,9 +718,7 @@ export class OrderService {
...data,
isDraft,
tags: {
connect: tags.map(({ id }) => {
return { id };
})
connect: tags
}
}
});

10
apps/api/src/app/order/update-order.dto.ts

@ -1,13 +1,7 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
import {
AssetClass,
AssetSubClass,
DataSource,
Tag,
Type
} from '@prisma/client';
import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsArray,
@ -71,7 +65,7 @@ export class UpdateOrderDto {
@IsArray()
@IsOptional()
tags?: Tag[];
tags?: string[];
@IsString()
type: Type;

4
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -495,7 +495,9 @@ export class GfCreateOrUpdateActivityDialog implements OnDestroy {
? undefined
: this.activityForm.get('searchSymbol')?.value?.symbol) ??
this.activityForm.get('name')?.value,
tags: this.activityForm.get('tags').value,
tags: this.activityForm.get('tags').value?.map(({ id }) => {
return id;
}),
type: this.activityForm.get('type').value,
unitPrice: this.activityForm.get('unitPrice').value
};

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

@ -1,3 +1,4 @@
import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
import { CreateAccountWithBalancesDto } from '@ghostfolio/api/app/import/create-account-with-balances.dto';
import { CreateAssetProfileWithMarketDataDto } from '@ghostfolio/api/app/import/create-asset-profile-with-market-data.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
@ -80,6 +81,7 @@ export class GfImportActivitiesDialog implements OnDestroy {
public activities: Activity[] = [];
public assetProfileForm: FormGroup;
public assetProfiles: CreateAssetProfileWithMarketDataDto[] = [];
public tags: CreateTagDto[] = [];
public dataSource: MatTableDataSource<Activity>;
public details: any[] = [];
public deviceType: string;
@ -169,7 +171,8 @@ export class GfImportActivitiesDialog implements OnDestroy {
await this.importActivitiesService.importSelectedActivities({
accounts: this.accounts,
activities: this.selectedActivities,
assetProfiles: this.assetProfiles
assetProfiles: this.assetProfiles,
tags: this.tags
});
this.snackBar.open(
@ -297,6 +300,7 @@ export class GfImportActivitiesDialog implements OnDestroy {
this.accounts = content.accounts;
this.assetProfiles = content.assetProfiles;
this.tags = content.tags;
if (!isArray(content.activities)) {
if (isArray(content.orders)) {
@ -328,6 +332,7 @@ export class GfImportActivitiesDialog implements OnDestroy {
accounts: content.accounts,
activities: content.activities,
assetProfiles: content.assetProfiles,
tags: content.tags,
isDryRun: true
});
this.activities = activities;

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

@ -1,3 +1,4 @@
import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
import { CreateAccountWithBalancesDto } from '@ghostfolio/api/app/import/create-account-with-balances.dto';
import { CreateAssetProfileWithMarketDataDto } from '@ghostfolio/api/app/import/create-asset-profile-with-market-data.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
@ -75,11 +76,13 @@ export class ImportActivitiesService {
accounts,
activities,
assetProfiles,
tags,
isDryRun = false
}: {
activities: CreateOrderDto[];
accounts?: CreateAccountWithBalancesDto[];
assetProfiles?: CreateAssetProfileWithMarketDataDto[];
tags?: CreateTagDto[];
isDryRun?: boolean;
}): Promise<{
activities: Activity[];
@ -89,7 +92,8 @@ export class ImportActivitiesService {
{
accounts,
activities,
assetProfiles
assetProfiles,
tags
},
isDryRun
)
@ -110,11 +114,13 @@ export class ImportActivitiesService {
public importSelectedActivities({
accounts,
activities,
assetProfiles
assetProfiles,
tags
}: {
accounts?: CreateAccountWithBalancesDto[];
activities: Activity[];
assetProfiles?: CreateAssetProfileWithMarketDataDto[];
tags?: CreateTagDto[];
}): Promise<{
activities: Activity[];
}> {
@ -124,7 +130,12 @@ export class ImportActivitiesService {
importData.push(this.convertToCreateOrderDto(activity));
}
return this.importJson({ accounts, assetProfiles, activities: importData });
return this.importJson({
accounts,
assetProfiles,
tags,
activities: importData
});
}
private convertToCreateOrderDto({
@ -135,6 +146,7 @@ export class ImportActivitiesService {
fee,
quantity,
SymbolProfile,
tags,
type,
unitPrice,
updateAccountBalance
@ -150,7 +162,8 @@ export class ImportActivitiesService {
currency: currency ?? SymbolProfile.currency,
dataSource: SymbolProfile.dataSource,
date: date.toString(),
symbol: SymbolProfile.symbol
symbol: SymbolProfile.symbol,
tags: tags?.map(({ id }) => id)
};
}
@ -391,6 +404,7 @@ export class ImportActivitiesService {
accounts?: CreateAccountWithBalancesDto[];
activities: CreateOrderDto[];
assetProfiles?: CreateAssetProfileWithMarketDataDto[];
tags?: CreateTagDto[];
},
aIsDryRun = false
) {

Loading…
Cancel
Save