Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package com.mifos.core.data.repository

import com.mifos.core.common.utils.DataState
import com.mifos.core.model.objects.account.loan.RepaymentSchedule
import com.mifos.core.model.objects.organisations.LoanProducts
import com.mifos.core.network.model.LoansPayload
import com.mifos.room.entities.templates.loans.LoanTemplate
Expand All @@ -26,4 +27,10 @@ interface LoanAccountRepository {
fun getLoansAccountTemplate(clientId: Int, productId: Int): Flow<DataState<LoanTemplate>>

fun createLoansAccount(loansPayload: LoansPayload): Flow<DataState<HttpResponse>>

/**
* Calculate loan repayment schedule without creating the loan.
* Used to preview the schedule before submitting the loan application.
*/
fun calculateLoanSchedule(loansPayload: LoansPayload): Flow<DataState<RepaymentSchedule>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package com.mifos.core.data.repositoryImp
import com.mifos.core.common.utils.DataState
import com.mifos.core.common.utils.asDataStateFlow
import com.mifos.core.data.repository.LoanAccountRepository
import com.mifos.core.model.objects.account.loan.RepaymentSchedule
import com.mifos.core.model.objects.organisations.LoanProducts
import com.mifos.core.network.datamanager.DataManagerLoan
import com.mifos.core.network.model.LoansPayload
Expand Down Expand Up @@ -42,4 +43,11 @@ class LoanAccountRepositoryImp(
return dataManagerLoan.createLoansAccount(loansPayload)
.asDataStateFlow()
}

override fun calculateLoanSchedule(
loansPayload: LoansPayload,
): Flow<DataState<RepaymentSchedule>> {
return dataManagerLoan.calculateLoanSchedule(loansPayload)
.asDataStateFlow()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.mifos.core.domain.useCases.AddDataTableEntryUseCase
import com.mifos.core.domain.useCases.AddNoteUseCase
import com.mifos.core.domain.useCases.ApproveCheckerUseCase
import com.mifos.core.domain.useCases.ApproveSavingsApplicationUseCase
import com.mifos.core.domain.useCases.CalculateLoanScheduleUseCase
import com.mifos.core.domain.useCases.CreateChargesUseCase
import com.mifos.core.domain.useCases.CreateClientIdentifierUseCase
import com.mifos.core.domain.useCases.CreateGroupLoansAccountUseCase
Expand Down Expand Up @@ -102,6 +103,7 @@ val UseCaseModule = module {
factoryOf(::ApproveSavingsApplicationUseCase)
factoryOf(::CreateChargesUseCase)
factoryOf(::CreateClientIdentifierUseCase)
factoryOf(::CalculateLoanScheduleUseCase)
factoryOf(::CreateGroupLoansAccountUseCase)
factoryOf(::CreateLoanAccountUseCase)
factoryOf(::CreateLoanChargesUseCase)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.domain.useCases

import com.mifos.core.common.utils.DataState
import com.mifos.core.data.repository.LoanAccountRepository
import com.mifos.core.model.objects.account.loan.RepaymentSchedule
import com.mifos.core.network.model.LoansPayload
import kotlinx.coroutines.flow.Flow

/**
* Use case to calculate loan repayment schedule without creating the loan.
* This is used to preview the schedule before submitting the loan application.
*/
class CalculateLoanScheduleUseCase(
private val loanAccountRepository: LoanAccountRepository,
) {

operator fun invoke(loansPayload: LoansPayload): Flow<DataState<RepaymentSchedule>> =
loanAccountRepository.calculateLoanSchedule(loansPayload)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ data class Currency(

val name: String? = null,

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

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

Repository: openMF/android-client

Length of output: 705


🏁 Script executed:

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

Repository: openMF/android-client

Length of output: 35017


🏁 Script executed:

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

Repository: openMF/android-client

Length of output: 4026


🏁 Script executed:

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

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

Repository: openMF/android-client

Length of output: 656


🏁 Script executed:

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

Repository: openMF/android-client

Length of output: 252


🏁 Script executed:

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

Repository: openMF/android-client

Length of output: 808


🏁 Script executed:

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

Repository: openMF/android-client

Length of output: 203


Verify API contract ensures integer format for decimalPlaces field.

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

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

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

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

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


val inMultiplesOf: Int? = null,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package com.mifos.core.network.datamanager
import com.mifos.core.common.utils.extractErrorMessage
import com.mifos.core.datastore.UserPreferencesRepository
import com.mifos.core.model.objects.account.loan.LoanDisbursement
import com.mifos.core.model.objects.account.loan.RepaymentSchedule
import com.mifos.core.network.BaseApiManager
import com.mifos.core.network.GenericResponse
import com.mifos.core.network.model.LoansPayload
Expand All @@ -24,13 +25,15 @@ import com.mifos.room.entities.templates.loans.LoanTemplate
import com.mifos.room.entities.templates.loans.LoanTransactionTemplate
import com.mifos.room.helper.LoanDaoHelper
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.isSuccess
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json

/**
* Created by Rajan Maurya on 15/07/16.
Expand Down Expand Up @@ -291,4 +294,23 @@ class DataManagerLoan(
): Flow<GenericResponse> {
return mBaseApiManager.loanService.disburseLoan(loanId, loanDisbursement)
}

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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

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

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

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

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

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

throw IllegalStateException(errorMessage)
}

Json { ignoreUnknownKeys = true }.decodeFromString<RepaymentSchedule>(response.bodyAsText())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ interface LoanService {
@POST(APIEndPoint.CREATE_LOANS_ACCOUNTS)
fun createLoansAccount(@Body loansPayload: LoansPayload?): Flow<HttpResponse>

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

@GET(APIEndPoint.CREATE_LOANS_ACCOUNTS + "/template?templateType=individual")
fun getLoansAccountTemplate(
@Query("clientId") clientId: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import kotlinx.serialization.Serializable
data class ClientApplyNewApplicationRoute(
val clientId: Int,
val status: String,
val accountNo: String,
)

fun NavGraphBuilder.clientApplyNewApplicationRoute(
onNavigateBack: () -> Unit,
onNavigateApplyLoanAccount: (Int) -> Unit,
onNavigateApplyLoanAccount: (Int, String) -> Unit,
onNavigateApplySavingsAccount: (Int) -> Unit,
onNavigateApplyShareAccount: (Int) -> Unit,
onNavigateApplyRecurringAccount: (Int) -> Unit,
Expand All @@ -42,6 +43,6 @@ fun NavGraphBuilder.clientApplyNewApplicationRoute(
}
}

fun NavController.navigateToClientApplyNewApplicationScreen(clientId: Int, status: String) {
this.navigate(ClientApplyNewApplicationRoute(clientId, status))
fun NavController.navigateToClientApplyNewApplicationScreen(clientId: Int, status: String, accountNo: String) {
this.navigate(ClientApplyNewApplicationRoute(clientId, status, accountNo))
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable
internal fun ClientApplyNewApplicationsScreen(
onNavigateBack: () -> Unit,
onNavigateApplyLoanAccount: (Int) -> Unit,
onNavigateApplyLoanAccount: (Int, String) -> Unit,
onNavigateApplySavingsAccount: (Int) -> Unit,
onNavigateApplyShareAccount: (Int) -> Unit,
onNavigateApplyRecurringAccount: (Int) -> Unit,
Expand All @@ -79,6 +79,7 @@ internal fun ClientApplyNewApplicationsScreen(

ClientApplyNewApplicationsItem.NewLoanAccount -> onNavigateApplyLoanAccount(
state.clientId,
state.accountNo,
)

ClientApplyNewApplicationsItem.NewRecurringAccount -> onNavigateApplyRecurringAccount(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ClientApplyNewApplicationsViewModel(
ClientApplyNewApplicationsState(
clientId = route.clientId,
status = route.status,
accountNo = route.accountNo,
)
},
) {
Expand All @@ -39,6 +40,7 @@ class ClientApplyNewApplicationsViewModel(

data class ClientApplyNewApplicationsState(
val clientId: Int,
val accountNo: String,
val status: String,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fun NavGraphBuilder.clientProfileDetailsDestination(
navigateToUpdateDefaultAccount: (Int) -> Unit,
navigateToCollateral: (Int) -> Unit,
navigateToUpdateSignature: (Int, String, String) -> Unit,
navigateToApplyNewApplication: (Int, String) -> Unit,
navigateToApplyNewApplication: (Int, String, String) -> Unit,
navigateToAddCharge: (Int) -> Unit,
) {
composable<ClientProfileDetailsRoute> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ internal fun ClientProfileDetailsScreen(
navigateToUpdateDetails: (Int) -> Unit,
navigateToClientTransfer: (Int) -> Unit,
navigateToClientClosure: (Int) -> Unit,
navigateToApplyNewApplication: (Int, String) -> Unit,
navigateToApplyNewApplication: (Int, String, String) -> Unit,
navigateToUpdateDefaultAccount: (Int) -> Unit,
navigateToCollateral: (Int) -> Unit,
navigateToUpdateSignature: (Int, String, String) -> Unit,
Expand All @@ -104,6 +104,7 @@ internal fun ClientProfileDetailsScreen(
navigateToApplyNewApplication(
state.client?.id ?: -1,
state.client?.status?.value ?: "",
state.client?.accountNo ?: "",
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class NewLoanAccountRoute(
val clientId: Int = -1,
val accountNo: String,
)

fun NavGraphBuilder.newLoanAccountDestination(
Expand All @@ -33,8 +34,8 @@ fun NavGraphBuilder.newLoanAccountDestination(
}
}

fun NavController.navigateToNewLoanAccountRoute(clientId: Int) {
fun NavController.navigateToNewLoanAccountRoute(clientId: Int, accountNo: String) {
this.navigate(
NewLoanAccountRoute(clientId),
NewLoanAccountRoute(clientId, accountNo),
)
}
Loading