diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java index 3043ceff6..30ce2a490 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java @@ -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 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 submitted, final String name, final Number value) { if (submitted.contains(name)) { root.addProperty(name, value);