fix(shares): persist product-default charges on submit; show real charge state; office-delete UX
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

Two share-account charge bugs found while chasing why a share account's
entry-fee charge wasn't showing up in the "Apply Share Advance
Collection" flow:

- create-shares-account.component.ts / edit-shares-account.component.ts:
  the submit payload built charges as { chargeId: charge.id, amount }.
  A charge seeded from the product's own default template arrives as
  ShareAccountChargeData, where the real charge id lives in `chargeId`
  and `id` is null (no saved ShareAccountCharge row yet); only a charge
  picked from this step's own "Charge" dropdown (plain ChargeData) has
  it in `id`. So `charge.id` alone silently produced chargeId: undefined
  for the common case (accepting the product's own default charges),
  Angular's JSON.stringify dropped the undefined field, and the backend
  received a charge with no id to attach -- the account got created with
  zero persisted charges no matter what the product had configured.
  Verified via the actual submitted command JSON in the DB
  (charges:[{"amount":200}], chargeId missing entirely). Fixed by
  falling back to charge.chargeId || charge.id in both places.

- shares-account-charges-step.component.ts: on the "edit an existing
  account" flow, the account's real saved charges (possibly a correctly
  empty array) get patched into chargesDataSource once the shared
  currencyCode control settles, but that patch never cleared the
  `pristine` guard. ngOnChanges fires again on the same input-update
  cycle (the sibling Terms step, declared earlier in the wizard, patches
  currencyCode from its own ngOnChanges, which synchronously triggers
  this component's currencyCode subscription, and Angular then runs
  this component's own ngOnChanges right after) -- with `pristine` still
  true, that re-seeded chargesDataSource from the product's *current*
  default template, silently overwriting the real (possibly empty)
  saved state. Set pristine = false once the real charges are patched
  in, so a later ngOnChanges can't clobber it.

Also: offices.component.ts now distinguishes an office delete that
actually completed from one that was queued for checker approval
(response.rollbackTransaction, the same signal every other
maker-checker-eligible screen in this app already keys off), instead of
always claiming "Office Removed" regardless of which happened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 15:18:54 +05:30
co-authored by Claude Sonnet 5
parent f30444c486
commit 811c535d99
4 changed files with 33 additions and 4 deletions
@@ -262,8 +262,15 @@ export class OfficesComponent implements OnInit {
ref.afterClosed().subscribe((response: any) => {
if (!response?.delete) return;
this.organizationService.deleteOffice(row.id).subscribe({
next: () => {
this.showSuccess('Office Removed', `"${row.name}" has been deleted.`);
next: (response: any) => {
if (response?.rollbackTransaction) {
this.showSuccess(
'Sent for Approval',
`Deleting "${row.name}" has been submitted and is awaiting checker approval.`
);
} else {
this.showSuccess('Office Removed', `"${row.name}" has been deleted.`);
}
this.refreshOffices();
},
error: (err: any) => {
@@ -129,7 +129,16 @@ export class CreateSharesAccountComponent {
const sharesAccount = {
...this.sharesAccount,
clientId: this.sharesAccountTemplate.clientId,
charges: this.sharesAccount.charges.map((charge: any) => ({ chargeId: charge.id, amount: charge.amount })),
// A charge seeded from the product's own default template arrives as ShareAccountChargeData --
// its `chargeId` field carries the real underlying charge id, `id` is null (it has no saved
// ShareAccountCharge row yet). A charge picked from this step's own "Charge" dropdown arrives as
// plain ChargeData instead, where `id` *is* the charge id. `charge.id` alone silently dropped the
// product-seeded case (chargeId ended up undefined and Angular's JSON.stringify just omits it),
// so an account created straight from the product defaults never actually got its charges saved.
charges: this.sharesAccount.charges.map((charge: any) => ({
chargeId: charge.chargeId || charge.id,
amount: charge.amount
})),
applicationDate: this.dateUtils.formatDate(this.sharesAccount.applicationDate, dateFormat),
submittedDate: this.dateUtils.formatDate(this.sharesAccount.submittedDate, dateFormat),
resolutionDate: this.dateUtils.formatDate(this.sharesAccount.resolutionDate, dateFormat),
@@ -135,7 +135,13 @@ export class EditSharesAccountComponent {
const sharesAccount = {
...this.sharesAccount,
clientId: this.sharesAccountAndTemplate.clientId,
charges: this.sharesAccount.charges.map((charge: any) => ({ chargeId: charge.id, amount: charge.amount })),
// See create-shares-account.component.ts's submit() for why chargeId falls back to charge.id --
// a product-default charge (ShareAccountChargeData) carries the real charge id in `chargeId`,
// not `id`; only a charge picked from this step's own dropdown (plain ChargeData) has it in `id`.
charges: this.sharesAccount.charges.map((charge: any) => ({
chargeId: charge.chargeId || charge.id,
amount: charge.amount
})),
applicationDate: this.dateUtils.formatDate(this.sharesAccount.applicationDate, dateFormat),
submittedDate: this.dateUtils.formatDate(this.sharesAccount.submittedDate, dateFormat),
resolutionDate: this.dateUtils.formatDate(this.sharesAccount.resolutionDate, dateFormat),
@@ -104,6 +104,13 @@ export class SharesAccountChargesStepComponent implements OnInit, OnChanges {
if (!this.isChargesPatched && this.sharesAccountTemplate.charges) {
this.chargesDataSource = this.sharesAccountTemplate.charges;
this.isChargesPatched = true;
// The account's real saved charges just landed (possibly an empty array, meaning the
// account genuinely has none). ngOnChanges fires again on the very same input-update cycle
// that triggered this subscription (currencyCode.patchValue is called from the terms step's
// own ngOnChanges, which runs before this component's) -- without this, its `pristine` guard
// would still be true and silently re-seed chargesDataSource from the product's *current*
// default template, overwriting the real state this just patched in.
this.pristine = false;
}
});
}