Skip to content

WEB-112: Sanitize GSIM fields before calculateLoanSchedule to avoid NPE#3569

Open
deepaksoni47 wants to merge 1 commit into
openMF:devfrom
deepaksoni47:WEB-112-fix-gsim-schedule-sanitization
Open

WEB-112: Sanitize GSIM fields before calculateLoanSchedule to avoid NPE#3569
deepaksoni47 wants to merge 1 commit into
openMF:devfrom
deepaksoni47:WEB-112-fix-gsim-schedule-sanitization

Conversation

@deepaksoni47
Copy link
Copy Markdown
Contributor

@deepaksoni47 deepaksoni47 commented May 10, 2026

Description

This PR adds a frontend safeguard to prevent GSIM-linked fields from being sent to the backend loan schedule calculation endpoint, which was causing a 500 Internal Server Error.

Changes Made:

  • Added sanitizeLoanPayload() private method to LoansService that recursively removes unsafe GSIM linkage fields (linkAccountId, linkAccountOwnerId, linkAccountOwnerName, linkedSavingsAccount) from loan payloads
  • Applied sanitization in calculateLoanSchedule() endpoint before posting to /loans?command=calculateLoanSchedule
  • Added comprehensive Jest unit tests to verify forbidden fields are removed at all nesting levels while allowed fields are preserved

Why These Changes:
The root cause of the 500 error is a NullPointerException in the Fineract backend (LoanScheduleCalculationPlatformServiceImpl) during GLIM + GSIM validation. While a permanent fix should be made in Apache Fineract, this frontend safeguard prevents invalid payloads from being sent, providing immediate relief for users.

Dependencies:
No new dependencies required. Uses existing Angular testing utilities (provideHttpClient, provideHttpClientTesting for Angular 20 compatibility).

Related issues and discussion

WEB-112

Screenshots, if any

N/A - Backend API safeguard (no UI changes)

Checklist

  • If you have multiple commits please combine them into one commit by squashing them.
  • Read and understood the contribution guidelines at web-app/.github/CONTRIBUTING.md.
  • Code follows project style (ran npm run lint, npm run headers:add)
  • Unit tests added and passing (npm run test - 2/2 tests passing)
  • Tested on dockerized Fineract backend - schedule generation no longer throws 500

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Loan schedule calculation requests now filter out certain account linkage fields before sending to the server, ensuring cleaner request payloads.
  • Tests

    • Added comprehensive unit tests for loan schedule calculation to verify proper request sanitization and field handling.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'pre_merge_checks'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

LoansService.calculateLoanSchedule now sanitizes the request payload before posting to the backend. A new private helper recursively deep-copies the payload and removes forbidden GSIM linkage fields from objects and arrays at any depth. Tests verify forbidden keys are removed while allowed fields and primitives are preserved.

Changes

Loan Payload Sanitization

Layer / File(s) Summary
Sanitization Helper
src/app/loans/loans.service.ts
New private sanitizeLoanPayload function recursively deep-copies the payload and removes forbidden GSIM keys (linkAccountId, linkAccountOwnerId, linkAccountOwnerName, linkedSavingsAccount) from objects and arrays at any nesting depth.
Service Integration
src/app/loans/loans.service.ts
calculateLoanSchedule method now sanitizes the input payload via the helper before posting to /loans?command=calculateLoanSchedule.
Test Suite and Cases
src/app/loans/loans.service.spec.ts
Test suite sets up HttpClientTestingModule and mocked dependencies. Tests verify forbidden GSIM fields are removed from top-level objects, nested objects, and arrays, while allowed fields and primitive values are preserved in the request body.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'WEB-112: Sanitize GSIM fields before calculateLoanSchedule to avoid NPE' directly matches the main change: sanitizing GSIM-related fields in the loan schedule calculation to prevent null pointer exceptions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/app/loans/loans.service.ts (1)

717-767: ⚡ Quick win

Simplify sanitizeLoanPayload with lodash and a single recursion.

