Browse Source

Merge 74f0cfdb31 into 084467ee9a

pull/3211/merge
Fedron 1 year ago
committed by GitHub
parent
commit
d167bcfe31
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      apps/api/src/app/order/create-order.dto.ts
  2. 9
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  3. 4
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 1
      apps/api/src/helper/portfolio.helper.ts
  5. 46
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  6. 24
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  7. 5
      libs/ui/src/lib/activity-type/activity-type.component.html
  8. 1
      libs/ui/src/lib/i18n.ts
  9. 2
      prisma/migrations/20240329190053_added_split_to_order_type/migration.sql
  10. 1
      prisma/schema.prisma

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

@ -15,10 +15,33 @@ import {
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
Min Min,
ValidationArguments,
ValidationOptions,
registerDecorator
} from 'class-validator'; } from 'class-validator';
import { isString } from 'lodash'; 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 { export class CreateOrderDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
@ -54,7 +77,7 @@ export class CreateOrderDto {
fee: number; fee: number;
@IsNumber() @IsNumber()
@Min(0) @IsQuantityValid()
quantity: number; quantity: number;
@IsString() @IsString()

9
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -792,9 +792,12 @@ export class PortfolioCalculator {
if (oldAccumulatedSymbol) { if (oldAccumulatedSymbol) {
let investment = oldAccumulatedSymbol.investment; let investment = oldAccumulatedSymbol.investment;
const newQuantity = quantity const newQuantity =
.mul(factor) type === 'SPLIT'
.plus(oldAccumulatedSymbol.quantity); ? quantity.s === 1
? oldAccumulatedSymbol.quantity.mul(quantity)
: oldAccumulatedSymbol.quantity.div(quantity.abs())
: quantity.mul(factor).plus(oldAccumulatedSymbol.quantity);
if (type === 'BUY') { if (type === 'BUY') {
investment = oldAccumulatedSymbol.investment.plus( investment = oldAccumulatedSymbol.investment.plus(

4
apps/api/src/app/portfolio/portfolio.service.ts

@ -724,7 +724,9 @@ export class PortfolioService {
activities: orders.filter((order) => { activities: orders.filter((order) => {
tags = tags.concat(order.tags); tags = tags.concat(order.tags);
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); return ['BUY', 'DIVIDEND', 'ITEM', 'SELL', 'SPLIT'].includes(
order.type
);
}), }),
currency: userCurrency, currency: userCurrency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,

1
apps/api/src/helper/portfolio.helper.ts

@ -19,6 +19,7 @@ export function getFactor(activityType: ActivityType) {
switch (activityType) { switch (activityType) {
case 'BUY': case 'BUY':
case 'ITEM': case 'ITEM':
case 'SPLIT':
factor = 1; factor = 1;
break; break;
case 'LIABILITY': case 'LIABILITY':

46
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) { if (this.activityForm.controls['searchSymbol'].invalid) {
this.data.activity.SymbolProfile = null; this.data.activity.SymbolProfile = null;
} else if ( } else if (
['BUY', 'DIVIDEND', 'SELL'].includes( ['BUY', 'DIVIDEND', 'SELL', 'SPLIT'].includes(
this.activityForm.controls['type'].value this.activityForm.controls['type'].value
) )
) { ) {
@ -394,6 +394,50 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.controls['updateAccountBalance'].disable(); this.activityForm.controls['updateAccountBalance'].disable();
this.activityForm.controls['updateAccountBalance'].setValue(false); 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 { } else {
this.activityForm.controls['accountId'].setValidators( this.activityForm.controls['accountId'].setValidators(
Validators.required Validators.required

24
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</small >Luxury items, real estate, private companies</small
> >
</mat-option> </mat-option>
<mat-option value="SPLIT">
<span
><b>{{ typesTranslationMap['SPLIT'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Forward split, reverse split</small
>
</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -180,13 +188,22 @@
}" }"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Quantity</mat-label> <mat-label
><ng-container [ngSwitch]="activityForm.controls['type']?.value">
<ng-container *ngSwitchCase="'SPLIT'" i18n>Ratio</ng-container>
<ng-container *ngSwitchDefault i18n>Quantity</ng-container>
</ng-container>
</mat-label>
<input formControlName="quantity" matInput type="number" /> <input formControlName="quantity" matInput type="number" />
</mat-form-field> </mat-form-field>
</div> </div>
<div <div
class="mb-3" class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' }" [ngClass]="{
'd-none':
activityForm.controls['type']?.value === 'FEE' ||
activityForm.controls['type']?.value === 'SPLIT'
}"
> >
<div class="align-items-start d-flex"> <div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
@ -279,7 +296,8 @@
'd-none': 'd-none':
activityForm.controls['type']?.value === 'INTEREST' || activityForm.controls['type']?.value === 'INTEREST' ||
activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'ITEM' ||
activityForm.controls['type']?.value === 'LIABILITY' activityForm.controls['type']?.value === 'LIABILITY' ||
activityForm.controls['type']?.value === 'SPLIT'
}" }"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">

5
libs/ui/src/lib/activity-type/activity-type.component.html

@ -7,7 +7,8 @@
interest: activityType === 'INTEREST', interest: activityType === 'INTEREST',
item: activityType === 'ITEM', item: activityType === 'ITEM',
liability: activityType === 'LIABILITY', liability: activityType === 'LIABILITY',
sell: activityType === 'SELL' sell: activityType === 'SELL',
split: activityType === 'SPLIT'
}" }"
> >
@if (activityType === 'BUY') { @if (activityType === 'BUY') {
@ -22,6 +23,8 @@
<ion-icon name="flame-outline" /> <ion-icon name="flame-outline" />
} @else if (activityType === 'SELL') { } @else if (activityType === 'SELL') {
<ion-icon name="arrow-down-circle-outline" /> <ion-icon name="arrow-down-circle-outline" />
} @else if (activityType === 'SPLIT') {
<ion-icon name="git-branch-outline" />
} }
<span class="d-none d-lg-block mx-1">{{ activityTypeLabel }}</span> <span class="d-none d-lg-block mx-1">{{ activityTypeLabel }}</span>
</div> </div>

1
libs/ui/src/lib/i18n.ts

@ -34,6 +34,7 @@ const locales = {
ITEM: $localize`Valuable`, ITEM: $localize`Valuable`,
LIABILITY: $localize`Liability`, LIABILITY: $localize`Liability`,
SELL: $localize`Sell`, SELL: $localize`Sell`,
SPLIT: $localize`Stock Split`,
// AssetClass (enum) // AssetClass (enum)
CASH: $localize`Cash`, CASH: $localize`Cash`,

2
prisma/migrations/20240329190053_added_split_to_order_type/migration.sql

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Type" ADD VALUE 'SPLIT';

1
prisma/schema.prisma

@ -301,6 +301,7 @@ enum Type {
ITEM ITEM
LIABILITY LIABILITY
SELL SELL
SPLIT
} }
enum ViewMode { enum ViewMode {

Loading…
Cancel
Save