168 lines
5.9 KiB
TypeScript
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);
|
|
}
|
|
}
|