fix(chequebook,ctsclearing): surface global stock allocation in UI; fix decided-state check
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

Receive Stock form now displays the actual highest-allocated serial across
every series (not just silently prefilling the field), so the operator
can see and trust the auto-suggested range start rather than taking it
on faith.

Fixes a real bug where item.decision (which InwardClearingItem.java always
sets to the literal string "PENDING" at creation, never null) was checked
with a plain truthy test everywhere a "has this been decided yet" branch
was needed. Since "PENDING" is a non-empty string, that check was always
true — silently breaking the Decide/View button label, the collapsed
validation summary, the "Awaiting settlement" text, and the decision
dialog itself (which never showed the actual Pass/Return form, even for
still-pending items). Replaced with a proper isDecided() check everywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 17:08:10 +05:30
co-authored by Claude Sonnet 5
parent ca85bae4fb
commit 97d0545f07
9 changed files with 73 additions and 23 deletions
@@ -45,6 +45,12 @@
@if (showReceiveForm) {
<mat-card class="add-form">
<mat-card-content>
<div class="global-allocation-note">
{{ 'labels.text.Highest leaf serial allocated so far, across every series' | translate }}:
<strong>{{ globalHighestAllocated }}</strong>
— {{ 'labels.text.next available start' | translate }}:
<strong>{{ globalHighestAllocated + 1 }}</strong>
</div>
<form [formGroup]="receiveForm" (ngSubmit)="submitReceive()">
<div class="form-grid">
<mat-form-field appearance="fill">
@@ -82,7 +88,12 @@
<mat-form-field appearance="fill">
<mat-label>{{ 'labels.inputs.Range Start' | translate }}</mat-label>
<input matInput type="number" formControlName="rangeStart" required />
<mat-hint>{{ 'labels.text.Auto-suggested to avoid overlap; editable' | translate }}</mat-hint>
<mat-hint
>{{ 'labels.text.Auto-suggested to avoid overlap across every series; editable' | translate }} ({{
globalHighestAllocated
}}
+ 1)</mat-hint
>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>{{ 'labels.inputs.Range End' | translate }}</mat-label>
@@ -92,6 +92,19 @@
margin-bottom: 16px;
}
.global-allocation-note {
margin-bottom: 14px;
padding: 8px 12px;
border-radius: 4px;
background: rgb(25 118 210 / 8%);
color: rgb(0 0 0 / 70%);
font-size: 0.85rem;
strong {
font-variant-numeric: tabular-nums;
}
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
@@ -281,17 +281,22 @@ export class StockManagementComponent implements OnInit {
return start + leaves - 1;
}
/** Highest leaf-serial allocated so far, across every series/office — the actual number the "Range Start"
* suggestion is based on. Shown directly in the form (not just silently applied to the field) so the operator
* can see and trust the reasoning instead of taking a pre-filled number on faith. */
get globalHighestAllocated(): number {
return this.allStockLots.reduce((m, l) => Math.max(m, l.rangeEnd || 0), 0);
}
/**
* Next unused serial WITHIN this series, so a fresh receipt continues that series' own numbering —
* each series is numbered independently (a separate stationery order from the press), so a different
* series' stock is not relevant here.
* Next unused serial across EVERY series, not just the one being received — a real bank allocates leaf-serial
* numbers from one bank-wide numbering authority, so two different cheque designs never legitimately reuse the
* same range. Suggesting a start that only avoids the same series' own lots is exactly what let two series end
* up overlapping (and, once both got issued to the same account, an unresolvable serial collision) before the
* backend's own overlap check was widened to match (ChequeStockLotRepository.countOverlappingAnySeries).
*/
private nextFreeSerial(): number {
const seriesId = this.receiveForm.get('seriesId')?.value;
const maxEnd = this.allStockLots
.filter((l) => l.seriesId === seriesId)
.reduce((m, l) => Math.max(m, l.rangeEnd || 0), 0);
return maxEnd + 1;
return this.globalHighestAllocated + 1;
}
onReceiveSeriesChange(): void {