The current implementation has a parallel src→dst traversal with two object-construction sites (object branch and array branch), two duplicated checks for null/undefined/typeof !== 'object', and an inline forbiddenKeys Set rebuilt on every call. Per the repo guideline to "re-use the lodash and moment libraries efficiently", and to reduce cognitive load, a single recursive walker (or _.cloneDeepWith + _.omit) expresses the intent more clearly. Pulling forbiddenKeys to a class/file-level readonly constant also avoids re-allocation per call.

Note: like the current code, any non-plain-object instance (e.g. Date, class instances) would be coerced to a plain {}. In practice the schedule payload is built with formatted-date strings, so this is acceptable — but worth keeping in mind if the calling code ever passes raw Date values.

♻️ Proposed simplification
+  private static readonly FORBIDDEN_LOAN_PAYLOAD_KEYS: ReadonlySet<string> = new Set([
+    'linkAccountId',
+    'linkAccountOwnerId',
+    'linkAccountOwnerName',
+    'linkedSavingsAccount'
+  ]);
+
   /**
    * Remove potentially unsafe GSIM linkage fields from payload recursively.
    * This is a defensive workaround until backend validation is fixed in Fineract.
    */
   private sanitizeLoanPayload(obj: any): any {
-    if (obj === null || obj === undefined) return obj;
-    // Primitive types
-    if (typeof obj !== 'object') return obj;
-
-    // Work on a deep copy to avoid mutating caller's data
-    const copy = Array.isArray(obj) ? [] : {};
-
-    const forbiddenKeys = new Set([
-      'linkAccountId',
-      'linkAccountOwnerId',
-      'linkAccountOwnerName',
-      'linkedSavingsAccount'
-    ]);
-
-    const recurse = (src: any, dst: any) => {
-      if (src === null || src === undefined) return src;
-      if (typeof src !== 'object') return src;
-      if (Array.isArray(src)) {
-        for (let i = 0; i < src.length; i++) {
-          const val = src[i];
-          if (typeof val === 'object' && val !== null) {
-            const child = Array.isArray(val) ? [] : {};
-            dst.push(recurse(val, child));
-          } else {
-            dst.push(val);
-          }
-        }
-        return dst;
-      }
-
-      for (const key of Object.keys(src)) {
-        if (forbiddenKeys.has(key)) {
-          // skip this key
-          continue;
-        }
-        const val = src[key];
-        if (val === null || val === undefined) {
-          dst[key] = val;
-        } else if (typeof val === 'object') {
-          dst[key] = Array.isArray(val) ? [] : {};
-          recurse(val, dst[key]);
-        } else {
-          dst[key] = val;
-        }
-      }
-      return dst;
-    };
-
-    return recurse(obj, copy);
+    if (obj === null || typeof obj !== 'object') return obj;
+    if (Array.isArray(obj)) return obj.map((item) => this.sanitizeLoanPayload(item));
+    const out: Record<string, any> = {};
+    for (const key of Object.keys(obj)) {
+      if (LoansService.FORBIDDEN_LOAN_PAYLOAD_KEYS.has(key)) continue;
+      out[key] = this.sanitizeLoanPayload(obj[key]);
+    }
+    return out;
   }

Equivalent lodash variant (if preferred):

import { cloneDeepWith, isPlainObject, omit } from 'lodash-es';

private sanitizeLoanPayload(obj: any): any {
  return cloneDeepWith(obj, (value) =>
    isPlainObject(value)
      ? cloneDeepWith(omit(value as object, [...LoansService.FORBIDDEN_LOAN_PAYLOAD_KEYS]),
                      (v) => (isPlainObject(v) ? this.sanitizeLoanPayload(v) : undefined))
      : undefined
  );
}

