Skip to content

feat(loan): Implementation of Export PDF functionality.#2622

Open
techsavvy185 wants to merge 8 commits intoopenMF:developmentfrom
techsavvy185:loanPdfExport
Open

feat(loan): Implementation of Export PDF functionality.#2622
techsavvy185 wants to merge 8 commits intoopenMF:developmentfrom
techsavvy185:loanPdfExport

Conversation

@techsavvy185
Copy link
Contributor

@techsavvy185 techsavvy185 commented Feb 23, 2026

Fixes - Jira-#649
Android

Screen_recording_20260310_003021.webm

Desktop

Screen.Recording.2026-03-10.at.12.33.33.AM.mov

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

  • Run the static analysis check ./gradlew check or ci-prepush.sh to make sure you didn't break anything

  • If you have multiple commits please combine them into one commit by squashing them.

Summary by CodeRabbit

  • New Features

    • Cross-platform PDF export (desktop, Android, Web/Wasm; iOS implementation pending), including export flow and error dialogs.
    • Enhanced repayment schedule UI: modular, data-driven table, improved headers, export-to-PDF action, and offline/network handling.
    • New Details Card component, refined table row styling/widths, and a new export/share icon.
  • Chores

    • Added PDF libraries, build/tooling tweaks, and new localized strings for repayment UI and PDF export.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Introduces a multiplatform PDF export pipeline (common API, platform implementations), refactors loan repayment schedule into a state-driven UI with PDF export, adds HTML templating for PDFs, updates design tokens/table components/icons, adds UI strings and a details card, and adds build/dependency changes for kotlinx-html and OpenHTMLToPDF.

Changes

