Optionally update cash balance when adding activity (#1926)

* Optionally update cash balance when adding activity

* Update changelog
This commit is contained in:
Francisco Silva 2023-05-06 09:01:09 +02:00 committed by GitHub
parent 876b66f324
commit 8ba15f8f72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 110 additions and 8 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Introduced the option to update the cash balance of an account when adding an activity
### Changed ### Changed
- Upgraded `class-transformer` from version `0.3.2` to `0.5.1` - Upgraded `class-transformer` from version `0.3.2` to `0.5.1`

View File

@ -172,4 +172,47 @@ export class AccountService {
where 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
}
}
});
}
}
} }

View File

@ -303,6 +303,7 @@ export class ImportService {
} }
} }
}, },
updateAccountBalance: false,
User: { connect: { id: userId } } User: { connect: { id: userId } }
}); });
} }

View File

@ -8,6 +8,7 @@ import {
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean,
IsEnum, IsEnum,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
@ -64,4 +65,8 @@ export class CreateOrderDto {
@IsNumber() @IsNumber()
unitPrice: number; unitPrice: number;
@IsBoolean()
@IsOptional()
updateAccountBalance: boolean;
} }

View File

@ -6,6 +6,7 @@ export interface Activities {
export interface Activity extends OrderWithAccount { export interface Activity extends OrderWithAccount {
feeInBaseCurrency: number; feeInBaseCurrency: number;
updateAccountBalance?: boolean;
value: number; value: number;
valueInBaseCurrency: number; valueInBaseCurrency: number;
} }

View File

@ -73,6 +73,7 @@ export class OrderService {
dataSource?: DataSource; dataSource?: DataSource;
symbol?: string; symbol?: string;
tags?: Tag[]; tags?: Tag[];
updateAccountBalance?: boolean;
userId: string; userId: string;
} }
): Promise<Order> { ): Promise<Order> {
@ -89,12 +90,16 @@ export class OrderService {
}; };
} }
const accountId = data.accountId;
let currency = data.currency;
const tags = data.tags ?? []; const tags = data.tags ?? [];
const updateAccountBalance = data.updateAccountBalance ?? false;
const userId = data.userId;
if (data.type === 'ITEM') { if (data.type === 'ITEM') {
const assetClass = data.assetClass; const assetClass = data.assetClass;
const assetSubClass = data.assetSubClass; const assetSubClass = data.assetSubClass;
const currency = data.SymbolProfile.connectOrCreate.create.currency; currency = data.SymbolProfile.connectOrCreate.create.currency;
const dataSource: DataSource = 'MANUAL'; const dataSource: DataSource = 'MANUAL';
const id = uuidv4(); const id = uuidv4();
const name = data.SymbolProfile.connectOrCreate.create.symbol; const name = data.SymbolProfile.connectOrCreate.create.symbol;
@ -149,11 +154,12 @@ export class OrderService {
delete data.dataSource; delete data.dataSource;
delete data.symbol; delete data.symbol;
delete data.tags; delete data.tags;
delete data.updateAccountBalance;
delete data.userId; delete data.userId;
const orderData: Prisma.OrderCreateInput = data; const orderData: Prisma.OrderCreateInput = data;
return this.prismaService.order.create({ const order = await this.prismaService.order.create({
data: { data: {
...orderData, ...orderData,
Account, 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( public async deleteOrder(

View File

@ -8,6 +8,7 @@ import {
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean,
IsEnum, IsEnum,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
@ -66,4 +67,8 @@ export class UpdateOrderDto {
@IsNumber() @IsNumber()
unitPrice: number; unitPrice: number;
@IsBoolean()
@IsOptional()
updateAccountBalance: boolean;
} }

View File

@ -139,7 +139,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
unitPriceInCustomCurrency: [ unitPriceInCustomCurrency: [
this.data.activity?.unitPrice, this.data.activity?.unitPrice,
Validators.required Validators.required
] ],
updateAccountBalance: [false]
}); });
this.activityForm.valueChanges this.activityForm.valueChanges
@ -297,6 +298,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
Validators.required Validators.required
); );
this.activityForm.controls['searchSymbol'].updateValueAndValidity(); this.activityForm.controls['searchSymbol'].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
@ -314,6 +317,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
Validators.required Validators.required
); );
this.activityForm.controls['searchSymbol'].updateValueAndValidity(); this.activityForm.controls['searchSymbol'].updateValueAndValidity();
this.activityForm.controls['updateAccountBalance'].enable();
} }
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
@ -411,7 +415,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
: this.activityForm.controls['searchSymbol'].value.symbol, : this.activityForm.controls['searchSymbol'].value.symbol,
tags: this.activityForm.controls['tags'].value, tags: this.activityForm.controls['tags'].value,
type: this.activityForm.controls['type'].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) { if (this.data.activity.id) {

View File

@ -18,8 +18,8 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3"> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="mb-1 without-hint w-100">
<mat-label i18n>Account</mat-label> <mat-label i18n>Account</mat-label>
<mat-select formControlName="accountId"> <mat-select formControlName="accountId">
<mat-option <mat-option
@ -32,6 +32,11 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3">
<mat-checkbox color="primary" formControlName="updateAccountBalance" i18n
>Update Cash Balance</mat-checkbox
>
</div>
<div <div
class="mb-3" class="mb-3"
[ngClass]="{ 'd-none': !activityForm.controls['searchSymbol'].hasValidator(Validators.required) }" [ngClass]="{ 'd-none': !activityForm.controls['searchSymbol'].hasValidator(Validators.required) }"

View File

@ -8,6 +8,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
@ -24,6 +25,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
GfValueModule, GfValueModule,
MatAutocompleteModule, MatAutocompleteModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule,
MatChipsModule, MatChipsModule,
MatDatepickerModule, MatDatepickerModule,
MatDialogModule, MatDialogModule,

View File

@ -59,7 +59,8 @@ export class ImportActivitiesService {
quantity: this.parseQuantity({ content, index, item }), quantity: this.parseQuantity({ content, index, item }),
symbol: this.parseSymbol({ content, index, item }), symbol: this.parseSymbol({ content, index, item }),
type: this.parseType({ 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, quantity,
SymbolProfile, SymbolProfile,
type, type,
unitPrice unitPrice,
updateAccountBalance
}: Activity): CreateOrderDto { }: Activity): CreateOrderDto {
return { return {
accountId, accountId,
@ -134,6 +136,7 @@ export class ImportActivitiesService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
updateAccountBalance,
currency: SymbolProfile.currency, currency: SymbolProfile.currency,
date: date.toString(), date: date.toString(),
symbol: SymbolProfile.symbol symbol: SymbolProfile.symbol