Skip to content

WEB-892 feat: Extract close client payload construction into utility …#3439

Open
Agaba-Ed wants to merge 1 commit intoopenMF:devfrom
Agaba-Ed:close-client-utility
Open

WEB-892 feat: Extract close client payload construction into utility …#3439
Agaba-Ed wants to merge 1 commit intoopenMF:devfrom
Agaba-Ed:close-client-utility

Conversation

@Agaba-Ed
Copy link
Copy Markdown
Contributor

@Agaba-Ed Agaba-Ed commented Mar 24, 2026

Description

Extracted close client API payload construction logic from the component into a dedicated buildCloseClientPayload() utility function to improve testability, maintainability, and code reusability.

Changes Made:

  • New close-client.utils.ts: Centralized utility for payload formatting and validation

    • buildCloseClientPayload() - Main function that transforms and validates form data
    • isValidDate() - Exported type guard for Date validation, reusable across the app
    • validateClosureReasonId() - Internal helper ensuring positive integer constraint
  • New close-client.utils.spec.ts: Comprehensive test coverage

    • Tests for valid inputs, immutability, edge cases, and error scenarios
    • Parameterized tests (it.each) for thorough validation coverage
  • Updated close-client.component.ts:

    • Removed direct payload construction logic in favor of utility function
    • Added readonly modifiers to 6 injected dependencies
    • Removed unused imports

Validation Details:

  • Closure date accepts Date objects or pre-formatted strings
  • Rejects invalid/malformed dates with descriptive error messages
  • Enforces closure reason ID as positive integer
  • Prevents silent failures by throwing TypeError on invalid input

Dependencies:

  • No new external dependencies

Related issues and discussion

#892

Screenshots, if any

N/A (backend/utility extraction)

Checklist

Please make sure these boxes are checked before submitting your pull request - thanks!

  • 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 guide
  • Added appropriate file headers (MPL 2.0 license)
  • Unit tests added and all tests passing

Summary by CodeRabbit

  • Refactor

    • Centralized and hardened client-closure handling: consistent date normalization/formatting, locale/date-format propagation, stricter validation of closure reason/date, and immutability guarantees to reduce invalid submissions.
  • Tests

    • Added comprehensive tests for payload creation, date validation/formatting behavior, closure-reason validation (including edge cases), and immutability checks.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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

Extracted payload construction and date validation/formatting from CloseClientComponent into a new utility; submit() now delegates to buildCloseClientPayload(). Added types, validation helpers, and comprehensive Jest tests for the utility.

Changes

Cohort / File(s) Summary
Close Client Component
src/app/clients/clients-view/client-actions/close-client/close-client.component.ts
Removed inline payload building and date handling from submit(); changed injected fields to private readonly; now calls buildCloseClientPayload(...) and passes returned payload to executeClientCommand('close', ...).
Close Client Utilities
src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts
Added CloseClientFormValue / CloseClientPayload exports, isValidDate() type guard, and buildCloseClientPayload() to coerce/validate closureReasonId, normalize closureDate (format Date via injected dateUtils or accept validated trimmed strings), and attach dateFormat/locale.
Close Client Tests
src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts
New comprehensive Jest tests covering date formatting behaviors, invalid-date and invalid-reasonId error cases, immutability of input, boundary cases, numeric-string handling for closureReasonId, and isValidDate() semantics.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Component as CloseClientComponent
    participant Util as buildCloseClientPayload
    participant DateUtils as dateUtils.formatDate
    participant API as Server API

    User->>Component: submit(form)
    Component->>Util: buildCloseClientPayload(form, dateUtils, locale, dateFormat)
    alt closureDate is Date
        Util->>DateUtils: formatDate(Date, dateFormat)
        DateUtils-->>Util: formattedDate
    else closureDate is string
        Util-->>Util: trim and validate string
    end
    Util-->>Component: payload (closureDate:string, closureReasonId, dateFormat, locale)
    Component->>API: executeClientCommand('close', payload)
    API-->>Component: response
    Component->>User: show result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • gkbishnoi07
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: extracting close client payload construction into a utility function for improved testability and maintainability.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

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

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/app/clients/clients-view/client-actions/close-client/close-client.component.ts`:
- Line 93: The form currently uses UntypedFormGroup and passes
this.closeClientForm.value (with closureReasonId possibly a string) directly to
buildCloseClientPayload, causing validateClosureReasonId to throw; fix by
normalizing closureReasonId to a number in submit() before calling
buildCloseClientPayload (e.g. read const fv = this.closeClientForm.value; set
fv.closureReasonId = Number(fv.closureReasonId) or parseInt(...) and handle
NaN/invalid cases) or alternatively replace UntypedFormGroup with a typed
FormGroup interface so closureReasonId is a number at compile time; ensure the
change touches the submit() method and references to buildCloseClientPayload,
closureReasonId, validateClosureReasonId.

In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts`:
- Around line 1-2: This test file is missing the required MPL 2.0 license
header; add the standard MPL 2.0 header block at the very top of the file that
contains the imports and tests referencing buildCloseClientPayload,
CloseClientFormValue, and isValidDate, then run the repository header tooling
(npm run headers:add and npm run headers:check) to ensure the header is
formatted correctly and committed.

