Compare commits

...
Author SHA1 Message Date
niteshandClaude Sonnet 5 244b70f926 fix(savings): show a clear message when an action is parked for checker approval
Approve, close, reject, undo-approval, withdraw-by-client, and post-interest
on a savings account all silently behaved as if nothing happened when the
backend parks the command in the Checker Inbox instead of executing it
(HTTP 200 with rollbackTransaction: true) -- misleading the maker into
thinking the action either failed or quietly succeeded. Each now shows an
explicit alert naming what was sent for approval and that the account/
application is unchanged until a checker approves it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-05 16:14:24 +05:30
nitesh 174a2e7b6b fix(accounting,deposits): allow future dates in premature close and GL search, fix FD/RD accounting mappings 2026-10-04 16:02:15 +05:30
nitesh 9f7d9d28fd feat: Fixed Deposit - premature close, payment dialog, account actions, translations and env updates 2026-10-04 09:12:06 +05:30
keshav 1ffac4b1e5 fix(savings-product): move savings interest paid to expenses mapping 2026-10-01 14:01:24 +05:30
niteshandClaude Sonnet 5 4dee1db360 Merge remote-tracking branch 'origin/dev-latest' into dev-latest
Brings in partner's RBI Reserve / Form 1 & 9 returns UI and interest
posting reports, alongside today's FD premature-closure work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 13:22:39 +05:30
niteshandClaude Sonnet 5 0c6ebfdc1a Merge branch 'feature/bugfix' into dev-latest
Brings in partner's "Enforce same-user maker-checker restriction" and
related client-KYC-step / product-edit-diff-dialog work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 13:21:19 +05:30
anshul 73871a0993 Enforce same-user maker-checker restriction 2026-10-01 12:04:01 +05:30
anshul 7b0cb5bb63 Merge branch 'dev-latest' into feature/bugfix 2026-09-30 16:32:46 +05:30
anshul 646e73d08a Merge branch 'dev-latest' into feature/bugfix 2026-09-29 10:16:58 +05:30
27 changed files with 482 additions and 89 deletions
@@ -279,7 +279,7 @@
<div class="gl-fgroup"> <div class="gl-fgroup">
<label class="gl-flabel"><fa-icon icon="calendar-alt"></fa-icon> From Date</label> <label class="gl-flabel"><fa-icon icon="calendar-alt"></fa-icon> From Date</label>
<mat-form-field appearance="outline" class="gl-ffield"> <mat-form-field appearance="outline" class="gl-ffield">
<input matInput [matDatepicker]="dpFrom" [min]="minDate" [max]="maxDate" [formControl]="transactionDateFrom" /> <input matInput [matDatepicker]="dpFrom" [min]="minDate" [formControl]="transactionDateFrom" />
<mat-datepicker-toggle matSuffix [for]="dpFrom"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dpFrom"></mat-datepicker-toggle>
<mat-datepicker #dpFrom></mat-datepicker> <mat-datepicker #dpFrom></mat-datepicker>
</mat-form-field> </mat-form-field>
@@ -288,7 +288,7 @@
<div class="gl-fgroup"> <div class="gl-fgroup">
<label class="gl-flabel"><fa-icon icon="calendar-alt"></fa-icon> To Date</label> <label class="gl-flabel"><fa-icon icon="calendar-alt"></fa-icon> To Date</label>
<mat-form-field appearance="outline" class="gl-ffield"> <mat-form-field appearance="outline" class="gl-ffield">
<input matInput [matDatepicker]="dpTo" [min]="minDate" [max]="maxDate" [formControl]="transactionDateTo" /> <input matInput [matDatepicker]="dpTo" [min]="minDate" [formControl]="transactionDateTo" />
<mat-datepicker-toggle matSuffix [for]="dpTo"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dpTo"></mat-datepicker-toggle>
<mat-datepicker #dpTo></mat-datepicker> <mat-datepicker #dpTo></mat-datepicker>
</mat-form-field> </mat-form-field>
@@ -357,7 +357,6 @@
matInput matInput
[matDatepicker]="dpSubFrom" [matDatepicker]="dpSubFrom"
[min]="minDate" [min]="minDate"
[max]="maxDate"
[formControl]="submittedOnDateFrom" [formControl]="submittedOnDateFrom"
/> />
<mat-datepicker-toggle matSuffix [for]="dpSubFrom"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dpSubFrom"></mat-datepicker-toggle>
@@ -368,7 +367,7 @@
<div class="gl-fgroup"> <div class="gl-fgroup">
<label class="gl-flabel"><fa-icon icon="calendar-check"></fa-icon> Submitted To</label> <label class="gl-flabel"><fa-icon icon="calendar-check"></fa-icon> Submitted To</label>
<mat-form-field appearance="outline" class="gl-ffield"> <mat-form-field appearance="outline" class="gl-ffield">
<input matInput [matDatepicker]="dpSubTo" [min]="minDate" [max]="maxDate" [formControl]="submittedOnDateTo" /> <input matInput [matDatepicker]="dpSubTo" [min]="minDate" [formControl]="submittedOnDateTo" />
<mat-datepicker-toggle matSuffix [for]="dpSubTo"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dpSubTo"></mat-datepicker-toggle>
<mat-datepicker #dpSubTo></mat-datepicker> <mat-datepicker #dpSubTo></mat-datepicker>
</mat-form-field> </mat-form-field>
@@ -90,7 +90,7 @@ export class SearchJournalEntryComponent implements OnInit, OnDestroy, AfterView
// ── Misc ────────────────────────────────────────────────────────────────── // ── Misc ──────────────────────────────────────────────────────────────────
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
maxDate = new Date(); maxDate: Date = null;
/** Accounting features switched off for this installation — see disabled-features.ts. */ /** Accounting features switched off for this installation — see disabled-features.ts. */
disabledFeatures = DISABLED_ACCOUNTING_FEATURES; disabledFeatures = DISABLED_ACCOUNTING_FEATURES;
@@ -208,7 +208,7 @@ export class SearchJournalEntryComponent implements OnInit, OnDestroy, AfterView
} }
ngOnInit() { ngOnInit() {
this.maxDate = this.settingsService.businessDate; this.maxDate = null;
this.paginator.pageSize = 10; this.paginator.pageSize = 10;
this.restoreState(); this.restoreState();
this.setFilteredOffices(); this.setFilteredOffices();
@@ -1321,6 +1321,11 @@
kycForm.get('kycDocumentNumber')?.hasError('pattern') && kycForm.get('kycDocumentNumber')?.touched kycForm.get('kycDocumentNumber')?.hasError('pattern') && kycForm.get('kycDocumentNumber')?.touched
) { ) {
<mat-error>Only letters, numbers, hyphens and slashes allowed</mat-error> <mat-error>Only letters, numbers, hyphens and slashes allowed</mat-error>
} @else if (
kycForm.get('kycDocumentNumber')?.hasError('aadhaarLast4Mismatch') &&
kycForm.get('kycDocumentNumber')?.touched
) {
<mat-error>Last 4 digits do not match the Aadhaar number entered above</mat-error>
} }
</mat-form-field> </mat-form-field>
@@ -8,7 +8,14 @@
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core'; import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
import { forkJoin, catchError, of } from 'rxjs'; import { forkJoin, catchError, of } from 'rxjs';
import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms'; import {
UntypedFormBuilder,
UntypedFormGroup,
Validators,
FormsModule,
AbstractControl,
ValidationErrors
} from '@angular/forms';
import { import {
MatDialog, MatDialog,
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
@@ -567,6 +574,28 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
/** Existing signature document ID (for re-upload/delete) */ /** Existing signature document ID (for re-upload/delete) */
existingSignatureDocId: string = ''; existingSignatureDocId: string = '';
/**
* Cross-field check for the OVD "Document Number" field (kycDocumentNumber): when Aadhaar Card
* is the selected KYC document, its value must identify the same Aadhaar as the aadhaarNumber
* field captured earlier in this step - the last 4 digits are what's compared, since
* aadhaarNumber may hold either the raw 12-digit number or a masked value (masking only ever
* obscures the first 8 digits - see aadhaarMasked in the kycData getter below - so the last 4
* characters are always the real digits either way). Not applicable to any other document type,
* where kycDocumentNumber is unrelated to Aadhaar entirely.
*/
private aadhaarLast4MatchValidator = (control: AbstractControl): ValidationErrors | null => {
if (this.selectedKycDoc?.code !== 'AADHAAR') {
return null;
}
const docNumber = (control.value || '').toString().trim();
const aadhaarValue = (control.parent?.get('aadhaarNumber')?.value || '').toString().trim();
if (docNumber.length < 4 || aadhaarValue.length < 4) {
// Required/pattern validators already flag an empty or too-short value on their own.
return null;
}
return docNumber.slice(-4) === aadhaarValue.slice(-4) ? null : { aadhaarLast4Mismatch: true };
};
/** KYC Form */ /** KYC Form */
kycForm: UntypedFormGroup = this.formBuilder.group({ kycForm: UntypedFormGroup = this.formBuilder.group({
hasPan: [true], hasPan: [true],
@@ -616,7 +645,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
[ [
Validators.required, Validators.required,
Validators.pattern(/^[A-Za-z0-9\-\/\s]+$/), Validators.pattern(/^[A-Za-z0-9\-\/\s]+$/),
Validators.maxLength(30) Validators.maxLength(30),
this.aadhaarLast4MatchValidator
] ]
], ],
kycDocIssueDate: [''], kycDocIssueDate: [''],
@@ -739,6 +769,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
this.kycForm.get('ecgcSalFlag')?.disable(); this.kycForm.get('ecgcSalFlag')?.disable();
this.kycForm.get('dormantFlag')?.disable(); this.kycForm.get('dormantFlag')?.disable();
} }
// aadhaarLast4MatchValidator (on kycDocumentNumber) reads this control's value directly rather
// than through its own valueChanges, so a re-check has to be triggered explicitly whenever the
// Aadhaar field itself changes - otherwise fixing aadhaarNumber after kycDocumentNumber was
// already entered would leave a stale mismatch error in place.
this.kycForm.get('aadhaarNumber')?.valueChanges.subscribe(() => {
this.kycForm.get('kycDocumentNumber')?.updateValueAndValidity({ onlySelf: true });
});
} }
/** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs, /** Seed the document-type picker from defaults (entity RBI docs vs. individual OVDs,
@@ -1009,6 +1047,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
// Reset uploads + merged preview when document type changes // Reset uploads + merged preview when document type changes
this.kycDocFiles = { front: null, back: null }; this.kycDocFiles = { front: null, back: null };
this.kycDocMergedPreview = null; this.kycDocMergedPreview = null;
// aadhaarLast4MatchValidator only applies while Aadhaar Card is selected - re-check so switching
// to/away from it immediately picks up (or clears) the mismatch error.
this.kycForm.get('kycDocumentNumber')?.updateValueAndValidity({ onlySelf: true });
} }
isKycDocSelected(proof: ProofType): boolean { isKycDocSelected(proof: ProofType): boolean {
@@ -1189,6 +1230,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
// Sync chip-picker selection with the prefilled document name // Sync chip-picker selection with the prefilled document name
if (data.kycDocumentName) { if (data.kycDocumentName) {
this.selectedKycDoc = this.findKycDocProofByName(data.kycDocumentName); this.selectedKycDoc = this.findKycDocProofByName(data.kycDocumentName);
// patchValue above ran kycDocumentNumber's validators before selectedKycDoc was known,
// so aadhaarLast4MatchValidator needs a fresh check now that it is.
this.kycForm.get('kycDocumentNumber')?.updateValueAndValidity({ onlySelf: true });
} }
// Set verification statuses from existing data // Set verification statuses from existing data
if (data.panVerified) { if (data.panVerified) {
@@ -24,6 +24,7 @@ import { UntypedFormBuilder, UntypedFormGroup, UntypedFormControl, ValidatorFn,
/** Custom Services */ /** Custom Services */
import { FixedDepositsService } from '../../../fixed-deposits.service'; import { FixedDepositsService } from '../../../fixed-deposits.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { FormatNumberPipe } from '../../../../../pipes/format-number.pipe'; import { FormatNumberPipe } from '../../../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -97,6 +98,7 @@ export class PaymentDialogComponent implements OnInit {
private fixedDepositsService = inject(FixedDepositsService); private fixedDepositsService = inject(FixedDepositsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
private alertService = inject(AlertService);
paymentForm: UntypedFormGroup; paymentForm: UntypedFormGroup;
/** Loaded once, on open, from the same closure template the existing Close action already uses. */ /** Loaded once, on open, from the same closure template the existing Close action already uses. */
@@ -375,7 +377,16 @@ export class PaymentDialogComponent implements OnInit {
this.fixedDepositsService this.fixedDepositsService
.executeFixedDepositsAccountCommand(this.data.accountId as string, 'payment', data) .executeFixedDepositsAccountCommand(this.data.accountId as string, 'payment', data)
.subscribe({ .subscribe({
next: () => { next: (response: any) => {
// Parked in the Checker Inbox rather than executed -- see the same handling in
// PrematureCloseDialogComponent for why an HTTP 200 here can still mean "not applied yet".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'This payment has been submitted for approval. The deposit is unchanged until a checker approves it.'
});
}
this.dialogRef.close({ confirm: true }); this.dialogRef.close({ confirm: true });
}, },
// Keep the dialog open with the entered values and say what went wrong, rather than leaving a // Keep the dialog open with the entered values and say what went wrong, rather than leaving a
@@ -140,7 +140,9 @@
<span class="pcd__preview-label" <span class="pcd__preview-label"
>Interest Earned (to {{ 'labels.inputs.Premature Close Date' | translate }})</span >Interest Earned (to {{ 'labels.inputs.Premature Close Date' | translate }})</span
> >
<span class="pcd__preview-value">+ {{ interestEarned | formatNumber }}</span> <span class="pcd__preview-value"
>{{ interestEarned >= 0 ? '+' : '' }} {{ interestEarned | formatNumber }}</span
>
</div> </div>
} }
<div class="pcd__preview-row pcd__preview-row--total"> <div class="pcd__preview-row pcd__preview-row--total">
@@ -205,8 +207,8 @@
@if (noDestinationAccounts) { @if (noDestinationAccounts) {
<p class="pcd__hint pcd__hint--warn" role="alert"> <p class="pcd__hint pcd__hint--warn" role="alert">
<fa-icon icon="info-circle" class="m-r-5"></fa-icon> <fa-icon icon="info-circle" class="m-r-5"></fa-icon>
This customer has no active savings account to transfer into. Open one first, or choose an action that This customer has no active savings account to transfer into. Premature closure has to be paid into a
pays out in cash instead. savings account, so open one for this customer before closing this deposit.
</p> </p>
} }
@@ -229,6 +231,16 @@
</mat-form-field> </mat-form-field>
} }
@if (prematureCloseForm.contains('toGlAccountId')) {
<mifosx-gl-account-selector
[inputFormControl]="toGlAccountControl"
[glAccountList]="glAccountsData"
[required]="true"
[inputLabel]="'Transfer to GL Account'"
>
</mifosx-gl-account-selector>
}
@if (prematureCloseForm.contains('transferDescription')) { @if (prematureCloseForm.contains('transferDescription')) {
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Transfer Description' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Transfer Description' | translate }}</mat-label>
@@ -23,13 +23,19 @@ import { UntypedFormBuilder, UntypedFormGroup, UntypedFormControl, Validators }
/** Custom Services */ /** Custom Services */
import { FixedDepositsService } from '../../../fixed-deposits.service'; import { FixedDepositsService } from '../../../fixed-deposits.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { AccountingService } from 'app/accounting/accounting.service';
import { AlertService } from 'app/core/alert/alert.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { FormatNumberPipe } from '../../../../../pipes/format-number.pipe'; import { FormatNumberPipe } from '../../../../../pipes/format-number.pipe';
import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component';
import { GLAccount } from 'app/shared/models/general.model';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/** /**
* "Premature Close" dialog -- Active-only, no reinvest option (the deposit hasn't matured, so there's * "Premature Close" dialog -- Active-only, no reinvest option (the deposit hasn't matured, so there's
* nothing to renew): pay out in cash, or transfer the pre-maturity balance to a savings account. Same * nothing to renew): transfer the pre-maturity balance to a savings account, or post it to a GL account.
* There is deliberately no cash payout -- the backend does not offer or accept one for a premature
* closure, so the proceeds always stay traceable to an account. Same
* `.pcd__*` visual language as the Payment dialog ({@link PaymentDialogComponent}), in this Danger Zone * `.pcd__*` visual language as the Payment dialog ({@link PaymentDialogComponent}), in this Danger Zone
* action's own red accent rather than Payment's blue, since it's an irreversible, usually * action's own red accent rather than Payment's blue, since it's an irreversible, usually
* penalty-bearing action rather than a routine one. * penalty-bearing action rather than a routine one.
@@ -49,22 +55,26 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatDialogClose, MatDialogClose,
CdkTextareaAutosize, CdkTextareaAutosize,
FaIconComponent, FaIconComponent,
FormatNumberPipe FormatNumberPipe,
GlAccountSelectorComponent
] ]
}) })
export class PrematureCloseDialogComponent implements OnInit { export class PrematureCloseDialogComponent implements OnInit {
/** The `onAccountClosureId` codes Premature Close can offer -- same codes Payment/Close use. */ /** The `onAccountClosureId` codes Premature Close can offer -- same codes Payment/Close use. */
private static readonly WITHDRAW_DEPOSIT_ID = 100; private static readonly WITHDRAW_DEPOSIT_ID = 100;
private static readonly TRANSFER_TO_SAVINGS_ID = 200; private static readonly TRANSFER_TO_SAVINGS_ID = 200;
private static readonly TRANSFER_TO_GL_ID = 600;
private static readonly ACTION_ICONS: Record<number, string> = { private static readonly ACTION_ICONS: Record<number, string> = {
[PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'money-bill-wave', [PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'money-bill-wave',
[PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'wallet' [PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'wallet',
[PrematureCloseDialogComponent.TRANSFER_TO_GL_ID]: 'book'
}; };
private static readonly ACTION_DESCRIPTIONS: Record<number, string> = { private static readonly ACTION_DESCRIPTIONS: Record<number, string> = {
[PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'Pay out the full balance in cash', [PrematureCloseDialogComponent.WITHDRAW_DEPOSIT_ID]: 'Pay out the full balance in cash',
[PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'Move the full balance to another savings account' [PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'Move the full balance to another savings account',
[PrematureCloseDialogComponent.TRANSFER_TO_GL_ID]: 'Post the full balance to a GL account'
}; };
/** /**
@@ -108,11 +118,15 @@ export class PrematureCloseDialogComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private fixedDepositsService = inject(FixedDepositsService); private fixedDepositsService = inject(FixedDepositsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private accountingService = inject(AccountingService);
private alertService = inject(AlertService);
private dateUtils = inject(Dates); private dateUtils = inject(Dates);
prematureCloseForm: UntypedFormGroup; prematureCloseForm: UntypedFormGroup;
onAccountClosureOptions: any[] = []; onAccountClosureOptions: any[] = [];
savingsAccountsData: any[] = []; savingsAccountsData: any[] = [];
/** Postable GL accounts, loaded once -- only read when the GL action is chosen. */
glAccountsData: GLAccount[] = [];
maturityAmount: number = null; maturityAmount: number = null;
/** The rate actually resolved for THIS preview by the backend (actual-period vs contracted, /** The rate actually resolved for THIS preview by the backend (actual-period vs contracted,
* penalty-deducted or not) -- distinct from data.nominalAnnualInterestRate, which is always the * penalty-deducted or not) -- distinct from data.nominalAnnualInterestRate, which is always the
@@ -228,19 +242,50 @@ export class PrematureCloseDialogComponent implements OnInit {
}); });
} }
/** Adds/removes the destination-account control to match what the chosen type needs. */ /** Adds/removes the destination control to match what the chosen type needs. */
private onClosureTypeChange(id: number): void { private onClosureTypeChange(id: number): void {
if (id === PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID) { const toSavings = id === PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID;
if (!this.prematureCloseForm.contains('toSavingsAccountId')) { const toGl = id === PrematureCloseDialogComponent.TRANSFER_TO_GL_ID;
this.prematureCloseForm.addControl('toSavingsAccountId', new UntypedFormControl('', Validators.required));
this.prematureCloseForm.addControl('transferDescription', new UntypedFormControl('')); if (toSavings && !this.prematureCloseForm.contains('toSavingsAccountId')) {
} this.prematureCloseForm.addControl('toSavingsAccountId', new UntypedFormControl('', Validators.required));
} else { } else if (!toSavings) {
this.prematureCloseForm.removeControl('toSavingsAccountId'); this.prematureCloseForm.removeControl('toSavingsAccountId');
}
if (toGl) {
if (!this.prematureCloseForm.contains('toGlAccountId')) {
this.prematureCloseForm.addControl('toGlAccountId', new UntypedFormControl('', Validators.required));
}
this.loadGlAccounts();
} else {
this.prematureCloseForm.removeControl('toGlAccountId');
}
// Both destinations carry the description through -- to the account transfer, or to the narration on
// the journal entry that credits the GL account.
if ((toSavings || toGl) && !this.prematureCloseForm.contains('transferDescription')) {
this.prematureCloseForm.addControl('transferDescription', new UntypedFormControl(''));
} else if (!toSavings && !toGl) {
this.prematureCloseForm.removeControl('transferDescription'); this.prematureCloseForm.removeControl('transferDescription');
} }
} }
/** Fetched on first use rather than with the dialog, since most closures never pick the GL action. */
private loadGlAccounts(): void {
if (this.glAccountsData.length) {
return;
}
this.accountingService.getGlAccounts().subscribe((accounts: GLAccount[]) => {
this.glAccountsData = accounts ?? [];
});
}
/** The GL selector takes a control instance, not a control name. */
get toGlAccountControl(): UntypedFormControl {
return this.prematureCloseForm.get('toGlAccountId') as UntypedFormControl;
}
/** The gap between the deposit and the computed maturity amount -- the actual interest this premature /** The gap between the deposit and the computed maturity amount -- the actual interest this premature
* closure earns, at whatever rate (contracted, or the lower pre-closure-penalty one below) applied. */ * closure earns, at whatever rate (contracted, or the lower pre-closure-penalty one below) applied. */
get interestEarned(): number | null { get interestEarned(): number | null {
@@ -373,7 +418,18 @@ export class PrematureCloseDialogComponent implements OnInit {
this.fixedDepositsService this.fixedDepositsService
.executeFixedDepositsAccountCommand(this.data.accountId as string, 'prematureClose', data) .executeFixedDepositsAccountCommand(this.data.accountId as string, 'prematureClose', data)
.subscribe({ .subscribe({
next: () => { next: (response: any) => {
// A command on a maker-checker permission is parked in the Checker Inbox, not executed:
// the platform rolls its transaction back and still answers HTTP 200 with
// rollbackTransaction: true. Without saying so, the dialog closes and the account reloads
// unchanged -- which reads as "nothing happened" rather than "waiting for approval".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'Premature closure has been submitted for approval. The deposit is unchanged until a checker approves it.'
});
}
this.dialogRef.close({ confirm: true }); this.dialogRef.close({ confirm: true });
}, },
// Keep the dialog open with the entered values and say what went wrong, rather than leaving a // Keep the dialog open with the entered values and say what went wrong, rather than leaving a
@@ -30,6 +30,7 @@ import { AccountsService } from 'app/accounts/accounts.service';
import { Currency } from 'app/shared/models/general.model'; import { Currency } from 'app/shared/models/general.model';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
import { AuthenticationService } from '../../../core/authentication/authentication.service'; import { AuthenticationService } from '../../../core/authentication/authentication.service';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { NgClass, CurrencyPipe } from '@angular/common'; import { NgClass, CurrencyPipe } from '@angular/common';
@@ -74,6 +75,7 @@ import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dor
export class FixedDepositAccountViewComponent implements OnInit { export class FixedDepositAccountViewComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private alertService = inject(AlertService);
private fixedDepositsService = inject(FixedDepositsService); private fixedDepositsService = inject(FixedDepositsService);
private savingsService = inject(SavingsService); private savingsService = inject(SavingsService);
private accountsService = inject(AccountsService); private accountsService = inject(AccountsService);
@@ -419,7 +421,16 @@ export class FixedDepositAccountViewComponent implements OnInit {
if (response.confirm) { if (response.confirm) {
this.fixedDepositsService this.fixedDepositsService
.executeFixedDepositsAccountCommand(this.fixedDepositsAccountData.id, 'postInterest', {}) .executeFixedDepositsAccountCommand(this.fixedDepositsAccountData.id, 'postInterest', {})
.subscribe(() => { .subscribe((commandResponse: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with
// rollbackTransaction: true), so the reload below shows no new interest posting.
if (commandResponse?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'Interest posting has been sent for checker approval. No interest is posted until a checker approves it.'
});
}
this.reload(); this.reload();
}); });
} }
@@ -11,6 +11,7 @@ import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
/** Custom Services */ /** Custom Services */
import { FixedDepositsService } from 'app/deposits/fixed-deposits/fixed-deposits.service'; import { FixedDepositsService } from 'app/deposits/fixed-deposits/fixed-deposits.service';
@@ -37,6 +38,7 @@ export class ApproveFixedDepositsAccountComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Minimum date allowed. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -98,17 +100,31 @@ export class ApproveFixedDepositsAccountComponent implements OnInit {
dateFormat, dateFormat,
locale locale
}; };
this.fixedDepositsService.executeFixedDepositsAccountCommand(this.accountId, 'approve', data).subscribe(() => { this.fixedDepositsService
const activationData = { .executeFixedDepositsAccountCommand(this.accountId, 'approve', data)
activatedOnDate: approveFixedDepositsAccountFormData.approvedOnDate, .subscribe((response: any) => {
dateFormat, // Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true).
locale // The account is therefore still pending, so chaining activation onto it would be applied to an
}; // unapproved account -- stop here and say what is actually waiting.
this.fixedDepositsService if (response?.rollbackTransaction) {
.executeFixedDepositsAccountCommand(this.accountId, 'activate', activationData) this.alertService.alert({
.subscribe(() => { type: 'Success',
message:
'This approval has been sent for checker approval. The account stays pending until a checker approves it.'
});
this.router.navigate(['../../'], { relativeTo: this.route }); this.router.navigate(['../../'], { relativeTo: this.route });
}); return;
}); }
const activationData = {
activatedOnDate: approveFixedDepositsAccountFormData.approvedOnDate,
dateFormat,
locale
};
this.fixedDepositsService
.executeFixedDepositsAccountCommand(this.accountId, 'activate', activationData)
.subscribe(() => {
this.router.navigate(['../../'], { relativeTo: this.route });
});
});
} }
} }
@@ -21,6 +21,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { FixedDepositsService } from '../../fixed-deposits.service'; import { FixedDepositsService } from '../../fixed-deposits.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -43,6 +44,7 @@ export class CloseFixedDepositsAccountComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Minimum date allowed. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -139,8 +141,18 @@ export class CloseFixedDepositsAccountComponent implements OnInit {
dateFormat, dateFormat,
locale locale
}; };
this.fixedDepositsService.executeFixedDepositsAccountCommand(this.accountId, 'close', data).subscribe(() => { this.fixedDepositsService
this.router.navigate(['../../'], { relativeTo: this.route }); .executeFixedDepositsAccountCommand(this.accountId, 'close', data)
}); .subscribe((response: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true),
// so the deposit reloads unchanged -- say so instead of letting that read as "nothing happened".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message: 'Closure has been sent for checker approval. The deposit is unchanged until a checker approves it.'
});
}
this.router.navigate(['../../'], { relativeTo: this.route });
});
} }
} }
@@ -11,6 +11,7 @@ import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
/** Custom Services */ /** Custom Services */
import { FixedDepositsService } from 'app/deposits/fixed-deposits/fixed-deposits.service'; import { FixedDepositsService } from 'app/deposits/fixed-deposits/fixed-deposits.service';
@@ -37,6 +38,7 @@ export class RejectFixedDepositsAccountComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Minimum date allowed. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -97,8 +99,19 @@ export class RejectFixedDepositsAccountComponent implements OnInit {
dateFormat, dateFormat,
locale locale
}; };
this.fixedDepositsService.executeFixedDepositsAccountCommand(this.accountId, 'reject', data).subscribe(() => { this.fixedDepositsService
this.router.navigate(['../../'], { relativeTo: this.route }); .executeFixedDepositsAccountCommand(this.accountId, 'reject', data)
}); .subscribe((response: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true),
// so the application is still open -- say so instead of letting that read as "nothing happened".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'Rejection has been sent for checker approval. The application is unchanged until a checker approves it.'
});
}
this.router.navigate(['../../'], { relativeTo: this.route });
});
} }
} }
@@ -12,6 +12,7 @@ import { UntypedFormGroup, UntypedFormBuilder, ReactiveFormsModule } from '@angu
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
/** Custom Services */ /** Custom Services */
import { AlertService } from 'app/core/alert/alert.service';
import { SavingsService } from 'app/savings/savings.service'; import { SavingsService } from 'app/savings/savings.service';
import { FixedDepositsService } from '../../fixed-deposits.service'; import { FixedDepositsService } from '../../fixed-deposits.service';
import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { CdkTextareaAutosize } from '@angular/cdk/text-field';
@@ -35,6 +36,7 @@ export class UndoApprovalFixedDepositsAccountComponent implements OnInit {
private fixedDepositsService = inject(FixedDepositsService); private fixedDepositsService = inject(FixedDepositsService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private alertService = inject(AlertService);
/** Undo Approval Fixed Deposits Account form. */ /** Undo Approval Fixed Deposits Account form. */
undoApprovalFixedDepositsAccountForm: UntypedFormGroup; undoApprovalFixedDepositsAccountForm: UntypedFormGroup;
@@ -87,12 +89,30 @@ export class UndoApprovalFixedDepositsAccountComponent implements OnInit {
if (this.undoAction === 'Undo Activation') { if (this.undoAction === 'Undo Activation') {
this.fixedDepositsService this.fixedDepositsService
.executeFixedDepositsAccountCommand(this.accountId, this.undoCommand, data) .executeFixedDepositsAccountCommand(this.accountId, this.undoCommand, data)
.subscribe(() => { .subscribe((response: any) => {
this.notifyIfSentForApproval(response);
this.router.navigate(['../../'], { relativeTo: this.route }); this.router.navigate(['../../'], { relativeTo: this.route });
}); });
} else { } else {
this.savingsService.executeSavingsAccountCommand(this.accountId, this.undoCommand, data).subscribe(() => { this.savingsService
this.router.navigate(['../../'], { relativeTo: this.route }); .executeSavingsAccountCommand(this.accountId, this.undoCommand, data)
.subscribe((response: any) => {
this.notifyIfSentForApproval(response);
this.router.navigate(['../../'], { relativeTo: this.route });
});
}
}
/**
* Both undo commands sit on maker-checker permissions, so either can come back parked in the
* Checker Inbox rather than executed -- HTTP 200 with rollbackTransaction: true, and the account
* reloaded unchanged. Without this the navigation away reads as "it worked".
*/
private notifyIfSentForApproval(response: any): void {
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message: `${this.undoAction} has been sent for checker approval. The account is unchanged until a checker approves it.`
}); });
} }
} }
@@ -11,6 +11,7 @@ import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
/** Custom Services */ /** Custom Services */
import { FixedDepositsService } from 'app/deposits/fixed-deposits/fixed-deposits.service'; import { FixedDepositsService } from 'app/deposits/fixed-deposits/fixed-deposits.service';
@@ -37,6 +38,7 @@ export class WithdrawByClientFixedDepositsAccountComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Minimum date allowed. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -99,7 +101,16 @@ export class WithdrawByClientFixedDepositsAccountComponent implements OnInit {
}; };
this.fixedDepositsService this.fixedDepositsService
.executeFixedDepositsAccountCommand(this.accountId, 'withdrawnByApplicant', data) .executeFixedDepositsAccountCommand(this.accountId, 'withdrawnByApplicant', data)
.subscribe(() => { .subscribe((response: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true),
// so the application is still open -- say so instead of letting that read as "nothing happened".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'Withdrawal has been sent for checker approval. The application is unchanged until a checker approves it.'
});
}
this.router.navigate(['../../'], { relativeTo: this.route }); this.router.navigate(['../../'], { relativeTo: this.route });
}); });
} }
@@ -24,6 +24,10 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income'; type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income';
const SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME = 'Savings Interest Payable';
const FIXED_DEPOSIT_INTEREST_PAID_ACCOUNT_NAME = 'Fixed Deposit Interest Paid';
const SAVINGS_INTEREST_PAID_ACCOUNT_NAME = 'Savings Interest Paid';
type AccountingRowConfig = { type AccountingRowConfig = {
key: string; key: string;
label: string; label: string;
@@ -128,6 +132,7 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
feesReceivableAccountId: [''], feesReceivableAccountId: [''],
penaltiesReceivableAccountId: [''], penaltiesReceivableAccountId: [''],
interestPayableAccountId: [''], interestPayableAccountId: [''],
savingsInterestPaidAccountId: [''],
escheatLiabilityId: [''], escheatLiabilityId: [''],
assetsOtherGlMappings: this.formBuilder.array([]), assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]), liabilitiesOtherGlMappings: this.formBuilder.array([]),
@@ -158,6 +163,20 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
return ''; return '';
} }
private extractNamedGlMapping(
otherGlMappings: any[],
accountName: string,
controlName: string,
options: GLAccount[]
): any[] {
const match = (otherGlMappings || []).find((mapping) => mapping?.accountName === accountName);
if (match) {
const glAccountId = match.glAccountId || options.find((option) => option.glCode === match.glCode)?.id || '';
this.fixedDepositProductAccountingForm.get(controlName)?.patchValue(glAccountId);
}
return (otherGlMappings || []).filter((mapping) => mapping?.accountName !== accountName);
}
private patchAccountingValues(mappings: any) { private patchAccountingValues(mappings: any) {
this.fixedDepositProductAccountingForm.patchValue({ this.fixedDepositProductAccountingForm.patchValue({
savingsReferenceAccountId: this.extractAccountId( savingsReferenceAccountId: this.extractAccountId(
@@ -200,7 +219,18 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
glMappings?.liabilities || [], glMappings?.liabilities || [],
this.liabilityAccountOptions this.liabilityAccountOptions
); );
this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions); const fromExpenses = this.extractNamedGlMapping(
this.extractNamedGlMapping(
glMappings?.expenses || [],
FIXED_DEPOSIT_INTEREST_PAID_ACCOUNT_NAME,
'savingsInterestPaidAccountId',
this.expenseAccountOptions
),
SAVINGS_INTEREST_PAID_ACCOUNT_NAME,
'savingsInterestPaidAccountId',
this.expenseAccountOptions
);
this.setOtherGlMappings('expensesOtherGlMappings', fromExpenses, this.expenseAccountOptions);
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
} }
} }
@@ -266,10 +296,26 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
})) }))
.filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode);
const namedAsOtherGlMapping = (controlName: string, accountName: string, options: GLAccount[]) => {
const glAccountId = this.fixedDepositProductAccountingForm.get(controlName)?.value;
if (!glAccountId) {
return [];
}
const glCode = options.find((o) => o.id === glAccountId)?.glCode || '';
return glCode ? [{ accountName, glCode }] : [];
};
const otherGlMappings = { const otherGlMappings = {
assets: map('assetsOtherGlMappings', this.assetAccountOptions), assets: map('assetsOtherGlMappings', this.assetAccountOptions),
liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions), liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions),
expenses: map('expensesOtherGlMappings', this.expenseAccountOptions), expenses: [
...namedAsOtherGlMapping(
'savingsInterestPaidAccountId',
FIXED_DEPOSIT_INTEREST_PAID_ACCOUNT_NAME,
this.expenseAccountOptions
),
...map('expensesOtherGlMappings', this.expenseAccountOptions)
],
income: map('incomeOtherGlMappings', this.incomeAccountOptions) income: map('incomeOtherGlMappings', this.incomeAccountOptions)
}; };
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null; return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null;
@@ -416,6 +462,13 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
controlName: 'interestOnSavingsAccountId', controlName: 'interestOnSavingsAccountId',
options: this.expenseAccountOptions, options: this.expenseAccountOptions,
required: true required: true
},
{
key: 'savingsInterestPaidAccountId',
label: 'Fixed Deposit Interest Paid',
inputLabel: 'Fixed deposit interest paid',
controlName: 'savingsInterestPaidAccountId',
options: this.expenseAccountOptions
} }
], ],
otherMappingsControlName: 'expensesOtherGlMappings', otherMappingsControlName: 'expensesOtherGlMappings',
@@ -456,6 +509,7 @@ export class FixedDepositProductAccountingStepComponent implements OnInit, OnCha
liabilitiesOtherGlMappings, liabilitiesOtherGlMappings,
expensesOtherGlMappings, expensesOtherGlMappings,
incomeOtherGlMappings, incomeOtherGlMappings,
savingsInterestPaidAccountId,
...accounting ...accounting
} = this.fixedDepositProductAccountingForm.value; } = this.fixedDepositProductAccountingForm.value;
const otherGlMappings = this.getOtherGlMappings(); const otherGlMappings = this.getOtherGlMappings();
@@ -24,6 +24,10 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income'; type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income';
const SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME = 'Savings Interest Payable';
const RECURRING_DEPOSIT_INTEREST_PAID_ACCOUNT_NAME = 'Recurring Deposit Interest Paid';
const SAVINGS_INTEREST_PAID_ACCOUNT_NAME = 'Savings Interest Paid';
type AccountingRowConfig = { type AccountingRowConfig = {
key: string; key: string;
label: string; label: string;
@@ -122,6 +126,7 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
feesReceivableAccountId: [''], feesReceivableAccountId: [''],
penaltiesReceivableAccountId: [''], penaltiesReceivableAccountId: [''],
interestPayableAccountId: [''], interestPayableAccountId: [''],
savingsInterestPaidAccountId: [''],
escheatLiabilityId: [''], escheatLiabilityId: [''],
assetsOtherGlMappings: this.formBuilder.array([]), assetsOtherGlMappings: this.formBuilder.array([]),
liabilitiesOtherGlMappings: this.formBuilder.array([]), liabilitiesOtherGlMappings: this.formBuilder.array([]),
@@ -136,6 +141,20 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
}); });
} }
private extractNamedGlMapping(
otherGlMappings: any[],
accountName: string,
controlName: string,
options: GLAccount[]
): any[] {
const match = (otherGlMappings || []).find((mapping) => mapping?.accountName === accountName);
if (match) {
const glAccountId = match.glAccountId || options.find((option) => option.glCode === match.glCode)?.id || '';
this.recurringDepositProductAccountingForm.get(controlName)?.patchValue(glAccountId);
}
return (otherGlMappings || []).filter((mapping) => mapping?.accountName !== accountName);
}
private patchAccountingValues(mappings: any) { private patchAccountingValues(mappings: any) {
this.recurringDepositProductAccountingForm.patchValue({ this.recurringDepositProductAccountingForm.patchValue({
savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '', savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
@@ -158,7 +177,18 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
glMappings?.liabilities || [], glMappings?.liabilities || [],
this.liabilityAccountOptions this.liabilityAccountOptions
); );
this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions); const fromExpenses = this.extractNamedGlMapping(
this.extractNamedGlMapping(
glMappings?.expenses || [],
RECURRING_DEPOSIT_INTEREST_PAID_ACCOUNT_NAME,
'savingsInterestPaidAccountId',
this.expenseAccountOptions
),
SAVINGS_INTEREST_PAID_ACCOUNT_NAME,
'savingsInterestPaidAccountId',
this.expenseAccountOptions
);
this.setOtherGlMappings('expensesOtherGlMappings', fromExpenses, this.expenseAccountOptions);
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
} }
} }
@@ -224,10 +254,26 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
})) }))
.filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode);
const namedAsOtherGlMapping = (controlName: string, accountName: string, options: GLAccount[]) => {
const glAccountId = this.recurringDepositProductAccountingForm.get(controlName)?.value;
if (!glAccountId) {
return [];
}
const glCode = options.find((o) => o.id === glAccountId)?.glCode || '';
return glCode ? [{ accountName, glCode }] : [];
};
const otherGlMappings = { const otherGlMappings = {
assets: map('assetsOtherGlMappings', this.assetAccountOptions), assets: map('assetsOtherGlMappings', this.assetAccountOptions),
liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions), liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions),
expenses: map('expensesOtherGlMappings', this.expenseAccountOptions), expenses: [
...namedAsOtherGlMapping(
'savingsInterestPaidAccountId',
RECURRING_DEPOSIT_INTEREST_PAID_ACCOUNT_NAME,
this.expenseAccountOptions
),
...map('expensesOtherGlMappings', this.expenseAccountOptions)
],
income: map('incomeOtherGlMappings', this.incomeAccountOptions) income: map('incomeOtherGlMappings', this.incomeAccountOptions)
}; };
return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null; return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null;
@@ -360,11 +406,18 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
rows: [ rows: [
{ {
key: 'interestOnSavingsAccountId', key: 'interestOnSavingsAccountId',
label: 'Interest On Savings Account', label: 'Interest On Recurring Deposits',
inputLabel: 'Interest on savings account', inputLabel: 'Interest on recurring deposits',
controlName: 'interestOnSavingsAccountId', controlName: 'interestOnSavingsAccountId',
options: this.expenseAccountOptions, options: this.expenseAccountOptions,
required: true required: true
},
{
key: 'savingsInterestPaidAccountId',
label: 'Recurring Deposit Interest Paid',
inputLabel: 'Recurring deposit interest paid',
controlName: 'savingsInterestPaidAccountId',
options: this.expenseAccountOptions
} }
], ],
otherMappingsControlName: 'expensesOtherGlMappings', otherMappingsControlName: 'expensesOtherGlMappings',
@@ -405,6 +458,7 @@ export class RecurringDepositProductAccountingStepComponent implements OnInit {
liabilitiesOtherGlMappings, liabilitiesOtherGlMappings,
expensesOtherGlMappings, expensesOtherGlMappings,
incomeOtherGlMappings, incomeOtherGlMappings,
savingsInterestPaidAccountId,
...accounting ...accounting
} = this.recurringDepositProductAccountingForm.value; } = this.recurringDepositProductAccountingForm.value;
const otherGlMappings = this.getOtherGlMappings(); const otherGlMappings = this.getOtherGlMappings();
@@ -88,7 +88,7 @@ export class SavingProductAccountingStepComponent implements OnInit {
liabilityAccountOptions: GLAccount[] = []; liabilityAccountOptions: GLAccount[] = [];
expenseAccountOptions: GLAccount[] = []; expenseAccountOptions: GLAccount[] = [];
incomeAccountOptions: GLAccount[] = []; incomeAccountOptions: GLAccount[] = [];
/** Savings Interest Paid nets to nil each posting, so it may be a liability clearing GL or an expense GL. */ /** Savings Interest Paid sits under Expenses and offers expense GLs; it nets to nil at each posting. */
paidAccountOptions: GLAccount[] = []; paidAccountOptions: GLAccount[] = [];
assetAccountOptions: GLAccount[] = []; assetAccountOptions: GLAccount[] = [];
paymentTypeOptions: any[] = []; paymentTypeOptions: any[] = [];
@@ -116,7 +116,7 @@ export class SavingProductAccountingStepComponent implements OnInit {
this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || [];
this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || [];
this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || [];
this.paidAccountOptions = this.liabilityAccountOptions.concat(this.expenseAccountOptions); this.paidAccountOptions = this.expenseAccountOptions;
this.paymentTypeOptions = this.savingProductsTemplate?.paymentTypeOptions || []; this.paymentTypeOptions = this.savingProductsTemplate?.paymentTypeOptions || [];
this.fundSourceAccountOptions = this.assetAccountOptions.concat(this.liabilityAccountOptions); this.fundSourceAccountOptions = this.assetAccountOptions.concat(this.liabilityAccountOptions);
this.buildAccountingSectionConfigs(); this.buildAccountingSectionConfigs();
@@ -204,9 +204,9 @@ export class SavingProductAccountingStepComponent implements OnInit {
// Backend serializes as 'otherGlMappingsMap'; fall back to 'otherGlMappings' for compatibility // Backend serializes as 'otherGlMappingsMap'; fall back to 'otherGlMappings' for compatibility
const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings; const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings;
this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions); this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions);
// Payable and Paid are liability rows, saved under liabilities. A product saved while they sat // Payable is a liability row, saved under liabilities; Paid an expense row, saved under
// under expenses still carries them there, so both buckets are read and the named entries are // expenses. Products saved before either move carry them in the other bucket, so both buckets
// stripped out of either one's "Other" rows. // are read and the named entries are stripped out of either one's "Other" rows.
const fromLiabilities = this.extractNamedGlMapping( const fromLiabilities = this.extractNamedGlMapping(
this.extractNamedGlMapping( this.extractNamedGlMapping(
glMappings?.liabilities || [], glMappings?.liabilities || [],
@@ -219,12 +219,13 @@ export class SavingProductAccountingStepComponent implements OnInit {
this.paidAccountOptions this.paidAccountOptions
); );
this.setOtherGlMappings('liabilitiesOtherGlMappings', fromLiabilities, this.liabilityAccountOptions); this.setOtherGlMappings('liabilitiesOtherGlMappings', fromLiabilities, this.liabilityAccountOptions);
const fromExpenses = [ const fromExpenses = this.extractNamedGlMapping(
SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME, (glMappings?.expenses || []).filter(
SAVINGS_INTEREST_PAID_ACCOUNT_NAME (mapping: any) => mapping?.accountName !== SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME
].reduce( ),
(remaining, name) => remaining.filter((mapping: any) => mapping?.accountName !== name), SAVINGS_INTEREST_PAID_ACCOUNT_NAME,
glMappings?.expenses || [] 'savingsInterestPaidAccountId',
this.paidAccountOptions
); );
this.setOtherGlMappings('expensesOtherGlMappings', fromExpenses, this.expenseAccountOptions); this.setOtherGlMappings('expensesOtherGlMappings', fromExpenses, this.expenseAccountOptions);
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
@@ -340,14 +341,18 @@ export class SavingProductAccountingStepComponent implements OnInit {
SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME, SAVINGS_INTEREST_PAYABLE_ACCOUNT_NAME,
this.liabilityAccountOptions this.liabilityAccountOptions
), ),
...mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions)
],
// The backend finds Savings Interest Paid by name in any bucket; it sits under expenses
// because that is where the screen shows it.
expenses: [
...namedAsOtherGlMapping( ...namedAsOtherGlMapping(
'savingsInterestPaidAccountId', 'savingsInterestPaidAccountId',
SAVINGS_INTEREST_PAID_ACCOUNT_NAME, SAVINGS_INTEREST_PAID_ACCOUNT_NAME,
this.paidAccountOptions this.paidAccountOptions
), ),
...mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions) ...mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions)
], ],
expenses: mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions),
income: mapOtherGlMappings('incomeOtherGlMappings', this.incomeAccountOptions) income: mapOtherGlMappings('incomeOtherGlMappings', this.incomeAccountOptions)
}; };
@@ -537,16 +542,6 @@ export class SavingProductAccountingStepComponent implements OnInit {
options: this.liabilityAccountOptions, options: this.liabilityAccountOptions,
required: true required: true
}, },
{
// A pass-through between the payable and the members that closes each posting at nil, so a
// liability clearing GL and an expense GL both work; both kinds are offered.
key: 'savingsInterestPaidAccountId',
label: 'Savings Interest Paid',
inputLabel: 'Savings interest paid',
controlName: 'savingsInterestPaidAccountId',
options: this.paidAccountOptions,
required: true
},
{ {
key: 'interestPayableAccountId', key: 'interestPayableAccountId',
label: 'Interest Payable Account', label: 'Interest Payable Account',
@@ -582,6 +577,15 @@ export class SavingProductAccountingStepComponent implements OnInit {
options: this.expenseAccountOptions, options: this.expenseAccountOptions,
required: true required: true
}, },
{
// A pass-through between the payable and the members that closes each posting at nil.
key: 'savingsInterestPaidAccountId',
label: 'Savings Interest Paid',
inputLabel: 'Savings interest paid',
controlName: 'savingsInterestPaidAccountId',
options: this.paidAccountOptions,
required: true
},
{ {
// Cash has no paise, so a closing balance of 9,883.79 is handed over as 9,883 and the // Cash has no paise, so a closing balance of 9,883.79 is handed over as 9,883 and the
// 79 paise land here rather than on the member's own ledger. // 79 paise land here rather than on the member's own ledger.
@@ -11,6 +11,7 @@ import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { AlertService } from 'app/core/alert/alert.service';
/** Custom Services */ /** Custom Services */
import { SavingsService } from 'app/savings/savings.service'; import { SavingsService } from 'app/savings/savings.service';
@@ -37,6 +38,7 @@ export class ApproveSavingsAccountComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Minimum date allowed. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -98,7 +100,19 @@ export class ApproveSavingsAccountComponent implements OnInit {
dateFormat, dateFormat,
locale locale
}; };
this.savingsService.executeSavingsAccountCommand(this.accountId, 'approve', data).subscribe(() => { this.savingsService.executeSavingsAccountCommand(this.accountId, 'approve', data).subscribe((response: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true).
// The account is therefore still pending, so chaining activation onto it would be applied to an
// unapproved account -- stop here and say what is actually waiting.
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'This approval has been sent for checker approval. The account stays pending until a checker approves it.'
});
this.router.navigate(['../../transactions'], { relativeTo: this.route });
return;
}
const activationData = { const activationData = {
activatedOnDate: approveSavingsAccountFormData.approvedOnDate, activatedOnDate: approveSavingsAccountFormData.approvedOnDate,
dateFormat, dateFormat,
@@ -20,6 +20,7 @@ import { Dates } from 'app/core/utils/dates';
/** Custom Services */ /** Custom Services */
import { SavingsService } from 'app/savings/savings.service'; import { SavingsService } from 'app/savings/savings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { MatSlideToggle } from '@angular/material/slide-toggle'; import { MatSlideToggle } from '@angular/material/slide-toggle';
@@ -47,6 +48,7 @@ export class CloseSavingsAccountComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Minimum date allowed. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -158,7 +160,15 @@ export class CloseSavingsAccountComponent implements OnInit {
dateFormat, dateFormat,
locale locale
}; };
this.savingsService.executeSavingsAccountCommand(this.accountId, 'close', data).subscribe(() => { this.savingsService.executeSavingsAccountCommand(this.accountId, 'close', data).subscribe((response: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true),
// so the account is still open -- say so instead of letting that read as "nothing happened".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message: 'Closure has been sent for checker approval. The account is unchanged until a checker approves it.'
});
}
this.router.navigate(['../../transactions'], { relativeTo: this.route }); this.router.navigate(['../../transactions'], { relativeTo: this.route });
}); });
} }
@@ -14,6 +14,7 @@ import { Dates } from 'app/core/utils/dates';
/** Custom Services */ /** Custom Services */
import { SavingsService } from 'app/savings/savings.service'; import { SavingsService } from 'app/savings/savings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -37,6 +38,7 @@ export class RejectSavingsAccountComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Minimum date allowed. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -97,7 +99,16 @@ export class RejectSavingsAccountComponent implements OnInit {
dateFormat, dateFormat,
locale locale
}; };
this.savingsService.executeSavingsAccountCommand(this.accountId, 'reject', data).subscribe(() => { this.savingsService.executeSavingsAccountCommand(this.accountId, 'reject', data).subscribe((response: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true),
// so the application is still open -- say so instead of letting that read as "nothing happened".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'Rejection has been sent for checker approval. The application is unchanged until a checker approves it.'
});
}
this.router.navigate(['../../'], { relativeTo: this.route }); this.router.navigate(['../../'], { relativeTo: this.route });
}); });
} }
@@ -13,6 +13,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
/** Custom Services */ /** Custom Services */
import { SavingsService } from 'app/savings/savings.service'; import { SavingsService } from 'app/savings/savings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -33,6 +34,7 @@ export class UndoApprovalSavingsAccountComponent implements OnInit {
private savingsService = inject(SavingsService); private savingsService = inject(SavingsService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private alertService = inject(AlertService);
/** Undo Approval Savings Account form. */ /** Undo Approval Savings Account form. */
undoApprovalSavingsAccountForm: UntypedFormGroup; undoApprovalSavingsAccountForm: UntypedFormGroup;
@@ -73,8 +75,19 @@ export class UndoApprovalSavingsAccountComponent implements OnInit {
const data = { const data = {
...this.undoApprovalSavingsAccountForm.value ...this.undoApprovalSavingsAccountForm.value
}; };
this.savingsService.executeSavingsAccountCommand(this.accountId, 'undoapproval', data).subscribe(() => { this.savingsService
this.router.navigate(['../../transactions'], { relativeTo: this.route }); .executeSavingsAccountCommand(this.accountId, 'undoapproval', data)
}); .subscribe((response: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true),
// so the account reloads unchanged -- say so instead of letting that read as "it worked".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'Undo approval has been sent for checker approval. The account is unchanged until a checker approves it.'
});
}
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
} }
} }
@@ -14,6 +14,7 @@ import { Dates } from 'app/core/utils/dates';
/** Custom Services */ /** Custom Services */
import { SavingsService } from 'app/savings/savings.service'; import { SavingsService } from 'app/savings/savings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { SettingsService } from 'app/settings/settings.service'; import { SettingsService } from 'app/settings/settings.service';
import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -37,6 +38,7 @@ export class WithdrawByClientSavingsAccountComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
private alertService = inject(AlertService);
/** Minimum date allowed. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
@@ -97,8 +99,19 @@ export class WithdrawByClientSavingsAccountComponent implements OnInit {
dateFormat, dateFormat,
locale locale
}; };
this.savingsService.executeSavingsAccountCommand(this.accountId, 'withdrawnByApplicant', data).subscribe(() => { this.savingsService
this.router.navigate(['../../'], { relativeTo: this.route }); .executeSavingsAccountCommand(this.accountId, 'withdrawnByApplicant', data)
}); .subscribe((response: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with rollbackTransaction: true),
// so the application is still open -- say so instead of letting that read as "nothing happened".
if (response?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'Withdrawal has been sent for checker approval. The application is unchanged until a checker approves it.'
});
}
this.router.navigate(['../../'], { relativeTo: this.route });
});
} }
} }
@@ -446,7 +446,16 @@ export class SavingsAccountViewComponent implements OnInit {
if (response.confirm) { if (response.confirm) {
this.savingsService this.savingsService
.executeSavingsAccountCommand(this.savingsAccountData.id, 'postInterest', {}) .executeSavingsAccountCommand(this.savingsAccountData.id, 'postInterest', {})
.subscribe(() => { .subscribe((commandResponse: any) => {
// Parked in the Checker Inbox rather than executed (HTTP 200 with
// rollbackTransaction: true), so the reload below shows no new interest posting.
if (commandResponse?.rollbackTransaction) {
this.alertService.alert({
type: 'Success',
message:
'Interest posting has been sent for checker approval. No interest is posted until a checker approves it.'
});
}
this.reload(); this.reload();
}); });
} }
+1 -1
View File
@@ -21,7 +21,7 @@
// in the tenants table of the tenant-store database, or the platform rejects the request as an // in the tenants table of the tenant-store database, or the platform rejects the request as an
// unknown tenant. // unknown tenant.
window["env"]["fineractPlatformTenantId"] = 'utkarshbank'; window["env"]["fineractPlatformTenantId"] = 'utkarshbank';
window["env"]["fineractPlatformTenantIds"] = 'utkarshbank'; window["env"]["fineractPlatformTenantIds"] = 'utkarshbank,lincpay';
window['env']['tenantLogoUrl'] = ''; window['env']['tenantLogoUrl'] = '';
window['env']['tenantLogoUrlDark'] = ''; window['env']['tenantLogoUrlDark'] = '';
+1
View File
@@ -2971,6 +2971,7 @@
"Transfer Interest to Savings Account": "Transfer Interest to Savings Account", "Transfer Interest to Savings Account": "Transfer Interest to Savings Account",
"Transfer Type": "Transfer Type", "Transfer Type": "Transfer Type",
"Transfer in suspense": "Transfer in suspense", "Transfer in suspense": "Transfer in suspense",
"Transfer to GL Account": "Transfer to GL Account",
"Transfer to Savings": "Transfer to Savings", "Transfer to Savings": "Transfer to Savings",
"Transfer to savings account": "Transfer to savings account", "Transfer to savings account": "Transfer to savings account",
"Trigger Type": "Trigger Type", "Trigger Type": "Trigger Type",
+1 -1
View File
@@ -10,7 +10,7 @@
export default { export default {
'mifos_x': { 'mifos_x': {
'version': '261001', 'version': '261001',
'hash': '4f02f24a' 'hash': '1ffac4b1'
}, },
'allow_switching_backend_instance': true 'allow_switching_backend_instance': true
}; };
+1 -1
View File
@@ -20,7 +20,7 @@ export const environment = {
hash: env.mifos_x.hash, hash: env.mifos_x.hash,
// For connecting to server running elsewhere update the tenant identifier // For connecting to server running elsewhere update the tenant identifier
fineractPlatformTenantId: loadedEnv['fineractPlatformTenantId'] || 'utkarshbank', fineractPlatformTenantId: loadedEnv['fineractPlatformTenantId'] || 'utkarshbank',
fineractPlatformTenantIds: loadedEnv['fineractPlatformTenantIds'] || 'utkarshbank', fineractPlatformTenantIds: loadedEnv['fineractPlatformTenantIds'] || 'utkarshbank,lincpay',
// For connecting to others servers running elsewhere update the base API URL // For connecting to others servers running elsewhere update the base API URL
baseApiUrls: baseApiUrls:
loadedEnv['fineractApiUrls'] || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin, loadedEnv['fineractApiUrls'] || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin,
+1 -1
View File
@@ -23,7 +23,7 @@ export const environment = {
hash: env.mifos_x.hash, hash: env.mifos_x.hash,
// For connecting to server running elsewhere update the tenant identifier // For connecting to server running elsewhere update the tenant identifier
fineractPlatformTenantId: loadedEnv.fineractPlatformTenantId || 'utkarshbank', fineractPlatformTenantId: loadedEnv.fineractPlatformTenantId || 'utkarshbank',
fineractPlatformTenantIds: loadedEnv.fineractPlatformTenantIds || 'utkarshbank', fineractPlatformTenantIds: loadedEnv.fineractPlatformTenantIds || 'utkarshbank,lincpay',
// For connecting to others servers running elsewhere update the base API URL // For connecting to others servers running elsewhere update the base API URL
baseApiUrls: baseApiUrls:
loadedEnv.fineractApiUrls || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin, loadedEnv.fineractApiUrls || 'https://demo.mifos.community,https://localhost:8443,' + window.location.origin,