feat: cash-only registers, day book sections and DD amendments
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

This commit is contained in:
2026-09-03 18:26:22 +05:30
parent 6442562df0
commit fbce73fd17
111 changed files with 9076 additions and 3507 deletions
@@ -0,0 +1,66 @@
/**
* 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/.
*/
/** Angular Imports */
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
/** rxjs Imports */
import { Observable, of } from 'rxjs';
import { catchError, map, shareReplay } from 'rxjs/operators';
/**
* Charges that are mapped to an account facility (ATM, UPI, Cheque Book, ...) are priced by the
* Facility Charge Mapping screen and levied automatically when that facility is enabled. Offering
* them again in a product/account charge picker would attach the same charge a second time and
* double-charge the customer, so they are filtered out of those pickers.
*/
@Injectable({
providedIn: 'root'
})
export class FacilityChargeFilterService {
private http = inject(HttpClient);
/** Facility mappings rarely change within a session, so the lookup is fetched once and shared. */
private mappedChargeIds$: Observable<Set<number>>;
/**
* Ids of every charge currently mapped to a facility.
* Falls back to an empty set if the lookup fails, so a charge picker still renders.
*/
getFacilityMappedChargeIds(): Observable<Set<number>> {
if (!this.mappedChargeIds$) {
this.mappedChargeIds$ = this.http.get('/facilitychargemappings').pipe(
map(
(mappings: any) =>
new Set<number>(
((mappings as any[]) || [])
.map((mapping) => Number(mapping?.chargeId))
.filter((chargeId) => Number.isFinite(chargeId))
)
),
catchError(() => of(new Set<number>())),
shareReplay({ bufferSize: 1, refCount: false })
);
}
return this.mappedChargeIds$;
}
/**
* Removes facility-mapped charges from a charge option list.
* @param charges Charge options straight off a product/account template.
* @returns The same list without any charge priced by a facility mapping.
*/
excludeFacilityCharges(charges: any[]): Observable<any[]> {
return this.getFacilityMappedChargeIds().pipe(
map((mappedChargeIds) =>
(charges || []).filter((charge) => !mappedChargeIds.has(Number(charge?.id ?? charge?.chargeId)))
)
);
}
}
@@ -12,7 +12,9 @@
<div class="dap__header">
<div class="dap__header-left">
<div class="dap__icon-wrap">
<i [class]="isProductMode ? 'fas fa-shield-alt' : 'fas fa-exclamation-triangle'"></i>
<!-- A healthy account gets the shield, not the warning triangle: the panel is showing it
its thresholds and the benefit exemption, not raising an alarm. -->
<i [class]="isProductMode || isHealthy ? 'fas fa-shield-alt' : 'fas fa-exclamation-triangle'"></i>
</div>
<div>
<div class="dap__title">
@@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export type DormancyRisk = 'safe' | 'low' | 'medium' | 'high' | 'critical' | 'escheat';
@@ -17,7 +17,7 @@ export type DormancyRisk = 'safe' | 'low' | 'medium' | 'high' | 'critical' | 'es
styleUrls: ['./dormancy-alert-panel.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class DormancyAlertPanelComponent implements OnInit {
export class DormancyAlertPanelComponent implements OnInit, OnChanges {
/** Pass full account data — for ACCOUNT VIEW (general tab) */
@Input() accountData: any;
/** Pass product template — for ACCOUNT CREATION (currency step) */
@@ -48,13 +48,40 @@ export class DormancyAlertPanelComponent implements OnInit {
}
ngOnInit(): void {
this.refresh();
}
/**
* The stepper reuses one panel instance while the user switches products, so the thresholds have
* to be recomputed on every input change — without this the panel keeps showing the previously
* selected product's days.
*/
ngOnChanges(changes: SimpleChanges): void {
if (changes['productTemplate'] || changes['accountData']) {
this.refresh();
}
}
private refresh(): void {
if (this.productTemplate && !this.accountData) {
this.initFromProductTemplate();
} else if (this.accountData) {
this.initFromAccountData();
} else {
this.resetThresholds();
}
}
/** Clears stale thresholds when neither a product nor an account is bound. */
private resetThresholds(): void {
this.daysToInactive = null;
this.daysToDormancy = null;
this.daysToEscheat = null;
this.isMonitoringActive = false;
this.isAlertEnabled = false;
this.shouldShow = false;
}
// ── Product Template Mode (Currency Step) ─────────────────────
private initFromProductTemplate(): void {
const pt = this.productTemplate;
@@ -113,10 +140,15 @@ export class DormancyAlertPanelComponent implements OnInit {
}
this.computeRiskFromAccount();
// Only surface this panel once the account has actually left "Active" —
// i.e. subStatus is Inactive/Dormant/Escheat. A healthy active account
// (statusId === 0) should never show a danger-colored dormancy alert.
this.shouldShow = this.statusId > 0;
// Shown wherever dormancy monitoring applies, so a branch can read the thresholds and how far
// the account is from each of them. The template drops the risk timeline while the account is
// still healthy, where there is no risk to show.
this.shouldShow = this.statusId > 0 || this.isMonitoringActive;
}
/** True when the account has not left "Active", so there is no risk timeline worth showing. */
get isHealthy(): boolean {
return this.statusId === 0;
}
private computeRiskFromAccount(): void {