Skip to content

fix: Loan repayment calendar showing wrong dates and status (MIFOSAC-705) - #2618

Merged
therajanmaurya merged 2 commits into
openMF:developmentfrom
therajanmaurya:fix/MIFOSAC-705-loan-repayment-calendar-wrong-dates
Feb 21, 2026
Merged

fix: Loan repayment calendar showing wrong dates and status (MIFOSAC-705)#2618
therajanmaurya merged 2 commits into
openMF:developmentfrom
therajanmaurya:fix/MIFOSAC-705-loan-repayment-calendar-wrong-dates

Conversation

@therajanmaurya

@therajanmaurya therajanmaurya commented Feb 20, 2026

Copy link
Copy Markdown
Member

Summary

Fixes MIFOSAC-705 - Repayment calendar for a new Loan Application shows wrong installment status and wrong dates.

Problem

When creating a new Loan Application, the Schedule (Calendar) step was showing:

  • Wrong dates (from an old/existing loan instead of the dates entered in Terms step)
  • Wrong installment statuses (showing "paid"/"due" instead of "pending" for a NEW loan)

Root Cause

In NewLoanAccountViewModel.kt, the repaymentScheduler() function was calling syncLoanById(state.clientId) which fetched an existing loan by client ID, instead of calculating a preview based on the form inputs the user just entered.

Solution

  • Added new API endpoint POST /loans?command=calculateLoanSchedule to calculate loan schedule preview without creating the loan
  • Created CalculateLoanScheduleUseCase to handle schedule calculation
  • Updated repaymentScheduler() to build LoansPayload from current form state and use the new API
  • Schedule now correctly shows dates entered by user and all installments as "pending"

Changes

Layer File Change
Network LoanService.kt Added calculateLoanSchedule endpoint
Network DataManagerLoan.kt Added method to call API
Data LoanAccountRepository.kt Added interface method
Data LoanAccountRepositoryImp.kt Added implementation
Domain CalculateLoanScheduleUseCase.kt New use case file
Domain UseCaseModule.kt Registered use case in DI
Feature NewLoanAccountViewModel.kt Updated repaymentScheduler() logic

Test plan

  • Create new loan application with specific dates in Terms step
  • Verify Schedule step shows the correct dates from Terms
  • Verify all installments show as "Pending" (not "Paid" or "Due")
  • Test with client that has existing loans (should not show old loan data)
  • Test different repayment frequencies (monthly, weekly)

Summary by CodeRabbit

  • New Features

    • Preview loan repayment schedules before creating the loan, showing a full repayment breakdown.
  • Refactor

    • Loan workflow and UI updated to compute and display schedule previews from current form state.
    • Navigation and screen APIs updated to propagate account numbers through the apply-new-application and new-loan flows.
  • Model

    • Loan currency representation adjusted (decimal places type changed), affecting schedule formatting.

MIFOSAC-705: Fixed the Schedule (Calendar) step in new loan application
showing wrong dates and installment statuses.

Root cause: repaymentScheduler() was fetching an existing loan by client
ID instead of calculating a preview based on the form inputs.

Changes:
- Added calculateLoanSchedule API endpoint (POST /loans?command=calculateLoanSchedule)
- Added CalculateLoanScheduleUseCase for calculating schedule preview
- Updated NewLoanAccountViewModel to use form data for schedule calculation
- Schedule now correctly shows dates entered in Terms step
- All installments show as pending (not paid/due from old loan data)

Files changed:
- core/network: Added calculateLoanSchedule endpoint to LoanService
- core/data: Added method to LoanAccountRepository
- core/domain: Created CalculateLoanScheduleUseCase
- feature/loan: Updated NewLoanAccountViewModel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a loan schedule preview flow: new API endpoint and data manager method, repository and use-case plumbing, ViewModel wiring to build a LoansPayload and request a RepaymentSchedule, plus related UI/navigation and model tweaks.

Changes

Cohort / File(s) Summary
API Service Layer
core/network/src/commonMain/kotlin/com/mifos/core/network/services/LoanService.kt
Added calculateLoanSchedule(@Body loansPayload: LoansPayload?): Flow<HttpResponse> POST method targeting CREATE_LOANS_ACCOUNTS?command=calculateLoanSchedule.
Network / Data Manager
core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerLoan.kt
Added calculateLoanSchedule(loansPayload) that calls the LoanService endpoint, checks HTTP status, parses body to RepaymentSchedule, and emits a Flow.
Repository Layer
core/data/src/commonMain/kotlin/com/mifos/core/data/repository/LoanAccountRepository.kt, core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/LoanAccountRepositoryImp.kt
Declared and implemented calculateLoanSchedule(loansPayload): Flow<DataState<RepaymentSchedule>>, delegating to data manager and converting to DataState flow.
Domain Layer
core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CalculateLoanScheduleUseCase.kt, core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt
New CalculateLoanScheduleUseCase added and registered in DI; exposes operator fun invoke(loansPayload): Flow<DataState<RepaymentSchedule>>.
Feature (Loan) — ViewModel & UI
feature/loan/src/.../NewLoanAccountViewModel.kt, feature/loan/src/.../pages/SchedulePage.kt, feature/loan/src/.../NewLoanAccountRoute.kt
ViewModel now injects use case, builds LoansPayload via buildLoansPayloadForSchedulePreview(), requests schedule preview and stores repaymentSchedule/repaymentSchedulesSummary; SchedulePage and route updated to consume the new state shape and accountNo.
Feature (Client) — Navigation & State
feature/client/src/.../ClientApplyNewApplicationRoute.kt, ClientApplyNewApplicationsScreen.kt, ClientApplyNewApplicationsViewModel.kt, ClientProfileDetailsNavigation.kt, ClientProfileDetailsScreen.kt
Expanded route/state to include accountNo and adjusted callback signatures to pass accountNo through navigation and composables.
Model
core/model/src/commonMain/kotlin/com/mifos/core/model/objects/account/loan/Currency.kt
Changed Currency.decimalPlaces type from Double? to Int?.

