fix(products): named GL account changes (e.g. Savings Interest Payable/Paid) were silently lost on a pending edit

Root cause: ProductPendingEditFieldMapper.populateCoreFields() captured
otherGlMappings with stringValueOfParameterNamed(), which only handles a
JSON string primitive. The maker's edit actually submits otherGlMappings
as a nested {assets,liabilities,expenses,income} object -- the same shape
SavingsProduct#update() already reads correctly via jsonFragment() -- so
the pending-edit row was staged with this field blank every time,
regardless of what the maker changed.

Two consequences, both now fixed:
 - The checker's diff dialog showed "No changes to review" even when a GL
   account mapping (e.g. Savings/Current/FD/RD Interest Payable or Paid)
   was genuinely changed, since the submitted value never made it into
   proposedChanges.
 - Approving the edit replayed a payload with no otherGlMappings at all,
   so the GL account change was silently dropped rather than applied --
   the edit showed as approved, but the product's accounting mapping
   never actually moved.

Fixed the write side (jsonFragment instead of stringValueOfParameterNamed)
and the read-side replay (writeCoreFieldsToJson now re-adds the stored
value as a parsed JSON element via the new putJsonFragment, not a quoted
string, which would have double-encoded it on replay). Shared by Savings,
Current, FD and RD products, since they all go through this one mapper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 12:39:58 +05:30
co-authored by Claude Sonnet 5
parent 82336cbe98
commit ce8bb7e5ca
@@ -158,7 +158,15 @@ public final class ProductPendingEditFieldMapper {
present.add("taxGroupId");
}
if (command.hasParameter("otherGlMappings")) {
f.setOtherGlMappings(command.stringValueOfParameterNamed("otherGlMappings"));
// otherGlMappings arrives as a nested JSON object ({assets:[...], liabilities:[...],
// expenses:[...]}), the same shape SavingsProduct#update() reads via jsonFragment() --
// NOT a JSON string primitive. stringValueOfParameterNamed() only handles the latter and
// silently came back blank for the former, so every "Savings Interest Payable"/"Savings
// Interest Paid" (and other named GL account) edit was staged with an empty value: it
// never reached the pending-edit row, so the checker's diff had nothing to show and
// approving replayed no GL account change at all. jsonFragment() is the same call the
// live (non-pending) update path already uses for this exact parameter.
f.setOtherGlMappings(command.jsonFragment("otherGlMappings"));
present.add("otherGlMappings");
}
if (command.hasParameter("isDormancyTrackingActive")) {
@@ -355,7 +363,7 @@ public final class ProductPendingEditFieldMapper {
putNumber(root, submitted, "minBalanceForInterestCalculation", f.getMinBalanceForInterestCalculation());
putBoolean(root, submitted, "withHoldTax", f.getWithHoldTax());
putNumber(root, submitted, "taxGroupId", f.getTaxGroupId());
putString(root, submitted, "otherGlMappings", f.getOtherGlMappings());
putJsonFragment(root, submitted, "otherGlMappings", f.getOtherGlMappings());
putBoolean(root, submitted, "isDormancyTrackingActive", f.getIsDormancyTrackingActive());
putNumber(root, submitted, "daysToInactive", f.getDaysToInactive());
putNumber(root, submitted, "daysToDormancy", f.getDaysToDormancy());
@@ -733,6 +741,24 @@ public final class ProductPendingEditFieldMapper {
}
}
/**
* Same as {@link #putString}, but for a value that is itself raw JSON (an object/array captured
* via {@code JsonCommand#jsonFragment}) rather than a plain string. {@code addProperty} would wrap
* it in quotes as an escaped string literal, which {@code jsonFragment} on the replayed command
* would then hand back as that literal quoted text instead of the original structure -- breaking
* the downstream `other_gl_mappings::jsonb` cast this field's value round-trips through.
*/
private static void putJsonFragment(final JsonObject root, final Set<String> submitted, final String name, final String rawJson) {
if (!submitted.contains(name)) {
return;
}
if (rawJson == null || rawJson.isBlank()) {
root.add(name, com.google.gson.JsonNull.INSTANCE);
return;
}
root.add(name, com.google.gson.JsonParser.parseString(rawJson));
}
private static void putNumber(final JsonObject root, final Set<String> submitted, final String name, final Number value) {
if (submitted.contains(name)) {
root.addProperty(name, value);