feat(shares): add Share Withdrawal maker-checker workflow with GL settlement
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
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s

Clerk submits a governed share redemption request (shares, Board Resolution
No., Meeting Date, settlement Payment Type); Bank Manager reviews with a
live backend-validated checklist (account/GL state, not just submit-time
snapshot) before approving, which redeems the shares and settles the
linked savings account in one transaction. Adds a Passbook tab to the
Share Account view, a reusable search-select control, and a Savings
Product "Map Payment Types to Specific Funds" fund-source override (only
Loan Products had this before) so settlement can be GL-mapped correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 16:55:21 +05:30
co-authored by Claude Sonnet 5
parent 8c7d8287f9
commit cc5cd4c6ab
31 changed files with 3565 additions and 55 deletions
@@ -146,7 +146,7 @@
[class.ws-f__ctl--req]="!row.paymentTypeId"
>
<option [ngValue]="null">— Select —</option>
@for (pt of paymentTypes; track pt.id) {
@for (pt of paymentTypesForRow(row); track pt.id) {
<option [ngValue]="pt.id">{{ pt.name }}</option>
}
</select>
@@ -231,8 +231,11 @@
<span class="ws-caption">Payment instrument</span>
<div class="ws-group__fields">
<div class="ws-f ws-f--amt">
<label class="ws-f__lbl" title="Transaction Amount">
AMOUNT
<label
class="ws-f__lbl"
[title]="row.acTyp === 'SHR' ? 'Number of Shares' : 'Transaction Amount'"
>
{{ row.acTyp === 'SHR' ? 'SHARE NO.' : 'AMOUNT' }}
@if (!isClerk && activeWithdrawalRequest) {
<span class="ws-amt-locked-badge" title="Amount locked — approved withdrawal">🔒</span>
}
@@ -242,20 +245,48 @@
[class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row"
[class.ws-amt-wrap--locked]="!isClerk && !!activeWithdrawalRequest"
>
<span class="ws-amt-prefix">₹</span>
@if (row.acTyp !== 'SHR') {
<span class="ws-amt-prefix">₹</span>
}
<input
class="ws-f__ctl ws-amt-input"
type="number"
[value]="row.amount ?? ''"
(input)="row.amount = +$any($event.target).value"
(input)="row.amount = +$any($event.target).value; syncShareLinkedAmounts()"
(click)="$event.stopPropagation()"
placeholder="0.00"
[placeholder]="row.acTyp === 'SHR' ? '0' : '0.00'"
[readonly]="!isClerk && !!activeWithdrawalRequest"
[class.ws-amt-input--locked]="!isClerk && !!activeWithdrawalRequest"
/>
</div>
@if (row.acTyp === 'SHR' && rowSharePrices[row.acNo]) {
<span class="ws-f__hint"
>= ₹{{ (row.amount ?? 0) * rowSharePrices[row.acNo] | number: '1.2-2' }} @ ₹{{
rowSharePrices[row.acNo] | number: '1.2-2'
}}/share</span
>
}
</div>
@if (row.typ !== 'Withdrawal Slip') {
@if (row.acTyp === 'SHR' && row.typ === 'Enter Bank Transfer') {
<div class="ws-f ws-f--cheqno">
<label class="ws-f__lbl" title="Board Resolution Number">RESOLUTION NO.</label>
<input
class="ws-f__ctl"
[(ngModel)]="row.resolutionNumber"
(click)="$event.stopPropagation()"
placeholder="e.g. BR-2026-014"
/>
</div>
<div class="ws-f ws-f--cheqdt">
<label class="ws-f__lbl" title="Date of Meeting">MEETING DATE</label>
<input
class="ws-f__ctl"
type="date"
[(ngModel)]="row.meetingDate"
(click)="$event.stopPropagation()"
/>
</div>
} @else if (row.typ !== 'Withdrawal Slip') {
<div class="ws-f ws-f--cheqno">
<label class="ws-f__lbl" title="Cheque Number">CHEQUE NO.</label>
<input
@@ -712,6 +743,32 @@
'—'
}}</span>
</div>
@if (selectedRow?.acTyp === 'SHR') {
<div class="te-field">
<span class="te-label">No. of Shares Held</span
><span class="te-value te-mono">{{ accountDetails.summary?.totalApprovedShares ?? 0 }}</span>
</div>
<div class="te-field">
<span class="te-label">Market Price / Share</span
><span class="te-value te-mono"
>{{ currencySymbol }}{{ accountDetails.currentMarketPrice | number: '1.2-2' }}</span
>
</div>
<div class="te-field">
<span class="te-label">Resolution No.</span
><span class="te-value te-mono">{{ accountDetails.resolutionNumber || '—' }}</span>
</div>
<div class="te-field">
<span class="te-label">Linked Savings A/C No.</span
><span class="te-value te-mono">{{ accountDetails.savingsAccountNumber || '—' }}</span>
</div>
@if (accountDetails.linkedLoanAccountNo) {
<div class="te-field">
<span class="te-label">Linked Loan A/C No.</span
><span class="te-value te-mono">{{ accountDetails.linkedLoanAccountNo }}</span>
</div>
}
}
</div>
<div class="te-panel__header te-panel__header--mt">
@@ -724,7 +781,13 @@
<div class="te-panel__body">
<div class="te-balance-row">
<span class="te-bal-num">1.</span>
<span class="te-bal-label">{{ accountDetails?.glCode ? 'GL Balance' : 'Current Balance' }}</span>
<span class="te-bal-label">{{
selectedRow?.acTyp === 'SHR'
? 'Share Balance'
: accountDetails?.glCode
? 'GL Balance'
: 'Current Balance'
}}</span>
<span class="te-bal-value te-bal-value--cr"
>{{ currencySymbol }}{{ currentBalance | number: '1.2-2' }}</span
>
@@ -1281,7 +1344,7 @@
[class.ws-f__ctl--req]="!row.paymentTypeId"
>
<option [ngValue]="null">— Select —</option>
@for (pt of paymentTypes; track pt.id) {
@for (pt of paymentTypesForRow(row); track pt.id) {
<option [ngValue]="pt.id">{{ pt.name }}</option>
}
</select>
@@ -1360,7 +1423,7 @@
<div class="ws-group__fields">
<div class="ws-f ws-f--amt">
<label class="ws-f__lbl">
AMOUNT
{{ row.acTyp === 'SHR' ? 'SHARE NO.' : 'AMOUNT' }}
@if (!isClerk && activeWithdrawalRequest) {
<span class="ws-amt-locked-badge" title="Amount locked — approved withdrawal">🔒</span>
}
@@ -1370,27 +1433,51 @@
[class.ws-amt-wrap--cash]="isCashPayment && selectedRow === row"
[class.ws-amt-wrap--locked]="!isClerk && !!activeWithdrawalRequest"
>
<span class="ws-amt-prefix">₹</span>
@if (row.acTyp !== 'SHR') {
<span class="ws-amt-prefix">₹</span>
}
<input
class="ws-f__ctl ws-amt-input"
type="number"
[value]="row.amount ?? ''"
(input)="row.amount = +$any($event.target).value"
(input)="row.amount = +$any($event.target).value; syncShareLinkedAmounts()"
(click)="$event.stopPropagation()"
placeholder="0.00"
[placeholder]="row.acTyp === 'SHR' ? '0' : '0.00'"
[readonly]="!isClerk && !!activeWithdrawalRequest"
[class.ws-amt-input--locked]="!isClerk && !!activeWithdrawalRequest"
/>
</div>
@if (row.acTyp === 'SHR' && rowSharePrices[row.acNo]) {
<span class="ws-f__hint"
>= ₹{{ (row.amount ?? 0) * rowSharePrices[row.acNo] | number: '1.2-2' }} @ ₹{{
rowSharePrices[row.acNo] | number: '1.2-2'
}}/share</span
>
}
</div>
<div class="ws-f ws-f--cheqno">
<label class="ws-f__lbl">CHEQUE NO.</label>
<input class="ws-f__ctl ws-f__ctl--mono" [(ngModel)]="row.chequeNo" placeholder="—" />
</div>
<div class="ws-f ws-f--cheqdt">
<label class="ws-f__lbl">CHEQ. DATE</label>
<input class="ws-f__ctl" [(ngModel)]="row.cheqDate" placeholder="dd-mm-yyyy" />
</div>
@if (row.acTyp === 'SHR' && row.typ === 'Enter Bank Transfer') {
<div class="ws-f ws-f--cheqno">
<label class="ws-f__lbl">RESOLUTION NO.</label>
<input
class="ws-f__ctl"
[(ngModel)]="row.resolutionNumber"
placeholder="e.g. BR-2026-014"
/>
</div>
<div class="ws-f ws-f--cheqdt">
<label class="ws-f__lbl">MEETING DATE</label>
<input class="ws-f__ctl" type="date" [(ngModel)]="row.meetingDate" />
</div>
} @else {
<div class="ws-f ws-f--cheqno">
<label class="ws-f__lbl">CHEQUE NO.</label>
<input class="ws-f__ctl ws-f__ctl--mono" [(ngModel)]="row.chequeNo" placeholder="—" />
</div>
<div class="ws-f ws-f--cheqdt">
<label class="ws-f__lbl">CHEQ. DATE</label>
<input class="ws-f__ctl" [(ngModel)]="row.cheqDate" placeholder="dd-mm-yyyy" />
</div>
}
<div class="ws-f ws-f--tok">
<label class="ws-f__lbl">TOK. NO.</label>
<input