Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a8c29cb
feat(loan): implement loan repayment form parity with Web App (MIFOSA…
gurnoorpannu Feb 2, 2026
c430781
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 2, 2026
094be7e
implemented requested changes
gurnoorpannu Feb 2, 2026
6be78cf
Merge remote-tracking branch 'origin/feature/loan-repayment-form-pari…
gurnoorpannu Feb 2, 2026
1a504bc
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 3, 2026
242c5dd
made requested changes
gurnoorpannu Feb 4, 2026
35e73c8
Merge remote-tracking branch 'origin/feature/loan-repayment-form-pari…
gurnoorpannu Feb 4, 2026
838a8a3
Made changes requested
gurnoorpannu Feb 4, 2026
34d3d40
Fixing changes requested
gurnoorpannu Feb 6, 2026
736575c
feat(loan): implement loan repayment form parity with Web App (MIFOSA…
gurnoorpannu Feb 2, 2026
3ccf92d
implemented requested changes
gurnoorpannu Feb 2, 2026
276cccb
made requested changes
gurnoorpannu Feb 4, 2026
9cfa094
Made changes requested
gurnoorpannu Feb 4, 2026
0b356ad
Fixing changes requested
gurnoorpannu Feb 6, 2026
12a57fa
Fixing conflitcts
gurnoorpannu Feb 7, 2026
5dca051
Merge remote-tracking branch 'origin/feature/loan-repayment-form-pari…
gurnoorpannu Feb 7, 2026
da0da1e
implemented requested changes
gurnoorpannu Feb 7, 2026
35a47c4
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 10, 2026
bfa45cb
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 11, 2026
fe61b9f
implemented coderabbit suggestions
gurnoorpannu Feb 12, 2026
518dcb7
Merge remote-tracking branch 'origin/feature/loan-repayment-form-pari…
gurnoorpannu Feb 12, 2026
0ebdc13
Added the externalId field
gurnoorpannu Feb 13, 2026
3782f02
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 13, 2026
70dd3fd
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 17, 2026
1c8f53a
implemented Requested code changes
gurnoorpannu Feb 19, 2026
c8c1f90
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 19, 2026
aaeb0d4
implemented Requested code changes
gurnoorpannu Feb 19, 2026
0c89f51
Merge remote-tracking branch 'origin/feature/loan-repayment-form-pari…
gurnoorpannu Feb 19, 2026
7d0902b
requested changes
gurnoorpannu Feb 20, 2026
d9675ce
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 20, 2026
5d7c3f0
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 22, 2026
ccca472
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 23, 2026
9ea3a7d
Made requested changes!
gurnoorpannu Feb 23, 2026
f03ab6c
Merge remote-tracking branch 'origin/feature/loan-repayment-form-pari…
gurnoorpannu Feb 23, 2026
95c69d7
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 23, 2026
2a87ebc
Fixed Merge Conflicts
gurnoorpannu Feb 24, 2026
ba2a0f7
Merge remote-tracking branch 'origin/feature/loan-repayment-form-pari…
gurnoorpannu Feb 24, 2026
00b8389
Fixed Merge Conflicts
gurnoorpannu Feb 24, 2026
5c0a8fa
Merge branch 'development' into feature/loan-repayment-form-parity-clean
gurnoorpannu Feb 24, 2026
f21ca5c
Solved Merge Conflicts
gurnoorpannu Feb 25, 2026
efea4a2
Solved Merge Conflicts
gurnoorpannu Feb 25, 2026
015ed1a
Merge branch 'development' into feature/loan-repayment-form-parity-clean
niyajali Feb 26, 2026
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 @@ -49,6 +49,8 @@ data class LoanRepaymentRequestEntity(

val accountNumber: String? = null,

val externalId: String? = null,

val checkNumber: String? = null,

val routingCode: String? = null,
Expand Down
16 changes: 16 additions & 0 deletions feature/loan/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -323,5 +323,21 @@
<!-- Handle Error-->
<string name="field_empty">This field cannot be empty</string>

<!-- Loan Repayment Form Enhancements (MIFOSAC-643) -->
<string name="feature_loan_transaction_breakdown">Transaction Breakdown</string>
<string name="feature_loan_interest">Interest</string>
<string name="feature_loan_fees">Fees</string>
<string name="feature_loan_penalties">Penalties</string>
<string name="feature_loan_show_payment_details">Show Payment Details</string>
<string name="feature_loan_external_id_field">External ID</string>
<string name="feature_loan_cheque_number">Cheque Number</string>
<string name="feature_loan_routing_code">Routing Code</string>
<string name="feature_loan_receipt_number">Receipt Number</string>
<string name="feature_loan_bank_number">Bank Number</string>
<string name="feature_loan_note">Note</string>
<string name="feature_loan_waive_penalties">Waive Penalties</string>
<string name="feature_loan_no_penalties_found">No penalties found</string>
<string name="feature_loan_payment_success_message">Payment submitted successfully. Transaction ID: %1$s</string>
<string name="label_value_format">%1$s : %2$s</string>

</resources>

Large diffs are not rendered by default.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,124 +13,231 @@ import androidclient.feature.loan.generated.resources.Res
import androidclient.feature.loan.generated.resources.feature_loan_failed_to_load_loan_repayment
import androidclient.feature.loan.generated.resources.feature_loan_payment_failed
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.mifos.core.common.utils.CurrencyFormatter
import com.mifos.core.common.utils.DataState
import com.mifos.core.data.repository.LoanRepaymentRepository
import com.mifos.core.ui.util.BaseViewModel
import com.mifos.room.entities.accounts.loans.LoanRepaymentRequestEntity
import com.mifos.room.entities.accounts.loans.LoanRepaymentResponseEntity
import com.mifos.room.entities.templates.loans.LoanRepaymentTemplateEntity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlin.math.round
import org.jetbrains.compose.resources.StringResource
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

class LoanRepaymentViewModel(
savedStateHandle: SavedStateHandle,
private val repository: LoanRepaymentRepository,
) : ViewModel() {
) : BaseViewModel<LoanRepaymentUiState, LoanRepaymentEvent, LoanRepaymentAction>(
initialState = LoanRepaymentUiState(),
) {

val arg = savedStateHandle.toRoute<LoanRepaymentScreenRoute>()

private val _loanRepaymentUiState =
MutableStateFlow<LoanRepaymentUiState>(LoanRepaymentUiState.ShowProgressbar)
val loanRepaymentUiState: StateFlow<LoanRepaymentUiState> get() = _loanRepaymentUiState
init {
mutableStateFlow.value = mutableStateFlow.value.copy(
repaymentDate = currentEpochMillis(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Directly move this on initial state

)
trySendAction(LoanRepaymentAction.CheckDatabaseLoanRepayment)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why we're doing like this, make it flowable and collect it and send the appropriate action

}

fun loanLoanRepaymentTemplate() {
@OptIn(ExperimentalTime::class)
private fun currentEpochMillis(): Long = Clock.System.now().toEpochMilliseconds()

override fun handleAction(action: LoanRepaymentAction) {
when (action) {
is LoanRepaymentAction.LoadLoanRepaymentTemplate -> {
loadLoanRepaymentTemplate()
}
is LoanRepaymentAction.CheckDatabaseLoanRepayment -> {
checkDatabaseLoanRepaymentByLoanId()
}
is LoanRepaymentAction.SubmitPayment -> {
submitPayment(action.request)
}
}
}

private fun loadLoanRepaymentTemplate() {
viewModelScope.launch {
repository.getLoanRepayTemplate(arg.loanId).collect { state ->
when (state) {
is DataState.Error ->
_loanRepaymentUiState.value =
LoanRepaymentUiState.ShowError(
Res.string
.feature_loan_failed_to_load_loan_repayment,
)
DataState.Loading ->
_loanRepaymentUiState.value = LoanRepaymentUiState.ShowProgressbar
is DataState.Success ->
_loanRepaymentUiState.value =
LoanRepaymentUiState.ShowLoanRepayTemplate(
state.data ?: LoanRepaymentTemplateEntity(),
)
is DataState.Error -> {
mutableStateFlow.value = mutableStateFlow.value.copy(
isLoading = false,
error = Res.string.feature_loan_failed_to_load_loan_repayment,
)
}
DataState.Loading -> {
mutableStateFlow.value = mutableStateFlow.value.copy(isLoading = true, error = null)
}
is DataState.Success -> {
mutableStateFlow.value = mutableStateFlow.value.copy(
isLoading = false,
error = null,
loanRepaymentTemplate = state.data ?: LoanRepaymentTemplateEntity(),
)
}
}
}
}
}

fun submitPayment(request: LoanRepaymentRequestEntity) {
private fun submitPayment(request: LoanRepaymentRequestEntity) {
viewModelScope.launch {
_loanRepaymentUiState.value = LoanRepaymentUiState.ShowProgressbar
mutableStateFlow.value = mutableStateFlow.value.copy(isLoading = true, error = null)

try {
val loanRepaymentResponse = repository.submitPayment(arg.loanId, request)
_loanRepaymentUiState.value =
LoanRepaymentUiState.ShowPaymentSubmittedSuccessfully(
loanRepaymentResponse,
)
mutableStateFlow.value = mutableStateFlow.value.copy(isLoading = false)
sendEvent(LoanRepaymentEvent.PaymentSubmittedSuccessfully(loanRepaymentResponse))
} catch (e: Exception) {
_loanRepaymentUiState.value =
LoanRepaymentUiState.ShowError(Res.string.feature_loan_payment_failed)
mutableStateFlow.value = mutableStateFlow.value.copy(
isLoading = false,
error = Res.string.feature_loan_payment_failed,
)
}
}
}

fun checkDatabaseLoanRepaymentByLoanId() {
private fun checkDatabaseLoanRepaymentByLoanId() {
viewModelScope.launch {
repository.getDatabaseLoanRepaymentByLoanId(arg.loanId).collect { state ->
when (state) {
is DataState.Error ->
_loanRepaymentUiState.value =
LoanRepaymentUiState.ShowError(
Res.string
.feature_loan_failed_to_load_loan_repayment,
)
DataState.Loading ->
_loanRepaymentUiState.value = LoanRepaymentUiState.ShowProgressbar
is DataState.Error -> {
mutableStateFlow.value = mutableStateFlow.value.copy(
isLoading = false,
hasCheckedDatabase = true,
error = Res.string.feature_loan_failed_to_load_loan_repayment,
)
}
DataState.Loading -> {
mutableStateFlow.value = mutableStateFlow.value.copy(isLoading = true, error = null)
}
is DataState.Success -> {
if (state.data != null) {
_loanRepaymentUiState.value =
LoanRepaymentUiState.ShowLoanRepaymentExistInDatabase
} else {
_loanRepaymentUiState.value =
LoanRepaymentUiState.ShowLoanRepaymentDoesNotExistInDatabase
val existsInDatabase = state.data != null
mutableStateFlow.value = mutableStateFlow.value.copy(
isLoading = false,
error = null,
hasCheckedDatabase = true,
loanRepaymentExistsInDatabase = existsInDatabase,
)
if (!existsInDatabase) {
trySendAction(LoanRepaymentAction.LoadLoanRepaymentTemplate)
}
}
}
}
}
}

fun calculateTotal(fees: String, amount: String, additionalPayment: String): Double {
fun setValue(value: String): Double {
if (value.isEmpty()) return 0.0
return try {
value.toDouble()
} catch (e: NumberFormatException) {
0.0
}
}
val total = setValue(fees) + setValue(amount) + setValue(additionalPayment)
return round(total * 100) / 100.0
fun updatePaymentType(paymentType: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(paymentType = paymentType)
}

fun updatePaymentTypeWithId(paymentType: String, paymentTypeId: Int) {
mutableStateFlow.value = mutableStateFlow.value.copy(
paymentType = paymentType,
paymentTypeId = paymentTypeId,
paymentTypeError = null,
)
}

fun formatCurrency(amount: Double?, code: String?, decimalPlaces: Int?): String {
return CurrencyFormatter.format(
balance = amount,
currencyCode = code,
maximumFractionDigits = decimalPlaces ?: 2,
fun updateAmount(amount: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(
amount = amount,
amountError = null,
)
}

fun isAllFieldsValid(
amount: String,
additionalPayment: String,
fees: String,
paymentType: String,
): Boolean {
return listOf(amount, additionalPayment, fees).all {
it.trim().toDoubleOrNull()?.let { n -> n >= 0 } == true
} && paymentType.isNotBlank()
fun updateAdditionalPayment(additionalPayment: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(additionalPayment = additionalPayment)
}

fun updateFees(fees: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(fees = fees)
}

fun updateRepaymentDate(date: Long) {
mutableStateFlow.value = mutableStateFlow.value.copy(repaymentDate = date)
}

fun updateShowPaymentDetails(show: Boolean) {
mutableStateFlow.value = mutableStateFlow.value.copy(showPaymentDetails = show)
}

fun updateAccountNumber(accountNumber: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(accountNumber = accountNumber)
}

fun updateExternalId(externalId: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(externalId = externalId)
}

fun updateChequeNumber(chequeNumber: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(chequeNumber = chequeNumber)
}

fun updateRoutingCode(routingCode: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(routingCode = routingCode)
}

fun updateReceiptNumber(receiptNumber: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(receiptNumber = receiptNumber)
}

fun updateBankNumber(bankNumber: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(bankNumber = bankNumber)
}

fun updateNote(note: String) {
mutableStateFlow.value = mutableStateFlow.value.copy(note = note)
}

fun updateWaivePenalties(waive: Boolean) {
mutableStateFlow.value = mutableStateFlow.value.copy(waivePenalties = waive)
}

fun setValidationErrors(amountError: String?, paymentTypeError: String?) {
mutableStateFlow.value = mutableStateFlow.value.copy(
amountError = amountError,
paymentTypeError = paymentTypeError,
)
}
}

data class LoanRepaymentUiState(
val isLoading: Boolean = false,
val error: StringResource? = null,
val loanRepaymentTemplate: LoanRepaymentTemplateEntity? = null,
val loanRepaymentExistsInDatabase: Boolean = false,
val hasCheckedDatabase: Boolean = false,
val showPaymentDetails: Boolean = false,
val paymentType: String = "",
val amount: String = "",
val additionalPayment: String = "",
val fees: String = "",
val paymentTypeId: Int = 0,
val repaymentDate: Long = 0L,
val accountNumber: String = "",
val externalId: String = "",
val chequeNumber: String = "",
val routingCode: String = "",
val receiptNumber: String = "",
val bankNumber: String = "",
val note: String = "",
val waivePenalties: Boolean = false,
val amountError: String? = null,
val paymentTypeError: String? = null,
)

sealed interface LoanRepaymentEvent {
data class PaymentSubmittedSuccessfully(val response: LoanRepaymentResponseEntity?) : LoanRepaymentEvent
}

sealed interface LoanRepaymentAction {
data object LoadLoanRepaymentTemplate : LoanRepaymentAction
data object CheckDatabaseLoanRepayment : LoanRepaymentAction
data class SubmitPayment(val request: LoanRepaymentRequestEntity) : LoanRepaymentAction
}
Loading