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. 24
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  4. 127
      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 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 assetSubClass = data.assetSubClass;
currency = data.SymbolProfile.connectOrCreate.create.currency;
@ -151,7 +155,7 @@ export class OrderService {
const orderData: Prisma.OrderCreateInput = data;
const isDraft =
data.type === 'LIABILITY'
data.type === 'FEE' || data.type === 'ITEM' || data.type === 'LIABILITY'
? false
: isAfter(data.date as Date, endOfToday());
@ -197,7 +201,11 @@ export class OrderService {
where
});
if (order.type === 'ITEM' || order.type === 'LIABILITY') {
if (
order.type === 'FEE' ||
order.type === 'ITEM' ||
order.type === 'LIABILITY'
) {
await this.symbolProfileService.deleteById(order.symbolProfileId);
}
@ -368,7 +376,11 @@ export class OrderService {
let isDraft = false;
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
if (
data.type === 'FEE' ||
data.type === 'ITEM' ||
data.type === 'LIABILITY'
) {
delete data.SymbolProfile.connect;
} else {
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>
</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="col"><hr /></div>
</div>
@ -75,10 +84,7 @@
</div>
</div>
<div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate" i18n>
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
{transaction} other {transactions}}
</div>
<div class="flex-grow-1 text-truncate" i18n>Fees</div>
<div class="d-flex justify-content-end">
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
<gf-value

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

127
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">
<span><b>{{ typesTranslationMap['BUY'] }}</b></span>
<br />
<small class="text-muted text-nowrap" i18n
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
>
</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">
<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 class="line-height-1" value="LIABILITY">
<span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span>
<br />
<small class="text-muted text-nowrap" i18n
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Mortgages, personal loans, credit cards</small
>
</mat-option>
<mat-option class="line-height-1" value="SELL">
<span><b>{{ typesTranslationMap['SELL'] }}</b></span>
<br />
<small class="text-muted text-nowrap" i18n
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
>
</mat-option>
<mat-option class="line-height-1" value="ITEM">
<span><b>{{ typesTranslationMap['ITEM'] }}</b></span>
<br />
<small class="text-muted text-nowrap" i18n
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Luxury items, real estate, private companies</small
>
</mat-option>
@ -125,60 +134,71 @@
</div>
<div
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-label i18n>Quantity</mat-label>
<input formControlName="quantity" matInput type="number" />
</mat-form-field>
</div>
<div class="align-items-start d-flex mb-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label
><ng-container [ngSwitch]="activityForm.controls['type']?.value">
<ng-container *ngSwitchCase="'DIVIDEND'" i18n
>Dividend</ng-container
<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-label
><ng-container [ngSwitch]="activityForm.controls['type']?.value">
<ng-container *ngSwitchCase="'DIVIDEND'" i18n
>Dividend</ng-container
>
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'LIABILITY'" i18n
>Value</ng-container
>
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
</ng-container>
</mat-label>
<input
formControlName="unitPriceInCustomCurrency"
matInput
type="number"
/>
<div
class="ml-2"
matTextSuffix
[ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }"
>
<mat-select formControlName="currencyOfUnitPrice">
<mat-option
*ngFor="let currency of currencies"
[value]="currency"
>
{{ currency }}
</mat-option>
</mat-select>
</div>
<mat-error
*ngIf="activityForm.controls['unitPriceInCustomCurrency'].hasError('invalid')"
><ng-container i18n
>Oops! Could not get the historical exchange rate
from</ng-container
>
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container>
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
</ng-container>
</mat-label>
<input
formControlName="unitPriceInCustomCurrency"
matInput
type="number"
/>
<div
class="ml-2"
matTextSuffix
[ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }"
>
<mat-select formControlName="currencyOfUnitPrice">
<mat-option *ngFor="let currency of currencies" [value]="currency">
{{ currency }}
</mat-option>
</mat-select>
</div>
<mat-error
*ngIf="activityForm.controls['unitPriceInCustomCurrency'].hasError('invalid')"
><ng-container i18n
>Oops! Could not get the historical exchange rate from</ng-container
{{ activityForm.controls['date']?.value | date: defaultDateFormat
}}</mat-error
>
{{ activityForm.controls['date']?.value | date: defaultDateFormat
}}</mat-error
</mat-form-field>
<button
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
class="ml-2 mt-1 no-min-width"
mat-button
title="Apply current market price"
type="button"
(click)="applyCurrentMarketPrice()"
>
</mat-form-field>
<button
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
class="ml-2 mt-1 no-min-width"
mat-button
title="Apply current market price"
type="button"
(click)="applyCurrentMarketPrice()"
>
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
</button>
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
</button>
</div>
</div>
<div class="d-none">
<mat-form-field appearance="outline" class="w-100">
@ -187,6 +207,7 @@
<ng-container *ngSwitchCase="'DIVIDEND'" i18n
>Dividend</ng-container
>
<ng-container *ngSwitchCase="'FEE'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</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]="{
buy: element.type === 'BUY',
dividend: element.type === 'DIVIDEND',
fee: element.type === 'FEE',
item: element.type === 'ITEM',
liability: element.type === 'LIABILITY',
sell: element.type === 'SELL'
@ -170,6 +171,10 @@
*ngIf="element.type === 'BUY' || element.type === 'DIVIDEND'"
name="arrow-up-circle-outline"
></ion-icon>
<ion-icon
*ngIf="element.type === 'FEE'"
name="hammer-outline"
></ion-icon>
<ion-icon
*ngIf="element.type === 'ITEM'"
name="cube-outline"
@ -545,6 +550,7 @@
'cursor-pointer':
hasPermissionToOpenDetails &&
!row.isDraft &&
row.type !== 'FEE' &&
row.type !== 'ITEM' &&
row.type !== 'LIABILITY'
}"

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

@ -33,6 +33,10 @@
color: var(--blue);
}
&.fee {
color: var(--gray);
}
&.item {
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 (
this.hasPermissionToOpenDetails &&
!activity.isDraft &&
activity.type !== 'FEE' &&
activity.type !== 'ITEM' &&
activity.type !== 'LIABILITY'
) {
@ -390,7 +391,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
if (isNumber(valueInBaseCurrency)) {
if (type === 'BUY' || type === 'ITEM') {
totalValue = totalValue.plus(valueInBaseCurrency);
} else if (type === 'LIABILITY' || type === 'SELL') {
} else if (type === 'FEE' || type === 'LIABILITY' || type === 'SELL') {
return null;
}
} else {

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

@ -29,6 +29,7 @@ const locales = {
// Activity types
BUY: $localize`Buy`,
DIVIDEND: $localize`Dividend`,
FEE: $localize`Fee`,
ITEM: $localize`Valuable`,
LIABILITY: $localize`Liability`,
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