Sequence Diagram

sequenceDiagram
    participant UI as UI Layer
    participant VM as NewLoanAccountViewModel
    participant UC as CalculateLoanScheduleUseCase
    participant Repo as LoanAccountRepository
    participant DM as DataManagerLoan
    participant API as LoanService

    UI->>VM: Request schedule preview
    VM->>VM: buildLoansPayloadForSchedulePreview()
    VM->>UC: invoke(loansPayload)
    UC->>Repo: calculateLoanSchedule(loansPayload)
    Repo->>DM: calculateLoanSchedule(loansPayload)
    DM->>API: POST /loans?command=calculateLoanSchedule
    API-->>DM: HttpResponse
    DM-->>Repo: RepaymentSchedule (parsed)
    Repo-->>UC: Flow<DataState<RepaymentSchedule>>
    UC-->>VM: Flow<DataState<RepaymentSchedule>>
    VM-->>UI: Update repaymentSchedule / summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • enhance new saving account flow #2529 — related changes to DataManagerLoan response handling and error extraction that overlap with the approach used in the new calculateLoanSchedule implementation.

Suggested reviewers

  • itsPronay
  • biplab1

Poem

🐰 A little hop, a schedule to see,
Payload built gently from form and decree.
Layers converse from UI down to API,
A preview of payments before loans say hi.
Hooray — the rabbit crunches numbers with glee!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main issue (loan repayment calendar showing wrong dates and status) and matches the primary change in the PR (adding schedule preview calculation).
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 docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt (1)

255-261: ⚠️ Potential issue | 🟡 Minor

Stale schedule preview when form terms are changed and the step is revisited.

The if (state.repaymentSchedules.isEmpty()) guard means the schedule is fetched exactly once per ViewModel instance. If the user views the schedule, navigates back, changes any term (e.g., numberOfRepayments, principalAmount, expectedDisbursementDate), and navigates forward again, they will see the preview built from the original inputs — directly contradicting the intent of this fix.

Consider clearing repaymentSchedules whenever key terms change (e.g., in handleNoOfRepaymentsChange, handlePrincipalAmountChange, handleExpectedDisbursementDateChange), so the next visit to the schedule step triggers a fresh calculation:

 private fun handleNoOfRepaymentsChange(action: NewLoanAccountAction.OnNoOfRepaymentsChange) {
     mutableStateFlow.update {
-        it.copy(noOfRepayments = action.number)
+        it.copy(noOfRepayments = action.number, repaymentSchedules = emptyMap())
     }
 }

Apply the same pattern to handlePrincipalAmountChange, handleNominalInterestRateChange, handleRepaidEveryChange, handleExpectedDisbursementDateChange, and handleTermFrequencyIndexChange.

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

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`
around lines 255 - 261, The repayment preview is stale because
NewLoanAccountAction.RepaymentScheduler only calls repaymentScheduler() when
state.repaymentSchedules.isEmpty(); fix by invalidating/clearing
repaymentSchedules in each term-change handler so revisiting the schedule step
forces recalculation: update handleNoOfRepaymentsChange,
handlePrincipalAmountChange, handleNominalInterestRateChange,
handleRepaidEveryChange, handleExpectedDisbursementDateChange, and
handleTermFrequencyIndexChange to reset state.repaymentSchedules (e.g., to
emptyList()) whenever their value changes before returning, so the existing
isEmpty() guard will trigger a fresh repaymentScheduler() call on next visit.
🧹 Nitpick comments (1)
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt (1)

938-999: Duplicate payload-building logic between buildLoansPayloadForSchedulePreview() and submitLoanApplication().

The two blocks construct a nearly identical LoansPayload. The main difference is that the preview helper correctly uses toDoubleOrNull() and getOrNull() for safety, while submitLoanApplication() uses toDouble() and direct index access (potential NumberFormatException/IndexOutOfBoundsException if input is invalid). A shared helper parameterised for strict vs. lenient parsing would eliminate the duplication and propagate the null-safety improvements to the submit path as well.

♻️ Suggested extraction
private fun buildLoansPayload(strict: Boolean = false): LoansPayload {
    return LoansPayload(
        principal = if (strict) state.principalAmount.toDouble()
                    else state.principalAmount.toDoubleOrNull(),
        interestRatePerPeriod = if (strict) state.nominalInterestRate.toDouble()
                                else state.nominalInterestRate.toDoubleOrNull(),
        // ... all shared fields using getOrNull() ...
    )
}
// submitLoanApplication  → buildLoansPayload(strict = true)
// schedule preview        → buildLoansPayload(strict = false)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`
around lines 938 - 999, There is duplicated LoansPayload construction between
buildLoansPayloadForSchedulePreview() and submitLoanApplication() with
inconsistent safety (preview uses toDoubleOrNull/getOrNull; submit uses
toDouble/direct index access). Extract a single helper (e.g., private fun
buildLoansPayload(strict: Boolean = false)) that builds the shared LoansPayload
using getOrNull() and toDoubleOrNull() when strict=false and toDouble()/direct
access when strict=true, then call buildLoansPayload(strict = false) from
buildLoansPayloadForSchedulePreview() and buildLoansPayload(strict = true) from
submitLoanApplication(); ensure all fields shown (principal,
interestRatePerPeriod, loanOfficerId, repaymentFrequency*, loanPurposeId,
fundId, linkAccountId, transactionProcessingStrategyCode, etc.) follow this
pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/network/src/commonMain/kotlin/com/mifos/core/network/services/LoanService.kt`:
- Around line 85-86: The POST handler calculateLoanSchedule currently
deserializes the API response into LoanWithAssociationsEntity which expects
repaymentSchedule.periods nested under repaymentSchedule, but the Fineract
endpoint returns currency and periods at the top level; update the Retrofit
service to use a dedicated response model (e.g., LoanScheduleResponse) matching
the API shape, or implement a custom deserializer that maps top-level currency
and periods into LoanWithAssociationsEntity.repaymentSchedule.periods before
returning the Flow, ensuring SchedulePage’s access to
state.loanWithAssociationsEntity.repaymentSchedule.periods.orEmpty() receives
the actual period data.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`:
- Around line 907-918: The map construction inside the DataState.Success branch
(schedulerDetails in NewLoanAccountViewModel) dereferences
dataState.data.currency directly; change those accesses to null-safe calls and
provide defaults (e.g., use dataState.data.currency?.code ?: "N/A" and
dataState.data.currency?.decimalPlaces ?: 0) so CurrencyFormatter.format and
other string interpolations never receive a null and cannot throw an NPE.

