From 8ba15f8f72d6099a5186b3a64af442e20e8ab68d Mon Sep 17 00:00:00 2001
From: Francisco Silva <franciscojjsilva@gmail.com>
Date: Sat, 6 May 2023 09:01:09 +0200
Subject: [PATCH] Optionally update cash balance when adding activity (#1926)

* Optionally update cash balance when adding activity

* Update changelog
---
 CHANGELOG.md                                  |  4 ++
 apps/api/src/app/account/account.service.ts   | 43 +++++++++++++++++++
 apps/api/src/app/import/import.service.ts     |  1 +
 apps/api/src/app/order/create-order.dto.ts    |  5 +++
 .../order/interfaces/activities.interface.ts  |  1 +
 apps/api/src/app/order/order.service.ts       | 31 ++++++++++++-
 apps/api/src/app/order/update-order.dto.ts    |  5 +++
 ...ate-or-update-activity-dialog.component.ts | 10 ++++-
 .../create-or-update-activity-dialog.html     |  9 +++-
 ...create-or-update-activity-dialog.module.ts |  2 +
 .../app/services/import-activities.service.ts |  7 ++-
 11 files changed, 110 insertions(+), 8 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5db1aa18c..41f949b84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## Unreleased
 
+### Added
+
+- Introduced the option to update the cash balance of an account when adding an activity
+
 ### Changed
 
 - Upgraded `class-transformer` from version `0.3.2` to `0.5.1`
diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts
index eb3ad8149..c6da815e4 100644
--- a/apps/api/src/app/account/account.service.ts
+++ b/apps/api/src/app/account/account.service.ts
@@ -172,4 +172,47 @@ export class AccountService {
       where
     });
   }
+
+  public async updateAccountBalance({
+    accountId,
+    amount,
+    currency,
+    date,
+    userId
+  }: {
+    accountId: string;
+    amount: number;
+    currency: string;
+    date: Date;
+    userId: string;
+  }) {
+    const { balance, currency: currencyOfAccount } = await this.account({
+      id_userId: {
+        userId,
+        id: accountId
+      }
+    });
+
+    const amountInCurrencyOfAccount =
+      await this.exchangeRateDataService.toCurrencyAtDate(
+        amount,
+        currency,
+        currencyOfAccount,
+        date
+      );
+
+    if (amountInCurrencyOfAccount) {
+      await this.prismaService.account.update({
+        data: {
+          balance: new Big(balance).plus(amountInCurrencyOfAccount).toNumber()
+        },
+        where: {
+          id_userId: {
+            userId,
+            id: accountId
+          }
+        }
+      });
+    }
+  }
 }
diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts
index c3b8f63b3..98dc0e249 100644
--- a/apps/api/src/app/import/import.service.ts
+++ b/apps/api/src/app/import/import.service.ts
@@ -303,6 +303,7 @@ export class ImportService {
               }
             }
           },
+          updateAccountBalance: false,
           User: { connect: { id: userId } }
         });
       }
