diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-products/product-edit-diff-dialog/product-edit-diff-dialog.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-products/product-edit-diff-dialog/product-edit-diff-dialog.component.ts index 43e1e7dea..c3970ff75 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-products/product-edit-diff-dialog/product-edit-diff-dialog.component.ts +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-products/product-edit-diff-dialog/product-edit-diff-dialog.component.ts @@ -69,7 +69,14 @@ export class ProductEditDiffDialogComponent implements OnInit { currencyCode: 'currency.code', digitsAfterDecimal: 'currency.decimalPlaces', inMultiplesOf: 'currency.inMultiplesOf', - taxGroupId: 'taxGroup.id' + taxGroupId: 'taxGroup.id', + // Enum fields the stored product exposes as "Type": {id, value}", not "TypeId" -- + // without these the "current" side always read as blank, so these showed as changed on every + // edit even when the maker never touched them. + preClosurePenalInterestOnTypeId: 'preClosurePenalInterestOnType.id', + minDepositTermTypeId: 'minDepositTermType.id', + maxDepositTermTypeId: 'maxDepositTermType.id', + inMultiplesOfDepositTermTypeId: 'inMultiplesOfDepositTermType.id' }; /** @@ -78,6 +85,17 @@ export class ProductEditDiffDialogComponent implements OnInit { * `accountingMappingOptions` rather than a per-field `Options` key, so both sides of the * diff need this table to turn an account id into the account's name. */ + /** + * Fields whose option list isn't named after the field at all -- min/max/in-multiples-of deposit + * term type all resolve against one shared frequency list (Days/Weeks/Months/Years), not a list of + * their own, so the `Options` convention below never finds them. + */ + private static readonly OPTIONS_KEY_ALIASES: Record = { + minDepositTermTypeId: 'periodFrequencyTypeOptions', + maxDepositTermTypeId: 'periodFrequencyTypeOptions', + inMultiplesOfDepositTermTypeId: 'periodFrequencyTypeOptions' + }; + private static readonly GL_FIELD_TO_MAPPING: Record = { savingsReferenceAccountId: 'savingsReferenceAccount', savingsControlAccountId: 'savingsControlAccount', @@ -149,23 +167,37 @@ export class ProductEditDiffDialogComponent implements OnInit { /** * Enum/option fields arrive from the edit as a bare id, which reads as a meaningless number in the - * table. The product fetched with template=true carries the matching option list under a predictable - * `Options` key (accountingRule -> accountingRuleOptions), so the id is resolved back to the - * label the maker actually picked. Returns null when there is no such list to resolve against. + * table. The product fetched with template=true usually carries the matching option list under a + * predictable `Options` key (accountingRule -> accountingRuleOptions), so the id is resolved + * back to the label the maker actually picked. Two other shapes this also has to cover: + * - a field ending in "...TypeId" whose list drops the "Id" (preClosurePenalInterestOnTypeId -> + * preClosurePenalInterestOnTypeOptions), and + * - a field resolving against a shared list that isn't named after the field at all + * (OPTIONS_KEY_ALIASES). + * All three are tried in turn; returns null only when none of them find a match, so the id is + * shown as-is rather than silently wrong. */ private resolveOptionLabel(fieldKey: string, id: any, template: any): string | null { if (ProductEditDiffDialogComponent.GL_FIELD_TO_MAPPING[fieldKey]) { return this.glAccountNamesById.get(String(id)) ?? null; } - const options = template?.[`${fieldKey}Options`]; - if (!Array.isArray(options)) { - return null; + const candidateKeys = [ + ProductEditDiffDialogComponent.OPTIONS_KEY_ALIASES[fieldKey], + `${fieldKey}Options`, + fieldKey.endsWith('Id') ? `${fieldKey.slice(0, -2)}Options` : null + ].filter((key): key is string => !!key); + + for (const optionsKey of candidateKeys) { + const options = template?.[optionsKey]; + if (!Array.isArray(options)) { + continue; + } + const match = options.find((option: any) => String(option?.id) === String(id)); + if (match) { + return match.value ?? match.name ?? match.code ?? null; + } } - const match = options.find((option: any) => String(option?.id) === String(id)); - if (!match) { - return null; - } - return match.value ?? match.name ?? match.code ?? null; + return null; } /**