From 811c535d998ad6a99aefe5436a8e16fe11f400f0 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Tue, 8 Sep 2026 15:18:54 +0530 Subject: [PATCH] fix(shares): persist product-default charges on submit; show real charge state; office-delete UX 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 --- src/app/organization/offices/offices.component.ts | 11 +++++++++-- .../create-shares-account.component.ts | 11 ++++++++++- .../edit-shares-account.component.ts | 8 +++++++- .../shares-account-charges-step.component.ts | 7 +++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/app/organization/offices/offices.component.ts b/src/app/organization/offices/offices.component.ts index 518b5bbd9..2f71d361f 100644 --- a/src/app/organization/offices/offices.component.ts +++ b/src/app/organization/offices/offices.component.ts @@ -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) => { diff --git a/src/app/shares/create-shares-account/create-shares-account.component.ts b/src/app/shares/create-shares-account/create-shares-account.component.ts index 302754f98..694c36fbb 100644 --- a/src/app/shares/create-shares-account/create-shares-account.component.ts +++ b/src/app/shares/create-shares-account/create-shares-account.component.ts @@ -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), diff --git a/src/app/shares/edit-shares-account/edit-shares-account.component.ts b/src/app/shares/edit-shares-account/edit-shares-account.component.ts index 0c0d9cb2b..919e5a3ac 100644 --- a/src/app/shares/edit-shares-account/edit-shares-account.component.ts +++ b/src/app/shares/edit-shares-account/edit-shares-account.component.ts @@ -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), diff --git a/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.ts b/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.ts index d5a89f5f5..a4af04a3a 100644 --- a/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.ts +++ b/src/app/shares/shares-account-stepper/shares-account-charges-step/shares-account-charges-step.component.ts @@ -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; } }); }