diff --git a/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.html b/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.html
index bb99accff..f820cdea8 100644
--- a/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.html
+++ b/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.html
@@ -160,24 +160,31 @@
| {{ 'labels.inputs.Validation' | translate }} |
-
- @for (rule of rules; track rule.key) {
- {{ rule.label }}
+ @if (it.decision) {
+
+ {{ hasFailedChecks(it) ? 'Some checks failed' : 'All checks passed' }}
+
+ See View → Comparison Details for the full breakdown
+ } @else {
+
+ @for (rule of rules; track rule.key) {
+ {{ rule.label }}
+ }
+
+ @if (it.validationDetail) {
+ {{ it.validationDetail }}
+ }
+ @if (it.iqaStatus && it.iqaStatus !== 'NA') {
+ IQA: {{ it.iqaStatus }}
+ }
+ @if (it.iqaDetail) {
+ {{ it.iqaDetail }}
+ }
+ @if (it.iqaSubChecks) {
+ {{ it.iqaSubChecks }}
}
-
- @if (it.validationDetail) {
- {{ it.validationDetail }}
- }
- @if (it.iqaStatus && it.iqaStatus !== 'NA') {
- IQA: {{ it.iqaStatus }}
- }
- @if (it.iqaDetail) {
- {{ it.iqaDetail }}
- }
- @if (it.iqaSubChecks) {
- {{ it.iqaSubChecks }}
}
|
@@ -192,6 +199,17 @@
@if (it.returnReasonCode) {
@@ -225,7 +243,9 @@
|
-
+
|
diff --git a/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.ts b/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.ts
index 14da7f8cc..b076a35f5 100644
--- a/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.ts
+++ b/src/app/ctsclearing/inward-batch-detail/inward-batch-detail.component.ts
@@ -11,6 +11,7 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewChild, inject } from '@an
import { ActivatedRoute, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
+import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@@ -42,6 +43,7 @@ import { pillClass, clearingError } from '../../clearing-network/clearing-status
MatButtonModule,
MatTableModule,
MatSnackBarModule,
+ MatTooltipModule,
FaIconComponent
]
})
@@ -200,6 +202,13 @@ export class InwardBatchDetailComponent implements OnInit, OnDestroy {
return good ? 'ok' : 'bad';
}
+ /** Once a cheque is decided, the clerk doesn't need the full rule-by-rule checklist sitting in the table
+ * row forever — a one-line outcome is enough, with the full breakdown one click away via View -> Comparison
+ * Details. Kept for PENDING items, where the full checklist is exactly what's needed to decide. */
+ hasFailedChecks(item: any): boolean {
+ return this.rules.some((rule) => this.ruleClass(item, rule) === 'bad') || item?.iqaStatus === 'FAIL';
+ }
+
runValidation(): void {
if (this.busy) {
return;
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html
index 55a4e89cb..f0c920fac 100644
--- a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html
+++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.html
@@ -22,14 +22,34 @@
{{ 'labels.inputs.Account' | translate }}{{ item.accountNo || item.accountId || '–' }}
+ @if (item.clientName) {
+
+ Account Holder{{ item.clientName }}
+
+ }
+ @if (accountInfo?.availableBalance !== null && accountInfo?.availableBalance !== undefined) {
+
+ Available Balance
+
+ {{ accountInfo!.availableBalance }}
+ @if (balanceSufficient !== null) {
+
+ {{ balanceSufficient ? 'Sufficient' : 'Insufficient' }}
+
+ }
+
+
+ }
+ @if (
+ accountInfo?.onHoldAmount !== null && accountInfo?.onHoldAmount !== undefined && accountInfo!.onHoldAmount !== 0
+ ) {
+
+ Existing Holds on Account{{ accountInfo!.onHoldAmount }}
+
+ }
Hold Placed{{ item.holdPlaced ? (item.holdAmount ?? item.amount) : '–' }}
- @if (accountInfo?.availableBalance !== null && accountInfo?.availableBalance !== undefined) {
-
- Available Balance{{ accountInfo!.availableBalance }}
-
- }
Cheque Image
@@ -142,12 +162,73 @@
}
- Automatic Validation
+ Instrument Details
+
+ @if (item.micrCode) {
+
+ MICR Code{{ item.micrCode }}
+
+ }
+ @if (item.ifsc) {
+
+ IFSC{{ item.ifsc }}
+
+ }
+ @if (item.instrumentDate) {
+
+ Instrument Date{{ item.instrumentDate | dateFormat }}
+
+ }
+ @if (item.amountInWords) {
+
+ Amount in Words{{ item.amountInWords }}
+
+ }
+ @if (item.drawerAccountNumberMasked) {
+
+ Drawer A/C (as on cheque){{ item.drawerAccountNumberMasked }}
+
+ }
+ @if (item.draweeBankName) {
+
+ Drawee Bank{{ item.draweeBankName }}
+
+ }
+ @if (item.instrumentTypeDescription || item.instrumentType) {
+
+ Instrument Type{{ item.instrumentTypeDescription || item.instrumentType }}
+
+ }
+ @if (item.ctsReferenceNumber) {
+
+ CTS Reference{{ item.ctsReferenceNumber }}
+
+ }
+
+
+
+ @if (item.instrumentStopPaymentFlag) {
+ Stop payment was flagged on this instrument by the depositing bank.
+ }
@for (rule of validationRules; track rule.key) {
{{ rule.label }}
}
+ @if (item.iqaStatus && item.iqaStatus !== 'NA') {
+ IQA: {{ item.iqaStatus }}
+ }
+ @if (item.iqaDetail) {
+ {{ item.iqaDetail }}
+ }
+ @if (item.iqaSubChecks) {
+ {{ item.iqaSubChecks }}
+ }
@if (item.validationDetail) {
{{ item.validationDetail }}
}
@@ -202,59 +283,105 @@
}
}
-
-
-
-
-
- @if (mode === 'PASS') {
-
+
+
+
+
+
+ @if (mode === 'PASS') {
+
+ } @else {
+
+ }
}
-
-
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.scss b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.scss
index cf95aacaa..7285f830a 100644
--- a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.scss
+++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.scss
@@ -24,6 +24,11 @@
color: rgb(0 0 0 / 50%);
margin-bottom: 2px;
}
+
+ .pill {
+ margin-left: 6px;
+ vertical-align: middle;
+ }
}
.mode-toggle {
@@ -36,6 +41,54 @@
}
}
+.decided-summary {
+ padding: 14px 16px;
+ border-radius: 8px;
+ border-left: 4px solid rgb(0 0 0 / 30%);
+ background: rgb(0 0 0 / 4%);
+
+ &.ok {
+ border-left-color: #1a9c63;
+ background: #e8f8f0;
+ }
+
+ &.bad {
+ border-left-color: #d32f2f;
+ background: #fdeaea;
+ }
+}
+
+.decided-headline {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 10px;
+}
+
+.compare-row {
+ display: grid;
+ grid-template-columns: minmax(120px, 38%) 1fr;
+ gap: 6px 16px;
+ padding: 4px 0;
+ border-top: 1px solid rgb(255 255 255 / 60%);
+
+ &:first-of-type {
+ border-top: none;
+ }
+}
+
+.compare-label {
+ color: rgb(0 0 0 / 55%);
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.compare-value {
+ font-size: 0.85rem;
+ word-break: break-word;
+}
+
mat-form-field {
width: 100%;
}
@@ -45,6 +98,17 @@ mat-form-field {
font-size: 0.95rem;
}
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+
+ .section-title {
+ margin: 20px 0 8px;
+ }
+}
+
.muted {
color: rgb(0 0 0 / 50%);
font-size: 0.82rem;
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts
index 0853bd33e..53a7aebc1 100644
--- a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts
+++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/item-decision-dialog.component.ts
@@ -10,14 +10,17 @@
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
import { FormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import {
+ MatDialog,
MatDialogRef,
MAT_DIALOG_DATA,
MatDialogTitle,
MatDialogContent,
- MatDialogActions
+ MatDialogActions,
+ MatDialogClose
} from '@angular/material/dialog';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { MatButtonModule } from '@angular/material/button';
+import { MatTooltipModule } from '@angular/material/tooltip';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatSnackBar } from '@angular/material/snack-bar';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -31,7 +34,8 @@ import type { GalleryItem } from 'lightgallery/lg-utils';
import { CtsClearingService } from '../../ctsclearing.service';
import { ClearingService } from '../../../clearing-network/clearing.service';
import { SignatoryService, AccountSignatoryInfo, JointHolder } from '../../../clearing-network/signatory.service';
-import { clearingError } from '../../../clearing-network/clearing-status';
+import { clearingError, pillClass } from '../../../clearing-network/clearing-status';
+import { ValidationDetailDialogComponent } from './validation-detail-dialog/validation-detail-dialog.component';
interface RequiredSigner {
clientId: number;
@@ -72,8 +76,10 @@ const OVERLAY_DEFAULT_WIDTH = 160;
CdkScrollable,
MatDialogContent,
MatDialogActions,
+ MatDialogClose,
MatButtonModule,
- MatCheckboxModule
+ MatCheckboxModule,
+ MatTooltipModule
]
})
export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
@@ -84,6 +90,7 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
private clearingService = inject(ClearingService);
private signatoryService = inject(SignatoryService);
private snackBar = inject(MatSnackBar);
+ private dialog = inject(MatDialog);
@ViewChild('lightboxRoot', { static: true }) lightboxRoot: ElementRef;
@ViewChild('chequeStage') chequeStageEl?: ElementRef;
@@ -165,6 +172,9 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
this.destroyLightbox();
}
+ /** Shared status-string -> pill-color mapping, same one the batch list table already uses for iqaStatus/decision. */
+ pillClass = pillClass;
+
/** ok / bad / neutral for a single boolean validation rule, respecting whether true is the good outcome. */
ruleClass(rule: { key: string; goodWhenTrue: boolean }): 'ok' | 'bad' | 'neutral' {
const value = this.item?.[rule.key];
@@ -175,6 +185,17 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
return good ? 'ok' : 'bad';
}
+ /** What each automatic rule actually compared (amount vs words, instrument date vs the valid-until window,
+ * balance vs amount, etc.) instead of just the pass/fail pill — opened on demand, not always-on clutter. */
+ openValidationDetail(): void {
+ this.dialog.open(ValidationDetailDialogComponent, {
+ data: { item: this.item, accountInfo: this.accountInfo },
+ width: '560px',
+ maxWidth: '95vw',
+ maxHeight: '85vh'
+ });
+ }
+
private loadImages(): void {
this.ctsService.getInwardItemImage(this.item.id, 'front').subscribe({
next: (url: string) => (this.frontImageUrl = url),
@@ -558,6 +579,22 @@ export class ItemDecisionDialogComponent implements OnInit, OnDestroy {
};
}
+ /** null when the account's balance couldn't be resolved at all — the automatic "Balance" check pill already
+ * covers that outcome, this just makes the actual sufficiency legible at a glance rather than requiring
+ * the clerk to mentally subtract Available Balance minus Amount themselves. */
+ get balanceSufficient(): boolean | null {
+ const available = this.accountInfo?.availableBalance;
+ if (
+ available === null ||
+ available === undefined ||
+ this.item?.amount === null ||
+ this.item?.amount === undefined
+ ) {
+ return null;
+ }
+ return available >= this.item.amount;
+ }
+
get modeOfOperationLabel(): string {
const labels: { [key: string]: string } = {
EITHER_OR_SURVIVOR: 'Either or Survivor — any one signatory',
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.html b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.html
new file mode 100644
index 000000000..2221cf77b
--- /dev/null
+++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.html
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+ Account Type{{ item.accountType || '–' }}
+
+
+ Account Number{{ item.accountNo || item.accountId || '–' }}
+
+
+ Account Holder{{ item.clientName || '–' }}
+
+
+
+
+
+
+
+
+ Available Balance{{ accountInfo?.availableBalance ?? 'Not available' }}
+
+
+ Cheque Amount{{ item.amount }}
+
+
+ Result{{
+ balanceSufficient === null ? 'Unknown' : balanceSufficient ? 'Sufficient' : 'Insufficient'
+ }}
+
+
+
+
+
+
+
+
+ Checked AgainstSame cheque number + account + amount already PASSed, and the cheque leaf's own USED status
+
+
+ Cheque Number{{ item.chequeNumber || '–' }}
+
+
+
+
+
+
+
+
+ Checked AgainstThe matching cheque leaf's own stop-payment status
+
+ @if (item.instrumentStopPaymentFlag) {
+
+ Depositing-Bank FlagStop payment was flagged on this instrument by the depositing bank
+
+ }
+
+
+
+
+
+
+
+ Amount (Figures){{ item.amount }}
+
+
+ Amount (Words){{ item.amountInWords || 'Not captured' }}
+
+ @if (amountWordsDigits) {
+
+ Digits Extracted From Words{{ amountWordsDigits }}
+
+ }
+
+
+
+
+
+
+
+ Instrument Date{{ item.instrumentDate ? (item.instrumentDate | dateFormat) : '–' }}
+
+
+ Valid Until (+3 months){{ instrumentValidUntil ? (instrumentValidUntil | dateFormat) : '–' }}
+
+
+ {{ asOfLabel }}{{ asOfDate | dateFormat }}
+
+
+
+
+
+
+
+
+ RBI PPS Threshold{{ positivePayThreshold }}
+
+
+ Cheque Amount{{ item.amount }}
+
+
+ Applicable{{
+ positivePayApplicable ? 'Yes — at or above threshold' : 'No — below threshold'
+ }}
+
+
+
+
+ @if (item.iqaStatus && item.iqaStatus !== 'NA') {
+
+
+ @if (item.iqaDetail || item.iqaSubChecks) {
+
+ @if (item.iqaDetail) {
+
+ Detail{{ item.iqaDetail }}
+
+ }
+ @if (item.iqaSubChecks) {
+
+ Sub-checks{{ item.iqaSubChecks }}
+
+ }
+
+ }
+
+ }
+
+ @if (item.validationDetail) {
+
+
Backend Validation Notes
+
{{ item.validationDetail }}
+
+ }
+
+
+
+ Close
+
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.scss b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.scss
new file mode 100644
index 000000000..85fed616b
--- /dev/null
+++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.scss
@@ -0,0 +1,144 @@
+/**
+ * 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/.
+ */
+
+$ok: #1a9c63;
+$ok-bg: #e8f8f0;
+$bad: #d32f2f;
+$bad-bg: #fdeaea;
+$neutral: #6b6b6b;
+$neutral-bg: #f0f0f0;
+
+.dialog-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 20px 12px 4px 24px;
+
+ [mat-dialog-title] {
+ margin: 0;
+ font-size: 1.15rem;
+ font-weight: 600;
+ }
+}
+
+.dialog-subtitle {
+ margin-top: 2px;
+ color: rgb(0 0 0 / 55%);
+ font-size: 0.8rem;
+}
+
+.header-close {
+ flex-shrink: 0;
+ color: rgb(0 0 0 / 45%);
+}
+
+.rule-card {
+ margin-bottom: 12px;
+ padding: 14px 16px;
+ border-radius: 8px;
+ border-left: 4px solid $neutral;
+ background: $neutral-bg;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+
+ &.ok {
+ border-left-color: $ok;
+ background: $ok-bg;
+
+ .rule-card-header {
+ color: $ok;
+ }
+ }
+
+ &.bad {
+ border-left-color: $bad;
+ background: $bad-bg;
+
+ .rule-card-header {
+ color: $bad;
+ }
+ }
+
+ &.neutral {
+ .rule-card-header {
+ color: $neutral;
+ }
+ }
+}
+
+.rule-card-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 10px;
+ font-size: 0.9rem;
+ font-weight: 600;
+
+ fa-icon {
+ font-size: 0.95rem;
+ }
+}
+
+.compare-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.compare-row {
+ display: grid;
+ grid-template-columns: minmax(120px, 38%) 1fr;
+ gap: 6px 16px;
+ padding: 4px 0;
+ border-top: 1px solid rgb(255 255 255 / 60%);
+
+ &:first-child {
+ border-top: none;
+ }
+}
+
+.compare-label {
+ color: rgb(0 0 0 / 55%);
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.compare-value {
+ font-size: 0.85rem;
+ word-break: break-word;
+
+ &.warn-text {
+ color: $bad;
+ font-weight: 600;
+ }
+}
+
+.backend-notes {
+ margin-top: 4px;
+ padding: 10px 14px;
+ border-radius: 8px;
+ background: rgb(0 0 0 / 4%);
+ font-size: 0.85rem;
+
+ strong {
+ display: block;
+ margin-bottom: 4px;
+ color: rgb(0 0 0 / 55%);
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+
+ p {
+ margin: 0;
+ }
+}
diff --git a/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.ts b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.ts
new file mode 100644
index 000000000..3eefcd764
--- /dev/null
+++ b/src/app/ctsclearing/inward-batch-detail/item-decision-dialog/validation-detail-dialog/validation-detail-dialog.component.ts
@@ -0,0 +1,126 @@
+/**
+ * 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 { Component, inject } from '@angular/core';
+import {
+ MatDialogRef,
+ MAT_DIALOG_DATA,
+ MatDialogTitle,
+ MatDialogContent,
+ MatDialogActions,
+ MatDialogClose
+} from '@angular/material/dialog';
+import { CdkScrollable } from '@angular/cdk/scrolling';
+import { MatButtonModule } from '@angular/material/button';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { pillClass } from '../../../../clearing-network/clearing-status';
+import { AccountSignatoryInfo } from '../../../../clearing-network/signatory.service';
+
+/** RBI Positive Pay System threshold — mirrors InwardClearingValidationService.POSITIVE_PAY_THRESHOLD. */
+const POSITIVE_PAY_THRESHOLD = 50000;
+/** Stale-instrument window — mirrors InwardClearingValidationService.STALE_MONTHS. */
+const STALE_MONTHS = 3;
+
+/**
+ * Shows exactly what each automatic validation rule actually compared, not just its pass/fail outcome —
+ * e.g. the amount in figures next to the amount in words, or the instrument date next to its valid-until
+ * date — mirroring the real values InwardClearingValidationService.java checks server-side.
+ */
+@Component({
+ selector: 'mifosx-validation-detail-dialog',
+ templateUrl: './validation-detail-dialog.component.html',
+ styleUrls: ['./validation-detail-dialog.component.scss'],
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ MatDialogTitle,
+ CdkScrollable,
+ MatDialogContent,
+ MatDialogActions,
+ MatDialogClose,
+ MatButtonModule,
+ MatTooltipModule,
+ FaIconComponent
+ ]
+})
+export class ValidationDetailDialogComponent {
+ dialogRef = inject>(MatDialogRef);
+ data = inject(MAT_DIALOG_DATA);
+
+ item: any = this.data?.item || {};
+ accountInfo: AccountSignatoryInfo | null = this.data?.accountInfo || null;
+
+ pillClass = pillClass;
+
+ ruleClass(value: boolean | null | undefined, goodWhenTrue = true): 'ok' | 'bad' | 'neutral' {
+ if (value === null || value === undefined) {
+ return 'neutral';
+ }
+ const good = goodWhenTrue ? value === true : value === false;
+ return good ? 'ok' : 'bad';
+ }
+
+ statusIcon(status: 'ok' | 'bad' | 'neutral'): string {
+ return status === 'ok' ? 'check-circle' : status === 'bad' ? 'times-circle' : 'minus-circle';
+ }
+
+ get balanceSufficient(): boolean | null {
+ const available = this.accountInfo?.availableBalance;
+ if (
+ available === null ||
+ available === undefined ||
+ this.item?.amount === null ||
+ this.item?.amount === undefined
+ ) {
+ return null;
+ }
+ return available >= this.item.amount;
+ }
+
+ /** The digits InwardClearingValidationService.amountWordsFiguresMatch() extracts from amountInWords, for a
+ * transparent side-by-side with the figure amount rather than just trusting the pass/fail pill. */
+ get amountWordsDigits(): string | null {
+ const words: string | undefined = this.item?.amountInWords;
+ if (!words) {
+ return null;
+ }
+ const digits = words.replace(/[^0-9]/g, '');
+ return digits || null;
+ }
+
+ get instrumentValidUntil(): Date | null {
+ if (!this.item?.instrumentDate) {
+ return null;
+ }
+ const d = new Date(this.item.instrumentDate);
+ d.setMonth(d.getMonth() + STALE_MONTHS);
+ return d;
+ }
+
+ /**
+ * The date the stale/postdated check actually ran against — for an already-decided item that's when it was
+ * decided (decisionedOn), NOT today. Showing today's live date here for a cheque passed 50 days ago would
+ * make it look like the comparison is being silently re-run against the current date, which it isn't — the
+ * decision already happened, using whatever "today" was back then.
+ */
+ get asOfDate(): Date {
+ return this.item?.decisionedOn ? new Date(this.item.decisionedOn) : new Date();
+ }
+
+ get asOfLabel(): string {
+ return this.item?.decisionedOn ? 'As Of Decision' : 'Today';
+ }
+
+ get positivePayApplicable(): boolean {
+ return (this.item?.amount ?? 0) >= POSITIVE_PAY_THRESHOLD;
+ }
+
+ readonly positivePayThreshold = POSITIVE_PAY_THRESHOLD;
+}