Cohort / File(s) Summary
Build & Dependency Files
gradle.properties, gradle/libs.versions.toml, core/ui/build.gradle.kts, feature/loan/build.gradle.kts, cmp-android/dependencies/*RuntimeClasspath.txt
Added kotlinx-html and openhtmltopdf library entries and module dependencies; updated Android runtime classpaths; increased Gradle JVM memory and enabled parallel/daemon/native flags.
Multiplatform PDF API & Config
core/ui/src/commonMain/.../pdf/PdfGenerator.kt, .../PdfGeneratorHelper.kt, .../PageConfig.kt, .../HtmlTemplateGenerator.kt
Added expect PdfGenerator and composable expect rememberPdfGenerator, PageConfig enums/data, and HtmlTemplateGenerator for XHTML-ready PDF HTML generation.
Platform PDF Implementations
core/ui/src/androidMain/.../PdfGenerator.android.kt, core/ui/src/desktopMain/.../PdfGenerator.desktop.kt, core/ui/src/jsMain/.../PdfGenerator.js.kt, core/ui/src/wasmJsMain/.../PdfGenerator.wasmJs.kt, core/ui/src/nativeMain/.../PdfGenerator.native.kt
Added actual PdfGenerator implementations per platform (Android WebView+PrintManager, Desktop OpenHTMLToPDF + file dialog, JS/Wasm iframe+print, Native iOS stub).
Compose PdfGenerator Helpers
core/ui/src/*Main/.../PdfGeneratorHelper.*.kt
Added platform-specific @Composable actual fun rememberPdfGenerator() helpers; Android variant wires Context into PdfGenerator.
Loan Repayment Schedule (UI, VM, PDF)
feature/loan/src/commonMain/kotlin/.../LoanRepaymentScheduleScreen.kt, .../LoanRepaymentScheduleViewModel.kt, .../pdf/RepaymentScheduleHtmlGenerator.kt
Refactored screen into content/dialog split, introduced LoanRepaymentScheduleState/actions/events, network-aware ViewModel, mapping logic, PDF export flow and internal HTML generator for repayment schedule PDFs.
Removed legacy UI state
feature/loan/src/commonMain/kotlin/.../LoanRepaymentScheduleUiState.kt
Deleted old sealed UI state; callers must use new LoanRepaymentScheduleState.
Design system & icons
core/designsystem/src/commonMain/kotlin/.../component/MifosTable.kt, .../theme/DesignToken.kt, .../icon/MifosIcons.kt
Added MifosTableRow composable with configurable cells/borders, adjusted table cell widths (small → 60.dp), added bottom corner shape, and added Export icon mapping.
UI components & strings
core/ui/src/commonMain/kotlin/.../MifosDetailsCard.kt, core/ui/src/commonMain/composeResources/values/strings.xml, feature/loan/src/commonMain/composeResources/values/strings.xml
Added MifosDetailsCard composable; added PDF-related and many loan/repayment localization strings; updated repayment table headers and export labels.
Misc. resource/key updates & small UI changes
feature/loan/src/commonMain/kotlin/.../LoanTransactionsScreen.kt, .../RepaymentPeriodCard.kt, .../NewLoanAccountViewModel.kt, .../pages/SchedulePage.kt
Replaced several resource keys to new feature_loan_* variants and updated imports/usages accordingly.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Screen as LoanRepaymentScreen
    participant VM as ViewModel
    participant Generator as HtmlGenerator
    participant Pdf as PdfGenerator
    participant Platform as OS/Browser

    User->>Screen: Tap "Export to PDF"
    Screen->>VM: send ExportToPdf action
    VM->>Generator: request HTML (tableData + strings)
    Generator-->>VM: return HTML content
    VM->>Pdf: generateAndSharePdf(html, fileName, pageConfig)
    Pdf->>Platform: platform-specific render/share (WebView/PrintManager, OpenHTMLToPDF+Dialog, iframe+print)
    Platform-->>User: show save/print UI or error
    Pdf-->>VM: success or error
    VM->>Screen: emit event (ExportPdf or PdfExportError)
    Screen-->>User: display result dialog
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • biplab1
  • revanthkumarJ

Poem

🐰
I stitched the HTML with carrot care,
Tables snug and strings laid bare,
From Compose I hopped to platform door,
Click—export—PDF! —hop, leap, encore! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main implementation: Export PDF functionality for the loan module. It is concise, avoids vague terms, and directly reflects the primary change across the changeset.

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

✨ Finishing Touches
🧪 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.

@techsavvy185 techsavvy185 marked this pull request as ready for review March 1, 2026 20:39
@techsavvy185 techsavvy185 marked this pull request as draft March 1, 2026 20:41
@techsavvy185 techsavvy185 marked this pull request as ready for review March 1, 2026 20:43
@techsavvy185
Copy link
Contributor Author

The code in this PR is exactly the same as the one implemented in Mifos Mobile.

Copy link

@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: 11

🧹 Nitpick comments (9)
core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt (2)

41-44: Minor: Remove the space before the colon in the heading format.

The string "$heading :" has an unconventional space before the colon. Standard formatting would be "$heading:" or "$heading: " (space after).

✏️ Suggested fix
                     Text(
-                        text = "$heading :",
+                        text = "$heading:",
                         style = MifosTypography.labelMediumEmphasized,
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt`
around lines 41 - 44, Update the Text composable in MifosDetailsCard where the
heading string is built as "$heading :"; change it to remove the space before
the colon so it reads "$heading:" (or "$heading: " if you prefer a trailing
space) to match conventional punctuation; locate the Text call that uses
MifosTypography.labelMediumEmphasized and adjust the interpolated string
accordingly.

33-51: Consider documenting that map insertion order is preserved.

The forEach iteration relies on the map's iteration order. If callers pass a LinkedHashMap (or use mapOf() which returns one in Kotlin), order is preserved. However, if a plain HashMap is passed, the display order becomes unpredictable. This is typically fine since mapOf() is the idiomatic Kotlin approach, but consider adding a KDoc comment noting that insertion order is respected for display.

📝 Suggested KDoc addition
+/**
+ * Displays a card with key-value detail rows.
+ *
+ * `@param` details Map of heading to value pairs. Iteration order determines display order,
+ *                so use [mapOf] or [LinkedHashMap] to preserve insertion order.
+ * `@param` modifier Modifier to be applied to the card.
+ */
 `@Composable`
 fun MifosDetailsCard(
     details: Map<String, String?>,
     modifier: Modifier = Modifier,
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt`
around lines 33 - 51, Add a KDoc comment to MifosDetailsCard above its
declaration indicating that the component iterates the provided details map in
its natural iteration order (so insertion order is preserved when callers pass a
LinkedHashMap or use mapOf()), and that using a plain HashMap will produce
unpredictable display order; mention callers should use LinkedHashMap/mapOf if a
stable order is required. Reference the MifosDetailsCard component and its
details parameter in the KDoc so readers know this behavior applies to the
forEach-driven rendering.
gradle.properties (1)

19-19: Prefer safer default daemon memory limits for broader dev/CI compatibility.

Hard-coding -Xmx8g plus -XX:MaxMetaspaceSize=1g can make builds brittle on lower-memory environments. Consider a more conservative default here and let high-memory machines override locally.

Proposed adjustment
-org.gradle.jvmargs=-Xmx8g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
+org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gradle.properties` at line 19, The org.gradle.jvmargs setting currently
hard-codes high memory (-Xmx8g and -XX:MaxMetaspaceSize=1g); change this to more
conservative defaults (for example -Xmx1g or -Xmx2g and remove or reduce
MaxMetaspaceSize) so CI and low-memory dev machines aren’t forced to allocate
8GB, and document/howto override locally (e.g., via GRADLE_OPTS or a local
gradle.properties) so machines that need larger heaps can opt-in; update the
org.gradle.jvmargs line and add a brief comment explaining how to override.
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt (3)

494-503: Preview uses empty default state which won't render meaningful content.

The preview uses LoanRepaymentScheduleState() which has screenState = ScreenState.Loading by default. Consider providing a sample Success state with mock data for more useful previews.

🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`
around lines 494 - 503, The preview PreviewLoanRepaymentSchedule currently
builds LoanRepaymentScheduleState() which defaults to ScreenState.Loading and
shows nothing; update PreviewLoanRepaymentSchedule to construct and pass a
meaningful LoanRepaymentScheduleState representing a ScreenState.Success with
mocked loan repayment data (e.g., sample schedule items, totals, and any flags
used by LoanRepaymentScheduleScreenContent) so the composable renders useful
content; locate PreviewLoanRepaymentSchedule and replace the default state with
a fabricated success state instance matching the state model used by
LoanRepaymentScheduleScreenContent.

186-193: Redundant null check on repaymentScheduleTableData in Success state.

When screenState is Success, the repaymentScheduleTableData should be guaranteed non-null based on the ViewModel logic (line 172 in ViewModel sets both together). Consider making this invariant explicit in the state model or using !! with a comment explaining the guarantee.

🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`
around lines 186 - 193, The null-check around repaymentScheduleTableData in the
LoanRepaymentScheduleState.ScreenState.Success branch is redundant; when the
ViewModel emits Success it guarantees repaymentScheduleTableData is non-null.
Replace the safe-let with a direct use of the non-null value (e.g., pass
state.repaymentScheduleTableData!! to LoanRepaymentScheduleContent) and add a
short comment referencing the ViewModel invariant, or better yet update the
state model so Success carries a non-null tableData (change
LoanRepaymentScheduleState.ScreenState.Success to include a non-null
repaymentScheduleTableData property) and then call
LoanRepaymentScheduleContent(tableData = state.repaymentScheduleTableData,
basicDetails = state.basicDetails) without a null check.

103-135: PDF export error handling could be more robust.

A few observations:

  1. Line 122: e.printStackTrace() is not ideal for production logging
  2. Lines 121-128: Catching generic Exception may hide the actual error type
  3. The coroutineScope.launch inside LaunchedEffect collection may outlive the composable if navigation happens during PDF generation
💡 Consider structured error handling
                     coroutineScope.launch {
                         try {
                             val pdfStrings = createPdfStrings()
                             val htmlGenerator = RepaymentScheduleHtmlGenerator(data, pdfStrings)
                             val htmlContent = htmlGenerator.generateHtml()
                             val fileName = "repayment_schedule_${data.accountNo}"
                             val pageConfig = PageConfig(
                                 size = PageSize.A4,
                                 orientation = Orientation.LANDSCAPE,
                                 marginMm = 8,
                             )
                             pdfGenerator.generateAndSharePdf(htmlContent, fileName, pageConfig)
                         } catch (e: Exception) {
-                            e.printStackTrace()
+                            // Consider using a proper logging framework
                             viewModel.trySendAction(
                                 LoanRepaymentScheduleAction.PdfExportError(
                                     title = getString(Res.string.feature_loan_export_pdf_error_title),
-                                    message = getString(Res.string.feature_loan_export_pdf_error),
+                                    message = e.message ?: getString(Res.string.feature_loan_export_pdf_error),
                                 ),
                             )
                         }
                     }
🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`
around lines 103 - 135, The PDF export handling is fragile: it uses
e.printStackTrace(), catches a generic Exception, and launches a coroutine via
coroutineScope.launch inside LaunchedEffect which can outlive the composable;
fix by moving the PDF generation out of the UI effect into a lifecycle-safe
place (e.g., add a viewModel.exportRepaymentSchedulePdf(data) or a suspend
viewModel.generateAndSharePdf(...) that calls createPdfStrings,
RepaymentScheduleHtmlGenerator, and pdfGenerator.generateAndSharePdf), replace
coroutineScope.launch with a call to that viewModel suspend function from
LaunchedEffect (or use launch tied to the LaunchedEffect coroutine scope) so
cancellation follows the composable lifecycle, catch and handle specific
exceptions (e.g., IOException, PdfGenerationException) instead of Exception, and
replace e.printStackTrace() with structured logging or include e.message/stack
in the LoanRepaymentScheduleAction.PdfExportError payload when calling
viewModel.trySendAction; reference functions/classes: LaunchedEffect,
viewModel.eventFlow, coroutineScope.launch, createPdfStrings,
RepaymentScheduleHtmlGenerator, pdfGenerator.generateAndSharePdf, and
LoanRepaymentScheduleAction.PdfExportError.
core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.js.kt (1)

88-90: Consider using structured logging instead of println.

Using println for error logging is not ideal for production. Consider using a proper logging framework or at minimum console.error for JS targets to ensure errors are visible in browser dev tools.

💡 Use console.error for JS logging
         } catch (e: Exception) {
-            println("Error generating PDF on Web: ${e.message}")
+            console.error("Error generating PDF on Web: ${e.message}")
             throw e
         } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.js.kt`
around lines 88 - 90, The catch block in PdfGenerator (the exception handler
shown in PdfGenerator.js.kt) currently calls println("Error generating PDF on
Web: ${e.message}"); replace this with a JS-appropriate structured log call—use
console.error with the error and message (or integrate the project's logging
utility if available) so errors appear in browser devtools; update the catch in
the PDF generation function to call console.error("Error generating PDF on
Web:", e) or route through the existing logger instead of println and keep
rethrowing the exception.
core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/HtmlTemplateGenerator.kt (1)

142-151: Date format is hardcoded and may not match user locale expectations.

The date format ${currentDate.day}/${currentDate.month.number}/${currentDate.year} produces d/M/yyyy format, which differs from many locales (e.g., US uses M/d/yyyy, ISO uses yyyy-MM-dd). For a PDF document, consider using a consistent ISO format or the locale-aware formatting.

💡 Consider using ISO date format for consistency
     `@OptIn`(ExperimentalTime::class)
     protected suspend fun getGenerationDateText(): String {
         val currentDate = kotlin.time.Clock.System.now()
             .toLocalDateTime(kotlinx.datetime.TimeZone.currentSystemDefault())
             .date

-        val dateString = "${currentDate.day}/${currentDate.month.number}/${currentDate.year}"
+        val dateString = "${currentDate.year}-${currentDate.month.number.toString().padStart(2, '0')}-${currentDate.day.toString().padStart(2, '0')}"
         val generationDateLabel = getString(Res.string.feature_pdf_generation_date)
         return "$generationDateLabel: $dateString"
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/HtmlTemplateGenerator.kt`
around lines 142 - 151, The date formatting in getGenerationDateText is
hardcoded as
"${currentDate.day}/${currentDate.month.number}/${currentDate.year}" which is
not locale-safe; change it to produce a consistent ISO-8601 date (yyyy-MM-dd) or
use a locale-aware formatter instead. Update getGenerationDateText to derive the
date string from the LocalDate's ISO representation (or, if you need
locale-specific presentation, use a platform DateTimeFormatter with the current
locale) and keep the rest of the method (generationDateLabel assembly) unchanged
so the label from getString(Res.string.feature_pdf_generation_date) is
preserved.
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt (1)

104-123: Consider potential state race with nested coroutine launch.

In handleNetworkStatus, there's a viewModelScope.launch that updates state based on network status. If network status changes rapidly, multiple coroutines could be launched and compete to update state. While update is thread-safe, the logic flow could become unpredictable.

💡 Consider removing unnecessary coroutine wrapper

The operations inside this launch block don't require suspension. Consider removing the nested launch:

     private fun handleNetworkStatus(isOnline: Boolean) {
         mutableStateFlow.update { it.copy(networkStatus = isOnline) }

-        viewModelScope.launch {
-            if (!isOnline) {
-                mutableStateFlow.update { current ->
-                    if (current.screenState is LoanRepaymentScheduleState.ScreenState.Loading ||
-                        current.screenState is LoanRepaymentScheduleState.ScreenState.Error ||
-                        current.screenState is LoanRepaymentScheduleState.ScreenState.Network
-                    ) {
-                        current.copy(screenState = LoanRepaymentScheduleState.ScreenState.Network)
-                    } else {
-                        current
-                    }
+        if (!isOnline) {
+            mutableStateFlow.update { current ->
+                if (current.screenState is LoanRepaymentScheduleState.ScreenState.Loading ||
+                    current.screenState is LoanRepaymentScheduleState.ScreenState.Error ||
+                    current.screenState is LoanRepaymentScheduleState.ScreenState.Network
+                ) {
+                    current.copy(screenState = LoanRepaymentScheduleState.ScreenState.Network)
+                } else {
+                    current
                 }
-            } else {
-                loadLoanRepaySchedule()
             }
+        } else {
+            loadLoanRepaySchedule()
         }
     }
🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`
around lines 104 - 123, In handleNetworkStatus, remove the unnecessary
viewModelScope.launch wrapper for the non-suspending offline branch to avoid
racing coroutines: perform the mutableStateFlow.update synchronously when
!isOnline (use the existing conditional update using
LoanRepaymentScheduleState.ScreenState checks) and only start a coroutine
(viewModelScope.launch) for the path that needs suspension by calling
loadLoanRepaySchedule(); keep references: handleNetworkStatus,
mutableStateFlow.update, LoanRepaymentScheduleState.ScreenState,
viewModelScope.launch, and loadLoanRepaySchedule().
🤖 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/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt`:
- Around line 41-46: The default borderColor in MifosTable.kt currently equals
backgroundColor (KptTheme.colorScheme.surfaceVariant) making dividers invisible;
update the default for the borderColor parameter in the MifosTable function
signature to a higher‑contrast token (for example KptTheme.colorScheme.outline
or KptTheme.colorScheme.onSurfaceVariant with a low alpha) so row separators are
visible against surfaceVariant, and ensure any usages that rely on the old
default still render correctly.

In `@core/ui/build.gradle.kts`:
- Around line 52-57: Update the openhtmltopdf dependencies used in desktopMain
to the current 1.1.37 release: replace the existing references for
openhtmltopdf.pdfbox and openhtmltopdf.svg.support (the identifiers used in the
build file) to point at version 1.1.37 in your version catalog (or hardcode
1.1.37 if you manage versions inline); ensure the artifacts referenced as
implementation(libs.openhtmltopdf.pdfbox) and
implementation(libs.openhtmltopdf.svg.support) resolve to 1.1.37 so the JVM-only
desktopMain uses the updated openhtmltopdf 1.1.37 artifacts.

In `@core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PageConfig.kt`:
- Around line 31-35: PageConfig currently allows negative marginMm which can
produce invalid `@page` CSS; add validation in the PageConfig constructor (e.g.,
an init block or require call) to reject negative values by throwing an
IllegalArgumentException with a clear message referencing marginMm. Locate the
data class PageConfig (alongside PageSize and Orientation) and add
require(marginMm >= 0) (or equivalent) to enforce non‑negative margins and
update any callers/tests that pass negative margins.

In `@core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.js.kt`:
- Around line 65-85: The onload handler for the iframe is being set after
writing/closing the frame document which can miss a synchronously fired load
event; move the iframe.onload assignment (the lambda that calls
iframe.contentWindow?.let { it.print(); completionSignal.complete(Unit) } or
completes exceptionally) so it is attached before calling
frameDoc.write(finalHtml) and frameDoc.close(), ensuring the handler is in place
prior to writing/closing the document and keeping use of completionSignal for
success/failure as-is.

In
`@core/ui/src/nativeMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.native.kt`:
- Around line 21-27: The native iOS implementation of generateAndSharePdf
currently calls TODO(...) which throws at runtime; replace this with a safe
interim fallback: in PdfGenerator.native.kt implement actual suspend fun
generateAndSharePdf(htmlContent: String, fileName: String, pageConfig:
PageConfig) to either (a) return early after logging a clear warning/error that
PDF export is unsupported on Native/iOS yet, or (b) throw a documented
UnsupportedOperationException that callers can catch; reference the
generateAndSharePdf function and PageConfig to locate the code and ensure the
fallback does not perform a hard TODO() crash and is gated so feature paths on
Native/iOS fail gracefully until a full
UIPrintPageRenderer/UIMarkupTextPrintFormatter implementation is added.

In
`@core/ui/src/wasmJsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.wasmJs.kt`:
- Around line 67-87: The await on completionSignal (completionSignal.await())
can hang if the iframe never calls its onload/print path; modify the
PdfGenerator wasmJs flow to add a timeout and explicit iframe error path: wrap
the wait in a cancellable timeout (e.g., use withTimeout or start a timer) and
on timeout completeExceptionally the same CompletableDeferred used by the iframe
handler, add iframe.onerror/onabort handlers that call
completionSignal.completeExceptionally with a descriptive exception, and ensure
all handlers/timers are cleaned up (remove onload/onerror and cancel timer)
after completion to avoid leaks; update the logic around iframe.onload, the
setTimeout callback, and any caller of completionSignal to handle the timeout
exception.

In `@feature/loan/src/commonMain/composeResources/values/strings.xml`:
- Line 284: The string resource named "principle_paid_off" contains a typo in
its value ("Principle Paid Off"); update the value to the correct financial term
"Principal Paid Off" in the <string name="principle_paid_off"> entry, and search
for any other occurrences of the misspelled word in resource files or UI text to
correct them as well.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`:
- Around line 181-211: mapToTableData uses loan.currency.code which is nullable;
ensure you replace uses of currencyCode with a non-null fallback (e.g.
currencyCode ?: "USD") before calling CurrencyFormatter.format and before
passing into mapPeriodsData and calculateTotals to avoid NPEs. Update
LoanRepaymentScheduleViewModel.mapToTableData so that the local currency
variable is the guarded value and use that guarded variable in all calls to
CurrencyFormatter.format, mapPeriodsData, and calculateTotals.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/pdf/RepaymentScheduleHtmlGenerator.kt`:
- Around line 218-233: The h1 CSS inside getAdditionalStyles() contains an
invalid fragment "solid `#1976d2`" (missing property name and width); update the
h1 rule in RepaymentScheduleHtmlGenerator.getAdditionalStyles() to use a
complete declaration such as "border-bottom: 1px solid `#1976d2`;" (or remove the
fragment) so the CSS is valid and the method returns a correct stylesheet
string.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanTransaction/LoanTransactionsScreen.kt`:
- Line 24: The table header currently imports and uses
feature_loan_table_header_principal_due in LoanTransactionsScreen.kt which
labels the transaction column as “Principal Due” even though the column renders
transaction values (row.principal); replace that import/use with a
transaction-specific header (e.g.,
feature_loan_table_header_principal_transaction or similar string key) and
update the header reference wherever LoanTransactionsScreen (and its column
rendering row.principal) sets the column label; if that transaction-oriented
string key was removed, restore it in
feature/loan/src/commonMain/composeResources/values/strings.xml with an
appropriate key/name for transaction principal header so the UI uses the correct
string.

In `@gradle/libs.versions.toml`:
- Around line 112-113: Update the openhtmltopdf dependency entry in
libs.versions.toml by changing the value for the openhtmltopdf key from "1.0.10"
to "1.1.37" so the project uses the latest stable release; locate the
openhtmltopdf = "1.0.10" line and replace the version string with "1.1.37".

---

Nitpick comments:
In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt`:
- Around line 41-44: Update the Text composable in MifosDetailsCard where the
heading string is built as "$heading :"; change it to remove the space before
the colon so it reads "$heading:" (or "$heading: " if you prefer a trailing
space) to match conventional punctuation; locate the Text call that uses
MifosTypography.labelMediumEmphasized and adjust the interpolated string
accordingly.
- Around line 33-51: Add a KDoc comment to MifosDetailsCard above its
declaration indicating that the component iterates the provided details map in
its natural iteration order (so insertion order is preserved when callers pass a
LinkedHashMap or use mapOf()), and that using a plain HashMap will produce
unpredictable display order; mention callers should use LinkedHashMap/mapOf if a
stable order is required. Reference the MifosDetailsCard component and its
details parameter in the KDoc so readers know this behavior applies to the
forEach-driven rendering.

In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/HtmlTemplateGenerator.kt`:
- Around line 142-151: The date formatting in getGenerationDateText is hardcoded
as "${currentDate.day}/${currentDate.month.number}/${currentDate.year}" which is
not locale-safe; change it to produce a consistent ISO-8601 date (yyyy-MM-dd) or
use a locale-aware formatter instead. Update getGenerationDateText to derive the
date string from the LocalDate's ISO representation (or, if you need
locale-specific presentation, use a platform DateTimeFormatter with the current
locale) and keep the rest of the method (generationDateLabel assembly) unchanged
so the label from getString(Res.string.feature_pdf_generation_date) is
preserved.

In `@core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.js.kt`:
- Around line 88-90: The catch block in PdfGenerator (the exception handler
shown in PdfGenerator.js.kt) currently calls println("Error generating PDF on
Web: ${e.message}"); replace this with a JS-appropriate structured log call—use
console.error with the error and message (or integrate the project's logging
utility if available) so errors appear in browser devtools; update the catch in
the PDF generation function to call console.error("Error generating PDF on
Web:", e) or route through the existing logger instead of println and keep
rethrowing the exception.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`:
- Around line 494-503: The preview PreviewLoanRepaymentSchedule currently builds
LoanRepaymentScheduleState() which defaults to ScreenState.Loading and shows
nothing; update PreviewLoanRepaymentSchedule to construct and pass a meaningful
LoanRepaymentScheduleState representing a ScreenState.Success with mocked loan
repayment data (e.g., sample schedule items, totals, and any flags used by
LoanRepaymentScheduleScreenContent) so the composable renders useful content;
locate PreviewLoanRepaymentSchedule and replace the default state with a
fabricated success state instance matching the state model used by
LoanRepaymentScheduleScreenContent.
- Around line 186-193: The null-check around repaymentScheduleTableData in the
LoanRepaymentScheduleState.ScreenState.Success branch is redundant; when the
ViewModel emits Success it guarantees repaymentScheduleTableData is non-null.
Replace the safe-let with a direct use of the non-null value (e.g., pass
state.repaymentScheduleTableData!! to LoanRepaymentScheduleContent) and add a
short comment referencing the ViewModel invariant, or better yet update the
state model so Success carries a non-null tableData (change
LoanRepaymentScheduleState.ScreenState.Success to include a non-null
repaymentScheduleTableData property) and then call
LoanRepaymentScheduleContent(tableData = state.repaymentScheduleTableData,
basicDetails = state.basicDetails) without a null check.
- Around line 103-135: The PDF export handling is fragile: it uses
e.printStackTrace(), catches a generic Exception, and launches a coroutine via
coroutineScope.launch inside LaunchedEffect which can outlive the composable;
fix by moving the PDF generation out of the UI effect into a lifecycle-safe
place (e.g., add a viewModel.exportRepaymentSchedulePdf(data) or a suspend
viewModel.generateAndSharePdf(...) that calls createPdfStrings,
RepaymentScheduleHtmlGenerator, and pdfGenerator.generateAndSharePdf), replace
coroutineScope.launch with a call to that viewModel suspend function from
LaunchedEffect (or use launch tied to the LaunchedEffect coroutine scope) so
cancellation follows the composable lifecycle, catch and handle specific
exceptions (e.g., IOException, PdfGenerationException) instead of Exception, and
replace e.printStackTrace() with structured logging or include e.message/stack
in the LoanRepaymentScheduleAction.PdfExportError payload when calling
viewModel.trySendAction; reference functions/classes: LaunchedEffect,
viewModel.eventFlow, coroutineScope.launch, createPdfStrings,
RepaymentScheduleHtmlGenerator, pdfGenerator.generateAndSharePdf, and
LoanRepaymentScheduleAction.PdfExportError.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`:
- Around line 104-123: In handleNetworkStatus, remove the unnecessary
viewModelScope.launch wrapper for the non-suspending offline branch to avoid
racing coroutines: perform the mutableStateFlow.update synchronously when
!isOnline (use the existing conditional update using
LoanRepaymentScheduleState.ScreenState checks) and only start a coroutine
(viewModelScope.launch) for the path that needs suspension by calling
loadLoanRepaySchedule(); keep references: handleNetworkStatus,
mutableStateFlow.update, LoanRepaymentScheduleState.ScreenState,
viewModelScope.launch, and loadLoanRepaySchedule().

In `@gradle.properties`:
- Line 19: The org.gradle.jvmargs setting currently hard-codes high memory
(-Xmx8g and -XX:MaxMetaspaceSize=1g); change this to more conservative defaults
(for example -Xmx1g or -Xmx2g and remove or reduce MaxMetaspaceSize) so CI and
low-memory dev machines aren’t forced to allocate 8GB, and document/howto
override locally (e.g., via GRADLE_OPTS or a local gradle.properties) so
machines that need larger heaps can opt-in; update the org.gradle.jvmargs line
and add a brief comment explaining how to override.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2986eba and d175b2c.

⛔ Files ignored due to path filters (1)
  • core/ui/src/commonMain/composeResources/drawable/ic_icon_mifos_logo.svg is excluded by !**/*.svg
📒 Files selected for processing (33)
  • cmp-android/dependencies/demoDebugRuntimeClasspath.txt
  • cmp-android/dependencies/demoReleaseRuntimeClasspath.txt
  • cmp-android/dependencies/prodDebugRuntimeClasspath.txt
  • cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/DesignToken.kt
  • core/ui/build.gradle.kts
  • core/ui/src/androidMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.android.kt
  • core/ui/src/androidMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.android.kt
  • core/ui/src/commonMain/composeResources/values/strings.xml
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/HtmlTemplateGenerator.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PageConfig.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.kt
  • core/ui/src/desktopMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.desktop.kt
  • core/ui/src/desktopMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.desktop.kt
  • core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.js.kt
  • core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.js.kt
  • core/ui/src/nativeMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.native.kt
  • core/ui/src/nativeMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.native.kt
  • core/ui/src/wasmJsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.wasmJs.kt
  • core/ui/src/wasmJsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.wasmJs.kt
  • feature/loan/build.gradle.kts
  • feature/loan/src/commonMain/composeResources/values/strings.xml
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleUiState.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/pdf/RepaymentScheduleHtmlGenerator.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanTransaction/LoanTransactionsScreen.kt
  • gradle.properties
  • gradle/libs.versions.toml
💤 Files with no reviewable changes (1)
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleUiState.kt

@biplab1
Copy link
Contributor

biplab1 commented Mar 6, 2026

@techsavvy185 Please resolve the merge conflicts.

@techsavvy185 techsavvy185 requested a review from a team March 8, 2026 11:30
Copy link

@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: 7

♻️ Duplicate comments (6)
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanTransaction/LoanTransactionsScreen.kt (1)

24-24: ⚠️ Potential issue | 🟡 Minor

Use a transaction-specific principal label here.

This column renders row.principal, so “Principal Due” is misleading on the loan transactions table.

💡 Suggested fix
-import androidclient.feature.loan.generated.resources.feature_loan_table_header_principal_due
+import androidclient.feature.loan.generated.resources.feature_loan_table_header_principal
...
-        stringResource(Res.string.feature_loan_table_header_principal_due),
+        stringResource(Res.string.feature_loan_table_header_principal),

Also applies to: 255-255

🤖 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/loanTransaction/LoanTransactionsScreen.kt`
at line 24, The import and header resource
feature_loan_table_header_principal_due used in LoanTransactionsScreen is
misleading because the column renders row.principal (transaction principal) —
replace the import and any usage of feature_loan_table_header_principal_due with
a transaction-specific label (e.g., a new resource like
feature_loan_transactions_table_header_principal or the existing
transaction-principal resource), update the import statement and the column
header reference in LoanTransactionsScreen (and the duplicate usage at the other
occurrence) so the UI reads a transaction-specific "Principal" label rather than
"Principal Due".
core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt (1)

41-46: ⚠️ Potential issue | 🟠 Major

Use a contrasting default for borderColor.

Line 46 still defaults borderColor to the same token as backgroundColor, so the row dividers disappear unless every caller overrides it.

💡 Proposed fix
-    borderColor: Color = KptTheme.colorScheme.surfaceVariant,
+    borderColor: Color = KptTheme.colorScheme.outlineVariant,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt`
around lines 41 - 46, The default borderColor currently equals backgroundColor
(backgroundColor: KptTheme.colorScheme.surfaceVariant, borderColor:
KptTheme.colorScheme.surfaceVariant) which hides row dividers; change the
default for the borderColor parameter in MifosTable (the function/constructor
using backgroundColor, cornerShape, showTopBorder, showBottomBorder,
showSideBorders) to a contrasting token such as KptTheme.colorScheme.outline or
KptTheme.colorScheme.onSurfaceVariant so dividers are visible by default,
keeping other parameter names (backgroundColor, borderColor) unchanged so
callers that override still work.
feature/loan/src/commonMain/composeResources/values/strings.xml (1)

326-326: ⚠️ Potential issue | 🟡 Minor

Fix the financial term typo.

“Principle Paid Off” should be “Principal Paid Off”. This shared resource is surfaced in the repayment summary and the generated PDF.

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

In `@feature/loan/src/commonMain/composeResources/values/strings.xml` at line 326,
The string resource with name "principle_paid_off" contains a typo in its value;
update the value for the resource string "principle_paid_off" to read "Principal
Paid Off" (capitalization preserved) so the repayment summary and generated PDF
display the correct financial term.
core/ui/src/wasmJsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.wasmJs.kt (1)

67-87: ⚠️ Potential issue | 🟠 Major

Guard the iframe completion path with timeout/error handlers.

If the iframe never reaches the load/print path, completionSignal.await() never completes and the export flow hangs indefinitely. Add onerror/onabort completion plus a timeout around the await.

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

In
`@core/ui/src/wasmJsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.wasmJs.kt`
around lines 67 - 87, The iframe print completion currently only resolves via
iframe.onload and then completionSignal.await(), which can hang if load/print
never occur; update the logic around completionSignal, iframe.onload, and
frameDoc.close to add iframe.onerror and iframe.onabort handlers that
completeExceptionally the completionSignal, and wrap the await with a
cancellable timeout (e.g., schedule a timer that completesExceptionally on
timeout and is cancelled on success) so that any error, abort, or timeout will
unblock completionSignal.await(); ensure handlers check
completionSignal.isCompleted before completing to avoid races.
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt (1)

181-210: ⚠️ Potential issue | 🔴 Critical

Normalize currencyCode before passing it downstream.

loan.currency.code is nullable here, but it is forwarded straight into CurrencyFormatter.format() and into mapPeriodsData / calculateTotals. A loan missing currency metadata can still crash this screen and the PDF path on platforms whose formatter expects a non-null ISO code. Normalize once in mapToTableData and pass the guarded value through the rest of the mapping. Based on learnings: When a Kotlin function parameter is nullable and downstream calls require a non-null value, add null-safety handling in all implementations.

🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`
around lines 181 - 210, mapToTableData currently reads a nullable
loan.currency.code into currencyCode and forwards it into
CurrencyFormatter.format, mapPeriodsData, and calculateTotals risking NPEs;
normalize currencyCode at the top of mapToTableData (e.g., fallback to a safe
default like an empty string or "USD" per app conventions) and then pass that
non-null guarded value into CurrencyFormatter.format, mapPeriodsData(periods,
currencyCode, maxDigits), and calculateTotals(periods, currencyCode, maxDigits)
so all downstream calls receive a non-null ISO code; update any local variable
name usage in mapToTableData to use the normalized value.
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/pdf/RepaymentScheduleHtmlGenerator.kt (1)

218-223: ⚠️ Potential issue | 🟡 Minor

Fix the invalid h1 CSS declaration.

solid #1976d2`` is not a valid CSS property, so that part of the heading style is ignored in the generated HTML. Replace it with a complete declaration such as border-bottom: 1px solid #1976d2`;`.

🤖 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/loanRepaymentSchedule/pdf/RepaymentScheduleHtmlGenerator.kt`
around lines 218 - 223, In getAdditionalStyles() of
RepaymentScheduleHtmlGenerator the h1 rule contains an invalid token "solid
`#1976d2`" which is ignored; replace that fragment with a proper declaration such
as "border-bottom: 1px solid `#1976d2`;" so the heading gets the intended bottom
border (update the h1 declaration inside the returned CSS string in
getAdditionalStyles()).
🧹 Nitpick comments (3)
core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt (1)

26-27: Consider using LinkedHashMap or documenting iteration order requirement.

The Map<String, String?> parameter doesn't guarantee iteration order. If display order matters (which it typically does for detail cards), callers must pass an ordered map implementation like LinkedHashMap (which mapOf() returns). Consider either:

  1. Documenting this expectation in a KDoc comment, or
  2. Changing the parameter type to List<Pair<String, String?>> to make ordering explicit.
📝 Option: Use List for explicit ordering
 `@Composable`
 fun MifosDetailsCard(
-    details: Map<String, String?>,
+    details: List<Pair<String, String?>>,
     modifier: Modifier = Modifier,
 ) {
     MifosCard(
         modifier = modifier.fillMaxWidth(),
     ) {
         Column(modifier = Modifier.padding(KptTheme.spacing.md)) {
-            details.forEach { (heading, value) ->
+            details.forEach { (heading, value) ->
                 Row(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt`
around lines 26 - 27, The MifosDetailsCard composable accepts details:
Map<String, String?> which does not guarantee iteration order; update the API so
ordering is explicit: either change the parameter to details: List<Pair<String,
String?>> (and update all call sites to pass an ordered list) or constrain it to
an ordered map type (e.g., LinkedHashMap) and update the parameter type
accordingly, or add a KDoc to the MifosDetailsCard declaration stating that
callers must pass an ordered map (such as LinkedHashMap or the mapOf() result)
if display order matters; reference the MifosDetailsCard function and its
details parameter when making the change so all usages are adjusted.
gradle.properties (2)

19-19: High memory allocation may fail on developer machines with limited RAM.

Allocating 8GB to the Gradle daemon is aggressive and could cause issues for developers with less available memory. Consider whether 4-6GB would suffice, or document the minimum RAM requirement.

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

In `@gradle.properties` at line 19, The org.gradle.jvmargs setting currently
reserves -Xmx8g which can cause failures on machines with limited RAM; change
the value in the org.gradle.jvmargs property to a lower default (e.g., -Xmx4g or
-Xmx6g) or make it configurable via an environment variable/CI override, and
update accompanying project documentation to state the minimum recommended RAM
for development so contributors know requirements.

30-38: Consider whether these Kotlin/Native settings are necessary for this PR.

This PR implements PDF export functionality, but introduces substantial build configuration changes (Kotlin/Native cache disabled, high memory settings, experimental flags). If these settings aren't required for PDF export, consider splitting them into a separate infrastructure PR to isolate concerns and simplify review.

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

In `@gradle.properties` around lines 30 - 38, This change bundles broad
Kotlin/Native build flags (org.jetbrains.compose.experimental.jscanvas.enabled,
kotlin.native.cacheKind, kotlin.native.jvmArgs,
kotlin.native.disableCompilerDaemon,
kotlin.kmp.unresolvedDependenciesDiagnostic,
kotlin.apple.xcodeCompatibility.nowarn) with the PDF export feature; revert or
extract those settings into a separate infrastructure/configuration commit or PR
and keep this PR focused on PDF export-only changes. Specifically, remove or
rollback the kotlin.native.* and org.jetbrains.compose.* lines from this PR,
create a dedicated commit/PR that documents why each flag is needed (with
benchmarks or reproduction steps), and update this PR’s description to state it
only contains PDF export logic so reviewers can evaluate functionality without
unrelated build-system changes.
🤖 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/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt`:
- Line 47: The onClick parameter in MifosTable (currently declared as onClick:
() -> Unit = {}) should be changed to a nullable callback (() -> Unit)? = null
to avoid adding click semantics to static/header rows; update the MifosTable
parameter signature accordingly and only apply the Modifier.clickable(...) when
onClick is non-null (e.g., check onClick != null before adding clickable) so
ripple, semantics and accessibility are only present for actionable rows.

In `@core/ui/build.gradle.kts`:
- Around line 55-56: The kotlinx-html dependency is currently declared as
implementation, so downstream modules extending HtmlTemplateGenerator (which
exposes the BODY type in its protected abstract fun BODY.generateBody()) cannot
see the BODY type; change the dependency declaration in core/ui's
build.gradle.kts from implementation(libs.kotlinx.html) to
api(libs.kotlinx.html) so kotlinx-html is exported on the compile classpath
(this will allow classes like RepaymentScheduleHtmlGenerator to compile without
re-declaring kotlinx-html).

In
`@core/ui/src/androidMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.android.kt`:
- Around line 81-83: The current code resumes the coroutine and defers WebView
cleanup immediately after calling printManager.print(fileName, nativeAdapter,
attributes), which can destroy the WebView before the PrintDocumentAdapter
callbacks finish; change this to capture the PrintJob returned by
printManager.print(...), then wait for the PrintJob to reach a terminal state
(via PrintJob.isCompleted/isFailed/isCancelled or a PrintJobStateChange
listener) before resuming the continuation and destroying the WebView (do not
rely on post()); update both places using
printManager.print/nativeAdapter/continuation to poll or register a completion
callback and only then resume Unit and call webView.destroy().

In
`@core/ui/src/desktopMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.desktop.kt`:
- Around line 48-59: Replace the pre-check using file.canWrite() with an actual
write attempt and map failures to the user dialog: try to open the target file
for write (e.g., RandomAccessFile(file, "rw") or FileOutputStream(file).use { }
/ file.outputStream().use { } ) inside a try block and close it immediately; if
that open fails with IOException, FileNotFoundException, or
OverlappingFileLockException, show the same JOptionPane.showMessageDialog
message and return null, otherwise return file. Update the code around the file
variable (where canWrite() is currently used) to catch those exceptions and
display the permission/locked-file dialog instead of relying on canWrite().

In `@core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.js.kt`:
- Around line 67-73: Inside the iframe.onload handler around the call to
iframe.contentWindow?.let { it.print(); completionSignal.complete(Unit) },
replace the current flow so that you attach an "afterprint" event listener to
iframe.contentWindow that will complete completionSignal and perform cleanup
(remove the listener and the iframe) when fired; still start the existing
2-second fallback timeout but only as a backup if "afterprint" doesn't fire, and
ensure you clear the fallback timeout when the "afterprint" handler runs to
avoid double cleanup.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`:
- Around line 137-178: The loadLoanRepaySchedule function can spawn overlapping
collectors; fix it by tracking and cancelling the previous load job before
starting a new one (add a Job property like loadLoanRepayJob on the ViewModel),
or alternatively use Flow.collectLatest on repository.getLoanRepaySchedule to
ensure only the latest emission is handled; in either approach cancel/replace
the previous job or switch to collectLatest inside loadLoanRepaySchedule so
repository.getLoanRepaySchedule(state.loanId) no longer produces concurrent
collectors that race updates to mutableStateFlow and fields like
repaymentScheduleTableData and screenState.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/pdf/RepaymentScheduleHtmlGenerator.kt`:
- Around line 155-167: In RepaymentScheduleHtmlGenerator.kt, the
disbursement-row currently merges columns using large colspans which shifts
values left; update the tr(classes = "disbursement-row") block to emit one td
per table column (no large colspan grouping) so tableData.disbursementDate and
tableData.loanAmount are placed in the specific Date and Loan Balance column
cells respectively (fill other columns with empty td entries as needed), or
alternatively adjust the colspan values so the disbursementDate cell aligns
under the Date column and the loanAmount cell aligns under the Loan Balance
column; locate the tr(classes = "disbursement-row") and replace the grouped
td(colspan=...) usage around tableData.disbursementDate and tableData.loanAmount
accordingly.

---

Duplicate comments:
In
`@core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt`:
- Around line 41-46: The default borderColor currently equals backgroundColor
(backgroundColor: KptTheme.colorScheme.surfaceVariant, borderColor:
KptTheme.colorScheme.surfaceVariant) which hides row dividers; change the
default for the borderColor parameter in MifosTable (the function/constructor
using backgroundColor, cornerShape, showTopBorder, showBottomBorder,
showSideBorders) to a contrasting token such as KptTheme.colorScheme.outline or
KptTheme.colorScheme.onSurfaceVariant so dividers are visible by default,
keeping other parameter names (backgroundColor, borderColor) unchanged so
callers that override still work.

In
`@core/ui/src/wasmJsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.wasmJs.kt`:
- Around line 67-87: The iframe print completion currently only resolves via
iframe.onload and then completionSignal.await(), which can hang if load/print
never occur; update the logic around completionSignal, iframe.onload, and
frameDoc.close to add iframe.onerror and iframe.onabort handlers that
completeExceptionally the completionSignal, and wrap the await with a
cancellable timeout (e.g., schedule a timer that completesExceptionally on
timeout and is cancelled on success) so that any error, abort, or timeout will
unblock completionSignal.await(); ensure handlers check
completionSignal.isCompleted before completing to avoid races.

In `@feature/loan/src/commonMain/composeResources/values/strings.xml`:
- Line 326: The string resource with name "principle_paid_off" contains a typo
in its value; update the value for the resource string "principle_paid_off" to
read "Principal Paid Off" (capitalization preserved) so the repayment summary
and generated PDF display the correct financial term.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`:
- Around line 181-210: mapToTableData currently reads a nullable
loan.currency.code into currencyCode and forwards it into
CurrencyFormatter.format, mapPeriodsData, and calculateTotals risking NPEs;
normalize currencyCode at the top of mapToTableData (e.g., fallback to a safe
default like an empty string or "USD" per app conventions) and then pass that
non-null guarded value into CurrencyFormatter.format, mapPeriodsData(periods,
currencyCode, maxDigits), and calculateTotals(periods, currencyCode, maxDigits)
so all downstream calls receive a non-null ISO code; update any local variable
name usage in mapToTableData to use the normalized value.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/pdf/RepaymentScheduleHtmlGenerator.kt`:
- Around line 218-223: In getAdditionalStyles() of
RepaymentScheduleHtmlGenerator the h1 rule contains an invalid token "solid
`#1976d2`" which is ignored; replace that fragment with a proper declaration such
as "border-bottom: 1px solid `#1976d2`;" so the heading gets the intended bottom
border (update the h1 declaration inside the returned CSS string in
getAdditionalStyles()).

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanTransaction/LoanTransactionsScreen.kt`:
- Line 24: The import and header resource
feature_loan_table_header_principal_due used in LoanTransactionsScreen is
misleading because the column renders row.principal (transaction principal) —
replace the import and any usage of feature_loan_table_header_principal_due with
a transaction-specific label (e.g., a new resource like
feature_loan_transactions_table_header_principal or the existing
transaction-principal resource), update the import statement and the column
header reference in LoanTransactionsScreen (and the duplicate usage at the other
occurrence) so the UI reads a transaction-specific "Principal" label rather than
"Principal Due".

---

Nitpick comments:
In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt`:
- Around line 26-27: The MifosDetailsCard composable accepts details:
Map<String, String?> which does not guarantee iteration order; update the API so
ordering is explicit: either change the parameter to details: List<Pair<String,
String?>> (and update all call sites to pass an ordered list) or constrain it to
an ordered map type (e.g., LinkedHashMap) and update the parameter type
accordingly, or add a KDoc to the MifosDetailsCard declaration stating that
callers must pass an ordered map (such as LinkedHashMap or the mapOf() result)
if display order matters; reference the MifosDetailsCard function and its
details parameter when making the change so all usages are adjusted.

In `@gradle.properties`:
- Line 19: The org.gradle.jvmargs setting currently reserves -Xmx8g which can
cause failures on machines with limited RAM; change the value in the
org.gradle.jvmargs property to a lower default (e.g., -Xmx4g or -Xmx6g) or make
it configurable via an environment variable/CI override, and update accompanying
project documentation to state the minimum recommended RAM for development so
contributors know requirements.
- Around line 30-38: This change bundles broad Kotlin/Native build flags
(org.jetbrains.compose.experimental.jscanvas.enabled, kotlin.native.cacheKind,
kotlin.native.jvmArgs, kotlin.native.disableCompilerDaemon,
kotlin.kmp.unresolvedDependenciesDiagnostic,
kotlin.apple.xcodeCompatibility.nowarn) with the PDF export feature; revert or
extract those settings into a separate infrastructure/configuration commit or PR
and keep this PR focused on PDF export-only changes. Specifically, remove or
rollback the kotlin.native.* and org.jetbrains.compose.* lines from this PR,
create a dedicated commit/PR that documents why each flag is needed (with
benchmarks or reproduction steps), and update this PR’s description to state it
only contains PDF export logic so reviewers can evaluate functionality without
unrelated build-system changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d290179-94a3-4e49-9751-49b1405dbce5

📥 Commits

Reviewing files that changed from the base of the PR and between d175b2c and a5c2092.

⛔ Files ignored due to path filters (1)
  • core/ui/src/commonMain/composeResources/drawable/ic_icon_mifos_logo.svg is excluded by !**/*.svg
📒 Files selected for processing (36)
  • cmp-android/dependencies/demoDebugRuntimeClasspath.txt
  • cmp-android/dependencies/demoReleaseRuntimeClasspath.txt
  • cmp-android/dependencies/prodDebugRuntimeClasspath.txt
  • cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/DesignToken.kt
  • core/ui/build.gradle.kts
  • core/ui/src/androidMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.android.kt
  • core/ui/src/androidMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.android.kt
  • core/ui/src/commonMain/composeResources/values/strings.xml
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosDetailsCard.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/HtmlTemplateGenerator.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PageConfig.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.kt
  • core/ui/src/desktopMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.desktop.kt
  • core/ui/src/desktopMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.desktop.kt
  • core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.js.kt
  • core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.js.kt
  • core/ui/src/nativeMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.native.kt
  • core/ui/src/nativeMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.native.kt
  • core/ui/src/wasmJsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.wasmJs.kt
  • core/ui/src/wasmJsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.wasmJs.kt
  • feature/loan/build.gradle.kts
  • feature/loan/src/commonMain/composeResources/values/strings.xml
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/component/RepaymentPeriodCard.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleUiState.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/pdf/RepaymentScheduleHtmlGenerator.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanTransaction/LoanTransactionsScreen.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/pages/SchedulePage.kt
  • gradle.properties
  • gradle/libs.versions.toml
💤 Files with no reviewable changes (1)
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleUiState.kt
🚧 Files skipped from review as they are similar to previous changes (12)
  • cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
  • cmp-android/dependencies/demoReleaseRuntimeClasspath.txt
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/DesignToken.kt
  • core/ui/src/nativeMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.native.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PageConfig.kt
  • core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.js.kt
  • cmp-android/dependencies/demoDebugRuntimeClasspath.txt
  • feature/loan/build.gradle.kts
  • cmp-android/dependencies/prodDebugRuntimeClasspath.txt
  • core/ui/src/desktopMain/kotlin/com/mifos/core/ui/util/pdf/PdfGeneratorHelper.desktop.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/pdf/PdfGenerator.kt

Copy link

@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 (2)
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt (2)

370-388: Consider extracting column count to avoid magic number duplication.

The column count 14 is duplicated across multiple composables (columnWidths, headers, List(14) here, periodValues, footerValues). If the table structure changes, this could lead to inconsistencies.

♻️ Optional refactor to derive count from headers
private const val COLUMN_COUNT = 14

// Or derive from headers list in a shared location
🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`
around lines 370 - 388, The code uses a hardcoded column count (List(14)) in the
cells creation which is duplicated across columnWidths, headers, periodValues,
and footerValues; replace the magic number by introducing a single shared
constant or deriving the count from an authoritative source (e.g., headers.size)
and use that constant/derived value wherever List(14) and other column-related
lists are constructed (update the cells variable, columnWidths, headers,
periodValues, footerValues to reference the single COLUMN_COUNT or headers.size
to keep them consistent).

162-169: Export button is clickable even when data is unavailable.

The export button is always enabled regardless of the data state. When clicked during Loading or Error states, the export action will silently fail since repaymentScheduleTableData is null. Consider conditionally enabling the button.

♻️ Suggested approach
         actions = {
-            IconButton(onClick = { onAction(LoanRepaymentScheduleAction.ExportToPdf) }) {
+            IconButton(
+                onClick = { onAction(LoanRepaymentScheduleAction.ExportToPdf) },
+                enabled = state.repaymentScheduleTableData != null,
+            ) {
                 Icon(
                     imageVector = MifosIcons.Export,
                     contentDescription = stringResource(Res.string.feature_loan_export_to_pdf),
                 )
             }
         },
🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`
around lines 162 - 169, The export IconButton is always enabled and can trigger
onAction(LoanRepaymentScheduleAction.ExportToPdf) when
repaymentScheduleTableData is null (e.g., UI states Loading or Error); update
the composable so IconButton is enabled only when repaymentScheduleTableData is
non-null and state indicates success (check the UI state enum/case, e.g., not
Loading/Error), by adding an enabled = repaymentScheduleTableData != null (or
equivalent isSuccess check) to the IconButton and guard the onClick to no-op
unless repaymentScheduleTableData is present to prevent silent failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`:
- Around line 108-133: The ExportPdf handler currently no-ops when
state.repaymentScheduleTableData is null and uses e.printStackTrace(); change it
so when repaymentScheduleTableData is null you immediately send a user-facing
error action (use viewModel.trySendAction with
LoanRepaymentScheduleAction.PdfExportError) instead of silently returning, and
replace e.printStackTrace() with a proper log call that includes the exception
(e.g., logger.error or processLogger.error) and include the exception
message/details when sending the PdfExportError so users get feedback if
pdfGenerator.generateAndSharePdf or RepaymentScheduleHtmlGenerator fails.

---

Nitpick comments:
In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`:
- Around line 370-388: The code uses a hardcoded column count (List(14)) in the
cells creation which is duplicated across columnWidths, headers, periodValues,
and footerValues; replace the magic number by introducing a single shared
constant or deriving the count from an authoritative source (e.g., headers.size)
and use that constant/derived value wherever List(14) and other column-related
lists are constructed (update the cells variable, columnWidths, headers,
periodValues, footerValues to reference the single COLUMN_COUNT or headers.size
to keep them consistent).
- Around line 162-169: The export IconButton is always enabled and can trigger
onAction(LoanRepaymentScheduleAction.ExportToPdf) when
repaymentScheduleTableData is null (e.g., UI states Loading or Error); update
the composable so IconButton is enabled only when repaymentScheduleTableData is
non-null and state indicates success (check the UI state enum/case, e.g., not
Loading/Error), by adding an enabled = repaymentScheduleTableData != null (or
equivalent isSuccess check) to the IconButton and guard the onClick to no-op
unless repaymentScheduleTableData is present to prevent silent failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b95ed568-9e59-414b-bca7-b7aa029d0470

📥 Commits

Reviewing files that changed from the base of the PR and between a5c2092 and 6048c17.

📒 Files selected for processing (6)
  • cmp-android/dependencies/demoDebugRuntimeClasspath.txt
  • cmp-android/dependencies/demoReleaseRuntimeClasspath.txt
  • cmp-android/dependencies/prodDebugRuntimeClasspath.txt
  • cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • cmp-android/dependencies/demoReleaseRuntimeClasspath.txt
  • cmp-android/dependencies/demoDebugRuntimeClasspath.txt
  • cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt

@biplab1
Copy link
Contributor

biplab1 commented Mar 11, 2026

@techsavvy185 Please address the @coderabbitai suggestions relevant to this PR.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch all review comments by coderabbitai on PR `#2622`
gh pr view 2622 --repo openMF/mifos-x-field-officer-app --json reviews,comments

echo "---REVIEW COMMENTS---"
gh api repos/openMF/mifos-x-field-officer-app/pulls/2622/comments --paginate \
  | jq '.[] | select(.user.login == "coderabbitai") | {id: .id, path: .path, line: .line, body: .body[0:300]}'

echo "---ISSUE COMMENTS---"
gh api repos/openMF/mifos-x-field-officer-app/issues/2622/comments --paginate \
  | jq '.[] | select(.user.login == "coderabbitai") | {id: .id, body: .body[0:500]}'

Length of output: 116712


@techsavvy185, here is a consolidated summary of all outstanding CodeRabbit suggestions for this PR. Please review and address each one:


🔴 Critical / High Priority

  1. LoanRepaymentScheduleViewModel.kt (Lines 181–211) — Nullable currencyCode crash risk
    loan.currency.code is nullable but passed directly into CurrencyFormatter.format(), mapPeriodsData(), and calculateTotals(). Add a null fallback (e.g., ?: "USD") at the top of mapToTableData and use the guarded value throughout.

  2. MifosTable.kt (Lines 41–46) — Invisible row dividers
    The default borderColor equals backgroundColor (KptTheme.colorScheme.surfaceVariant), making all dividers invisible. Change the default to a contrasting token:

    - borderColor: Color = KptTheme.colorScheme.surfaceVariant,
    + borderColor: Color = KptTheme.colorScheme.outlineVariant,
  3. core/ui/build.gradle.kts (Lines 52–57) — kotlinx-html needs to be api() not implementation()
    HtmlTemplateGenerator exposes the BODY type from kotlinx-html in protected abstract fun BODY.generateBody(). If it's implementation, downstream modules like RepaymentScheduleHtmlGenerator cannot see that type. Change:

    - implementation(libs.kotlinx.html)
    + api(libs.kotlinx.html)
  4. PdfGenerator.wasmJs.kt (Lines 67–87) — completionSignal.await() can hang indefinitely
    If the iframe never triggers load/print, the coroutine hangs forever. Add iframe.onerror/iframe.onabort handlers that call completionSignal.completeExceptionally(...), and wrap the await with a cancellable timeout.

  5. PdfGenerator.android.kt (Lines 81–83) — WebView destroyed before print callbacks finish
    The WebView is destroyed immediately after calling printManager.print(...), but PrintDocumentAdapter callbacks may still be in progress. Wait for the PrintJob to reach a terminal state before resuming the continuation and calling webView.destroy().

  6. PdfGenerator.desktop.kt (Lines 48–59) — Use actual write attempt instead of canWrite()
    canWrite() can return stale results on locked files. Replace with a try/catch on an actual file open (FileOutputStream(file).use{}) and show the error dialog on IOException.

  7. PdfGenerator.js.kt (Lines 65–85) — onload handler set after frameDoc.write()/close() (race condition)
    The iframe.onload handler must be attached before calling frameDoc.write(finalHtml) and frameDoc.close(), otherwise a synchronously fired load event may be missed.

  8. LoanRepaymentScheduleViewModel.kt (Lines 137–178) — Overlapping loadLoanRepaySchedule collectors
    Every call to loadLoanRepaySchedule launches a new coroutine without cancelling the previous one, leading to racing state updates. Track and cancel the previous job, or use collectLatest.

  9. RepaymentScheduleHtmlGenerator.kt (Lines 155–167) — Disbursement row colspan misalignment
    The large colspan groupings shift values leftward. Emit one td per column with appropriate colspan values so disbursementDate aligns under the Date column and loanAmount aligns under Loan Balance.

  10. LoanTransactionsScreen.kt (Line 24) — "Principal Due" label is misleading for the transactions table
    This column renders row.principal (transaction principal), not a "due" amount. Restore a transaction-specific resource key (e.g., feature_loan_table_header_principal) instead of using feature_loan_table_header_principal_due.


🟡 Minor / Nitpick

  1. strings.xml (Line 326) — Typo: "Principle Paid Off" → "Principal Paid Off"
    Fix the financial term in the string resource value.

  2. RepaymentScheduleHtmlGenerator.kt (Lines 218–223) — Invalid CSS in h1 rule
    The fragment solid #1976d2`` is not a valid CSS property. Replace with:

    border-bottom: 1px solid `#1976d2`;
  3. PageConfig.kt (Lines 31–35) — Allow negative marginMm
    Add an init { require(marginMm >= 0) { "marginMm must be non-negative" } } block to PageConfig.

  4. PdfGenerator.native.kt (Lines 21–27) — TODO() crashes at runtime on iOS
    Replace TODO("iOS PDF generation not yet implemented") with a graceful fallback that logs a warning or throws UnsupportedOperationException so the app fails gracefully.

  5. gradle/libs.versions.toml — Update openhtmltopdf from 1.0.10 to 1.1.37
    The current version 1.0.10 is outdated. The latest stable release is 1.1.37.

  6. MifosDetailsCard.kt (Lines 41–44) — Remove space before colon
    Change "$heading :" to "$heading:".

  7. LoanRepaymentScheduleScreen.kt (Lines 103–135) — Fragile PDF error handling

    • Replace e.printStackTrace() with structured logging.
    • Catch specific exceptions instead of generic Exception.
    • Include e.message in the PdfExportError payload so the user gets useful feedback.
  8. LoanRepaymentScheduleScreen.kt (Lines 162–169) — Export button always enabled
    Disable the export IconButton when data isn't loaded yet:

    + enabled = state.repaymentScheduleTableData != null,
  9. LoanRepaymentScheduleScreen.kt (Lines 370–388) — Magic number 14 for column count
    Extract 14 into a named constant (e.g., private const val COLUMN_COUNT = 14) and use it wherever column count appears.

  10. LoanRepaymentScheduleScreen.kt (Lines 186–193) — Redundant null check in Success state
    When screenState is Success, repaymentScheduleTableData is guaranteed non-null by the ViewModel. Either use !! with a comment or embed tableData directly in the Success state.

  11. LoanRepaymentScheduleViewModel.kt (Lines 104–123) — Unnecessary viewModelScope.launch in handleNetworkStatus
    The offline branch does not suspend; remove the nested launch and update state synchronously to avoid racing coroutines.

  12. HtmlTemplateGenerator.kt (Lines 142–151) — Hardcoded date format
    The format d/M/yyyy is not consistent across locales. Use ISO-8601 (yyyy-MM-dd) or a locale-aware formatter.

  13. PdfGenerator.js.kt (Lines 88–90) — Use console.error instead of println
    Replace println(...) with console.error(...) so errors show up in browser devtools.

  14. gradle.properties (Lines 19 & 30–38) — High memory / unrelated build flags

    • -Xmx8g is aggressive; consider 6g or make it overridable via GRADLE_OPTS.
    • The Kotlin/Native flags (kotlin.native.cacheKind, kotlin.native.jvmArgs, etc.) are unrelated to PDF export. Consider moving them to a separate infrastructure PR.
  15. MifosDetailsCard.kt (Lines 33–51) — Document map iteration order
    Add a KDoc comment noting that callers should pass mapOf() or LinkedHashMap to ensure consistent display order.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me fetch the CodeRabbit review comments on this PR first.


🏁 Script executed:

#!/bin/bash
# Get all review comments by coderabbitai on PR `#2622`
gh pr view 2622 --repo openMF/mifos-x-field-officer-app --comments --json comments | jq -r '.comments[] | select(.author.login == "coderabbitai") | "---\n" + .body' | head -300

# Also get review thread comments
gh api repos/openMF/mifos-x-field-officer-app/pulls/2622/comments --paginate | jq -r '.[] | select(.user.login == "coderabbitai") | "FILE: " + .path + "\nLINE: " + (.line|tostring) + "\nBODY: " + .body + "\n---"' | head -500

Length of output: 39767


Let me fetch the inline review comments specifically.


🏁 Script executed:

#!/bin/bash
# Get all PR review comments (inline) by coderabbitai
gh api repos/openMF/mifos-x-field-officer-app/pulls/2622/comments --paginate \
  | jq -r '.[] | select(.user.login == "coderabbitai") | "FILE: " + .path + "\nLINE: " + (.line|tostring) + "\n" + .body + "\n===END==="' \
  | head -600

echo "---REVIEWS---"
# Also get full review bodies
gh api repos/openMF/mifos-x-field-officer-app/pulls/2622/reviews --paginate \
  | jq -r '.[] | select(.user.login == "coderabbitai") | "STATE: " + .state + "\nBODY: " + .body + "\n===END==="' \
  | head -400

Length of output: 13


🏁 Script executed:

#!/bin/bash
# Check all reviews on the PR (not filtered by user)
echo "=== ALL REVIEWS ==="
gh api repos/openMF/mifos-x-field-officer-app/pulls/2622/reviews --paginate \
  | jq -r '.[] | "USER: " + .user.login + " | STATE: " + .state + " | ID: " + (.id|tostring)'

echo ""
echo "=== ALL ISSUE COMMENTS (coderabbitai) ==="
gh api repos/openMF/mifos-x-field-officer-app/issues/2622/comments --paginate \
  | jq -r '.[] | select(.user.login == "coderabbitai") | "ID: " + (.id|tostring) + "\n" + .body[0:3000] + "\n===END==="'

Length of output: 242


🏁 Script executed:

#!/bin/bash
# Fetch body of each review
for review_id in 3873368024 3911352658 3918148149; do
  echo "=== REVIEW $review_id ==="
  gh api repos/openMF/mifos-x-field-officer-app/pulls/2622/reviews/$review_id \
    | jq -r '.body'
  echo ""
  echo "--- INLINE COMMENTS FOR REVIEW $review_id ---"
  gh api repos/openMF/mifos-x-field-officer-app/pulls/2622/reviews/$review_id/comments \
    | jq -r '.[] | "FILE: " + .path + "\nLINE: " + (.line|tostring) + "\nBODY:\n" + .body + "\n---COMMENT_END---"'
  echo ""
done

Length of output: 133678


@techsavvy185 Here is a consolidated summary of all actionable CodeRabbit suggestions for this PR, organized by severity:


🔴 Critical

1. LoanRepaymentScheduleViewModel.kt — Nullable currencyCode causes NPE

loan.currency.code is nullable but is passed directly into CurrencyFormatter.format(), mapPeriodsData(), and calculateTotals(). Android/Desktop implementations crash on null input.
Fix: Add val safeCode = currencyCode ?: "USD" at the top of mapToTableData() and use it throughout.


🟠 Major

2. MifosTable.kt — Default borderColor equals backgroundColor, making dividers invisible

Both default to KptTheme.colorScheme.surfaceVariant. Row separators are hidden.
Fix: Change the default for borderColor to KptTheme.colorScheme.outlineVariant.

3. PdfGenerator.js.kt — Race condition: onload handler set after document.close()

The onload handler is assigned after frameDoc.close(), which can fire the load event synchronously in some browsers — causing completionSignal.await() to hang forever.
Fix: Move the iframe.onload = { ... } assignment to before frameDoc.open() / frameDoc.write() / frameDoc.close().

4. PdfGenerator.native.ktTODO() crashes at runtime on iOS

The native implementation throws NotImplementedError at runtime.
Fix: Replace with throw UnsupportedOperationException("PDF export is not supported on iOS/native yet.") for a graceful failure, or gate the UI action on non-native platforms.

5. PdfGenerator.wasmJs.ktcompletionSignal.await() can hang indefinitely

If the iframe never fires onload/print, the coroutine suspends forever.
Fix: Add iframe.onerror/iframe.onabort handlers calling completionSignal.completeExceptionally(...), and wrap the await with withTimeout(10_000) { completionSignal.await() }.

6. PdfGenerator.android.kt — WebView destroyed before PrintDocumentAdapter finishes

printManager.print() is non-blocking; the framework calls onLayout() and onWrite() asynchronously after the user confirms. The current post() cleanup races with those callbacks.
Fix: Capture the PrintJob returned by print(), poll isCompleted()/isFailed()/isCancelled() (or register a listener), and only destroy the WebView / resume the coroutine when the job reaches a terminal state.

7. core/ui/build.gradle.ktskotlinx-html declared as implementation instead of api

HtmlTemplateGenerator exposes protected abstract fun BODY.generateBody()BODY is a kotlinx-html type. Subclasses in other modules (e.g., RepaymentScheduleHtmlGenerator) need it on their compile classpath.
Fix: Change implementation(libs.kotlinx.html)api(libs.kotlinx.html) in commonMain.

8. LoanRepaymentScheduleViewModel.kt — Overlapping schedule load collectors

loadLoanRepaySchedule() launches a new collect without cancelling the previous one. Retries/reconnects can produce concurrent state updates.
Fix: Track the load job (private var loadLoanRepayJob: Job? = null), cancel it before starting a new one, or use collectLatest.

9. RepaymentScheduleHtmlGenerator.kt — Disbursement row misaligned due to colspan

The current large colspans shift the disbursement date and loan amount into the wrong columns in the exported PDF.
Fix: Emit one td per column, placing disbursementDate under the Date column and loanAmount under the Loan Balance column, filling other cells with empty td elements.


🟡 Minor

10. gradle/libs.versions.toml + core/ui/build.gradle.ktsopenhtmltopdf version 1.0.10 is outdated

Latest stable release is 1.1.37 (Jan 21, 2026). Note also that the group ID changed from com.openhtmltopdf to io.github.openhtmltopdf in newer releases — verify the group ID when upgrading.
Fix: Update openhtmltopdf = "1.0.10""1.1.37" in libs.versions.toml.

11. PageConfig.kt — No validation for negative marginMm

Negative values produce invalid @page CSS.
Fix:

data class PageConfig(...) {
    init { require(marginMm >= 0) { "marginMm must be >= 0" } }
}

12. feature/loan/src/commonMain/composeResources/values/strings.xml — Typo "Principle Paid Off"

Line 284/326: "Principle Paid Off" should be "Principal Paid Off" (financial term).
Fix: <string name="principle_paid_off">Principal Paid Off</string>

13. LoanTransactionsScreen.kt — Wrong header label for transaction principal column

The column renders row.principal (a transaction amount), but the header now says "Principal Due" (a repayment-schedule term). This is misleading.
Fix: Restore a transaction-specific string key (e.g., feature_loan_table_header_principal) and use it for this table's column header.

14. RepaymentScheduleHtmlGenerator.kt — Invalid CSS in h1 style

solid #1976d2`` is not a valid CSS property. The partial border-bottom value is missing the property name and width.
Fix: Replace with `border-bottom: 2px solid `#1976d2`;` in the h1 rule inside `getAdditionalStyles()`.

15. PdfGenerator.desktop.ktcanWrite() doesn't detect locked files

If the target PDF is open in another app, canWrite() returns true but the write will fail.
Fix: Replace the canWrite() check with an actual write attempt (e.g., FileOutputStream(file).use { }) inside a try/catch, and map IOException/OverlappingFileLockException to the existing user-facing error dialog.

16. MifosTable.ktonClick: () -> Unit = {} adds false click affordance to static rows

Non-null onClick always attaches .clickable(), adding ripple and TalkBack "clickable" semantics to read-only/header rows.
Fix: Change to onClick: (() -> Unit)? = null and conditionally apply Modifier.clickable(onClick = onClick) only when non-null.

17. LoanRepaymentScheduleScreen.kt — Export button always enabled, even when data is null

Clicking Export during Loading/Error states silently does nothing.
Fix: Add enabled = state.repaymentScheduleTableData != null to the IconButton.

18. LoanRepaymentScheduleScreen.kt — Silent no-op + e.printStackTrace() in PDF export handler

When repaymentScheduleTableData is null during the ExportPdf event, the code silently returns. e.printStackTrace() is also a debug artifact.
Fix: Send a PdfExportError action with a user-facing message when data is null; replace e.printStackTrace() with a proper logger call (include e.message in the error action payload).


🔵 Nitpick

File Issue
MifosDetailsCard.kt L41 Remove the space before colon: "$heading :""$heading:"
MifosDetailsCard.kt L33 Add KDoc noting that map iteration order determines display order
HtmlTemplateGenerator.kt L142 Date format d/M/yyyy is locale-dependent; use ISO-8601 yyyy-MM-dd
LoanRepaymentScheduleScreen.kt L494 Preview defaults to ScreenState.Loading; provide mock Success state for useful previews
LoanRepaymentScheduleScreen.kt L186 Redundant null check in ScreenState.Success branch; repaymentScheduleTableData is guaranteed non-null in that state
LoanRepaymentScheduleScreen.kt L370 Hardcoded column count 14 duplicated across composables; extract as const val COLUMN_COUNT = 14
PdfGenerator.js.kt L67 Use afterprint event for iframe cleanup instead of a fixed 2-second timeout
gradle.properties L19 -Xmx8g is aggressive; consider -Xmx6g or making it configurable
gradle.properties L30–38 Kotlin/Native build flags are unrelated to PDF export; consider splitting into a separate infrastructure PR

@sonarqubecloud
Copy link

Copy link

@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: 2

♻️ Duplicate comments (3)
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt (2)

183-211: ⚠️ Potential issue | 🔴 Critical

Normalize currencyCode before formatting.

loan.currency.code is still nullable here, but the formatter path on some targets expects a non-null code. Passing it through loanAmount, principalPaid, mapPeriodsData, and calculateTotals keeps the NPE risk in place. Normalize it once in common code and store the guarded value in state.

Suggested fix
-        val currencyCode = loan.currency.code
+        val currencyCode = loan.currency.code ?: "USD"
         val maxDigits = loan.currency.decimalPlaces

Based on learnings: when a Kotlin parameter like currencyCode is nullable, normalize it before calling downstream currency formatters that require a non-null value.

🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`
around lines 183 - 211, In mapToTableData (LoanRepaymentScheduleViewModel),
guard the nullable loan.currency.code before any formatting by normalizing it
(e.g., val currencyCode = loan.currency.code ?: "") and use that
non-null/default value when calling CurrencyFormatter.format, mapPeriodsData,
calculateTotals and when storing currencyCode in the returned
LoanRepaymentScheduleState.RepaymentScheduleTableData so downstream functions
receive a non-null string; update references to currencyCode in mapToTableData
accordingly and ensure any signatures/uses of mapPeriodsData and calculateTotals
expect a non-null String if needed.

139-180: ⚠️ Potential issue | 🟠 Major

Keep a single repayment-schedule collector alive.

Every Retry / online transition starts a fresh collect without cancelling the previous one, so multiple repository streams can race dataState, basicDetails, and repaymentScheduleTableData.

Suggested fix
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+
 class LoanRepaymentScheduleViewModel(
     private val repository: LoanRepaymentScheduleRepository,
     private val networkMonitor: NetworkMonitor,
     savedStateHandle: SavedStateHandle,
 ) : BaseViewModel<LoanRepaymentScheduleState, LoanRepaymentScheduleEvent, LoanRepaymentScheduleAction>(
@@
 ) {
+    private var loadLoanRepayScheduleJob: Job? = null
+
     init {
         observeNetwork()
     }
@@
     private fun loadLoanRepaySchedule() {
         mutableStateFlow.update {
             it.copy(dataState = DataState.Loading)
         }
 
-        viewModelScope.launch {
-            repository.getLoanRepaySchedule(state.loanId).collect { dataState ->
+        loadLoanRepayScheduleJob?.cancel()
+        loadLoanRepayScheduleJob = viewModelScope.launch {
+            repository.getLoanRepaySchedule(state.loanId).collectLatest { dataState ->
                 when (dataState) {
🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`
around lines 139 - 180, loadLoanRepaySchedule currently starts a new collector
on repository.getLoanRepaySchedule every time (via viewModelScope.launch { ...
collect { ... } }), which allows multiple concurrent collectors to race updating
mutableStateFlow; fix by ensuring only one active collector: either replace
collect with collectLatest on repository.getLoanRepaySchedule(...) inside
loadLoanRepaySchedule so new emissions/calls cancel the previous handling, or
implement a Job property (e.g., private var repayScheduleJob: Job?) and before
launching call repayScheduleJob?.cancel() then assign repayScheduleJob =
viewModelScope.launch { repository.getLoanRepaySchedule(state.loanId).collect {
... } } so previous coroutine is cancelled before starting a new one. Use the
symbols loadLoanRepaySchedule, viewModelScope, repository.getLoanRepaySchedule,
collect/collectLatest, and mutableStateFlow in your change.
feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt (1)

100-132: ⚠️ Potential issue | 🟠 Major

Use the latest screen state inside the export collector.

LaunchedEffect(Unit) keeps the initial state in its closure. After the schedule loads, this handler can still see repaymentScheduleTableData == null and drop ExportPdf as a no-op. Please read the latest state via rememberUpdatedState (or move the export payload into the event/ViewModel), and turn the null-data path into PdfExportError instead of silently returning. While you're here, printStackTrace() should be replaced with the app logger and the exception message should be forwarded to the dialog.

Suggested fix
+import androidx.compose.runtime.rememberUpdatedState
+
 `@Composable`
 internal fun LoanRepaymentScheduleScreen(
     viewModel: LoanRepaymentScheduleViewModel = koinViewModel(),
     navigateBack: () -> Unit,
 ) {
     val state by viewModel.stateFlow.collectAsStateWithLifecycle()
+    val currentState by rememberUpdatedState(state)
     val coroutineScope = rememberCoroutineScope()
     val pdfGenerator = rememberPdfGenerator()
@@
                 LoanRepaymentScheduleEvent.NavigateBack -> navigateBack()
                 LoanRepaymentScheduleEvent.ExportPdf -> {
-                    state.repaymentScheduleTableData?.let { data ->
+                    val data = currentState.repaymentScheduleTableData
+                        ?: run {
+                            viewModel.trySendAction(
+                                LoanRepaymentScheduleAction.PdfExportError(
+                                    title = getString(Res.string.feature_loan_export_pdf_error_title),
+                                    message = getString(Res.string.feature_loan_export_pdf_error),
+                                ),
+                            )
+                            return@collect
+                        }
                         coroutineScope.launch {
                             try {
@@
                             } catch (e: Exception) {
-                                e.printStackTrace()
                                 viewModel.trySendAction(
                                     LoanRepaymentScheduleAction.PdfExportError(
                                         title = getString(Res.string.feature_loan_export_pdf_error_title),
-                                        message = getString(Res.string.feature_loan_export_pdf_error),
+                                        message = e.message ?: getString(Res.string.feature_loan_export_pdf_error),
                                     ),
                                 )
                             }
                         }
-                    }
                 }
             }
         }
     }
🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`
around lines 100 - 132, The ExportPdf collector inside LaunchedEffect captures
the initial state and may see repaymentScheduleTableData as null; update the
code to read the latest state via rememberUpdatedState (e.g., val latestState by
rememberUpdatedState(state)) or move the payload into the ExportPdf event so the
handler always has current data; change the null branch to send
LoanRepaymentScheduleAction.PdfExportError via viewModel.trySendAction instead
of silently returning; replace e.printStackTrace() with the app logger and
include e.message (or the exception) in the PdfExportError dialog payload; keep
the existing flow body that calls createPdfStrings,
RepaymentScheduleHtmlGenerator(...).generateHtml(), and
pdfGenerator.generateAndSharePdf(...) when data is present.
🧹 Nitpick comments (2)
core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt (1)

68-69: Consider adding validation when cells and widths sizes differ.

The getOrElse fallback on line 69 silently handles mismatched list sizes, which is defensive but could mask programming errors at call sites. Since the relevant call sites (RepaymentTableHeader, RepaymentPeriodRow, RepaymentTableFooter) all create exactly 14 cells and expect 14 widths, a mismatch would indicate a bug.

Consider adding a require check or debug assertion at the start of the function:

require(cells.size == widths.size) {
    "cells (${cells.size}) and widths (${widths.size}) must have the same size"
}

Alternatively, keep the fallback but log a warning in debug builds to aid troubleshooting.

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

In
`@core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt`
around lines 68 - 69, Add a size validation for the cells vs widths lists in the
MifosTable rendering logic: in the function containing the cells.forEachIndexed
loop (the MifosTable component), assert that cells.size == widths.size using a
require or debug-only assertion and provide a clear message like "cells
(${cells.size}) and widths (${widths.size}) must have the same size";
alternatively, if you want to preserve the fallback behavior, emit a debug/log
warning when sizes differ so mismatches originating from callers such as
RepaymentTableHeader, RepaymentPeriodRow, and RepaymentTableFooter are visible
during development.
feature/loan/src/commonMain/composeResources/values/strings.xml (1)

332-334: Minor capitalization inconsistency.

Line 333 uses "Current balance" (lowercase 'b'), while line 179 (feature_loan_profile_label_balance) uses "Current Balance" (title case). Consider aligning the capitalization for consistency across the UI, unless the different casing is intentional for distinct contexts.

✏️ Optional fix for consistency
     <string name="feature_loan_client_name_label">Client Name</string>
-    <string name="feature_loan_current_balance_label">Current balance</string>
+    <string name="feature_loan_current_balance_label">Current Balance</string>
     <string name="feature_loan_arrears_label">Arrears</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/loan/src/commonMain/composeResources/values/strings.xml` around lines
332 - 334, The string resource feature_loan_current_balance_label currently uses
"Current balance" (lowercase 'b') which is inconsistent with
feature_loan_profile_label_balance ("Current Balance"); update
feature_loan_current_balance_label to "Current Balance" to match title case
across the UI (or, if the lowercase is intentional for a different context,
verify and document that distinction), ensuring the change is made to the string
value in the resource entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`:
- Around line 162-168: The export IconButton in LoanRepaymentScheduleScreen
should be disabled until repayment data exists; change the IconButton (the one
invoking onAction(LoanRepaymentScheduleAction.ExportToPdf)) to set its enabled
property based on repaymentScheduleTableData availability (e.g., enabled =
repaymentScheduleTableData != null or combined with state check such as state
!is Loading && state !is Error) so users cannot trigger ExportToPdf when
repaymentScheduleTableData is missing.
- Around line 216-245: The productNameLabel is populated with the screen title
in createPdfStrings(), causing the PDF to show “Loan Repayment Schedule” instead
of the loan product name; update productNameLabel in createPdfStrings() to use
the actual product-name source (e.g., replace
getString(Res.string.feature_loan_loan_repayment_schedule) with the product name
resource such as getString(Res.string.feature_loan_product_name) if it exists,
or pull the product name from the screen state/viewModel like
viewModel.loanProductName or a passed-in parameter) so productNameLabel contains
the real product name rather than the screen title.

---

Duplicate comments:
In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`:
- Around line 100-132: The ExportPdf collector inside LaunchedEffect captures
the initial state and may see repaymentScheduleTableData as null; update the
code to read the latest state via rememberUpdatedState (e.g., val latestState by
rememberUpdatedState(state)) or move the payload into the ExportPdf event so the
handler always has current data; change the null branch to send
LoanRepaymentScheduleAction.PdfExportError via viewModel.trySendAction instead
of silently returning; replace e.printStackTrace() with the app logger and
include e.message (or the exception) in the PdfExportError dialog payload; keep
the existing flow body that calls createPdfStrings,
RepaymentScheduleHtmlGenerator(...).generateHtml(), and
pdfGenerator.generateAndSharePdf(...) when data is present.

In
`@feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt`:
- Around line 183-211: In mapToTableData (LoanRepaymentScheduleViewModel), guard
the nullable loan.currency.code before any formatting by normalizing it (e.g.,
val currencyCode = loan.currency.code ?: "") and use that non-null/default value
when calling CurrencyFormatter.format, mapPeriodsData, calculateTotals and when
storing currencyCode in the returned
LoanRepaymentScheduleState.RepaymentScheduleTableData so downstream functions
receive a non-null string; update references to currencyCode in mapToTableData
accordingly and ensure any signatures/uses of mapPeriodsData and calculateTotals
expect a non-null String if needed.
- Around line 139-180: loadLoanRepaySchedule currently starts a new collector on
repository.getLoanRepaySchedule every time (via viewModelScope.launch { ...
collect { ... } }), which allows multiple concurrent collectors to race updating
mutableStateFlow; fix by ensuring only one active collector: either replace
collect with collectLatest on repository.getLoanRepaySchedule(...) inside
loadLoanRepaySchedule so new emissions/calls cancel the previous handling, or
implement a Job property (e.g., private var repayScheduleJob: Job?) and before
launching call repayScheduleJob?.cancel() then assign repayScheduleJob =
viewModelScope.launch { repository.getLoanRepaySchedule(state.loanId).collect {
... } } so previous coroutine is cancelled before starting a new one. Use the
symbols loadLoanRepaySchedule, viewModelScope, repository.getLoanRepaySchedule,
collect/collectLatest, and mutableStateFlow in your change.

---

Nitpick comments:
In
`@core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt`:
- Around line 68-69: Add a size validation for the cells vs widths lists in the
MifosTable rendering logic: in the function containing the cells.forEachIndexed
loop (the MifosTable component), assert that cells.size == widths.size using a
require or debug-only assertion and provide a clear message like "cells
(${cells.size}) and widths (${widths.size}) must have the same size";
alternatively, if you want to preserve the fallback behavior, emit a debug/log
warning when sizes differ so mismatches originating from callers such as
RepaymentTableHeader, RepaymentPeriodRow, and RepaymentTableFooter are visible
during development.

In `@feature/loan/src/commonMain/composeResources/values/strings.xml`:
- Around line 332-334: The string resource feature_loan_current_balance_label
currently uses "Current balance" (lowercase 'b') which is inconsistent with
feature_loan_profile_label_balance ("Current Balance"); update
feature_loan_current_balance_label to "Current Balance" to match title case
across the UI (or, if the lowercase is intentional for a different context,
verify and document that distinction), ensuring the change is made to the string
value in the resource entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5442b398-f415-422e-b6fa-4011eba7e28b

📥 Commits

Reviewing files that changed from the base of the PR and between 6048c17 and eaa3fa4.

📒 Files selected for processing (5)
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTable.kt
  • feature/loan/src/commonMain/composeResources/values/strings.xml
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/loanRepaymentSchedule/LoanRepaymentScheduleViewModel.kt
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/loan/src/commonMain/kotlin/com/mifos/feature/loan/newLoanAccount/NewLoanAccountViewModel.kt

Comment on lines +162 to 168
actions = {
IconButton(onClick = { onAction(LoanRepaymentScheduleAction.ExportToPdf) }) {
Icon(
imageVector = MifosIcons.Export,
contentDescription = stringResource(Res.string.feature_loan_export_to_pdf),
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Disable export until repayment data is ready.

The action is enabled even in Loading / Error states, so users can trigger export before repaymentScheduleTableData exists. Wiring enabled to data availability will avoid dead taps and duplicate error dialogs.

🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`
around lines 162 - 168, The export IconButton in LoanRepaymentScheduleScreen
should be disabled until repayment data exists; change the IconButton (the one
invoking onAction(LoanRepaymentScheduleAction.ExportToPdf)) to set its enabled
property based on repaymentScheduleTableData availability (e.g., enabled =
repaymentScheduleTableData != null or combined with state check such as state
!is Loading && state !is Error) so users cannot trigger ExportToPdf when
repaymentScheduleTableData is missing.

Comment on lines +216 to +245
private suspend fun createPdfStrings(): RepaymentSchedulePdfStrings {
return RepaymentSchedulePdfStrings(
title = getString(Res.string.feature_loan_repayment_schedule_pdf_title),
clientNameLabel = getString(Res.string.feature_loan_client_name_label),
accountNumberLabel = getString(Res.string.feature_loan_account_number),
productNameLabel = getString(Res.string.feature_loan_loan_repayment_schedule),
disbursementDateLabel = getString(Res.string.feature_loan_disbursed_date),
installmentsLabel = getString(Res.string.total_installments),
paidLabel = getString(Res.string.feature_loan_paid_label),
totalLabel = getString(Res.string.feature_loan_total_label),
principalPaidLabel = getString(Res.string.principal_paid_off),
periodDetailsHeader = getString(Res.string.feature_loan_period_details),
loanAmountBalanceHeader = getString(Res.string.feature_loan_amount_and_balance),
totalCostLoanHeader = getString(Res.string.feature_loan_total_cost_of_loan),
installmentTotalsHeader = getString(Res.string.feature_loan_installment_totals),
hNo = getString(Res.string.feature_loan_table_header_number),
hDays = getString(Res.string.feature_loan_table_header_days),
hDate = getString(Res.string.feature_loan_table_header_date),
hPaidDate = getString(Res.string.feature_loan_table_header_paid_date),
hBalance = getString(Res.string.feature_loan_table_header_loan_balance),
hPrincipal = getString(Res.string.feature_loan_table_header_principal_due),
hInterest = getString(Res.string.feature_loan_table_header_interest),
hFees = getString(Res.string.feature_loan_table_header_fees),
hPenalties = getString(Res.string.feature_loan_table_header_penalties),
hDue = getString(Res.string.feature_loan_table_header_due),
hPaid = getString(Res.string.feature_loan_table_header_paid),
hInAdvance = getString(Res.string.feature_loan_table_header_in_advance),
hLate = getString(Res.string.feature_loan_table_header_late),
hOutstanding = getString(Res.string.feature_loan_table_header_outstanding),
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Populate productNameLabel with a product-name string, not the screen title.

productNameLabel currently uses feature_loan_loan_repayment_schedule, so the generated PDF will label that field as “Loan Repayment Schedule” instead of “Product Name”.

🤖 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/loanRepaymentSchedule/LoanRepaymentScheduleScreen.kt`
around lines 216 - 245, The productNameLabel is populated with the screen title
in createPdfStrings(), causing the PDF to show “Loan Repayment Schedule” instead
of the loan product name; update productNameLabel in createPdfStrings() to use
the actual product-name source (e.g., replace
getString(Res.string.feature_loan_loan_repayment_schedule) with the product name
resource such as getString(Res.string.feature_loan_product_name) if it exists,
or pull the product name from the screen state/viewModel like
viewModel.loanProductName or a passed-in parameter) so productNameLabel contains
the real product name rather than the screen title.

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