diff --git a/apps/api/src/app/order/create-order.dto.ts b/apps/api/src/app/order/create-order.dto.ts
index 33e6f9cc8..e43d1dad4 100644
--- a/apps/api/src/app/order/create-order.dto.ts
+++ b/apps/api/src/app/order/create-order.dto.ts
@@ -8,6 +8,7 @@ import {
 import { Transform, TransformFnParams } from 'class-transformer';
 import {
   IsArray,
+  IsBoolean,
   IsEnum,
   IsISO8601,
   IsNumber,
@@ -64,4 +65,8 @@ export class CreateOrderDto {
 
   @IsNumber()
   unitPrice: number;
+
+  @IsBoolean()
+  @IsOptional()
+  updateAccountBalance: boolean;
 }
diff --git a/apps/api/src/app/order/interfaces/activities.interface.ts b/apps/api/src/app/order/interfaces/activities.interface.ts
index 31b345b46..33fe8b40d 100644
--- a/apps/api/src/app/order/interfaces/activities.interface.ts
+++ b/apps/api/src/app/order/interfaces/activities.interface.ts
@@ -6,6 +6,7 @@ export interface Activities {
 
 export interface Activity extends OrderWithAccount {
   feeInBaseCurrency: number;
+  updateAccountBalance?: boolean;
   value: number;
   valueInBaseCurrency: number;
 }
diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts
index 9bc2c6a8e..696f5442e 100644
--- a/apps/api/src/app/order/order.service.ts
+++ b/apps/api/src/app/order/order.service.ts
@@ -73,6 +73,7 @@ export class OrderService {
       dataSource?: DataSource;
       symbol?: string;
       tags?: Tag[];
+      updateAccountBalance?: boolean;
       userId: string;
     }
   ): Promise<Order> {
@@ -89,12 +90,16 @@ export class OrderService {
       };
     }
 
+    const accountId = data.accountId;
+    let currency = data.currency;
     const tags = data.tags ?? [];
+    const updateAccountBalance = data.updateAccountBalance ?? false;
+    const userId = data.userId;
 
     if (data.type === 'ITEM') {
       const assetClass = data.assetClass;
       const assetSubClass = data.assetSubClass;
-      const currency = data.SymbolProfile.connectOrCreate.create.currency;
+      currency = data.SymbolProfile.connectOrCreate.create.currency;
       const dataSource: DataSource = 'MANUAL';
       const id = uuidv4();
       const name = data.SymbolProfile.connectOrCreate.create.symbol;
@@ -149,11 +154,12 @@ export class OrderService {
     delete data.dataSource;
     delete data.symbol;
     delete data.tags;
+    delete data.updateAccountBalance;
     delete data.userId;
 
     const orderData: Prisma.OrderCreateInput = data;
 
-    return this.prismaService.order.create({
+    const order = await this.prismaService.order.create({
       data: {
         ...orderData,
         Account,
@@ -165,6 +171,27 @@ export class OrderService {
         }
       }
     });
+
+    if (updateAccountBalance === true) {
+      let amount = new Big(data.unitPrice)
+        .mul(data.quantity)
+        .plus(data.fee)
+        .toNumber();
+
+      if (data.type === 'BUY') {
+        amount = new Big(amount).mul(-1).toNumber();
+      }
+
+      await this.accountService.updateAccountBalance({
+        accountId,
+        amount,
+        currency,
+        userId,
+        date: data.date as Date
+      });
+    }
+
+    return order;
   }
 
   public async deleteOrder(
diff --git a/apps/api/src/app/order/update-order.dto.ts b/apps/api/src/app/order/update-order.dto.ts
index 7c709ea7c..6802c1357 100644
--- a/apps/api/src/app/order/update-order.dto.ts
+++ b/apps/api/src/app/order/update-order.dto.ts
@@ -8,6 +8,7 @@ import {
 import { Transform, TransformFnParams } from 'class-transformer';
 import {
   IsArray,
+  IsBoolean,
   IsEnum,
   IsISO8601,
   IsNumber,
@@ -66,4 +67,8 @@ export class UpdateOrderDto {
 
   @IsNumber()
   unitPrice: number;
+
+  @IsBoolean()
+  @IsOptional()
+  updateAccountBalance: boolean;
 }
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 b64fe5481..4336e9417 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
@@ -139,7 +139,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
       unitPriceInCustomCurrency: [
         this.data.activity?.unitPrice,
         Validators.required
-      ]
+      ],
+      updateAccountBalance: [false]
     });
 
     this.activityForm.valueChanges
@@ -297,6 +298,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
             Validators.required
           );
           this.activityForm.controls['searchSymbol'].updateValueAndValidity();
+          this.activityForm.controls['updateAccountBalance'].disable();
+          this.activityForm.controls['updateAccountBalance'].setValue(false);
         } else {
           this.activityForm.controls['accountId'].setValidators(
             Validators.required
@@ -314,6 +317,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
             Validators.required
           );
           this.activityForm.controls['searchSymbol'].updateValueAndValidity();
+          this.activityForm.controls['updateAccountBalance'].enable();
         }
 
         this.changeDetectorRef.markForCheck();
@@ -411,7 +415,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
           : this.activityForm.controls['searchSymbol'].value.symbol,
       tags: this.activityForm.controls['tags'].value,
       type: this.activityForm.controls['type'].value,
-      unitPrice: this.activityForm.controls['unitPrice'].value
+      unitPrice: this.activityForm.controls['unitPrice'].value,
+      updateAccountBalance:
+        this.activityForm.controls['updateAccountBalance'].value
     };
 
     if (this.data.activity.id) {
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 4ef9b4487..9ad56c907 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
@@ -18,8 +18,8 @@
         </mat-select>
       </mat-form-field>
     </div>
-    <div class="mb-3">
-      <mat-form-field appearance="outline" class="w-100">
+    <div>
+      <mat-form-field appearance="outline" class="mb-1 without-hint w-100">
         <mat-label i18n>Account</mat-label>
         <mat-select formControlName="accountId">
           <mat-option
@@ -32,6 +32,11 @@
         </mat-select>
       </mat-form-field>
     </div>
+    <div class="mb-3">
+      <mat-checkbox color="primary" formControlName="updateAccountBalance" i18n
+        >Update Cash Balance</mat-checkbox
+      >
+    </div>
     <div
       class="mb-3"
       [ngClass]="{ 'd-none': !activityForm.controls['searchSymbol'].hasValidator(Validators.required) }"
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
index b83f72518..3bcfa9eb8 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
@@ -8,6 +8,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
 import { MatDialogModule } from '@angular/material/dialog';
 import { MatFormFieldModule } from '@angular/material/form-field';
 import { MatInputModule } from '@angular/material/input';
+import { MatCheckboxModule } from '@angular/material/checkbox';
 import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
 import { MatSelectModule } from '@angular/material/select';
 import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
@@ -24,6 +25,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
     GfValueModule,
     MatAutocompleteModule,
     MatButtonModule,
+    MatCheckboxModule,
     MatChipsModule,
     MatDatepickerModule,
     MatDialogModule,
diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts
index 859558877..487a47a6c 100644
--- a/apps/client/src/app/services/import-activities.service.ts
+++ b/apps/client/src/app/services/import-activities.service.ts
@@ -59,7 +59,8 @@ export class ImportActivitiesService {
         quantity: this.parseQuantity({ content, index, item }),
         symbol: this.parseSymbol({ content, index, item }),
         type: this.parseType({ content, index, item }),
-        unitPrice: this.parseUnitPrice({ content, index, item })
+        unitPrice: this.parseUnitPrice({ content, index, item }),
+        updateAccountBalance: false
       });
     }
 
@@ -126,7 +127,8 @@ export class ImportActivitiesService {
     quantity,
     SymbolProfile,
     type,
-    unitPrice
+    unitPrice,
+    updateAccountBalance
   }: Activity): CreateOrderDto {
     return {
       accountId,
@@ -134,6 +136,7 @@ export class ImportActivitiesService {
       quantity,
       type,
       unitPrice,
+      updateAccountBalance,
       currency: SymbolProfile.currency,
       date: date.toString(),
       symbol: SymbolProfile.symbol