---

Outside diff comments:
In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`:
- Around line 255-261: The repayment preview is stale because
NewLoanAccountAction.RepaymentScheduler only calls repaymentScheduler() when
state.repaymentSchedules.isEmpty(); fix by invalidating/clearing
repaymentSchedules in each term-change handler so revisiting the schedule step
forces recalculation: update handleNoOfRepaymentsChange,
handlePrincipalAmountChange, handleNominalInterestRateChange,
handleRepaidEveryChange, handleExpectedDisbursementDateChange, and
handleTermFrequencyIndexChange to reset state.repaymentSchedules (e.g., to
emptyList()) whenever their value changes before returning, so the existing
isEmpty() guard will trigger a fresh repaymentScheduler() call on next visit.

---

Nitpick comments:
In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`:
- Around line 938-999: There is duplicated LoansPayload construction between
buildLoansPayloadForSchedulePreview() and submitLoanApplication() with
inconsistent safety (preview uses toDoubleOrNull/getOrNull; submit uses
toDouble/direct index access). Extract a single helper (e.g., private fun
buildLoansPayload(strict: Boolean = false)) that builds the shared LoansPayload
using getOrNull() and toDoubleOrNull() when strict=false and toDouble()/direct
access when strict=true, then call buildLoansPayload(strict = false) from
buildLoansPayloadForSchedulePreview() and buildLoansPayload(strict = true) from
submitLoanApplication(); ensure all fields shown (principal,
interestRatePerPeriod, loanOfficerId, repaymentFrequency*, loanPurposeId,
fundId, linkAccountId, transactionProcessingStrategyCode, etc.) follow this
pattern.

Comment on lines +85 to +86
@POST(APIEndPoint.CREATE_LOANS_ACCOUNTS + "?command=calculateLoanSchedule")
fun calculateLoanSchedule(@Body loansPayload: LoansPayload?): Flow<LoanWithAssociationsEntity>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the LoanWithAssociationsEntity definition
echo "=== LoanWithAssociationsEntity definition ==="
rg -nP "class LoanWithAssociationsEntity" --type=kotlin -A 80

echo -e "\n=== Check for repaymentSchedule field ==="
rg -nP "repaymentSchedule" --type=kotlin -B 3 -A 3 | head -100

echo -e "\n=== Check syncLoanById method ==="
rg -nP "fun syncLoanById" --type=kotlin -A 10

Repository: openMF/android-client

Length of output: 33041


🏁 Script executed:

#!/bin/bash
# Check the LoanScheduleData or similar response structure
echo "=== Search for LoanScheduleData or schedule-related entities ==="
rg -nP "class Loan.*Schedule|data class.*Schedule" --type=kotlin -A 20

echo -e "\n=== Check the LoansPayload structure ==="
rg -nP "class LoansPayload|data class LoansPayload" --type=kotlin -A 30

Repository: openMF/android-client

Length of output: 21504


🏁 Script executed:

#!/bin/bash
# Check the DataManagerLoan implementation
echo "=== Check DataManagerLoan.calculateLoanSchedule call ==="
rg -nP "calculateLoanSchedule" --type=kotlin -B 5 -A 10

Repository: openMF/android-client

Length of output: 11692


The response type mismatch requires investigation or fixing.

The Fineract POST /loans?command=calculateLoanSchedule endpoint returns a flat JSON structure with currency and periods at the top level, but the code deserializes it into LoanWithAssociationsEntity, which expects these fields nested under repaymentSchedule. After deserialization, repaymentSchedule.periods will be null or empty, even though the API returned the period data. This breaks the repayment schedule preview shown in the loan account creation flow (SchedulePage accesses state.loanWithAssociationsEntity.repaymentSchedule.periods.orEmpty()).

Consider creating a dedicated response type for the calculateLoanSchedule endpoint (e.g., LoanScheduleResponse) that matches the actual API response structure, or verify that custom JSON deserialization logic exists to handle the structural mismatch.

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

In
`@core/network/src/commonMain/kotlin/com/mifos/core/network/services/LoanService.kt`
around lines 85 - 86, The POST handler calculateLoanSchedule currently
deserializes the API response into LoanWithAssociationsEntity which expects
repaymentSchedule.periods nested under repaymentSchedule, but the Fineract
endpoint returns currency and periods at the top level; update the Retrofit
service to use a dedicated response model (e.g., LoanScheduleResponse) matching
the API shape, or implement a custom deserializer that maps top-level currency
and periods into LoanWithAssociationsEntity.repaymentSchedule.periods before
returning the Flow, ensuring SchedulePage’s access to
state.loanWithAssociationsEntity.repaymentSchedule.periods.orEmpty() receives
the actual period data.

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/pages/SchedulePage.kt (1)

90-108: ⚠️ Potential issue | 🔴 Critical

RepaymentPeriodCard will crash on Android/Desktop with IllegalArgumentException when currencyCode is null.

