Skip to content

Commit

Permalink
technical: Handle insufficient balance error
Browse files Browse the repository at this point in the history
  • Loading branch information
IlyaPavlovskii committed Feb 6, 2023
1 parent 96ce802 commit 42783c2
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Effect
import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Message
import net.humans.kmm.mvi.sample.domain.model.usd
import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionInput
import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionResult
import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionUseCase
import net.humans.kmm.mvi.sample.domain.usecase.GetBalanceUseCase
import net.humans.kmm.mvi.sample.domain.usecase.impl.RandomCalculateCashbackAndCommissionUseCase
Expand All @@ -17,21 +18,31 @@ class CommissionCalculatorEffectHandler(
RandomCalculateCashbackAndCommissionUseCase()
) : CoroutineEffectHandler<Effect, Message> {
override suspend fun handle(eff: Effect): Message = when (eff) {
is Effect.CalculateCommissionAndCashback ->
calculateCashbackAndCommissionUseCase.execute(
input = CalculateCashbackAndCommissionInput(inputAmount = eff.inputAmount)
).let { result ->
Message.UpdateCommissionAndCashback(
commission = result.commission,
cashback = result.cashback,
)
}

is Effect.CalculateCommissionAndCashback -> calculateCommissionAndCashback(eff)
Effect.Initialize -> Message.UpdateState(
balance = getBalanceUseCase.execute().balance,
inputAmount = BigDecimal.ZERO.usd,
commission = BigDecimal.ZERO.usd,
cashback = BigDecimal.ZERO.usd,
)
}

private suspend fun calculateCommissionAndCashback(
eff: Effect.CalculateCommissionAndCashback
): Message {
val input = CalculateCashbackAndCommissionInput(
balance = eff.balance,
inputAmount = eff.inputAmount
)
return when (val result = calculateCashbackAndCommissionUseCase.execute(input = input)) {
CalculateCashbackAndCommissionResult.Failed.InsufficientBalance ->
Message.SetError(CommissionCalculatorRedux.State.Error.InsufficientBalance)

is CalculateCashbackAndCommissionResult.Success -> Message.UpdateCommissionAndCashback(
inputAmount = result.inputAmount,
commission = result.commission,
cashback = result.cashback,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,24 @@ import net.humans.kmm.mvi.withEffect
class CommissionCalculatorReducer : ComplexReducer<State, Message, Effect> {
override fun invoke(state: State, msg: Message): Return<State, Effect> = when (msg) {
is Message.UpdateCommissionAndCashback -> state.copy(
inputAmount = msg.inputAmount,
commission = msg.commission,
cashback = msg.cashback,
).pure()

is Message.UpdateInput -> state.copy(
is Message.UpdateInput -> state withEffect Effect.CalculateCommissionAndCashback(
balance = state.balance,
inputAmount = msg.inputAmount
) withEffect Effect.CalculateCommissionAndCashback(inputAmount = msg.inputAmount)
)

is Message.UpdateState -> State(
balance = msg.balance,
inputAmount = msg.inputAmount,
commission = msg.commission,
cashback = msg.cashback,
).pure()

Message.ErrorHandled -> state.copy(error = null).pure()
is Message.SetError -> state.copy(error = msg.error).pure()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ object CommissionCalculatorRedux {
val inputAmount: MoneyAmount,
val commission: MoneyAmount,
val cashback: MoneyAmount,
val error: Error? = null,
) {

sealed class Error {
object InsufficientBalance : Error()
}

companion object {
val DEFAULT = State(
balance = BigDecimal.ZERO.usd,
Expand All @@ -34,15 +40,23 @@ object CommissionCalculatorRedux {
) : Message()

data class UpdateCommissionAndCashback(
val inputAmount: MoneyAmount,
val commission: MoneyAmount,
val cashback: MoneyAmount,
) : Message()

data class SetError(
val error: State.Error,
) : Message()

object ErrorHandled : Message()
}

sealed class Effect {
object Initialize : Effect()

data class CalculateCommissionAndCashback(
val balance: MoneyAmount,
val inputAmount: MoneyAmount,
) : Effect()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ data class MoneyAmount(
) {
operator fun times(value: Float): MoneyAmount =
this.copy(amount = amount * BigDecimal.fromFloat(value))

operator fun compareTo(moneyAmount: MoneyAmount): Int {
check(this.currency == moneyAmount.currency) {
"Impossible to compare money amount. Non consistent currencies."
}
return this.amount.compareTo(moneyAmount.amount)
}
}

val BigDecimal.usd: MoneyAmount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ interface CalculateCashbackAndCommissionUseCase {
}

data class CalculateCashbackAndCommissionInput(
val balance: MoneyAmount,
val inputAmount: MoneyAmount,
)

data class CalculateCashbackAndCommissionResult(
val cashback: MoneyAmount,
val commission: MoneyAmount,
)
sealed class CalculateCashbackAndCommissionResult {
data class Success(
val inputAmount: MoneyAmount,
val cashback: MoneyAmount,
val commission: MoneyAmount,
) : CalculateCashbackAndCommissionResult()

sealed class Failed : CalculateCashbackAndCommissionResult() {
object InsufficientBalance : Failed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ internal class RandomCalculateCashbackAndCommissionUseCase(
) : CalculateCashbackAndCommissionUseCase {
override suspend fun execute(
input: CalculateCashbackAndCommissionInput
): CalculateCashbackAndCommissionResult {
return CalculateCashbackAndCommissionResult(
cashback = input.inputAmount * commissionPercentage,
commission = input.inputAmount * cashbackPercentage,
): CalculateCashbackAndCommissionResult = if (input.inputAmount > input.balance) {
CalculateCashbackAndCommissionResult.Failed.InsufficientBalance
} else {
CalculateCashbackAndCommissionResult.Success(
inputAmount = input.inputAmount,
cashback = input.inputAmount * cashbackPercentage,
commission = input.inputAmount * commissionPercentage,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
Expand Down Expand Up @@ -61,12 +62,20 @@ internal fun CommissionCalculatorView(
onValueChange = {
onValueChange(it.text)
},
isError = viewState.hasError(),
visualTransformation = CurrencyAmountInputVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
textStyle = MaterialTheme.typography.displaySmall.copy(
color = MaterialTheme.colorScheme.primary
)
)
viewState.error?.also { safeError ->
Text(
text = stringResource(id = safeError),
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.error,
)
}
}
}

Expand All @@ -77,7 +86,7 @@ private fun PreviewCommissionCalculatorView() {
CommissionCalculatorView(
viewState = CommissionCalculatorViewState(
balance = "100",
inputAmount = TextFieldValue(text="50.00",selection = TextRange(5)),
inputAmount = TextFieldValue(text = "50.00", selection = TextRange(5)),
commission = "1",
cashback = "0.5",
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal class CommissionCalculatorViewModel(
fun inputAmountChange(input: String) {
val amount = input.toFloatOrNull()?.let { it / INPUT_AMOUNT_DIVIDER } ?: DEFAULT_AMOUNT
val moneyAmount = BigDecimal.fromFloat(amount).usd
engine send Message.ErrorHandled
engine send Message.UpdateInput(moneyAmount)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.humans.kmm.mvi.sample

import androidx.annotation.StringRes
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue

Expand All @@ -8,4 +9,7 @@ internal data class CommissionCalculatorViewState(
val inputAmount: TextFieldValue = TextFieldValue(text = "", selection = TextRange(0)),
val commission: String = "",
val cashback: String = "",
)
@StringRes val error: Int? = null,
) {
fun hasError(): Boolean = error != null
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.humans.kmm.mvi.sample

import androidx.annotation.StringRes
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.ionspin.kotlin.bignum.decimal.RoundingMode
Expand All @@ -21,6 +22,7 @@ internal class CommissionCalculatorViewStateConverter :
},
commission = state.commission.toPresentationString(),
cashback = state.cashback.toPresentationString(),
error = state.error?.toPresentationError(),
)

private fun MoneyAmount.toPresentationString(): String =
Expand All @@ -31,4 +33,10 @@ internal class CommissionCalculatorViewStateConverter :
private fun Currency.toPresentationString(): String = when (this) {
Currency.USD -> "$"
}

@StringRes
private fun CommissionCalculatorRedux.State.Error.toPresentationError(): Int = when(this) {
CommissionCalculatorRedux.State.Error.InsufficientBalance ->
R.string.commission_calculator__error_insufficient_balance
}
}
1 change: 1 addition & 0 deletions mvi-sample/presentation/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<resources>
<string name="app_name">Humans MVI</string>
<string name="commission_calculator__error_insufficient_balance">Insufficient balance</string>
</resources>

0 comments on commit 42783c2

Please sign in to comment.