fix(products): show named GL account changes in the pending-edit diff dialog

otherGlMappings (the nested document behind Savings/Current/FD/RD Interest
Payable/Paid and similar named accounts) was excluded from the generic
field diff entirely, and the backend was silently dropping its value
anyway (see the paired backend fix). Now that the backend correctly
stages this field, diff it explicitly: find each reserved account name
across original.otherGlMappingsMap and proposed.otherGlMappings, resolve
the GL code to a readable name, and show a row only when it actually
moved -- same convention as every other field in this dialog, instead of
a raw JSON blob or nothing at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 12:40:10 +05:30
co-authored by Claude Sonnet 5
parent a7ed3883df
commit 74a51a0626
@@ -52,16 +52,29 @@ export class ProductEditDiffDialogComponent implements OnInit {
productName = '';
rows: DiffRow[] = [];
/** Request plumbing the edit form has to send, but which isn't a product setting a checker reviews. */
/** Request plumbing the edit form has to send, but which isn't a product setting a checker reviews.
* otherGlMappings is here too -- it is a nested {assets,liabilities,expenses,income} document, not
* a scalar, so it is handled separately by namedGlMappingRows() rather than the generic loop below
* (which would otherwise dump it as a raw, unreadable JSON blob). */
private static readonly IGNORED_KEYS = new Set([
'locale',
'dateFormat',
'monthDayFormat',
'id',
'productId',
'savingsProductId'
'savingsProductId',
'otherGlMappings'
]);
/** The two reserved otherGlMappings entries a checker actually needs to see by name -- see
* AccountingProcessorHelper.SAVINGS_INTEREST_PAYABLE_MAPPING / SAVINGS_INTEREST_PAID_MAPPING. Any
* other entry in the assets/liabilities/expenses/income lists is a free-form "Other" row with no
* fixed identity to diff against by name, so those are left out of this dialog same as before. */
private static readonly NAMED_GL_MAPPINGS = [
'Savings Interest Payable',
'Savings Interest Paid'
];
/**
* Payload fields the stored product exposes under a different path, so the "current" side has
* something to compare against instead of always reading as newly set.
@@ -115,6 +128,9 @@ export class ProductEditDiffDialogComponent implements OnInit {
};
private glAccountNamesById = new Map<string, string>();
/** Same pool as glAccountNamesById, keyed by GL code instead of id -- otherGlMappings entries (see
* namedGlMappingRows) identify an account by its code, not its id. */
private glAccountNamesByCode = new Map<string, string>();
ngOnInit(): void {
this.productName = this.data.productName ?? '';
@@ -131,10 +147,11 @@ export class ProductEditDiffDialogComponent implements OnInit {
original: this.displayValue(originalValue, key, original),
proposed: this.displayValue(proposed[key], key, original)
}))
.concat(this.namedGlMappingRows(original, proposed))
.sort((a, b) => a.label.localeCompare(b.label));
}
/** Pools every GL account the template offers so an account id can be shown as its name. */
/** Pools every GL account the template offers so an account id/code can be shown as its name. */
private buildGlAccountLookup(template: any): void {
const optionGroups = template?.accountingMappingOptions ?? {};
for (const group of Object.values<any>(optionGroups)) {
@@ -146,13 +163,68 @@ export class ProductEditDiffDialogComponent implements OnInit {
continue;
}
const label = account.name ?? account.glCode ?? null;
if (label) {
this.glAccountNamesById.set(String(account.id), account.glCode ? `${label} (${account.glCode})` : label);
if (!label) {
continue;
}
const display = account.glCode ? `${label} (${account.glCode})` : label;
this.glAccountNamesById.set(String(account.id), display);
if (account.glCode) {
this.glAccountNamesByCode.set(String(account.glCode), display);
}
}
}
}
/**
* Diffs the two reserved otherGlMappings entries by name across every category (assets/liabilities/
* expenses/income) rather than the generic key loop above, since otherGlMappings is a nested
* {assets,liabilities,expenses,income} document whose entries move category if the maker changes
* which account type they picked -- comparing category-by-category would miss that. `original`
* carries this document as otherGlMappingsMap (falling back to the older otherGlMappings key for a
* template response that hasn't been re-fetched since), `proposed` as otherGlMappings.
*/
private namedGlMappingRows(original: any, proposed: any): DiffRow[] {
const proposedOther = proposed?.otherGlMappings;
if (proposedOther === undefined) {
// Not part of this edit at all -- nothing to compare, same as any other field the maker didn't touch.
return [];
}
const originalOther = original?.otherGlMappingsMap ?? original?.otherGlMappings;
const findByName = (doc: any, accountName: string): { glCode?: string } | null => {
const categories: any[] = doc ? Object.values(doc) : [];
for (const category of categories) {
if (!Array.isArray(category)) {
continue;
}
const match = category.find((entry: any) => entry?.accountName === accountName);
if (match) {
return match;
}
}
return null;
};
const resolve = (entry: { glCode?: string } | null): string => {
if (!entry?.glCode) {
return '—';
}
return this.glAccountNamesByCode.get(String(entry.glCode)) ?? entry.glCode;
};
return ProductEditDiffDialogComponent.NAMED_GL_MAPPINGS.map((accountName) => {
const before = findByName(originalOther, accountName);
const after = findByName(proposedOther, accountName);
return { accountName, before, after };
})
.filter(({ before, after }) => (before?.glCode ?? null) !== (after?.glCode ?? null))
.map(({ accountName, before, after }) => ({
label: accountName,
original: resolve(before),
proposed: resolve(after)
}));
}
/** Reads the current value for a payload field, following an alias or GL mapping when needed. */
private originalValueFor(key: string, original: any): any {
const glMappingKey = ProductEditDiffDialogComponent.GL_FIELD_TO_MAPPING[key];