/** * 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); 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(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); } }