Browse Source

Add business logic for fees

pull/1954/head
Thomas 2 years ago
parent
commit
c17b52ad89
  1. 20
      apps/api/src/app/order/order.service.ts
  2. 14
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  3. 22
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  4. 47
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  5. 6
      libs/ui/src/lib/activities-table/activities-table.component.html
  6. 4
      libs/ui/src/lib/activities-table/activities-table.component.scss
  7. 3
      libs/ui/src/lib/activities-table/activities-table.component.ts
  8. 1
      libs/ui/src/lib/i18n.ts
  9. 0
      prisma/migrations/20230917074305_added_fee_to_order_type/migration.sql

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

@ -97,7 +97,11 @@ export class OrderService {
const updateAccountBalance = data.updateAccountBalance ?? false; const updateAccountBalance = data.updateAccountBalance ?? false;
const userId = data.userId; const userId = data.userId;
if (data.type === 'ITEM' || data.type === 'LIABILITY') { if (
data.type === 'FEE' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
const assetClass = data.assetClass; const assetClass = data.assetClass;
const assetSubClass = data.assetSubClass; const assetSubClass = data.assetSubClass;
currency = data.SymbolProfile.connectOrCreate.create.currency; currency = data.SymbolProfile.connectOrCreate.create.currency;
@ -151,7 +155,7 @@ export class OrderService {
const orderData: Prisma.OrderCreateInput = data; const orderData: Prisma.OrderCreateInput = data;
const isDraft = const isDraft =
data.type === 'LIABILITY' data.type === 'FEE' || data.type === 'ITEM' || data.type === 'LIABILITY'
? false ? false
: isAfter(data.date as Date, endOfToday()); : isAfter(data.date as Date, endOfToday());
@ -197,7 +201,11 @@ export class OrderService {
where where
}); });
if (order.type === 'ITEM' || order.type === 'LIABILITY') { if (
order.type === 'FEE' ||
order.type === 'ITEM' ||
order.type === 'LIABILITY'
) {
await this.symbolProfileService.deleteById(order.symbolProfileId); await this.symbolProfileService.deleteById(order.symbolProfileId);
} }
@ -368,7 +376,11 @@ export class OrderService {
let isDraft = false; let isDraft = false;
if (data.type === 'ITEM' || data.type === 'LIABILITY') { if (
data.type === 'FEE' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
delete data.SymbolProfile.connect; delete data.SymbolProfile.connect;
} else { } else {
delete data.SymbolProfile.update; delete data.SymbolProfile.update;

14
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -5,6 +5,15 @@
<gf-value class="justify-content-end" [value]="timeInMarket"></gf-value> <gf-value class="justify-content-end" [value]="timeInMarket"></gf-value>
</div> </div>
</div> </div>
<div
class="flex-nowrap px-3 py-1 row"
[hidden]="summary?.ordersCount === null"
>
<div class="flex-grow-1 ml-3 text-truncate" i18n>
{{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 {transaction}
other {transactions}}
</div>
</div>
<div class="row"> <div class="row">
<div class="col"><hr /></div> <div class="col"><hr /></div>
</div> </div>
@ -75,10 +84,7 @@
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate" i18n> <div class="flex-grow-1 text-truncate" i18n>Fees</div>
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
{transaction} other {transactions}}
</div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span> <span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
<gf-value <gf-value

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

@ -217,6 +217,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
if ( if (
this.activityForm.controls['type'].value === 'BUY' || this.activityForm.controls['type'].value === 'BUY' ||
this.activityForm.controls['type'].value === 'FEE' ||
this.activityForm.controls['type'].value === 'ITEM' this.activityForm.controls['type'].value === 'ITEM'
) { ) {
this.total = this.total =
@ -290,7 +291,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.controls['searchSymbol'].updateValueAndValidity(); this.activityForm.controls['searchSymbol'].updateValueAndValidity();
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 === 'LIABILITY') { } else if (type === 'FEE' || type === 'LIABILITY') {
this.activityForm.controls['accountId'].removeValidators( this.activityForm.controls['accountId'].removeValidators(
Validators.required Validators.required
); );
@ -308,13 +309,32 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
Validators.required Validators.required
); );
this.activityForm.controls['dataSource'].updateValueAndValidity(); this.activityForm.controls['dataSource'].updateValueAndValidity();
if (
type === 'FEE' &&
this.activityForm.controls['feeInCustomCurrency'].value === 0
) {
this.activityForm.controls['feeInCustomCurrency'].reset();
}
this.activityForm.controls['name'].setValidators(Validators.required); this.activityForm.controls['name'].setValidators(Validators.required);
this.activityForm.controls['name'].updateValueAndValidity(); this.activityForm.controls['name'].updateValueAndValidity();
if (type === 'FEE') {
this.activityForm.controls['quantity'].setValue(0);
} else if (type === 'LIABILITY') {
this.activityForm.controls['quantity'].setValue(1); this.activityForm.controls['quantity'].setValue(1);
}
this.activityForm.controls['searchSymbol'].removeValidators( this.activityForm.controls['searchSymbol'].removeValidators(
Validators.required Validators.required
); );
this.activityForm.controls['searchSymbol'].updateValueAndValidity(); this.activityForm.controls['searchSymbol'].updateValueAndValidity();
if (type === 'FEE') {
this.activityForm.controls['unitPriceInCustomCurrency'].setValue(0);
}
this.activityForm.controls['updateAccountBalance'].disable(); this.activityForm.controls['updateAccountBalance'].disable();
this.activityForm.controls['updateAccountBalance'].setValue(false); this.activityForm.controls['updateAccountBalance'].setValue(false);
} else { } else {

47
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -17,32 +17,41 @@
> >
<mat-option class="line-height-1" value="BUY"> <mat-option class="line-height-1" value="BUY">
<span><b>{{ typesTranslationMap['BUY'] }}</b></span> <span><b>{{ typesTranslationMap['BUY'] }}</b></span>
<br /> <small class="d-block line-height-1 text-muted text-nowrap" i18n
<small class="text-muted text-nowrap" i18n
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small >Stocks, ETFs, bonds, cryptocurrencies, commodities</small
> >
</mat-option> </mat-option>
<mat-option
*ngIf="data.user?.settings?.isExperimentalFeatures"
class="line-height-1"
value="FEE"
>
<span><b>{{ typesTranslationMap['FEE'] }}</b></span>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>One-time fee, annual account fees</small
>
</mat-option>
<mat-option class="line-height-1" value="DIVIDEND"> <mat-option class="line-height-1" value="DIVIDEND">
<span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span> <span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Distribution of corporate earnings</small
>
</mat-option> </mat-option>
<mat-option class="line-height-1" value="LIABILITY"> <mat-option class="line-height-1" value="LIABILITY">
<span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span> <span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span>
<br /> <small class="d-block line-height-1 text-muted text-nowrap" i18n
<small class="text-muted text-nowrap" i18n
>Mortgages, personal loans, credit cards</small >Mortgages, personal loans, credit cards</small
> >
</mat-option> </mat-option>
<mat-option class="line-height-1" value="SELL"> <mat-option class="line-height-1" value="SELL">
<span><b>{{ typesTranslationMap['SELL'] }}</b></span> <span><b>{{ typesTranslationMap['SELL'] }}</b></span>
<br /> <small class="d-block line-height-1 text-muted text-nowrap" i18n
<small class="text-muted text-nowrap" i18n
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small >Stocks, ETFs, bonds, cryptocurrencies, commodities</small
> >
</mat-option> </mat-option>
<mat-option class="line-height-1" value="ITEM"> <mat-option class="line-height-1" value="ITEM">
<span><b>{{ typesTranslationMap['ITEM'] }}</b></span> <span><b>{{ typesTranslationMap['ITEM'] }}</b></span>
<br /> <small class="d-block line-height-1 text-muted text-nowrap" i18n
<small class="text-muted text-nowrap" i18n
>Luxury items, real estate, private companies</small >Luxury items, real estate, private companies</small
> >
</mat-option> </mat-option>
@ -125,14 +134,18 @@
</div> </div>
<div <div
class="mb-3" class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }" [ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' || activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Quantity</mat-label> <mat-label i18n>Quantity</mat-label>
<input formControlName="quantity" matInput type="number" /> <input formControlName="quantity" matInput type="number" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="align-items-start d-flex mb-3"> <div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' }"
>
<div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label <mat-label
><ng-container [ngSwitch]="activityForm.controls['type']?.value"> ><ng-container [ngSwitch]="activityForm.controls['type']?.value">
@ -140,7 +153,9 @@
>Dividend</ng-container >Dividend</ng-container
> >
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container> <ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container> <ng-container *ngSwitchCase="'LIABILITY'" i18n
>Value</ng-container
>
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container> <ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
</ng-container> </ng-container>
</mat-label> </mat-label>
@ -155,7 +170,10 @@
[ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }" [ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }"
> >
<mat-select formControlName="currencyOfUnitPrice"> <mat-select formControlName="currencyOfUnitPrice">
<mat-option *ngFor="let currency of currencies" [value]="currency"> <mat-option
*ngFor="let currency of currencies"
[value]="currency"
>
{{ currency }} {{ currency }}
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -163,7 +181,8 @@
<mat-error <mat-error
*ngIf="activityForm.controls['unitPriceInCustomCurrency'].hasError('invalid')" *ngIf="activityForm.controls['unitPriceInCustomCurrency'].hasError('invalid')"
><ng-container i18n ><ng-container i18n
>Oops! Could not get the historical exchange rate from</ng-container >Oops! Could not get the historical exchange rate
from</ng-container
> >
{{ activityForm.controls['date']?.value | date: defaultDateFormat {{ activityForm.controls['date']?.value | date: defaultDateFormat
}}</mat-error }}</mat-error
@ -180,6 +199,7 @@
<ion-icon class="text-muted" name="refresh-outline"></ion-icon> <ion-icon class="text-muted" name="refresh-outline"></ion-icon>
</button> </button>
</div> </div>
</div>
<div class="d-none"> <div class="d-none">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label <mat-label
@ -187,6 +207,7 @@
<ng-container *ngSwitchCase="'DIVIDEND'" i18n <ng-container *ngSwitchCase="'DIVIDEND'" i18n
>Dividend</ng-container >Dividend</ng-container
> >
<ng-container *ngSwitchCase="'FEE'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container> <ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container> <ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container>
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container> <ng-container *ngSwitchDefault i18n>Unit Price</ng-container>

6
libs/ui/src/lib/activities-table/activities-table.component.html

@ -161,6 +161,7 @@
[ngClass]="{ [ngClass]="{
buy: element.type === 'BUY', buy: element.type === 'BUY',
dividend: element.type === 'DIVIDEND', dividend: element.type === 'DIVIDEND',
fee: element.type === 'FEE',
item: element.type === 'ITEM', item: element.type === 'ITEM',
liability: element.type === 'LIABILITY', liability: element.type === 'LIABILITY',
sell: element.type === 'SELL' sell: element.type === 'SELL'
@ -170,6 +171,10 @@
*ngIf="element.type === 'BUY' || element.type === 'DIVIDEND'" *ngIf="element.type === 'BUY' || element.type === 'DIVIDEND'"
name="arrow-up-circle-outline" name="arrow-up-circle-outline"
></ion-icon> ></ion-icon>
<ion-icon
*ngIf="element.type === 'FEE'"
name="hammer-outline"
></ion-icon>
<ion-icon <ion-icon
*ngIf="element.type === 'ITEM'" *ngIf="element.type === 'ITEM'"
name="cube-outline" name="cube-outline"
@ -545,6 +550,7 @@
'cursor-pointer': 'cursor-pointer':
hasPermissionToOpenDetails && hasPermissionToOpenDetails &&
!row.isDraft && !row.isDraft &&
row.type !== 'FEE' &&
row.type !== 'ITEM' && row.type !== 'ITEM' &&
row.type !== 'LIABILITY' row.type !== 'LIABILITY'
}" }"

4
libs/ui/src/lib/activities-table/activities-table.component.scss

@ -33,6 +33,10 @@
color: var(--blue); color: var(--blue);
} }
&.fee {
color: var(--gray);
}
&.item { &.item {
color: var(--purple); color: var(--purple);
} }

3
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -206,6 +206,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
} else if ( } else if (
this.hasPermissionToOpenDetails && this.hasPermissionToOpenDetails &&
!activity.isDraft && !activity.isDraft &&
activity.type !== 'FEE' &&
activity.type !== 'ITEM' && activity.type !== 'ITEM' &&
activity.type !== 'LIABILITY' activity.type !== 'LIABILITY'
) { ) {
@ -390,7 +391,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
if (isNumber(valueInBaseCurrency)) { if (isNumber(valueInBaseCurrency)) {
if (type === 'BUY' || type === 'ITEM') { if (type === 'BUY' || type === 'ITEM') {
totalValue = totalValue.plus(valueInBaseCurrency); totalValue = totalValue.plus(valueInBaseCurrency);
} else if (type === 'LIABILITY' || type === 'SELL') { } else if (type === 'FEE' || type === 'LIABILITY' || type === 'SELL') {
return null; return null;
} }
} else { } else {

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

@ -29,6 +29,7 @@ const locales = {
// Activity types // Activity types
BUY: $localize`Buy`, BUY: $localize`Buy`,
DIVIDEND: $localize`Dividend`, DIVIDEND: $localize`Dividend`,
FEE: $localize`Fee`,
ITEM: $localize`Valuable`, ITEM: $localize`Valuable`,
LIABILITY: $localize`Liability`, LIABILITY: $localize`Liability`,
SELL: $localize`Sell`, SELL: $localize`Sell`,

0
prisma/migrations/20230509124205_added_fee_to_order_type/migration.sql → prisma/migrations/20230917074305_added_fee_to_order_type/migration.sql

Loading…
Cancel
Save