From 864160280872fdb400c2e0e61aaaa1b858fba3f8 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 24 Mar 2020 09:08:00 -0300 Subject: Allow fractional withdrawals with cashier app --- build.gradle | 2 +- .../main/java/net/taler/cashier/BalanceFragment.kt | 12 ++++++---- .../src/main/java/net/taler/cashier/HttpHelper.kt | 4 ++-- .../taler/cashier/withdraw/TransactionFragment.kt | 2 +- .../net/taler/cashier/withdraw/WithdrawManager.kt | 24 ++++++++++--------- .../main/res/layout-w550dp/fragment_balance.xml | 2 +- cashier/src/main/res/layout/fragment_balance.xml | 2 +- .../src/main/java/net/taler/common/Amount.kt | 27 ++++++++++++++++++---- .../src/test/java/net/taler/common/AmountTest.kt | 21 +++++++++++++++++ 9 files changed, 69 insertions(+), 27 deletions(-) diff --git a/build.gradle b/build.gradle index b1f47dd..57a780b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.70' + ext.kotlin_version = '1.3.71' ext.nav_version = "2.2.1" ext.build_tools_version = "29.0.2" repositories { diff --git a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt index fffb21b..c4802ee 100644 --- a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt +++ b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt @@ -33,6 +33,7 @@ import kotlinx.android.synthetic.main.fragment_balance.* import net.taler.cashier.BalanceFragmentDirections.Companion.actionBalanceFragmentToTransactionFragment import net.taler.cashier.withdraw.LastTransaction import net.taler.cashier.withdraw.WithdrawStatus +import net.taler.common.Amount import net.taler.common.SignedAmount import net.taler.common.fadeIn import net.taler.common.fadeOut @@ -142,14 +143,15 @@ class BalanceFragment : Fragment() { amountView.error = null } - private fun getAmountFromView(): Int { + private fun getAmountFromView(): Amount { val str = amountView.editText!!.text.toString() - if (str.isBlank()) return 0 - return Integer.parseInt(str) + val currency = viewModel.currency.value!! + if (str.isBlank()) return Amount.zero(currency) + return Amount.fromString(currency, str) } - private fun onAmountConfirmed(amount: Int) { - if (amount <= 0) { + private fun onAmountConfirmed(amount: Amount) { + if (amount.isZero()) { amountView.error = getString(R.string.withdraw_error_zero) } else if (!withdrawManager.hasSufficientBalance(amount)) { amountView.error = getString(R.string.withdraw_error_insufficient_balance) diff --git a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt index 06b06db..43ba4d8 100644 --- a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt +++ b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt @@ -57,11 +57,11 @@ object HttpHelper { private val MEDIA_TYPE_JSON = MediaType.parse("$MIME_TYPE_JSON; charset=utf-8") @WorkerThread - fun makeJsonPostRequest(url: String, body: String, config: Config): HttpJsonResult { + fun makeJsonPostRequest(url: String, body: JSONObject, config: Config): HttpJsonResult { val request = Request.Builder() .addHeader("Accept", MIME_TYPE_JSON) .url(url) - .post(RequestBody.create(MEDIA_TYPE_JSON, body)) + .post(RequestBody.create(MEDIA_TYPE_JSON, body.toString())) .build() val response = try { getHttpClient(config.username, config.password) diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt b/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt index 8857bfa..e433540 100644 --- a/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt +++ b/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt @@ -54,7 +54,7 @@ class TransactionFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { withdrawManager.withdrawAmount.observe(viewLifecycleOwner, Observer { amount -> - amountView.text = amount + amountView.text = amount?.toString() }) withdrawManager.withdrawResult.observe(viewLifecycleOwner, Observer { result -> onWithdrawResultReceived(result) diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt index 88df6b7..317d1bf 100644 --- a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt +++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt @@ -34,6 +34,7 @@ import net.taler.cashier.HttpJsonResult.Error import net.taler.cashier.HttpJsonResult.Success import net.taler.cashier.MainViewModel import net.taler.cashier.R +import net.taler.common.Amount import net.taler.common.QrCodeManager.makeQrCode import org.json.JSONObject import java.util.concurrent.TimeUnit.MINUTES @@ -59,8 +60,8 @@ class WithdrawManager( private var withdrawStatusCheck: Job? = null - private val mWithdrawAmount = MutableLiveData() - val withdrawAmount: LiveData = mWithdrawAmount + private val mWithdrawAmount = MutableLiveData() + val withdrawAmount: LiveData = mWithdrawAmount private val mWithdrawResult = MutableLiveData() val withdrawResult: LiveData = mWithdrawResult @@ -72,22 +73,23 @@ class WithdrawManager( val lastTransaction: LiveData = mLastTransaction @UiThread - fun hasSufficientBalance(amount: Int): Boolean { + fun hasSufficientBalance(amount: Amount): Boolean { val balanceResult = viewModel.balance.value if (balanceResult !is BalanceResult.Success) return false - return balanceResult.amount.positive && amount <= balanceResult.amount.amount.value + return balanceResult.amount.positive && amount <= balanceResult.amount.amount } @UiThread - fun withdraw(amount: Int) { - check(amount > 0) { "Withdraw amount was <= 0" } + fun withdraw(amount: Amount) { + check(!amount.isZero()) { "Withdraw amount was 0" } check(currency != null) { "Currency is null" } mWithdrawResult.value = null - mWithdrawAmount.value = "$amount $currency" + mWithdrawAmount.value = amount scope.launch(Dispatchers.IO) { val url = "${config.bankUrl}/accounts/${config.username}/withdrawals" Log.d(TAG, "Starting withdrawal at $url") - val body = JSONObject(mapOf("amount" to "${currency}:${amount}")).toString() + val map = mapOf("amount" to amount.toJSONString()) + val body = JSONObject(map) when (val result = makeJsonPostRequest(url, body, config)) { is Success -> { val talerUri = result.json.getString("taler_withdraw_uri") @@ -178,7 +180,7 @@ class WithdrawManager( private fun abort(withdrawalId: String) = scope.launch(Dispatchers.IO) { val url = "${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/abort" Log.d(TAG, "Aborting withdrawal at $url") - makeJsonPostRequest(url, "", config) + makeJsonPostRequest(url, JSONObject(), config) } @UiThread @@ -188,7 +190,7 @@ class WithdrawManager( val url = "${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/confirm" Log.d(TAG, "Confirming withdrawal at $url") - when (val result = makeJsonPostRequest(url, "", config)) { + when (val result = makeJsonPostRequest(url, JSONObject(), config)) { is Success -> { // no-op still waiting for [timer] to confirm our confirmation } @@ -226,6 +228,6 @@ sealed class WithdrawStatus { } data class LastTransaction( - val withdrawAmount: String, + val withdrawAmount: Amount, val withdrawStatus: WithdrawStatus ) diff --git a/cashier/src/main/res/layout-w550dp/fragment_balance.xml b/cashier/src/main/res/layout-w550dp/fragment_balance.xml index d04698b..40fa6af 100644 --- a/cashier/src/main/res/layout-w550dp/fragment_balance.xml +++ b/cashier/src/main/res/layout-w550dp/fragment_balance.xml @@ -184,7 +184,7 @@ android:layout_height="wrap_content" android:ems="6" android:imeOptions="actionGo" - android:inputType="number" + android:inputType="number|numberDecimal" android:maxLength="4" /> diff --git a/cashier/src/main/res/layout/fragment_balance.xml b/cashier/src/main/res/layout/fragment_balance.xml index 5dafc59..b50cfa9 100644 --- a/cashier/src/main/res/layout/fragment_balance.xml +++ b/cashier/src/main/res/layout/fragment_balance.xml @@ -185,7 +185,7 @@ android:layout_height="wrap_content" android:ems="6" android:imeOptions="actionGo" - android:inputType="number" + android:inputType="number|numberDecimal" android:maxLength="4" /> diff --git a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt index 48bd643..49b699f 100644 --- a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt +++ b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt @@ -64,7 +64,7 @@ data class Amount( * of 50_000_000 would correspond to 50 cents. */ val fraction: Int -) { +) : Comparable { companion object { @@ -88,10 +88,14 @@ data class Amount( fun fromJSONString(str: String): Amount { val split = str.split(":") if (split.size != 2) throw AmountParserException("Invalid Amount Format") - // currency - val currency = checkCurrency(split[0]) + return fromString(split[0], split[1]) + } + + @Throws(AmountParserException::class) + @SuppressLint("CheckedExceptions") + fun fromString(currency: String, str: String): Amount { // value - val valueSplit = split[1].split(".") + val valueSplit = str.split(".") val value = checkValue(valueSplit[0].toLongOrNull()) // fraction val fraction: Int = if (valueSplit.size > 1) { @@ -103,7 +107,7 @@ data class Amount( ?.roundToInt() checkFraction(fraction) } else 0 - return Amount(currency, value, fraction) + return Amount(checkCurrency(currency), value, fraction) } @Throws(AmountParserException::class) @@ -197,4 +201,17 @@ data class Amount( return "$amountStr $currency" } + override fun compareTo(other: Amount): Int { + check(currency == other.currency) { "Can only compare amounts with the same currency" } + when { + value == other.value -> { + if (fraction < other.fraction) return -1 + if (fraction > other.fraction) return 1 + return 0 + } + value < other.value -> return -1 + else -> return 1 + } + } + } diff --git a/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt b/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt index c09da3c..97d9667 100644 --- a/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt +++ b/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt @@ -275,6 +275,27 @@ class AmountTest { assertFalse(Amount.fromJSONString("EUR:0001.0").isZero()) } + @Test + fun `test comparision`() { + assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0")) + assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:1")) + assertTrue(Amount.fromJSONString("EUR:0") == Amount.fromJSONString("EUR:0")) + assertTrue(Amount.fromJSONString("EUR:42") == Amount.fromJSONString("EUR:42")) + assertTrue(Amount.fromJSONString("EUR:42.00000001") == Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:42.00000001") >= Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:42.00000002") >= Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:42.00000002") > Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:0.00000002") > Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0.00000001") > Amount.fromJSONString("EUR:0")) + assertTrue(Amount.fromJSONString("EUR:2") > Amount.fromJSONString("EUR:1")) + + assertThrows("could compare amounts with different currencies") { + Amount.fromJSONString("EUR:0.5") < Amount.fromJSONString("USD:0.50000001") + } + } + private inline fun assertThrows( msg: String? = null, function: () -> Any -- cgit v1.2.3