In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts`:
- Around line 81-82: The code checks typeof formValue.closureDate === 'string'
and uses trim() for validation but returns the untrimmed value; update the
branch in close-client.utils.ts so that closureDate is set to the normalized
trimmed string (e.g., closureDate = formValue.closureDate.trim()) before
returning the payload to avoid sending whitespace-padded dates to the API; keep
the existing empty-string guard in place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0af15b21-4874-4ece-8ad3-67aebbc85d61

📥 Commits

Reviewing files that changed from the base of the PR and between f82ad09 and 500e69a.

📒 Files selected for processing (3)
  • src/app/clients/clients-view/client-actions/close-client/close-client.component.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts

@Agaba-Ed Agaba-Ed force-pushed the close-client-utility branch from 500e69a to e938738 Compare March 24, 2026 15:50
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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts (1)

58-65: Add regression cases for malformed-but-non-empty string dates (and string closureReasonId behavior).

Current cases don’t explicitly lock behavior for whitespace-only and malformed non-empty date strings, and they also don’t assert the utility’s string closureReasonId conversion path.

Suggested test additions
   describe('closureDate validation', () => {
+    it.each([
+      '   ',
+      'not-a-date'
+    ])('rejects malformed string closureDate: %p', (value) => {
+      expect(() =>
+        callBuildPayload(
+          makeValidForm({
+            closureDate: value
+          })
+        )
+      ).toThrow(TypeError);
+    });
+
     it('rejects invalid Date object', () => {
       const invalidDate = new Date('invalid');
@@
   describe('closureReasonId validation', () => {
+    it('accepts numeric string closureReasonId when coercion is intended', () => {
+      const result = callBuildPayload(
+        makeValidForm({
+          closureReasonId: '42' as unknown as number
+        })
+      );
+      expect(result.closureReasonId).toBe(42);
+    });
+

Also applies to: 114-128, 131-167

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts`
around lines 58 - 65, Add regression tests in close-client.utils.spec.ts to
cover malformed-but-non-empty date strings and closureReasonId string
conversion: create specs that pass a whitespace-only closureDate (e.g., '   ')
and a malformed date string (e.g., 'not-a-date') into
callBuildPayload(makeValidForm({ closureDate: '...' })) and assert formatDateSpy
is not called and result.closureDate equals the original string; also add tests
that pass closureReasonId as a stringified number and as a string to ensure the
utility converts/handles it consistently (asserting the expected type/value on
result.closureReasonId). Use the existing helpers callBuildPayload,
makeValidForm and formatDateSpy to locate and follow current test patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts`:
- Around line 84-86: The code currently accepts any non-empty string for
formValue.closureDate and assigns it to closureDate; replace this with strict
validation by checking the trimmed string against a date format (e.g., a
YYYY-MM-DD regex) or attempting to parse it and confirming it yields a valid
date that round-trips to the same formatted string before assigning to
closureDate; if validation fails, do not accept the value (return/omit it from
the payload or surface a validation error) so malformed strings like "abc" are
not used in payload generation. Ensure you update the code paths that build the
payload to expect a validated closureDate and add/update tests for
formValue.closureDate validation.

---

Nitpick comments:
In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts`:
- Around line 58-65: Add regression tests in close-client.utils.spec.ts to cover
malformed-but-non-empty date strings and closureReasonId string conversion:
create specs that pass a whitespace-only closureDate (e.g., '   ') and a
malformed date string (e.g., 'not-a-date') into callBuildPayload(makeValidForm({
closureDate: '...' })) and assert formatDateSpy is not called and
result.closureDate equals the original string; also add tests that pass
closureReasonId as a stringified number and as a string to ensure the utility
converts/handles it consistently (asserting the expected type/value on
result.closureReasonId). Use the existing helpers callBuildPayload,
makeValidForm and formatDateSpy to locate and follow current test patterns.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 49b1d972-224b-4d7e-993f-75a6b9ed006b

