diff --git a/apps/api/src/app/order/create-order.dto.ts b/apps/api/src/app/order/create-order.dto.ts index aecec842a..0023eab41 100644 --- a/apps/api/src/app/order/create-order.dto.ts +++ b/apps/api/src/app/order/create-order.dto.ts @@ -15,10 +15,33 @@ import { IsNumber, IsOptional, IsString, - Min + Min, + ValidationArguments, + ValidationOptions, + registerDecorator } from 'class-validator'; import { isString } from 'lodash'; +function IsQuantityValid(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isQuantityValid', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + const order = args.object as CreateOrderDto; + return order.type !== 'SPLIT' ? value >= 0 : true; + }, + defaultMessage(args: ValidationArguments) { + return `Quantity must not be less than 0 unless the type is ${Type.SPLIT}`; + } + } + }); + }; +} + export class CreateOrderDto { @IsOptional() @IsString() @@ -54,7 +77,7 @@ export class CreateOrderDto { fee: number; @IsNumber() - @Min(0) + @IsQuantityValid() quantity: number; @IsString() diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index 6ea93a670..441779289 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -792,9 +792,12 @@ export class PortfolioCalculator { if (oldAccumulatedSymbol) { let investment = oldAccumulatedSymbol.investment; - const newQuantity = quantity - .mul(factor) - .plus(oldAccumulatedSymbol.quantity); + const newQuantity = + type === 'SPLIT' + ? quantity.s === 1 + ? oldAccumulatedSymbol.quantity.mul(quantity) + : oldAccumulatedSymbol.quantity.div(quantity.abs()) + : quantity.mul(factor).plus(oldAccumulatedSymbol.quantity); if (type === 'BUY') { investment = oldAccumulatedSymbol.investment.plus( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 8384427c3..7a15d898c 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -724,7 +724,9 @@ export class PortfolioService { activities: orders.filter((order) => { tags = tags.concat(order.tags); - return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); + return ['BUY', 'DIVIDEND', 'ITEM', 'SELL', 'SPLIT'].includes( + order.type + ); }), currency: userCurrency, currentRateService: this.currentRateService, diff --git a/apps/api/src/helper/portfolio.helper.ts b/apps/api/src/helper/portfolio.helper.ts index 730f34bde..d3d3856af 100644 --- a/apps/api/src/helper/portfolio.helper.ts +++ b/apps/api/src/helper/portfolio.helper.ts @@ -19,6 +19,7 @@ export function getFactor(activityType: ActivityType) { switch (activityType) { case 'BUY': case 'ITEM': + case 'SPLIT': factor = 1; break; case 'LIABILITY': diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index b628aba46..4cc405505 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -279,7 +279,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { if (this.activityForm.controls['searchSymbol'].invalid) { this.data.activity.SymbolProfile = null; } else if ( - ['BUY', 'DIVIDEND', 'SELL'].includes( + ['BUY', 'DIVIDEND', 'SELL', 'SPLIT'].includes( this.activityForm.controls['type'].value ) ) { @@ -394,6 +394,50 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.activityForm.controls['updateAccountBalance'].disable(); this.activityForm.controls['updateAccountBalance'].setValue(false); } + } else if (type === 'SPLIT') { + this.activityForm.controls['currency'].removeValidators( + Validators.required + ); + this.activityForm.controls['currency'].updateValueAndValidity(); + + this.activityForm.controls['currencyOfFee'].removeValidators( + Validators.required + ); + this.activityForm.controls['currencyOfFee'].updateValueAndValidity(); + + this.activityForm.controls['currencyOfUnitPrice'].removeValidators( + Validators.required + ); + this.activityForm.controls[ + 'currencyOfUnitPrice' + ].updateValueAndValidity(); + + this.activityForm.controls['fee'].removeValidators( + Validators.required + ); + this.activityForm.controls['fee'].updateValueAndValidity(); + + this.activityForm.controls['feeInCustomCurrency'].removeValidators( + Validators.required + ); + this.activityForm.controls[ + 'feeInCustomCurrency' + ].updateValueAndValidity(); + + this.activityForm.controls['unitPrice'].removeValidators( + Validators.required + ); + this.activityForm.controls['unitPrice'].updateValueAndValidity(); + + this.activityForm.controls[ + 'unitPriceInCustomCurrency' + ].removeValidators(Validators.required); + this.activityForm.controls[ + 'unitPriceInCustomCurrency' + ].updateValueAndValidity(); + + this.activityForm.controls['updateAccountBalance'].disable(); + this.activityForm.controls['updateAccountBalance'].setValue(false); } else { this.activityForm.controls['accountId'].setValidators( Validators.required diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 81c41a316..87ffc2e18 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -70,6 +70,14 @@ >Luxury items, real estate, private companies + + {{ typesTranslationMap['SPLIT'] }} + Forward split, reverse split + @@ -180,13 +188,22 @@ }" > - Quantity + + Ratio + Quantity + +
@@ -279,7 +296,8 @@ 'd-none': activityForm.controls['type']?.value === 'INTEREST' || activityForm.controls['type']?.value === 'ITEM' || - activityForm.controls['type']?.value === 'LIABILITY' + activityForm.controls['type']?.value === 'LIABILITY' || + activityForm.controls['type']?.value === 'SPLIT' }" > diff --git a/libs/ui/src/lib/activity-type/activity-type.component.html b/libs/ui/src/lib/activity-type/activity-type.component.html index 78eb2f17b..5c2abad2b 100644 --- a/libs/ui/src/lib/activity-type/activity-type.component.html +++ b/libs/ui/src/lib/activity-type/activity-type.component.html @@ -7,7 +7,8 @@ interest: activityType === 'INTEREST', item: activityType === 'ITEM', liability: activityType === 'LIABILITY', - sell: activityType === 'SELL' + sell: activityType === 'SELL', + split: activityType === 'SPLIT' }" > @if (activityType === 'BUY') { @@ -22,6 +23,8 @@ } @else if (activityType === 'SELL') { + } @else if (activityType === 'SPLIT') { + } {{ activityTypeLabel }}
diff --git a/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts index 9687f461c..c5af1b6e0 100644 --- a/libs/ui/src/lib/i18n.ts +++ b/libs/ui/src/lib/i18n.ts @@ -34,6 +34,7 @@ const locales = { ITEM: $localize`Valuable`, LIABILITY: $localize`Liability`, SELL: $localize`Sell`, + SPLIT: $localize`Stock Split`, // AssetClass (enum) CASH: $localize`Cash`, diff --git a/prisma/migrations/20240329190053_added_split_to_order_type/migration.sql b/prisma/migrations/20240329190053_added_split_to_order_type/migration.sql new file mode 100644 index 000000000..618c241b8 --- /dev/null +++ b/prisma/migrations/20240329190053_added_split_to_order_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Type" ADD VALUE 'SPLIT'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0b40109e8..3c35e2110 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -301,6 +301,7 @@ enum Type { ITEM LIABILITY SELL + SPLIT } enum ViewMode {