Files
cbsnexora/src/app/home/dashboard/crr-slr-dialog/reserve-transfer-dialog.component.ts
T

168 lines
5.9 KiB
TypeScript

/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { RbiCircularsService } from '../../../rbi-circulars/rbi-circulars.service';
/** A reserve account as the position screen lists it. */
export interface TransferAccount {
glAccountId: number;
glCode: string;
name: string;
balance: number;
item: string;
head: string;
}
/** What the dialog is opened with: the account clicked, and everything it may move to. */
export interface ReserveTransferData {
from: TransferAccount;
accounts: TransferAccount[];
/** The office the entry posts against, and the date it posts on. */
officeId: number;
currencyCode: string;
}
/**
* Moves money between the accounts the cash reserve is held in.
*
* The entry posts through the platform's own journal command, so it goes to maker-checker like any
* other entry against the ledger. That is deliberate: the amounts settle a statutory requirement,
* and a transfer that changes whether the bank is compliant is not something one person should be
* able to post alone.
*
* It records the movement; it does not make it. The money reaches the other bank when the treasury
* desk sends the NEFT or RTGS, and this entry is the bank's own record of having done so — which is
* why the dialog says as much before it posts.
*/
@Component({
selector: 'mifosx-reserve-transfer-dialog',
standalone: true,
templateUrl: './reserve-transfer-dialog.component.html',
styleUrls: ['./reserve-transfer-dialog.component.scss'],
imports: [
CommonModule,
FormsModule,
MatDialogModule,
MatButtonModule,
FaIconComponent
]
})
export class ReserveTransferDialogComponent {
private dialogRef = inject(MatDialogRef<ReserveTransferDialogComponent>);
private rbi = inject(RbiCircularsService);
/** The account money leaves. Fixed — the dialog is opened from its row. */
readonly from: TransferAccount;
/** Everything it can move to: the other reserve accounts, never itself. */
readonly destinations: TransferAccount[];
toAccountId: number | null = null;
amount: number | null = null;
reference = '';
comments = '';
submitting = false;
error = '';
readonly data = inject<ReserveTransferData>(MAT_DIALOG_DATA);
constructor() {
this.from = this.data.from;
this.destinations = (this.data.accounts ?? []).filter((a) => a.glAccountId !== this.data.from.glAccountId);
}
/** The account chosen to receive the money, while one is chosen. */
get to(): TransferAccount | null {
return this.destinations.find((a) => a.glAccountId === this.toAccountId) ?? null;
}
/** What the source would hold once this posts — the figure that decides whether it may. */
get fromAfter(): number {
return this.from.balance - (this.amount ?? 0);
}
get toAfter(): number {
return (this.to?.balance ?? 0) + (this.amount ?? 0);
}
/**
* Whether the amount can actually be sent.
*
* An account cannot send more than it holds: the ledger would carry a negative balance on a cash
* or bank account, which is not a position that exists.
*/
get overdrawn(): boolean {
return this.amount !== null && this.amount > this.from.balance;
}
get canSubmit(): boolean {
return !this.submitting && this.toAccountId !== null && (this.amount ?? 0) > 0 && !this.overdrawn;
}
money(value: number | null | undefined): string {
if (value === null || value === undefined) {
return '—';
}
return `₹${value.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
/**
* Posts the transfer as a single journal entry.
*
* These are asset accounts, so the one money LEAVES is credited and the one it arrives in is
* debited — the opposite of how the screen words it, and deliberately so: a treasury user thinks
* "send from here, receive there", and making them think in debits and credits to move their own
* money would be asking them to do the ledger's job.
*/
submit(): void {
if (!this.canSubmit || !this.to) {
return;
}
this.submitting = true;
this.error = '';
// The reserve endpoint, not the manual journal entry one: that is turned off at the HTTP edge
// on this installation. The sides are named from and to, as a desk thinks of them; the debit
// and credit are worked out on the server, where the accounts can also be checked.
const transfer = {
fromGlAccountId: this.from.glAccountId,
toGlAccountId: this.to.glAccountId,
amount: this.amount as number,
officeId: this.data.officeId,
reference: this.reference || undefined,
comments:
this.comments || `Reserve transfer: ${this.from.glCode} ${this.from.name} to ${this.to.glCode} ${this.to.name}`
};
this.rbi.transferReserve(transfer).subscribe({
next: () => this.dialogRef.close(true),
error: (err: any) => {
this.submitting = false;
// The platform's own message, not a rewritten one: it names the rule that stopped the
// entry — a closed accounting period, a disabled account — and a generic "could not post"
// sends the user to look for a fault that has already been explained.
this.error =
err?.error?.developerMessage ||
err?.error?.defaultUserMessage ||
err?.error?.errors?.[0]?.defaultUserMessage ||
'The transfer could not be posted.';
}
});
}
close(): void {
this.dialogRef.close(false);
}
}