As per coding guidelines: "Re-use the lodash and moment libraries efficiently."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/loans/loans.service.ts` around lines 717 - 767, The
sanitizeLoanPayload function duplicates traversal logic, rebuilds forbiddenKeys
on every call, and repeats null/type checks; replace it with a single recursive
walker (or use lodash cloneDeepWith + omit) and move the forbiddenKeys Set to a
class/file-level readonly constant (e.g.
LoansService.FORBIDDEN_LOAN_PAYLOAD_KEYS) so it isn’t reallocated per call;
update sanitizeLoanPayload to perform one traversal that omits any key in that
constant for plain objects and correctly handles arrays by recursing into
elements, removing the duplicated object/array construction and redundant
typeof/null checks.
src/app/loans/loans.service.spec.ts (2)

38-70: ⚡ Quick win

Strengthen sanitization coverage: assert all four forbidden keys and payload immutability.

The test only exercises linkAccountId and linkAccountOwnerId. The service explicitly forbids two more keys (linkAccountOwnerName, linkedSavingsAccount) which are currently uncovered, so a future change that drops one of them from the Set would go undetected. The helper also documents that it must not mutate the caller's payload — that invariant is also untested.

🧪 Suggested additions
   it('should sanitize GSIM fields before posting calculateLoanSchedule', () => {
     const payload = {
       principal: 1000,
       linkAccountId: 42,
+      linkAccountOwnerName: 'Alice',
+      linkedSavingsAccount: { id: 5 },
       nested: {
         linkAccountOwnerId: 7,
+        linkAccountOwnerName: 'Bob',
         ok: 'keep'
       },
       members: [
         { id: 1, linkAccountId: 99 },
-        { id: 2, name: 'foo' }
+        { id: 2, name: 'foo', linkedSavingsAccount: { id: 9 } }
       ]
     } as any;
+    const original = JSON.parse(JSON.stringify(payload));

     service.calculateLoanSchedule(payload).subscribe(() => {});

     const req = httpMock.expectOne('/loans?command=calculateLoanSchedule');
     expect(req.request.method).toBe('POST');

     const body = req.request.body;

-    // Forbidden keys should be removed at top level and nested locations
+    // All forbidden keys should be removed at every depth
     expect(body.linkAccountId).toBeUndefined();
+    expect(body.linkAccountOwnerName).toBeUndefined();
+    expect(body.linkedSavingsAccount).toBeUndefined();
     expect(body.nested.linkAccountOwnerId).toBeUndefined();
+    expect(body.nested.linkAccountOwnerName).toBeUndefined();
     expect(body.members[0].linkAccountId).toBeUndefined();
+    expect(body.members[1].linkedSavingsAccount).toBeUndefined();

     // Allowed keys should be preserved
     expect(body.principal).toBe(1000);
     expect(body.nested.ok).toBe('keep');
     expect(body.members[1].name).toBe('foo');

+    // Original caller payload must remain unmutated
+    expect(payload).toEqual(original);
+
     req.flush({});
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/loans/loans.service.spec.ts` around lines 38 - 70, The test for
service.calculateLoanSchedule currently only checks removal of linkAccountId and
linkAccountOwnerId; update the test to also include and assert removal of the
other two forbidden keys linkAccountOwnerName and linkedSavingsAccount (in both
top-level and nested/array locations as appropriate), and assert that the
original payload object is not mutated (compare deep-equality or check original
fields still exist on the input after calling calculateLoanSchedule) so the
helper's immutability guarantee is covered.

2-2: 💤 Low value

Use provideHttpClient() and provideHttpClientTesting() instead of the deprecated HttpClientTestingModule.

HttpClientTestingModule has been deprecated since Angular 18 in favor of function-based providers. Angular 20.3.17 used in this project recommends using provideHttpClient() and provideHttpClientTesting() in the providers array instead of importing the module. This is non-blocking but aligns the test file with current Angular testing best practices.