📥 Commits

Reviewing files that changed from the base of the PR and between 500e69a and e938738.

📒 Files selected for processing (3)
  • src/app/clients/clients-view/client-actions/close-client/close-client.component.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/clients/clients-view/client-actions/close-client/close-client.component.ts

@Agaba-Ed Agaba-Ed force-pushed the close-client-utility branch from e938738 to 777a6f8 Compare March 24, 2026 16:14
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 (2)
src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts (2)

93-100: Immutability check is shallow and can miss mutable Date changes.

Line 95 snapshots by spread only; if a Date object were mutated in-place, this test could still pass.

♻️ Proposed tweak
     it('does not mutate input object', () => {
       const formValue = makeValidForm();
-      const snapshot = { ...formValue };
+      const snapshot: CloseClientFormValue = {
+        ...formValue,
+        closureDate:
+          formValue.closureDate instanceof Date
+            ? new Date(formValue.closureDate.getTime())
+            : formValue.closureDate
+      };

       callBuildPayload(formValue);

       expect(formValue).toEqual(snapshot);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts`
around lines 93 - 100, The immutability test for callBuildPayload is only doing
a shallow copy via spread on the result of makeValidForm, so in-place mutations
of nested objects like Date would be missed; update the test to take a deep
snapshot of the input (for example using structuredClone, a deepClone helper, or
JSON serialization where appropriate) before calling callBuildPayload(formValue)
and then assert deep equality with the snapshot to ensure nested/mutable objects
(e.g., Date instances) were not mutated.

109-109: Assert descriptive error messages, not only TypeError.

Line 109 (and similar invalid-input assertions) checks error type only. Since this utility promises descriptive failures, assert message patterns too so regressions are caught.

🧪 Example assertion hardening
-      expect(() => callBuildPayload(makeValidForm({ closureDate: invalidDate }))).toThrow(TypeError);
+      const act = () => callBuildPayload(makeValidForm({ closureDate: invalidDate }));
+      expect(act).toThrow(TypeError);
+      expect(act).toThrow(/Invalid closureDate/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts`
at line 109, Update the tests that currently assert only the error type (e.g.,
expect(() => callBuildPayload(makeValidForm({ closureDate: invalidDate
}))).toThrow(TypeError)) to assert the error message content as well: call the
same invocation but use toThrowError or toThrow with a string/RegExp that
matches the expected descriptive message from callBuildPayload (e.g., a pattern
indicating an invalid closure date). Apply the same change for other
invalid-input assertions in this spec so tests verify both error type and the
descriptive message produced by callBuildPayload / makeValidForm input
validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts`:
- Around line 93-100: The immutability test for callBuildPayload is only doing a
shallow copy via spread on the result of makeValidForm, so in-place mutations of
nested objects like Date would be missed; update the test to take a deep
snapshot of the input (for example using structuredClone, a deepClone helper, or
JSON serialization where appropriate) before calling callBuildPayload(formValue)
and then assert deep equality with the snapshot to ensure nested/mutable objects
(e.g., Date instances) were not mutated.
- Line 109: Update the tests that currently assert only the error type (e.g.,
expect(() => callBuildPayload(makeValidForm({ closureDate: invalidDate
}))).toThrow(TypeError)) to assert the error message content as well: call the
same invocation but use toThrowError or toThrow with a string/RegExp that
matches the expected descriptive message from callBuildPayload (e.g., a pattern
indicating an invalid closure date). Apply the same change for other
invalid-input assertions in this spec so tests verify both error type and the
descriptive message produced by callBuildPayload / makeValidForm input
validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 31c96c87-d54b-45a6-a273-981e331ebb03

📥 Commits

Reviewing files that changed from the base of the PR and between e938738 and 777a6f8.

📒 Files selected for processing (3)
  • src/app/clients/clients-view/client-actions/close-client/close-client.component.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts
✅ Files skipped from review due to trivial changes (1)
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/clients/clients-view/client-actions/close-client/close-client.component.ts

@Agaba-Ed Agaba-Ed force-pushed the close-client-utility branch from 777a6f8 to e2d878f Compare March 24, 2026 22:17
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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/app/clients/clients-view/client-actions/close-client/close-client.component.ts`:
- Around line 90-94: The submit() method currently calls
buildCloseClientPayload(...) which can throw a TypeError and will bubble up;
wrap the call to buildCloseClientPayload(this.closeClientForm.value,
this.dateUtils, locale, dateFormat) in a try/catch inside submit(), catch
TypeError (and other errors), set form-level validation feedback on
closeClientForm (e.g., mark invalid and attach a generic payload error message)
and avoid calling clientsService.executeClientCommand when payload building
fails; keep the existing success subscription flow when no error occurs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4fff882b-77c0-4a30-b80d-d3c61ddecf8a

📥 Commits

Reviewing files that changed from the base of the PR and between 777a6f8 and e2d878f.

📒 Files selected for processing (3)
  • src/app/clients/clients-view/client-actions/close-client/close-client.component.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts

Copy link
Copy Markdown
Contributor

@IOhacker IOhacker left a comment

Choose a reason for hiding this comment

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

Why not use the Fineract Client that has been introduced by @gkbishnoi07

@Agaba-Ed
Copy link
Copy Markdown
Contributor Author

Agaba-Ed commented Apr 1, 2026

@IOhacker Thanks for the feedback! I wasn't aware of the Fineract client. Could you please point me to the PR it was introduced?

@gkbishnoi07
Copy link
Copy Markdown
Collaborator

@IOhacker Thanks for the feedback! I wasn't aware of the Fineract client. Could you please point me to the PR it was introduced?

https://github.com/[openMF/fineract-client-ts](https://github.com/openMF/fineract-client-ts)

@Agaba-Ed Agaba-Ed force-pushed the close-client-utility branch from e2d878f to cf02a62 Compare April 2, 2026 10:40
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 (1)
src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts (1)

13-17: Consider aligning type with runtime reality.

The closureReasonId is typed as number, but the runtime coercion at line 97-98 handles string inputs (from UntypedFormGroup). Consider typing it as number | string to accurately reflect the actual form input:

 export interface CloseClientFormValue {
   closureDate: Date | string;
-  closureReasonId: number;
+  closureReasonId: number | string;
   [key: string]: unknown;
 }

This improves type accuracy and makes the string-to-number coercion in buildCloseClientPayload self-documenting rather than appearing as defensive code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts`
around lines 13 - 17, Update the CloseClientFormValue interface to reflect
actual runtime values by changing closureReasonId from number to number |
string; then ensure buildCloseClientPayload remains the place where you coerce
the possibly-string closureReasonId to a number (so the coercion in
buildCloseClientPayload is clearly intentional and self-documenting when
consuming values from an UntypedFormGroup).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts`:
- Around line 13-17: Update the CloseClientFormValue interface to reflect actual
runtime values by changing closureReasonId from number to number | string; then
ensure buildCloseClientPayload remains the place where you coerce the
possibly-string closureReasonId to a number (so the coercion in
buildCloseClientPayload is clearly intentional and self-documenting when
consuming values from an UntypedFormGroup).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59fa168e-5832-4323-b104-94e985adbbf9

📥 Commits

Reviewing files that changed from the base of the PR and between e2d878f and cf02a62.

📒 Files selected for processing (3)
  • src/app/clients/clients-view/client-actions/close-client/close-client.component.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.ts
✅ Files skipped from review due to trivial changes (1)
  • src/app/clients/clients-view/client-actions/close-client/close-client.utils.spec.ts

@Agaba-Ed
Copy link
Copy Markdown
Contributor Author

Agaba-Ed commented Apr 2, 2026

@IOhacker @gkbishnoi07 I have looked into the Fineract client and have a few questions. It currently depends on @angular/core v19, which suggests we may need to introduce some legacy dependencies into the codebase. I also noticed that there doesn’t appear to be explicit support for the closureDate and closureReasonId fields

@Agaba-Ed Agaba-Ed force-pushed the close-client-utility branch from cf02a62 to afea074 Compare April 2, 2026 11:52
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.

3 participants