Skip to content
Open
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 @@ -213,24 +213,33 @@ internal class TransferConfirmViewModel(
}
}

private fun validateTransfer() = when {
state.amount.isBlank() -> updateValidationError(Res.string.feature_make_transfer_error_empty_amount)
private fun validateTransfer() {
if (state.amount.isBlank()) {
updateValidationError(Res.string.feature_make_transfer_error_empty_amount)
return
}

state.amount.toDoubleOrNull() == null -> updateValidationError(Res.string.feature_make_transfer_error_invalid_amount)
val transferAmount = state.amount.toDoubleOrNull()
if (transferAmount == null || transferAmount <= 0.0) {
updateValidationError(Res.string.feature_make_transfer_error_invalid_amount)
return
}

state.description.isBlank() -> updateValidationError(Res.string.feature_make_transfer_error_empty_description)
when {
state.description.isBlank() -> updateValidationError(Res.string.feature_make_transfer_error_empty_description)

state.selectedAccount == null -> updateValidationError(Res.string.feature_make_transfer_error_select_account)
state.selectedAccount == null -> updateValidationError(Res.string.feature_make_transfer_error_select_account)

state.selectedAccount?.accountId == state.toAccountId -> {
updateValidationError(Res.string.feature_make_transfer_error_same_account)
}
state.selectedAccount?.accountId == state.toAccountId -> {
updateValidationError(Res.string.feature_make_transfer_error_same_account)
}

state.amount.toDouble() > state.selectedAccountBalance -> {
updateValidationError(Res.string.feature_make_transfer_error_insufficient_balance)
}
transferAmount > state.selectedAccountBalance -> {
updateValidationError(Res.string.feature_make_transfer_error_insufficient_balance)
}

else -> initiateTransfer()
else -> initiateTransfer()
}
}

private fun initiateTransfer() {
Expand Down Expand Up @@ -341,6 +350,7 @@ internal class TransferConfirmViewModel(
private fun updateValidationError(message: StringResource) {
mutableStateFlow.update {
it.copy(
isProcessing = false,
dialogState = TransferConfirmState.DialogState.Error.ValidationError(message),
)
}
Expand Down Expand Up @@ -371,7 +381,8 @@ internal data class TransferConfirmState(
val userVerificationResult: Boolean? = null,
) {
val amountIsValid: Boolean
get() = amount.isNotEmpty() && amount.toDoubleOrNull() != null && amount.toDouble() <= selectedAccountBalance
get() = amount.isNotEmpty() &&
amount.toDoubleOrNull()?.let { it > 0.0 && it <= selectedAccountBalance } == true

val descriptionIsValid: Boolean
get() = description.trim().isNotEmpty()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* Copyright 2026 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/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.transfer.intrabank.confirm

import androidx.lifecycle.SavedStateHandle
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.mifospay.core.common.DataState
import org.mifospay.core.data.repository.ClientRepository
import org.mifospay.core.data.repository.ThirdPartyTransferRepository
import org.mifospay.core.data.repository.UserVerificationRepository
import org.mifospay.core.model.account.Account
import org.mifospay.core.model.client.Client
import org.mifospay.core.model.client.NewClient
import org.mifospay.core.model.client.UpdatedClient
import org.mifospay.core.network.model.entity.Page
import org.mifospay.core.network.model.entity.TPTResponse
import org.mifospay.core.network.model.entity.client.ClientAccountsEntity
import org.mifospay.core.network.model.entity.payload.TransferPayload
import org.mifospay.core.network.model.entity.templates.account.AccountOptionsTemplate
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertTrue

@OptIn(ExperimentalCoroutinesApi::class)
class TransferConfirmStateTest {
private val testDispatcher: CoroutineDispatcher = StandardTestDispatcher()

@BeforeTest
fun setUp() {
Dispatchers.setMain(testDispatcher)
}

@AfterTest
fun tearDown() {
Dispatchers.resetMain()
}

@Test
fun amountIsValidReturnsFalseForZeroAmount() {
val state = TransferConfirmState(
amount = "0",
selectedAccountBalance = 100.0,
)

assertFalse(state.amountIsValid)
}

@Test
fun amountIsValidReturnsFalseForNegativeAmount() {
val state = TransferConfirmState(
amount = "-1",
selectedAccountBalance = 100.0,
)

assertFalse(state.amountIsValid)
}

@Test
fun amountIsValidReturnsTrueForPositiveAmountWithinBalance() {
val state = TransferConfirmState(
amount = "10.50",
selectedAccountBalance = 100.0,
)

assertTrue(state.amountIsValid)
}

@Test
fun amountIsValidReturnsFalseWhenAmountExceedsBalance() {
val state = TransferConfirmState(
amount = "101",
selectedAccountBalance = 100.0,
)

assertFalse(state.amountIsValid)
}

@Test
fun amountIsValidReturnsFalseForNonNumericAmount() {
val state = TransferConfirmState(
amount = "abc",
selectedAccountBalance = 100.0,
)

assertFalse(state.amountIsValid)
}

@Test
fun initiateTransferWithInvalidAmountClearsProcessing() = runTest {
val viewModel = TransferConfirmViewModel(
repository = FakeTransferRepository(),
clientRepo = FakeClientRepository(),
userVerificationRepository = FakeUserVerificationRepository(),
savedStateHandle = savedStateHandle,
)
advanceUntilIdle()

viewModel.trySendAction(TransferConfirmAction.AmountChanged("-1"))
viewModel.trySendAction(TransferConfirmAction.DescriptionChanged("test transfer"))
viewModel.trySendAction(TransferConfirmAction.InitiateTransfer)
advanceUntilIdle()

assertFalse(viewModel.stateFlow.value.isProcessing)
assertIs<TransferConfirmState.DialogState.Error.ValidationError>(
viewModel.stateFlow.value.dialogState,
)
}

private inner class FakeTransferRepository : ThirdPartyTransferRepository {
override suspend fun getTransferTemplate(): AccountOptionsTemplate = AccountOptionsTemplate()

override fun makeTransfer(payload: TransferPayload): Flow<DataState<TPTResponse>> = error("unused")
}

private inner class FakeClientRepository : ClientRepository {
override fun getClientInfo(clientId: Long): Flow<DataState<Client>> = error("unused")

override suspend fun getClients(): Flow<DataState<Page<Client>>> = error("unused")

override suspend fun getClient(clientId: Long): DataState<Client> = error("unused")

override suspend fun updateClient(clientId: Long, client: UpdatedClient): DataState<String> = error("unused")

override fun getClientImage(clientId: Long): Flow<DataState<String>> = error("unused")

override suspend fun updateClientImage(clientId: Long, image: String): DataState<String> = error("unused")

override suspend fun getClientAccounts(clientId: Long): ClientAccountsEntity = error("unused")

override suspend fun getAccounts(clientId: Long, accountType: String): Flow<DataState<List<Account>>> =
error("unused")

override suspend fun createClient(newClient: NewClient): DataState<Int> = error("unused")

override suspend fun deleteClient(clientId: Int): DataState<Int> = error("unused")
}

private class FakeUserVerificationRepository : UserVerificationRepository {
override fun recordVerification() = Unit

override fun consumeVerification(): Boolean = false
}

private val savedStateHandle: SavedStateHandle
get() = SavedStateHandle(
mapOf(
"amount" to 0,
"accountId" to 2L,
"toOfficeId" to 2,
"toClientId" to 2L,
"toAccountTypeId" to 2,
"toAccountName" to "Receiver",
"toAccountNo" to "0002",
"returnDestination" to "home",
),
)
}