fix(shares): mandatory GL+voucher entry for advance collection, split entry-fee verification from settlement
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

- Apply Collection dialog: replace product-config auto-resolved GL account with a required GL +
  voucher search (mirrored for Share and Entry Fee receipts). The entry-fee receipt is tracked
  separately from the share receipts and sent as its own field so its amount is never swept into the
  Sundry-side total or double-posted — it only verifies the fee was paid, since that money already
  sits wherever the cashier deposited it.
- Shows the account's required total (share value + entry fee) and live coverage up front, instead of
  only after a failed apply.
- Apply Additional Shares: same mandatory GL + voucher pattern for its by-voucher lookup, plus a GL
  account picker fed by the new /shareadvancecollection/glaccounts endpoint.
- shares-account-charges-step: fixed a race where the currency-control subscription (meant only for
  the edit-an-existing-application flow) wiped out product charges that ngOnChanges had just seeded
  from the same product-selection event, leaving new accounts with no charges.
- shares-account-view: hide "Share Certificate" until collection_applied is true, so a certificate
  can't be printed for shares nobody has actually paid for yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 20:05:11 +05:30
co-authored by Claude Sonnet 5
parent 9f95d01196
commit 44b23ef110
11 changed files with 475 additions and 69 deletions
@@ -109,6 +109,22 @@
Share Sundry against them, and approving this request moves it into share capital.
</p>
<div class="gl-select-field">
<select
class="gl-select"
[(ngModel)]="glAccountNo"
[ngModelOptions]="{ standalone: true }"
[disabled]="lookingUpVoucher || submitting || loadingGlAccounts"
>
<option value="" disabled>
{{ loadingGlAccounts ? 'Loading GL accounts…' : 'Select GL account…' }}
</option>
@for (gl of glAccounts; track gl.id) {
<option [value]="gl.gl_code">{{ gl.gl_code }} — {{ gl.name }}</option>
}
</select>
</div>
<div class="voucher-lookup">
<input
#voucherInput
@@ -121,7 +137,7 @@
<button
mat-stroked-button
type="button"
[disabled]="!voucherInput.value.trim() || lookingUpVoucher || submitting"
[disabled]="!voucherInput.value.trim() || !glAccountNo.trim() || lookingUpVoucher || submitting"
(click)="lookupVoucher(voucherInput.value)"
>
{{ lookingUpVoucher ? 'Finding…' : 'Find' }}
@@ -49,6 +49,30 @@ $danger: #b3261e;
margin: 0 0 12px;
}
.gl-select-field {
margin: 0 0 10px;
}
.gl-select {
width: 100%;
padding: 9px 11px;
border: 1px solid $border;
border-radius: 6px;
font-size: 13px;
color: $primary;
background: #fff;
&:focus {
outline: none;
border-color: $primary;
}
&:disabled {
background: #f4f7fb;
color: $muted;
}
}
.voucher-lookup {
display: flex;
gap: 8px;
@@ -8,7 +8,7 @@
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { MatRadioModule } from '@angular/material/radio';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -55,6 +55,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./apply-shares.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FormsModule,
MatRadioModule,
MatProgressSpinnerModule,
MatIconModule
@@ -99,6 +100,13 @@ export class ApplySharesComponent implements OnInit {
lookingUpVoucher = false;
voucherError = '';
/** GL account the deposit was posted to — required, and always picked by hand. Never resolved
* from a share product's accounting setup, so a broken or missing product mapping can never
* make a genuine voucher unreachable. */
glAccountNo = '';
glAccounts: any[] = [];
loadingGlAccounts = true;
/** What the member's savings can cover, for the savings route. */
savingsEligibility: {
eligible: boolean;
@@ -120,6 +128,14 @@ export class ApplySharesComponent implements OnInit {
this.createApplySharesAccountForm();
this.applySharesForm.get('unitPrice').patchValue(this.sharesAccountData.currentMarketPrice || '');
this.applySharesForm.get('amount').patchValue(this.amount || '');
this.cashierService
.getShareAdvanceGlAccounts()
.pipe(catchError(() => of(null)))
.subscribe((res: any) => {
this.glAccounts = res?.glAccounts || [];
this.loadingGlAccounts = false;
});
}
/**
@@ -266,14 +282,15 @@ export class ApplySharesComponent implements OnInit {
*/
lookupVoucher(voucherNo: string): void {
const trimmed = (voucherNo || '').trim();
if (!trimmed || this.lookingUpVoucher) {
const glAccountNo = this.glAccountNo.trim();
if (!trimmed || !glAccountNo || this.lookingUpVoucher) {
return;
}
this.lookingUpVoucher = true;
this.voucherError = '';
this.cashierService
.getShareAdvanceByVoucher(trimmed, this.sharesAccountData.clientId)
.getShareAdvanceByVoucher(trimmed, glAccountNo, this.sharesAccountData.clientId)
.pipe(
catchError((err: any) => {
this.voucherError = this.readError(err, `Voucher ${trimmed} could not be found.`);