The component passes currencyCode ?: "N/A" to CurrencyFormatter.format(), but "N/A" is not a valid ISO 4217 currency code. On Android and Desktop platforms, Currency.getInstance("N/A") throws IllegalArgumentException. The Native implementation correctly uses currencyCode ?: "$" as its fallback. Align all platform implementations to use a valid currency code as the default, or add null-safety at the call site.

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

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/pages/SchedulePage.kt`
around lines 90 - 108, RepaymentPeriodCard crashes when currencyCode is null
because CurrencyFormatter.format receives "N/A" (invalid ISO code); update the
call site in RepaymentScheduleList (and any callers of RepaymentPeriodCard) to
pass a valid fallback like "$" instead of "N/A" or add null-safety before
calling CurrencyFormatter.format so it never invokes Currency.getInstance with
an invalid code; specifically change usages that currently propagate
currencyCode ?: "N/A" to use currencyCode ?: "$" (or guard and format with a
currency-symbol-only path) so CurrencyFormatter.format always receives a valid
currency identifier.
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationRoute.kt (1)

17-22: ⚠️ Potential issue | 🟡 Minor

Add a default value to accountNo for process-death back-stack safety.

When navigating, the route is saved to persist system-initiated process death and the saving mechanism is a binder transaction. With accountNo: String declared without a default, any Navigation back-stack state that was serialized under the previous schema (e.g., from an earlier install still on the device) will throw a kotlinx.serialization.MissingFieldException when the OS tries to restore it via toRoute<ClientApplyNewApplicationRoute>(). Adding = "" costs nothing and makes the schema forward-compatible.

🛡️ Proposed fix
 data class ClientApplyNewApplicationRoute(
     val clientId: Int,
     val status: String,
-    val accountNo: String,
+    val accountNo: String = "",
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationRoute.kt`
around lines 17 - 22, The ClientApplyNewApplicationRoute data class must provide
a default for accountNo to avoid kotlinx.serialization.MissingFieldException
during process-death back-stack restore; update the class definition
(ClientApplyNewApplicationRoute) so the accountNo property has a default value
(e.g., accountNo = "") while keeping clientId and status unchanged so older
serialized routes can be deserialized safely.
🧹 Nitpick comments (3)
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/pages/SchedulePage.kt (1)

57-57: Nit: prefer idiomatic isNotEmpty() over !...isEmpty().