♻️ Proposed change
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
+import { provideHttpClient } from '@angular/common/http';
@@
     TestBed.configureTestingModule({
-      imports: [HttpClientTestingModule],
       providers: [
         LoansService,
+        provideHttpClient(),
+        provideHttpClientTesting(),
         { provide: SettingsService, useValue: mockSettings },
         { provide: Dates, useValue: mockDates }]
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/loans/loans.service.spec.ts` at line 2, Replace the deprecated
HttpClientTestingModule import and usage with the function-based providers:
remove HttpClientTestingModule from the TestBed.configureTestingModule imports
and instead add provideHttpClient() and provideHttpClientTesting() to the
providers array; update top-level imports to use provideHttpClient (from
`@angular/common/http`) and provideHttpClientTesting (from
`@angular/common/http/testing`) and keep HttpTestingController usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/app/loans/loans.service.spec.ts`:
- Around line 38-70: The test for service.calculateLoanSchedule currently only
checks removal of linkAccountId and linkAccountOwnerId; update the test to also
include and assert removal of the other two forbidden keys linkAccountOwnerName
and linkedSavingsAccount (in both top-level and nested/array locations as
appropriate), and assert that the original payload object is not mutated
(compare deep-equality or check original fields still exist on the input after
calling calculateLoanSchedule) so the helper's immutability guarantee is
covered.
- Line 2: Replace the deprecated HttpClientTestingModule import and usage with
the function-based providers: remove HttpClientTestingModule from the
TestBed.configureTestingModule imports and instead add provideHttpClient() and
provideHttpClientTesting() to the providers array; update top-level imports to
use provideHttpClient (from `@angular/common/http`) and provideHttpClientTesting
(from `@angular/common/http/testing`) and keep HttpTestingController usage
unchanged.

In `@src/app/loans/loans.service.ts`:
- Around line 717-767: The sanitizeLoanPayload function duplicates traversal
logic, rebuilds forbiddenKeys on every call, and repeats null/type checks;
replace it with a single recursive walker (or use lodash cloneDeepWith + omit)
and move the forbiddenKeys Set to a class/file-level readonly constant (e.g.
LoansService.FORBIDDEN_LOAN_PAYLOAD_KEYS) so it isn’t reallocated per call;
update sanitizeLoanPayload to perform one traversal that omits any key in that
constant for plain objects and correctly handles arrays by recursing into
elements, removing the duplicated object/array construction and redundant
typeof/null checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3b842986-7780-4a9c-8025-ef0744ad67af

📥 Commits

Reviewing files that changed from the base of the PR and between d89dfdb and 7a2b853.

📒 Files selected for processing (2)
  • src/app/loans/loans.service.spec.ts
  • src/app/loans/loans.service.ts

- Add sanitizeLoanPayload method to LoansService to remove unsafe GSIM linkage fields (linkAccountId, linkAccountOwnerId, linkAccountOwnerName, linkedSavingsAccount) before posting to backend
- Apply sanitization in calculateLoanSchedule endpoint to prevent payload from triggering NullPointerException in Fineract LoanScheduleCalculationPlatformServiceImpl
- Add unit tests for payload sanitization to verify forbidden fields are removed and allowed fields are preserved
- Fixes 500 Internal Server Error when generating repayment schedule for GLIM loans linked to GSIM accounts
@deepaksoni47 deepaksoni47 force-pushed the WEB-112-fix-gsim-schedule-sanitization branch from 7a2b853 to ca7060f Compare May 10, 2026 08:37
@deepaksoni47
Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit review comments and updated the implementation accordingly. Also re-ran tests and checks after the changes.

providedIn: 'root'
})
export class LoansService {
private static readonly FORBIDDEN_LOAN_PAYLOAD_KEYS: ReadonlySet<string> = new Set([
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if a person requires to disburse a loan to a linked savings account?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IOhacker Thanks for pointing this out.

The intention here is only to sanitize payloads for the calculateLoanSchedule() flow, which is used for repayment schedule preview/calculation and does not perform actual loan disbursement operations.

Actual disbursement to linked savings accounts goes through separate actions/endpoints such as disburse and disbursetosavings, so those flows remain unaffected by this change.

The issue I observed was specific to the schedule calculation endpoint, where the backend was throwing an NPE when GSIM-linked fields were included in the calculation payload. Since these fields are not required for repayment schedule computation itself, this change removes them only for that calculation request as a temporary defensive workaround until the backend-side validation is addressed.

Please let me know if there's a specific disbursement scenario that uses calculateLoanSchedule() that I may have missed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants