feat(clearing): add CTS/NACH clearing UI with real NPCI batch sessions
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s

- CTS clearing module: inward batches (validate/pass/return), outward presentment
  capture, positive pay, settlement, reconciliation
- Outward Clearing merged into one screen: Pending Cheques + Batches tabs (a real
  clearing session per NPCI submission, either the 2-hourly job or manual send),
  drill into a batch to settle/dishonour its cheques
- Date-range, text, and quick (Today/Yesterday/All) filters with pagination across
  inward batches, outward presentments, outward batches, and batch detail
- NACH clearing module (mandates, batches, transaction decisioning)
- Shell/sidenav wiring for both new modules

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-21 15:57:45 +05:30
co-authored by Claude Sonnet 5
parent ed2a40a8bf
commit 6d205c0ed2
55 changed files with 6801 additions and 8 deletions
@@ -0,0 +1,225 @@
<!--
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/.
-->
<div class="container">
<div class="page-header">
<h2>{{ 'labels.inputs.Inward Batch' | translate }} #{{ batch.id }}</h2>
<button mat-raised-button [routerLink]="['/ctsclearing', 'inward']">{{ 'labels.buttons.Back' | translate }}</button>
</div>
<mat-card class="info-card">
<mat-card-content>
<div class="info-grid">
<div>
<span class="k">{{ 'labels.inputs.Batch Reference' | translate }}</span
><span>{{ batch.batchReference }}</span>
</div>
<div>
<span class="k">{{ 'labels.inputs.Session Date' | translate }}</span
><span>{{ batch.sessionDate ? (batch.sessionDate | dateFormat) : '–' }}</span>
</div>
<div>
<span class="k">{{ 'labels.inputs.Presentment Type' | translate }}</span
><span>{{ batch.presentmentType }}</span>
</div>
<div>
<span class="k">{{ 'labels.inputs.Items' | translate }}</span
><span>{{ batch.itemCount ?? batch.totalCount ?? items.length }}</span>
</div>
<div>
<span class="k">{{ 'labels.inputs.Total Amount' | translate }}</span
><span>{{ batch.totalAmount ?? '–' }}</span>
</div>
<div>
<span class="k">{{ 'labels.inputs.Status' | translate }}</span>
<span
><span class="pill" [ngClass]="pillClass(batch.status)">{{ batch.status }}</span></span
>
</div>
@if (batch.presentingBankName || batch.presentingBankCode) {
<div>
<span class="k">{{ 'labels.inputs.Presenting Bank' | translate }}</span
><span
>{{ batch.presentingBankName || '–' }} <span class="muted">{{ batch.presentingBankCode }}</span></span
>
</div>
}
@if (batch.returnTatDeadline) {
<div>
<span class="k">{{ 'labels.inputs.Return TAT Deadline' | translate }}</span
><span>{{ batch.returnTatDeadline | dateFormat }}</span>
</div>
}
</div>
</mat-card-content>
</mat-card>
<div class="action-toolbar">
<button mat-raised-button color="primary" (click)="runValidation()" [disabled]="busy">
<fa-icon icon="check-circle"></fa-icon>&nbsp;{{ 'labels.buttons.Run Validation' | translate }}
</button>
<button mat-raised-button (click)="prepareSettlement()" [disabled]="busy">
<fa-icon icon="money-check"></fa-icon>&nbsp;{{ 'labels.buttons.Prepare Settlement' | translate }}
</button>
</div>
<div class="table-wrapper mat-elevation-z1">
<table mat-table [dataSource]="items">
<ng-container matColumnDef="chequeNumber">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Cheque Number' | translate }}</th>
<td mat-cell *matCellDef="let it">
{{ it.chequeNumber }}
@if (it.instrumentStopPaymentFlag) {
<span class="pill bad" matTooltip="Stop payment was flagged on the depositing side">STOP</span>
}
@if (it.micrCode) {
<span class="muted">{{ it.micrCode }}</span>
}
@if (it.ifsc) {
<span class="muted">{{ it.ifsc }}</span>
}
@if (it.ctsReferenceNumber) {
<span class="muted">{{ it.ctsReferenceNumber }}</span>
}
@if (it.instrumentDate) {
<span class="muted">Dated {{ it.instrumentDate | dateFormat }}</span>
}
</td>
</ng-container>
<ng-container matColumnDef="drawer">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Account' | translate }}</th>
<td mat-cell *matCellDef="let it">
{{ it.accountNo || it.accountId || '–' }}
@if (it.clientName) {
<span class="muted">{{ it.clientName }}</span>
}
</td>
</ng-container>
<ng-container matColumnDef="payeeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payee Name' | translate }}</th>
<td mat-cell *matCellDef="let it">{{ it.payeeName }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let it" class="num">
{{ it.amount }}
@if (it.amountInWords) {
<div class="muted">{{ it.amountInWords }}</div>
}
</td>
</ng-container>
<ng-container matColumnDef="image">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Image' | translate }}</th>
<td mat-cell *matCellDef="let it">
<div class="image-cell">
@if (getItemImageUrl(it, 'front')) {
<img
[src]="getItemImageUrl(it, 'front')"
class="cheque-thumb"
alt="front"
matTooltip="Front image — click to zoom"
(click)="openImageZoom(it, 'front')"
/>
} @else {
<input #frontFile type="file" accept="image/*" hidden (change)="uploadItemImage(it, 'front', $event)" />
<button mat-icon-button (click)="frontFile.click()" matTooltip="Upload front image">
<fa-icon icon="plus"></fa-icon>
</button>
}
@if (getItemImageUrl(it, 'back')) {
<img
[src]="getItemImageUrl(it, 'back')"
class="cheque-thumb"
alt="back"
matTooltip="Back image — click to zoom"
(click)="openImageZoom(it, 'back')"
/>
} @else {
<input #backFile type="file" accept="image/*" hidden (change)="uploadItemImage(it, 'back', $event)" />
<button mat-icon-button (click)="backFile.click()" matTooltip="Upload back image">
<fa-icon icon="plus"></fa-icon>
</button>
}
</div>
</td>
</ng-container>
<ng-container matColumnDef="checks">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Validation' | translate }}</th>
<td mat-cell *matCellDef="let it">
<div class="check-badges">
@for (rule of rules; track rule.key) {
<span class="pill" [ngClass]="ruleClass(it, rule)" [title]="rule.label">{{ rule.label }}</span>
}
</div>
@if (it.validationDetail) {
<div class="validation-detail muted">{{ it.validationDetail }}</div>
}
@if (it.iqaStatus && it.iqaStatus !== 'NA') {
<span class="pill" [ngClass]="pillClass(it.iqaStatus)" matTooltip="Image Quality Assessment"
>IQA: {{ it.iqaStatus }}</span
>
}
@if (it.iqaDetail) {
<div class="muted">{{ it.iqaDetail }}</div>
}
</td>
</ng-container>
<ng-container matColumnDef="decision">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Decision' | translate }}</th>
<td mat-cell *matCellDef="let it">
@if (it.decision) {
<span class="pill" [ngClass]="pillClass(it.decision)">{{ it.decision }}</span>
} @else {
<span class="pill neutral">{{ it.itemStatus || 'PENDING' }}</span>
}
@if (it.returnReasonCode) {
<div class="muted">{{ it.returnReasonCode }}</div>
}
</td>
</ng-container>
<ng-container matColumnDef="hold">
<th mat-header-cell *matHeaderCellDef class="num">{{ 'labels.inputs.Hold' | translate }}</th>
<td mat-cell *matCellDef="let it" class="num">
@if (it.holdPlaced) {
{{ it.holdAmount ?? '✓' }}
} @else {
–
}
</td>
</ng-container>
<ng-container matColumnDef="returnDeadline">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Return Deadline' | translate }}</th>
<td mat-cell *matCellDef="let it">
{{ it.returnDeadline ? (it.returnDeadline | dateFormat) : '–' }}
@if (it.originalSerialNumber) {
<span class="muted">Orig # {{ it.originalSerialNumber }}</span>
}
@if (it.depositDate) {
<span class="muted">Deposited {{ it.depositDate | dateFormat }}</span>
}
@if (it.presentingBranchCode) {
<span class="muted">Branch {{ it.presentingBranchCode }}</span>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let it">
<button mat-button color="primary" (click)="decide(it)">{{ 'labels.buttons.Decide' | translate }}</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let row; columns: columns"></tr>
</table>
@if (items.length === 0) {
<div class="empty-state">{{ 'labels.text.No records to display' | translate }}</div>
}
</div>
<div #lightboxRoot style="display: none"></div>
</div>