♻️ Suggested change
-            if (!state.repaymentSchedulesSummary.isEmpty()) {
+            if (state.repaymentSchedulesSummary.isNotEmpty()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/pages/SchedulePage.kt`
at line 57, Replace the negated isEmpty() check with the idiomatic isNotEmpty()
on the repaymentSchedulesSummary property: find the conditional using
state.repaymentSchedulesSummary (in SchedulePage.kt) and change the expression
from "!state.repaymentSchedulesSummary.isEmpty()" to
"state.repaymentSchedulesSummary.isNotEmpty()" to improve readability and follow
Kotlin conventions.
core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerLoan.kt (1)

305-314: Json instance is recreated on every invocation — allocate once and reuse.

A new Json { ignoreUnknownKeys = true } is instantiated on each call. This is unnecessary overhead. Consider extracting it to a companion object or a top-level val, especially since extractErrorMessage in ErrorHandling.kt already creates its own. A shared instance would benefit both call sites.

♻️ Suggested improvement
+private val lenientJson = Json { ignoreUnknownKeys = true }
+
 fun calculateLoanSchedule(loansPayload: LoansPayload): Flow<RepaymentSchedule> {
     return mBaseApiManager.loanService.calculateLoanSchedule(loansPayload).map { response ->
         if (!response.status.isSuccess()) {
             val errorMessage = extractErrorMessage(response)
             throw IllegalStateException(errorMessage)
         }
-        Json { ignoreUnknownKeys = true }.decodeFromString<RepaymentSchedule>(response.bodyAsText())
+        lenientJson.decodeFromString<RepaymentSchedule>(response.bodyAsText())
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerLoan.kt`
around lines 305 - 314, The calculateLoanSchedule function currently creates a
new Json { ignoreUnknownKeys = true } on every call; extract a single shared
Json instance (e.g., a private val json = Json { ignoreUnknownKeys = true }) and
reuse it instead of instantiating inside calculateLoanSchedule, update the
decodeFromString call to use that shared json to parse RepaymentSchedule, and
consider placing the shared val in the same class companion object or a
top-level file so ErrorHandling.extractErrorMessage can also reuse it.
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt (1)

957-1018: Payload construction is largely duplicated with submitLoanApplication().

buildLoansPayloadForSchedulePreview() constructs a LoansPayload very similarly to lines 275-302 in submitLoanApplication(), but with safer accessors (getOrNull, toDoubleOrNull). This creates a maintenance risk — if a field is added or changed, both sites must be updated in sync.

Consider extracting a shared buildLoansPayload() method that both functions call. The preview version could use the same builder (the safer one), and submitLoanApplication could add its own validation on top.

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

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`
around lines 957 - 1018, Extract a single shared builder function (e.g.,
buildLoansPayload or buildLoansPayloadSafe) that constructs and returns a
LoansPayload using the safe accessors currently used in
buildLoansPayloadForSchedulePreview (getOrNull, toDoubleOrNull, nullable
indexing), then have both buildLoansPayloadForSchedulePreview() and
submitLoanApplication() call that shared builder; keep
buildLoansPayloadForSchedulePreview() as a thin wrapper returning the shared
result, and in submitLoanApplication() call the shared builder and then perform
any additional validation/strict conversions before submission so you avoid
duplicating field mapping logic between buildLoansPayloadForSchedulePreview(),
submitLoanApplication(), and the LoansPayload construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/model/src/commonMain/kotlin/com/mifos/core/model/objects/account/loan/Currency.kt`:
- Line 26: The field decimalPlaces was changed to Int? which is correct
semantically but kotlinx.serialization will throw JsonDecodingException if the
API emits a float literal like 2.0; to fix, either (A) confirm and enforce the
API contract to emit integer literals for decimalPlaces, or (B) make
deserialization tolerant by adding a custom serializer (or
JsonTransformingSerializer) for decimalPlaces that accepts both integer and
float JSON numbers and converts them to Int, and apply it where decimalPlaces is
declared in com.mifos.core.model.objects.account.loan.Currency (and update the
other Currency classes template/loan/Currency.kt and template/client/Currency.kt
to use the same Int? type or the same serializer) so all domain models are
consistent and runtime decoding won't crash.

In
`@core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerLoan.kt`:
- Around line 298-303: The KDoc for the method in DataManagerLoan
(DataManagerLoan.kt) incorrectly documents the return type as
LoanWithAssociationsEntity while the function actually returns a
RepaymentSchedule; update the KDoc `@return` to describe and name
RepaymentSchedule (and adjust the brief description to match the actual returned
object) or, if the implementation was intended to return
LoanWithAssociationsEntity, change the function return type and implementation
accordingly—look for the function calculateLoanRepaymentSchedule / similar in
DataManagerLoan and make the KDoc and signature/implementation consistent.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`:
- Around line 926-936: The schedulerDetails map is using stale
state.repaymentSchedule (which is updated later) causing totalPrincipalPaid to
be wrong; update the map to read totalPrincipalPaid from the fresh response
dataState.data.repaymentSchedule (e.g., use
dataState.data.repaymentSchedule.totalPrincipalPaid) and keep the existing
null-safety for currency code and decimalPlaces when calling
CurrencyFormatter.format; ensure you reference dataState.data rather than state
for repayment values inside the schedulerDetails construction in
NewLoanAccountViewModel.kt.
- Around line 254-266: The current guard in the
NewLoanAccountAction.RepaymentScheduler branch only recalculates the schedule
when state.noOfRepayments changes (checking noOfRepaymentsPreviousState !=
noOfRepayments), which can leave the schedule stale when other inputs change;
either (A) always call repaymentScheduler() when handling
NewLoanAccountAction.RepaymentScheduler / after moveToNextStep() so the schedule
is recalculated whenever the user navigates to the schedule step, or (B) broaden
the staleness check by comparing a snapshot of all schedule-affecting fields
(e.g., principal, interestRate, disbursementDate,
repaymentFrequency/periodicity, loanTerm, fees) stored in state (e.g.,
repaymentSchedulePreviousSnapshot) against the current values and run
repaymentScheduler() when any differ, then update that snapshot via
mutableStateFlow.update; use the existing repaymentScheduler(),
moveToNextStep(), mutableStateFlow.update, and state.* fields to locate and
implement the change.
- Line 1025: The current NewLoanAccountViewModel state property accountNo
defaults to an empty string and can be passed through from
ClientProfileDetailsScreen (via navigateToClientApplyNewApplicationScreen →
ClientApplyNewApplicationsState → navigateToNewLoanAccountRoute), which means
the schedule summary may show no account number; decide whether empty is
acceptable or enforce a fallback/validation: either change the flow to pass a
non-empty fallback (e.g., "N/A" or generate a placeholder) at the caller
(ClientProfileDetailsScreen) before calling
navigateToClientApplyNewApplicationScreen, or add validation in
NewLoanAccountViewModel (or the code that renders the schedule summary) to
detect accountNo == "" and substitute a clear fallback or show a
validation/error state; update the relevant places (ClientProfileDetailsScreen,
navigateToClientApplyNewApplicationScreen, ClientApplyNewApplicationsState,
navigateToNewLoanAccountRoute, and NewLoanAccountViewModel) so the chosen
behavior is consistent.

---

Outside diff comments:
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationRoute.kt`:
- Around line 17-22: The ClientApplyNewApplicationRoute data class must provide
a default for accountNo to avoid kotlinx.serialization.MissingFieldException
during process-death back-stack restore; update the class definition
(ClientApplyNewApplicationRoute) so the accountNo property has a default value
(e.g., accountNo = "") while keeping clientId and status unchanged so older
serialized routes can be deserialized safely.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/pages/SchedulePage.kt`:
- Around line 90-108: RepaymentPeriodCard crashes when currencyCode is null
because CurrencyFormatter.format receives "N/A" (invalid ISO code); update the
call site in RepaymentScheduleList (and any callers of RepaymentPeriodCard) to
pass a valid fallback like "$" instead of "N/A" or add null-safety before
calling CurrencyFormatter.format so it never invokes Currency.getInstance with
an invalid code; specifically change usages that currently propagate
currencyCode ?: "N/A" to use currencyCode ?: "$" (or guard and format with a
currency-symbol-only path) so CurrencyFormatter.format always receives a valid
currency identifier.

---

Nitpick comments:
In
`@core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerLoan.kt`:
- Around line 305-314: The calculateLoanSchedule function currently creates a
new Json { ignoreUnknownKeys = true } on every call; extract a single shared
Json instance (e.g., a private val json = Json { ignoreUnknownKeys = true }) and
reuse it instead of instantiating inside calculateLoanSchedule, update the
decodeFromString call to use that shared json to parse RepaymentSchedule, and
consider placing the shared val in the same class companion object or a
top-level file so ErrorHandling.extractErrorMessage can also reuse it.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`:
- Around line 957-1018: Extract a single shared builder function (e.g.,
buildLoansPayload or buildLoansPayloadSafe) that constructs and returns a
LoansPayload using the safe accessors currently used in
buildLoansPayloadForSchedulePreview (getOrNull, toDoubleOrNull, nullable
indexing), then have both buildLoansPayloadForSchedulePreview() and
submitLoanApplication() call that shared builder; keep
buildLoansPayloadForSchedulePreview() as a thin wrapper returning the shared
result, and in submitLoanApplication() call the shared builder and then perform
any additional validation/strict conversions before submission so you avoid
duplicating field mapping logic between buildLoansPayloadForSchedulePreview(),
submitLoanApplication(), and the LoansPayload construction.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/pages/SchedulePage.kt`:
- Line 57: Replace the negated isEmpty() check with the idiomatic isNotEmpty()
on the repaymentSchedulesSummary property: find the conditional using
state.repaymentSchedulesSummary (in SchedulePage.kt) and change the expression
from "!state.repaymentSchedulesSummary.isEmpty()" to
"state.repaymentSchedulesSummary.isNotEmpty()" to improve readability and follow
Kotlin conventions.

val name: String? = null,

val decimalPlaces: Double? = null,
val decimalPlaces: Int? = null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the Currency class definition and confirm the change
fd -e kt -x grep -l "class Currency" {} \;

Repository: openMF/android-client

Length of output: 705


🏁 Script executed:

# Search for all usages of decimalPlaces across the codebase
rg -n "decimalPlaces" --type kotlin -B 2 -A 2

Repository: openMF/android-client

Length of output: 35017


🏁 Script executed:

# Look specifically for CurrencyFormatter to understand how it uses decimal places
fd -e kt "CurrencyFormatter" -x cat -n {}

Repository: openMF/android-client

Length of output: 4026


🏁 Script executed:

# Check if there are any .toInt() conversions on decimalPlaces that would become redundant
rg -n "decimalPlaces.*\.toInt\(\)" --type kotlin

# Verify the actual type signature of maximumFractionDigits in CurrencyFormatter to ensure compatibility
rg -n "maximumFractionDigits.*Int" core/common/src/commonMain/kotlin/com/mifos/core/common/utils/ -A 1 -B 1

Repository: openMF/android-client

Length of output: 656


🏁 Script executed:

# Check for any Double-typed parameters that receive decimalPlaces
rg -n "fun.*Double.*decimalPlaces|maximumFractionDigits.*Double" --type kotlin

Repository: openMF/android-client

Length of output: 252


🏁 Script executed:

# Verify which Currency class is actually being modified in this PR
# and check if there are inconsistencies in how different Currency classes handle decimalPlaces
fd -e kt "Currency.kt" -x grep -l "decimalPlaces.*Int\|decimalPlaces.*Double" {}

Repository: openMF/android-client

Length of output: 808


🏁 Script executed:

# Also check the specific file from the review to confirm it's account/loan/Currency.kt
cat -n core/model/src/commonMain/kotlin/com/mifos/core/model/objects/account/loan/Currency.kt | grep -A 2 -B 2 "decimalPlaces"

Repository: openMF/android-client

Length of output: 203


Verify API contract ensures integer format for decimalPlaces field.

Changing decimalPlaces from Double? to Int? is semantically correct — decimal-place counts must be integers. This is the right type. However, one critical verification remains:

With kotlinx.serialization, deserializing a JSON float literal ("decimalPlaces": 2.0) into Int? throws JsonDecodingException at runtime. If the Mifos API ever emits float-formatted values for this field, the app will crash during response parsing. Confirm the API contract always returns an integer literal (not 2.0).

Downstream callers all expect Int? for maximumFractionDigits and pass decimalPlaces directly without transformation—no compilation issues. However, note that other Currency classes in the codebase (template/loan/Currency.kt, template/client/Currency.kt) still use Double? for the same field, creating inconsistency across the domain model.

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

In
`@core/model/src/commonMain/kotlin/com/mifos/core/model/objects/account/loan/Currency.kt`
at line 26, The field decimalPlaces was changed to Int? which is correct
semantically but kotlinx.serialization will throw JsonDecodingException if the
API emits a float literal like 2.0; to fix, either (A) confirm and enforce the
API contract to emit integer literals for decimalPlaces, or (B) make
deserialization tolerant by adding a custom serializer (or
JsonTransformingSerializer) for decimalPlaces that accepts both integer and
float JSON numbers and converts them to Int, and apply it where decimalPlaces is
declared in com.mifos.core.model.objects.account.loan.Currency (and update the
other Currency classes template/loan/Currency.kt and template/client/Currency.kt
to use the same Int? type or the same serializer) so all domain models are
consistent and runtime decoding won't crash.

Comment on lines +298 to +303
/**
* Calculate loan repayment schedule without creating the loan.
* Used to preview the schedule before submitting the loan application.
*
* @param loansPayload The loan parameters to calculate the schedule for
* @return LoanWithAssociationsEntity containing the calculated repayment schedule

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

KDoc @return is stale — says LoanWithAssociationsEntity but returns RepaymentSchedule.

-     * `@return` LoanWithAssociationsEntity containing the calculated repayment schedule
+     * `@return` RepaymentSchedule containing the calculated repayment schedule
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Calculate loan repayment schedule without creating the loan.
* Used to preview the schedule before submitting the loan application.
*
* @param loansPayload The loan parameters to calculate the schedule for
* @return LoanWithAssociationsEntity containing the calculated repayment schedule
/**
* Calculate loan repayment schedule without creating the loan.
* Used to preview the schedule before submitting the loan application.
*
* `@param` loansPayload The loan parameters to calculate the schedule for
* `@return` RepaymentSchedule containing the calculated repayment schedule
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerLoan.kt`
around lines 298 - 303, The KDoc for the method in DataManagerLoan
(DataManagerLoan.kt) incorrectly documents the return type as
LoanWithAssociationsEntity while the function actually returns a
RepaymentSchedule; update the KDoc `@return` to describe and name
RepaymentSchedule (and adjust the brief description to match the actual returned
object) or, if the implementation was intended to return
LoanWithAssociationsEntity, change the function return type and implementation
accordingly—look for the function calculateLoanRepaymentSchedule / similar in
DataManagerLoan and make the KDoc and signature/implementation consistent.

Comment on lines 254 to 266
is NewLoanAccountAction.RepaymentScheduler -> {
moveToNextStep()
if (state.repaymentSchedules.isEmpty()) {
if (state.noOfRepaymentsPreviousState != state.noOfRepayments) {
viewModelScope.launch {
repaymentScheduler()
}

mutableStateFlow.update {
it.copy(
noOfRepaymentsPreviousState = state.noOfRepayments,
)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Schedule recalculation is only triggered when noOfRepayments changes.

The guard at line 256 compares noOfRepaymentsPreviousState != noOfRepayments, but the schedule depends on many other inputs (principal, interest rate, disbursement date, repayment frequency, etc.). If the user changes any of those fields after viewing the schedule once without also changing the number of repayments, they'll see a stale schedule.

Consider either broadening the staleness check to cover all schedule-affecting fields, or always recalculating when the user navigates to the schedule step.

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

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`
around lines 254 - 266, The current guard in the
NewLoanAccountAction.RepaymentScheduler branch only recalculates the schedule
when state.noOfRepayments changes (checking noOfRepaymentsPreviousState !=
noOfRepayments), which can leave the schedule stale when other inputs change;
either (A) always call repaymentScheduler() when handling
NewLoanAccountAction.RepaymentScheduler / after moveToNextStep() so the schedule
is recalculated whenever the user navigates to the schedule step, or (B) broaden
the staleness check by comparing a snapshot of all schedule-affecting fields
(e.g., principal, interestRate, disbursementDate,
repaymentFrequency/periodicity, loanTerm, fees) stored in state (e.g.,
repaymentSchedulePreviousSnapshot) against the current values and run
repaymentScheduler() when any differ, then update that snapshot via
mutableStateFlow.update; use the existing repaymentScheduler(),
moveToNextStep(), mutableStateFlow.update, and state.* fields to locate and
implement the change.

Comment on lines 926 to 936
val schedulerDetails = mapOf(
Res.string.account_number to dataState.data.accountNo,
Res.string.disbursement_date to (
dataState.data.timeline.actualDisbursementDate?.filterNotNull()
?.let { date ->
DateHelper.getDateAsString(date)
} ?: "N/A"
),
Res.string.account_number to (state.accountNo),
Res.string.disbursement_date to state.expectedDisbursementDate.ifEmpty { "N/A" },
Res.string.principle_paid_off to CurrencyFormatter.format(
balance = dataState.data.summary.principalPaid ?: 0.0,
currencyCode = dataState.data.currency.code ?: "N/A",
maximumFractionDigits = dataState.data.currency.decimalPlaces ?: 0,
balance = state.repaymentSchedule.totalPrincipalPaid,
currencyCode = dataState.data.currency?.code ?: "N/A",
maximumFractionDigits = dataState.data.currency?.decimalPlaces ?: 0,
),
Res.string.installment_paid to (
dataState.data.repaymentSchedule.periods?.count { it.complete == true }
?.toString() ?: "N/A"
),
Res.string.installment_paid to (
dataState.data.repaymentSchedule.periods?.count { it.complete == false }
?.toString() ?: "N/A"
),
Res.string.total_installments to dataState.data.termFrequency.toString(),
Res.string.installment_paid to "0",
Res.string.total_installments to state.noOfRepayments.toString(),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: state.repaymentSchedule.totalPrincipalPaid reads stale state, not the fresh response.

On line 930, state.repaymentSchedule still holds the previous value because the state update assigning dataState.data happens later at line 942. On the first invocation, this will be RepaymentSchedule() defaults (likely null or 0). Use dataState.data instead:

🐛 Proposed fix
                         val schedulerDetails = mapOf(
                             Res.string.account_number to (state.accountNo),
                             Res.string.disbursement_date to state.expectedDisbursementDate.ifEmpty { "N/A" },
                             Res.string.principle_paid_off to CurrencyFormatter.format(
-                                balance = state.repaymentSchedule.totalPrincipalPaid,
+                                balance = dataState.data.totalPrincipalPaid,
                                 currencyCode = dataState.data.currency?.code ?: "N/A",
                                 maximumFractionDigits = dataState.data.currency?.decimalPlaces ?: 0,
                             ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`
around lines 926 - 936, The schedulerDetails map is using stale
state.repaymentSchedule (which is updated later) causing totalPrincipalPaid to
be wrong; update the map to read totalPrincipalPaid from the fresh response
dataState.data.repaymentSchedule (e.g., use
dataState.data.repaymentSchedule.totalPrincipalPaid) and keep the existing
null-safety for currency code and decimalPlaces when calling
CurrencyFormatter.format; ensure you reference dataState.data rather than state
for repayment values inside the schedulerDetails construction in
NewLoanAccountViewModel.kt.

@OptIn(ExperimentalTime::class)
constructor(
val launchEffectKey: Int? = null,
val accountNo: String = "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'navigateToNewLoanAccountRoute|NewLoanAccountRoute\(' --type=kt -C 3

Repository: openMF/android-client

Length of output: 91


🏁 Script executed:

rg -n 'navigateToNewLoanAccountRoute|NewLoanAccountRoute\(' -C 3

Repository: openMF/android-client

Length of output: 4282


🏁 Script executed:

# View the ViewModel state definition around line 1025
sed -n '1020,1030p' feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt

# Find all actual invocation sites of navigateToNewLoanAccountRoute with arguments
rg 'navigateToNewLoanAccountRoute\s*\(' -A 2

Repository: openMF/android-client

Length of output: 908


🏁 Script executed:

# Find where onNavigateApplyLoanAccount callback is used
rg 'onNavigateApplyLoanAccount' -B 3 -A 3

# Also search for direct calls to navigateToNewLoanAccountRoute with actual parameter passing
rg 'navigateToNewLoanAccountRoute\s*\([^)]*\)' -B 2 -A 2

Repository: openMF/android-client

Length of output: 6667


🏁 Script executed:

# Find ClientApplyNewApplicationsScreen state definition
rg 'data class.*ClientApplyNewApplicationsState|ClientApplyNewApplicationsState\(' -A 10 | head -50

# Also check the full invocation context around the navigation call
rg -B 5 -A 2 'onNavigateApplyLoanAccount\(' feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationsScreen.kt

Repository: openMF/android-client

Length of output: 4022


🏁 Script executed:

# Find ClientApplyNewApplicationsRoute definition
rg 'data class ClientApplyNewApplicationsRoute|@Serializable.*ClientApplyNewApplicationsRoute' -A 5

# Also search for all places that navigate to ClientApplyNewApplicationsRoute
rg 'navigateToClientApplyNewApplicationsRoute|ClientApplyNewApplicationsRoute\(' -B 2 -A 2

Repository: openMF/android-client

Length of output: 47


🏁 Script executed:

# Find files containing ClientApplyNewApplicationsRoute
fd 'ClientApplyNewApplications' --type f

# Also search for where clientApplyNewApplicationRoute is navigated to
rg 'clientApplyNewApplicationRoute\(' -B 2 -A 2

Repository: openMF/android-client

Length of output: 1757


🏁 Script executed:

# Find and read the ClientApplyNewApplicationRoute.kt file
cat feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientApplyNewApplications/ClientApplyNewApplicationRoute.kt | head -50

Repository: openMF/android-client

Length of output: 1885


🏁 Script executed:

# Find all places that navigate to ClientApplyNewApplicationScreen
rg 'navigateToClientApplyNewApplicationScreen' -B 3 -A 3

Repository: openMF/android-client

Length of output: 3461


🏁 Script executed:

# Find where navigateToApplyNewApplication is invoked with arguments
rg 'navigateToApplyNewApplication' -B 2 -A 2 | grep -v "^feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt"

# Also check ClientScreen for the callback invocation
rg 'navigateToApplyNewApplication\(' feature/client/src/commonMain/kotlin/com/mifos/feature/client -B 2 -A 2

Repository: openMF/android-client

Length of output: 4212


🏁 Script executed:

# Get the complete invocation with all parameters
rg 'ApplyNewApplication' -A 5 feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetailsProfile/ClientProfileDetailsScreen.kt

Repository: openMF/android-client

Length of output: 739


Confirm that accountNo defaults to empty string when client has no account number.

The review concern is valid. The accountNo can indeed be empty when navigating to NewLoanAccountRoute. In ClientProfileDetailsScreen, when the "Apply New Application" action is triggered, it passes state.client?.accountNo ?: "" to the navigation chain. If the client object lacks an accountNo, an empty string is passed through navigateToClientApplyNewApplicationScreenClientApplyNewApplicationsStatenavigateToNewLoanAccountRoute, ultimately reaching the ViewModel state as an empty string.

Clarify whether this behavior is intentional. If the schedule summary should always display a valid account number, consider adding validation or a fallback mechanism at the point where the client's account number is accessed in ClientProfileDetailsScreen.

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

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt`
at line 1025, The current NewLoanAccountViewModel state property accountNo
defaults to an empty string and can be passed through from
ClientProfileDetailsScreen (via navigateToClientApplyNewApplicationScreen →
ClientApplyNewApplicationsState → navigateToNewLoanAccountRoute), which means
the schedule summary may show no account number; decide whether empty is
acceptable or enforce a fallback/validation: either change the flow to pass a
non-empty fallback (e.g., "N/A" or generate a placeholder) at the caller
(ClientProfileDetailsScreen) before calling
navigateToClientApplyNewApplicationScreen, or add validation in
NewLoanAccountViewModel (or the code that renders the schedule summary) to
detect accountNo == "" and substitute a clear fallback or show a
validation/error state; update the relevant places (ClientProfileDetailsScreen,
navigateToClientApplyNewApplicationScreen, ClientApplyNewApplicationsState,
navigateToNewLoanAccountRoute, and NewLoanAccountViewModel) so the chosen
behavior is consistent.

* Used to preview the schedule before submitting the loan application.
*/
@POST(APIEndPoint.CREATE_LOANS_ACCOUNTS + "?command=calculateLoanSchedule")
fun calculateLoanSchedule(@Body loansPayload: LoansPayload?): Flow<LoanWithAssociationsEntity>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

it should have reponse type instead the of HttpResponse

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So sir how can we handle this error msg

{
  "developerMessage": "The request was invalid. This typically will happen due to validation errors which are provided.",
  "httpStatusCode": "400",
  "defaultUserMessage": "Validation errors exist.",
  "userMessageGlobalisationCode": "validation.msg.validation.errors.exist",
  "errors": [
    {
      "defaultUserMessage": "The parameter `numberOfRepayments` must be between 2 and 5.",
      "parameterName": "numberOfRepayments",
      "developerMessage": "The parameter `numberOfRepayments` must be between 2 and 5.",
      "userMessageGlobalisationCode": "validation.msg.loan.numberOfRepayments.is.not.within.expected.range",
      "args": [
        {
          "value": 60
        },
        {
          "value": 2
        },
        {
          "value": 5
        }
      ]
    }
  ]
}

which is I implemented like this in datamanager

fun calculateLoanSchedule(loansPayload: LoansPayload): Flow<RepaymentSchedule> {
        return mBaseApiManager.loanService.calculateLoanSchedule(loansPayload).map { response ->
            if (!response.status.isSuccess()) {
                val errorMessage = extractErrorMessage(response)

                throw IllegalStateException(errorMessage)
            }

            Json { ignoreUnknownKeys = true }.decodeFromString<RepaymentSchedule>(response.bodyAsText())
        }
    }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Look other repositories, we are already doing it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sir this same approach use everywhere

@Arinyadav1

Copy link
Copy Markdown
Member
WhatsApp.Video.2026-02-21.at.6.32.34.PM.mp4

@therajanmaurya
therajanmaurya merged commit 800ad6a into openMF:development Feb 21, 2026
5 checks passed
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