From 35f7ed512ed7445362d6caee1bf60441f4ce979e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 4 Aug 2020 09:46:38 -0300 Subject: [pos] Implement new refund API (untested since there is no wallet support) Also do a bit of code cleanup and minor refactorings This also removes the volley HTTP library which is not needed anymore --- merchant-lib/src/main/AndroidManifest.xml | 1 - .../src/main/java/net/taler/merchantlib/Config.kt | 53 +++++++ .../java/net/taler/merchantlib/ConfigResponse.kt | 34 ----- .../main/java/net/taler/merchantlib/MerchantApi.kt | 12 ++ .../java/net/taler/merchantlib/MerchantConfig.kt | 38 ----- .../src/main/java/net/taler/merchantlib/Orders.kt | 85 +++++++++++ .../java/net/taler/merchantlib/PostOrderRequest.kt | 85 ----------- .../src/main/java/net/taler/merchantlib/Refunds.kt | 43 ++++++ .../java/net/taler/merchantlib/MerchantApiTest.kt | 98 +++++++++++- merchant-terminal/build.gradle | 3 - merchant-terminal/src/main/AndroidManifest.xml | 1 - .../java/net/taler/merchantpos/MainViewModel.kt | 7 +- .../src/main/java/net/taler/merchantpos/Utils.kt | 15 -- .../net/taler/merchantpos/config/ConfigFragment.kt | 164 +++++++++++++++++++++ .../merchantpos/config/MerchantConfigFragment.kt | 164 --------------------- .../taler/merchantpos/config/MerchantRequest.kt | 59 -------- .../taler/merchantpos/history/HistoryFragment.kt | 108 ++++++++++++++ .../merchantpos/history/MerchantHistoryFragment.kt | 108 -------------- .../taler/merchantpos/history/RefundFragment.kt | 109 -------------- .../net/taler/merchantpos/history/RefundManager.kt | 134 ----------------- .../taler/merchantpos/history/RefundUriFragment.kt | 69 --------- .../taler/merchantpos/order/CategoriesFragment.kt | 40 ----- .../net/taler/merchantpos/order/CategoryAdapter.kt | 62 ++++++++ .../net/taler/merchantpos/order/OrderAdapter.kt | 114 ++++++++++++++ .../taler/merchantpos/order/OrderStateFragment.kt | 92 ------------ .../taler/merchantpos/payment/PaymentManager.kt | 4 +- .../net/taler/merchantpos/refund/RefundFragment.kt | 108 ++++++++++++++ .../net/taler/merchantpos/refund/RefundManager.kt | 91 ++++++++++++ .../taler/merchantpos/refund/RefundUriFragment.kt | 69 +++++++++ .../main/res/layout/fragment_merchant_config.xml | 2 +- .../src/main/res/layout/fragment_refund.xml | 2 +- .../src/main/res/navigation/nav_graph.xml | 8 +- 32 files changed, 1013 insertions(+), 969 deletions(-) create mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/Config.kt delete mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/ConfigResponse.kt delete mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt create mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt delete mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt create mode 100644 merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryFragment.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoryAdapter.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderAdapter.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundFragment.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundUriFragment.kt diff --git a/merchant-lib/src/main/AndroidManifest.xml b/merchant-lib/src/main/AndroidManifest.xml index 7318c07..1408b33 100644 --- a/merchant-lib/src/main/AndroidManifest.xml +++ b/merchant-lib/src/main/AndroidManifest.xml @@ -17,7 +17,6 @@ - diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Config.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Config.kt new file mode 100644 index 0000000..eb09485 --- /dev/null +++ b/merchant-lib/src/main/java/net/taler/merchantlib/Config.kt @@ -0,0 +1,53 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantlib + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ConfigResponse( + /** + * libtool-style representation of the Merchant protocol version, see + * https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + * The format is "current:revision:age". + */ + val version: String, + + /** + Currency supported by this backend. + */ + val currency: String +) + +@Serializable +data class MerchantConfig( + @SerialName("base_url") + val baseUrl: String, + // TODO remove instance when it is part of baseURL + val instance: String? = null, + @SerialName("api_key") + val apiKey: String +) { + fun urlFor(endpoint: String): String { + val sb = StringBuilder(baseUrl) + if (sb.last() != '/') sb.append('/') + instance?.let { sb.append("instances/$it/") } + sb.append(endpoint) + return sb.toString() + } +} diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/ConfigResponse.kt b/merchant-lib/src/main/java/net/taler/merchantlib/ConfigResponse.kt deleted file mode 100644 index 49164e6..0000000 --- a/merchant-lib/src/main/java/net/taler/merchantlib/ConfigResponse.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantlib - -import kotlinx.serialization.Serializable - -@Serializable -data class ConfigResponse( - /** - * libtool-style representation of the Merchant protocol version, see - * https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning - * The format is "current:revision:age". - */ - val version: String, - - /** - Currency supported by this backend. - */ - val currency: String -) diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt index 96892f5..c92d4d2 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt @@ -72,6 +72,18 @@ class MerchantApi(private val httpClient: HttpClient) { } as OrderHistory } + suspend fun giveRefund( + merchantConfig: MerchantConfig, + orderId: String, + request: RefundRequest + ): Response = response { + httpClient.post(merchantConfig.urlFor("private/orders/$orderId/refund")) { + header(Authorization, "ApiKey ${merchantConfig.apiKey}") + contentType(Json) + body = request + } as RefundResponse + } + } fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) { diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt deleted file mode 100644 index a8d113e..0000000 --- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantConfig.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantlib - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class MerchantConfig( - @SerialName("base_url") - val baseUrl: String, - // TODO remove instance when it is part of baseURL - val instance: String? = null, - @SerialName("api_key") - val apiKey: String -) { - fun urlFor(endpoint: String): String { - val sb = StringBuilder(baseUrl) - if (sb.last() != '/') sb.append('/') - instance?.let { sb.append("instances/$it/") } - sb.append(endpoint) - return sb.toString() - } -} diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt new file mode 100644 index 0000000..783dd19 --- /dev/null +++ b/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt @@ -0,0 +1,85 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantlib + +import kotlinx.serialization.Decoder +import kotlinx.serialization.Encoder +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.json.JsonInput +import kotlinx.serialization.json.JsonObject +import net.taler.common.ContractTerms +import net.taler.common.Duration + +@Serializable +data class PostOrderRequest( + @SerialName("order") + val contractTerms: ContractTerms, + @SerialName("refund_delay") + val refundDelay: Duration? = null +) + +@Serializable +data class PostOrderResponse( + @SerialName("order_id") + val orderId: String +) + +@Serializable +sealed class CheckPaymentResponse { + abstract val paid: Boolean + + @Serializer(forClass = CheckPaymentResponse::class) + companion object : KSerializer { + override fun deserialize(decoder: Decoder): CheckPaymentResponse { + val input = decoder as JsonInput + val tree = input.decodeJson() as JsonObject + val orderStatus = tree.getPrimitive("order_status").content +// return if (orderStatus == "paid") decoder.json.fromJson(Paid.serializer(), tree) +// else decoder.json.fromJson(Unpaid.serializer(), tree) + // manual parsing due to https://github.com/Kotlin/kotlinx.serialization/issues/576 + return if (orderStatus == "paid") Paid( + refunded = tree.getPrimitive("refunded").boolean + ) else Unpaid( + talerPayUri = tree.getPrimitive("taler_pay_uri").content + ) + } + + override fun serialize(encoder: Encoder, value: CheckPaymentResponse) = when (value) { + is Unpaid -> Unpaid.serializer().serialize(encoder, value) + is Paid -> Paid.serializer().serialize(encoder, value) + } + } + + @Serializable + data class Unpaid( + override val paid: Boolean = false, + @SerialName("taler_pay_uri") + val talerPayUri: String, + @SerialName("already_paid_order_id") + val alreadyPaidOrderId: String? = null + ) : CheckPaymentResponse() + + @Serializable + data class Paid( + override val paid: Boolean = true, + val refunded: Boolean + ) : CheckPaymentResponse() + +} diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt b/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt deleted file mode 100644 index 783dd19..0000000 --- a/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantlib - -import kotlinx.serialization.Decoder -import kotlinx.serialization.Encoder -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Serializer -import kotlinx.serialization.json.JsonInput -import kotlinx.serialization.json.JsonObject -import net.taler.common.ContractTerms -import net.taler.common.Duration - -@Serializable -data class PostOrderRequest( - @SerialName("order") - val contractTerms: ContractTerms, - @SerialName("refund_delay") - val refundDelay: Duration? = null -) - -@Serializable -data class PostOrderResponse( - @SerialName("order_id") - val orderId: String -) - -@Serializable -sealed class CheckPaymentResponse { - abstract val paid: Boolean - - @Serializer(forClass = CheckPaymentResponse::class) - companion object : KSerializer { - override fun deserialize(decoder: Decoder): CheckPaymentResponse { - val input = decoder as JsonInput - val tree = input.decodeJson() as JsonObject - val orderStatus = tree.getPrimitive("order_status").content -// return if (orderStatus == "paid") decoder.json.fromJson(Paid.serializer(), tree) -// else decoder.json.fromJson(Unpaid.serializer(), tree) - // manual parsing due to https://github.com/Kotlin/kotlinx.serialization/issues/576 - return if (orderStatus == "paid") Paid( - refunded = tree.getPrimitive("refunded").boolean - ) else Unpaid( - talerPayUri = tree.getPrimitive("taler_pay_uri").content - ) - } - - override fun serialize(encoder: Encoder, value: CheckPaymentResponse) = when (value) { - is Unpaid -> Unpaid.serializer().serialize(encoder, value) - is Paid -> Paid.serializer().serialize(encoder, value) - } - } - - @Serializable - data class Unpaid( - override val paid: Boolean = false, - @SerialName("taler_pay_uri") - val talerPayUri: String, - @SerialName("already_paid_order_id") - val alreadyPaidOrderId: String? = null - ) : CheckPaymentResponse() - - @Serializable - data class Paid( - override val paid: Boolean = true, - val refunded: Boolean - ) : CheckPaymentResponse() - -} diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt new file mode 100644 index 0000000..61f0ab7 --- /dev/null +++ b/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt @@ -0,0 +1,43 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantlib + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.taler.common.Amount + +@Serializable +data class RefundRequest( + /** + * Amount to be refunded + */ + val refund: Amount, + + /** + * Human-readable refund justification + */ + val reason: String +) + +@Serializable +data class RefundResponse( + /** + * URL (handled by the backend) that the wallet should access to trigger refund processing. + */ + @SerialName("taler_refund_uri") + val talerRefundUri: String +) diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt index 437697b..f9f5e87 100644 --- a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt +++ b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt @@ -16,11 +16,12 @@ package net.taler.merchantlib -import io.ktor.http.HttpStatusCode +import io.ktor.http.HttpStatusCode.Companion.NotFound import kotlinx.coroutines.runBlocking import net.taler.common.Amount import net.taler.common.ContractProduct import net.taler.common.ContractTerms +import net.taler.common.Timestamp import net.taler.merchantlib.MockHttpClient.giveJsonResponse import net.taler.merchantlib.MockHttpClient.httpClient import org.junit.Assert.assertEquals @@ -35,6 +36,7 @@ class MerchantApiTest { instance = "testInstance", apiKey = "apiKeyFooBar" ) + private val orderId = "orderIdFoo" @Test fun testGetConfig() = runBlocking { @@ -95,7 +97,7 @@ class MerchantApiTest { httpClient.giveJsonResponse( "http://example.net/instances/testInstance/private/orders", - statusCode = HttpStatusCode.NotFound + statusCode = NotFound ) { """{ "code": 2000, @@ -110,7 +112,6 @@ class MerchantApiTest { @Test fun testCheckOrder() = runBlocking { - val orderId = "orderIdFoo" val unpaidResponse = CheckPaymentResponse.Unpaid(false, "http://taler.net/foo") httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders/$orderId") { """{ @@ -125,7 +126,7 @@ class MerchantApiTest { httpClient.giveJsonResponse( "http://example.net/instances/testInstance/private/orders/$orderId", - statusCode = HttpStatusCode.NotFound + statusCode = NotFound ) { """{ "code": 2909, @@ -138,4 +139,93 @@ class MerchantApiTest { } } + @Test + fun testDeleteOrder() = runBlocking { + httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders/$orderId") { + "{}" + } + api.deleteOrder(merchantConfig, orderId).assertSuccess {} + + httpClient.giveJsonResponse( + "http://example.net/instances/testInstance/private/orders/$orderId", + statusCode = NotFound + ) { + """{ + "code": 2511, + "hint": "Order unknown" + } + """.trimIndent() + } + api.deleteOrder(merchantConfig, orderId).assertFailure { + assertTrue(it.contains("2511")) + assertTrue(it.contains("Order unknown")) + } + } + + @Test + fun testGetOrderHistory() = runBlocking { + httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders") { + """{ "orders": [ + { + "order_id": "2020.217-0281FGXCS25P2", + "row_id": 183, + "timestamp": { + "t_ms": 1596542338000 + }, + "amount": "TESTKUDOS:1", + "summary": "Chips", + "refundable": true, + "paid": true + }, + { + "order_id": "2020.216-01G2ZPXSP6BYT", + "row_id": 154, + "timestamp": { + "t_ms": 1596468174000 + }, + "amount": "TESTKUDOS:0.8", + "summary": "Peanuts", + "refundable": false, + "paid": false + } + ] + }""".trimIndent() + } + api.getOrderHistory(merchantConfig).assertSuccess { + assertEquals(2, it.orders.size) + + val order1 = it.orders[0] + assertEquals(Amount("TESTKUDOS", 1, 0), order1.amount) + assertEquals("2020.217-0281FGXCS25P2", order1.orderId) + assertEquals(true, order1.paid) + assertEquals(true, order1.refundable) + assertEquals("Chips", order1.summary) + assertEquals(Timestamp(1596542338000), order1.timestamp) + + val order2 = it.orders[1] + assertEquals(Amount("TESTKUDOS", 0, 80000000), order2.amount) + assertEquals("2020.216-01G2ZPXSP6BYT", order2.orderId) + assertEquals(false, order2.paid) + assertEquals(false, order2.refundable) + assertEquals("Peanuts", order2.summary) + assertEquals(Timestamp(1596468174000), order2.timestamp) + } + } + + @Test + fun testGiveRefund() = runBlocking { + httpClient.giveJsonResponse("http://example.net/instances/testInstance/private/orders/$orderId/refund") { + """{ + "taler_refund_uri": "taler://refund/foo/bar" + }""".trimIndent() + } + val request = RefundRequest( + refund = Amount("TESTKUDOS", 5, 0), + reason = "Give me my money back now!!!" + ) + api.giveRefund(merchantConfig, orderId, request).assertSuccess { + assertEquals("taler://refund/foo/bar", it.talerRefundUri) + } + } + } diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle index 4499892..1bdc138 100644 --- a/merchant-terminal/build.gradle +++ b/merchant-terminal/build.gradle @@ -68,9 +68,6 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - // HTTP Requests - implementation 'com.android.volley:volley:1.1.1' - implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" testImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/merchant-terminal/src/main/AndroidManifest.xml b/merchant-terminal/src/main/AndroidManifest.xml index 3d89fee..1518293 100644 --- a/merchant-terminal/src/main/AndroidManifest.xml +++ b/merchant-terminal/src/main/AndroidManifest.xml @@ -19,7 +19,6 @@ package="net.taler.merchantpos"> - Any) : - Response.ErrorListener { - - override fun onErrorResponse(error: VolleyError) { - val body = error.networkResponse.data?.let { String(it) } - Log.e(TAG, "$error $body") - onError.invoke(error) - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt new file mode 100644 index 0000000..daddbff --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt @@ -0,0 +1,164 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantpos.config + +import android.net.Uri +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.fragment_merchant_config.* +import net.taler.common.navigate +import net.taler.merchantpos.MainViewModel +import net.taler.merchantpos.R +import net.taler.merchantpos.config.ConfigFragmentDirections.Companion.actionSettingsToOrder +import net.taler.merchantpos.topSnackbar + +/** + * Fragment that displays merchant settings. + */ +class ConfigFragment : Fragment() { + + private val model: MainViewModel by activityViewModels() + private val configManager by lazy { model.configManager } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_merchant_config, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + configUrlView.editText!!.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) checkForUrlCredentials() + } + okButton.setOnClickListener { + checkForUrlCredentials() + val inputUrl = configUrlView.editText!!.text + val url = if (inputUrl.startsWith("http")) { + inputUrl.toString() + } else { + "https://$inputUrl".also { configUrlView.editText!!.setText(it) } + } + progressBar.visibility = VISIBLE + okButton.visibility = INVISIBLE + val config = Config( + configUrl = url, + username = usernameView.editText!!.text.toString(), + password = passwordView.editText!!.text.toString() + ) + configManager.fetchConfig(config, true, savePasswordCheckBox.isChecked) + configManager.configUpdateResult.observe(viewLifecycleOwner, Observer { result -> + if (onConfigUpdate(result)) { + configManager.configUpdateResult.removeObservers(viewLifecycleOwner) + } + }) + } + forgetPasswordButton.setOnClickListener { + configManager.forgetPassword() + passwordView.editText!!.text = null + forgetPasswordButton.visibility = GONE + } + configDocsView.movementMethod = LinkMovementMethod.getInstance() + updateView(savedInstanceState == null) + } + + override fun onStart() { + super.onStart() + // focus password if this is the only empty field + if (passwordView.editText!!.text.isBlank() + && !configUrlView.editText!!.text.isBlank() + && !usernameView.editText!!.text.isBlank() + ) { + passwordView.requestFocus() + } + } + + private fun updateView(isInitialization: Boolean = false) { + val config = configManager.config + configUrlView.editText!!.setText( + if (isInitialization && config.configUrl.isBlank()) CONFIG_URL_DEMO + else config.configUrl + ) + usernameView.editText!!.setText( + if (isInitialization && config.username.isBlank()) CONFIG_USERNAME_DEMO + else config.username + ) + passwordView.editText!!.setText( + if (isInitialization && config.password.isBlank()) CONFIG_PASSWORD_DEMO + else config.password + ) + forgetPasswordButton.visibility = if (config.hasPassword()) VISIBLE else GONE + } + + private fun checkForUrlCredentials() { + val text = configUrlView.editText!!.text.toString() + Uri.parse(text)?.userInfo?.let { userInfo -> + if (userInfo.contains(':')) { + val (user, pass) = userInfo.split(':') + val strippedUrl = text.replace("${userInfo}@", "") + configUrlView.editText!!.setText(strippedUrl) + usernameView.editText!!.setText(user) + passwordView.editText!!.setText(pass) + } + } + } + + /** + * Processes updated config and returns true, if observer can be removed. + */ + private fun onConfigUpdate(result: ConfigUpdateResult?) = when (result) { + null -> false + is ConfigUpdateResult.Error -> { + onError(result.msg) + true + } + is ConfigUpdateResult.Success -> { + onConfigReceived(result.currency) + true + } + } + + private fun onConfigReceived(currency: String) { + onResultReceived() + updateView() + topSnackbar(requireView(), getString(R.string.config_changed, currency), LENGTH_LONG) + navigate(actionSettingsToOrder()) + } + + private fun onError(msg: String) { + onResultReceived() + Snackbar.make(requireView(), msg, LENGTH_LONG).show() + } + + private fun onResultReceived() { + progressBar.visibility = INVISIBLE + okButton.visibility = VISIBLE + } + +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt deleted file mode 100644 index 77a87fb..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantpos.config - -import android.net.Uri -import android.os.Bundle -import android.text.method.LinkMovementMethod -import android.view.LayoutInflater -import android.view.View -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG -import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.fragment_merchant_config.* -import net.taler.common.navigate -import net.taler.merchantpos.MainViewModel -import net.taler.merchantpos.R -import net.taler.merchantpos.config.MerchantConfigFragmentDirections.Companion.actionSettingsToOrder -import net.taler.merchantpos.topSnackbar - -/** - * Fragment that displays merchant settings. - */ -class MerchantConfigFragment : Fragment() { - - private val model: MainViewModel by activityViewModels() - private val configManager by lazy { model.configManager } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_merchant_config, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - configUrlView.editText!!.setOnFocusChangeListener { _, hasFocus -> - if (!hasFocus) checkForUrlCredentials() - } - okButton.setOnClickListener { - checkForUrlCredentials() - val inputUrl = configUrlView.editText!!.text - val url = if (inputUrl.startsWith("http")) { - inputUrl.toString() - } else { - "https://$inputUrl".also { configUrlView.editText!!.setText(it) } - } - progressBar.visibility = VISIBLE - okButton.visibility = INVISIBLE - val config = Config( - configUrl = url, - username = usernameView.editText!!.text.toString(), - password = passwordView.editText!!.text.toString() - ) - configManager.fetchConfig(config, true, savePasswordCheckBox.isChecked) - configManager.configUpdateResult.observe(viewLifecycleOwner, Observer { result -> - if (onConfigUpdate(result)) { - configManager.configUpdateResult.removeObservers(viewLifecycleOwner) - } - }) - } - forgetPasswordButton.setOnClickListener { - configManager.forgetPassword() - passwordView.editText!!.text = null - forgetPasswordButton.visibility = GONE - } - configDocsView.movementMethod = LinkMovementMethod.getInstance() - updateView(savedInstanceState == null) - } - - override fun onStart() { - super.onStart() - // focus password if this is the only empty field - if (passwordView.editText!!.text.isBlank() - && !configUrlView.editText!!.text.isBlank() - && !usernameView.editText!!.text.isBlank() - ) { - passwordView.requestFocus() - } - } - - private fun updateView(isInitialization: Boolean = false) { - val config = configManager.config - configUrlView.editText!!.setText( - if (isInitialization && config.configUrl.isBlank()) CONFIG_URL_DEMO - else config.configUrl - ) - usernameView.editText!!.setText( - if (isInitialization && config.username.isBlank()) CONFIG_USERNAME_DEMO - else config.username - ) - passwordView.editText!!.setText( - if (isInitialization && config.password.isBlank()) CONFIG_PASSWORD_DEMO - else config.password - ) - forgetPasswordButton.visibility = if (config.hasPassword()) VISIBLE else GONE - } - - private fun checkForUrlCredentials() { - val text = configUrlView.editText!!.text.toString() - Uri.parse(text)?.userInfo?.let { userInfo -> - if (userInfo.contains(':')) { - val (user, pass) = userInfo.split(':') - val strippedUrl = text.replace("${userInfo}@", "") - configUrlView.editText!!.setText(strippedUrl) - usernameView.editText!!.setText(user) - passwordView.editText!!.setText(pass) - } - } - } - - /** - * Processes updated config and returns true, if observer can be removed. - */ - private fun onConfigUpdate(result: ConfigUpdateResult?) = when (result) { - null -> false - is ConfigUpdateResult.Error -> { - onError(result.msg) - true - } - is ConfigUpdateResult.Success -> { - onConfigReceived(result.currency) - true - } - } - - private fun onConfigReceived(currency: String) { - onResultReceived() - updateView() - topSnackbar(requireView(), getString(R.string.config_changed, currency), LENGTH_LONG) - navigate(actionSettingsToOrder()) - } - - private fun onError(msg: String) { - onResultReceived() - Snackbar.make(requireView(), msg, LENGTH_LONG).show() - } - - private fun onResultReceived() { - progressBar.visibility = INVISIBLE - okButton.visibility = VISIBLE - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt deleted file mode 100644 index 5d41196..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantpos.config - -import android.net.Uri -import android.util.ArrayMap -import com.android.volley.Response -import com.android.volley.toolbox.JsonObjectRequest -import net.taler.merchantlib.MerchantConfig -import net.taler.merchantpos.LogErrorListener -import org.json.JSONObject - -class MerchantRequest( - method: Int, - private val merchantConfig: MerchantConfig, - endpoint: String, - params: Map?, - jsonRequest: JSONObject?, - listener: Response.Listener, - errorListener: LogErrorListener -) : - JsonObjectRequest( - method, - merchantConfig.legacyUrl(endpoint, params), - jsonRequest, - listener, - errorListener - ) { - - override fun getHeaders(): MutableMap { - val headerMap = ArrayMap() - headerMap["Authorization"] = "ApiKey " + merchantConfig.apiKey - return headerMap - } - -} - -private fun MerchantConfig.legacyUrl(endpoint: String, params: Map?): String { - val uriBuilder = Uri.parse(baseUrl).buildUpon() - uriBuilder.appendPath(endpoint) - params?.forEach { - uriBuilder.appendQueryParameter(it.key, it.value) - } - return uriBuilder.toString() -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryFragment.kt new file mode 100644 index 0000000..8cc435a --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryFragment.kt @@ -0,0 +1,108 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantpos.history + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.fragment_merchant_history.* +import net.taler.common.exhaustive +import net.taler.common.navigate +import net.taler.merchantlib.OrderHistoryEntry +import net.taler.merchantpos.MainViewModel +import net.taler.merchantpos.R +import net.taler.merchantpos.history.HistoryFragmentDirections.Companion.actionGlobalMerchantSettings +import net.taler.merchantpos.history.HistoryFragmentDirections.Companion.actionNavHistoryToRefundFragment + +internal interface RefundClickListener { + fun onRefundClicked(item: OrderHistoryEntry) +} + +/** + * Fragment to display the merchant's payment history, received from the backend. + */ +class HistoryFragment : Fragment(), RefundClickListener { + + companion object { + const val TAG = "taler-merchant" + } + + private val model: MainViewModel by activityViewModels() + private val historyManager by lazy { model.historyManager } + private val refundManager by lazy { model.refundManager } + + private val historyListAdapter = HistoryItemAdapter(this) + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_merchant_history, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + list_history.apply { + layoutManager = LinearLayoutManager(requireContext()) + addItemDecoration(DividerItemDecoration(context, VERTICAL)) + adapter = historyListAdapter + } + + swipeRefresh.setOnRefreshListener { + Log.v(TAG, "refreshing!") + historyManager.fetchHistory() + } + historyManager.isLoading.observe(viewLifecycleOwner, Observer { loading -> + Log.v(TAG, "setting refreshing to $loading") + swipeRefresh.isRefreshing = loading + }) + historyManager.items.observe(viewLifecycleOwner, Observer { result -> + when (result) { + is HistoryResult.Error -> onError(result.msg) + is HistoryResult.Success -> historyListAdapter.setData(result.items) + }.exhaustive + }) + } + + override fun onStart() { + super.onStart() + if (model.configManager.merchantConfig?.baseUrl == null) { + navigate(actionGlobalMerchantSettings()) + } else { + historyManager.fetchHistory() + } + } + + private fun onError(msg: String) { + Snackbar.make(requireView(), msg, LENGTH_LONG).show() + } + + override fun onRefundClicked(item: OrderHistoryEntry) { + refundManager.startRefund(item) + navigate(actionNavHistoryToRefundFragment()) + } + +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt deleted file mode 100644 index 596b8b0..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantpos.history - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG -import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.fragment_merchant_history.* -import net.taler.common.exhaustive -import net.taler.common.navigate -import net.taler.merchantlib.OrderHistoryEntry -import net.taler.merchantpos.MainViewModel -import net.taler.merchantpos.R -import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionGlobalMerchantSettings -import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionNavHistoryToRefundFragment - -internal interface RefundClickListener { - fun onRefundClicked(item: OrderHistoryEntry) -} - -/** - * Fragment to display the merchant's payment history, received from the backend. - */ -class MerchantHistoryFragment : Fragment(), RefundClickListener { - - companion object { - const val TAG = "taler-merchant" - } - - private val model: MainViewModel by activityViewModels() - private val historyManager by lazy { model.historyManager } - private val refundManager by lazy { model.refundManager } - - private val historyListAdapter = HistoryItemAdapter(this) - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_merchant_history, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - list_history.apply { - layoutManager = LinearLayoutManager(requireContext()) - addItemDecoration(DividerItemDecoration(context, VERTICAL)) - adapter = historyListAdapter - } - - swipeRefresh.setOnRefreshListener { - Log.v(TAG, "refreshing!") - historyManager.fetchHistory() - } - historyManager.isLoading.observe(viewLifecycleOwner, Observer { loading -> - Log.v(TAG, "setting refreshing to $loading") - swipeRefresh.isRefreshing = loading - }) - historyManager.items.observe(viewLifecycleOwner, Observer { result -> - when (result) { - is HistoryResult.Error -> onError(result.msg) - is HistoryResult.Success -> historyListAdapter.setData(result.items) - }.exhaustive - }) - } - - override fun onStart() { - super.onStart() - if (model.configManager.merchantConfig?.baseUrl == null) { - navigate(actionGlobalMerchantSettings()) - } else { - historyManager.fetchHistory() - } - } - - private fun onError(msg: String) { - Snackbar.make(requireView(), msg, LENGTH_LONG).show() - } - - override fun onRefundClicked(item: OrderHistoryEntry) { - refundManager.startRefund(item) - navigate(actionNavHistoryToRefundFragment()) - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt deleted file mode 100644 index 17d78f6..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantpos.history - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.StringRes -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import androidx.navigation.fragment.findNavController -import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG -import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.fragment_refund.* -import net.taler.common.Amount -import net.taler.common.AmountParserException -import net.taler.common.fadeIn -import net.taler.common.fadeOut -import net.taler.common.navigate -import net.taler.merchantlib.OrderHistoryEntry -import net.taler.merchantpos.MainViewModel -import net.taler.merchantpos.R -import net.taler.merchantpos.history.RefundFragmentDirections.Companion.actionRefundFragmentToRefundUriFragment -import net.taler.merchantpos.history.RefundResult.AlreadyRefunded -import net.taler.merchantpos.history.RefundResult.Error -import net.taler.merchantpos.history.RefundResult.PastDeadline -import net.taler.merchantpos.history.RefundResult.Success - -class RefundFragment : Fragment() { - - private val model: MainViewModel by activityViewModels() - private val refundManager by lazy { model.refundManager } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_refund, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val item = refundManager.toBeRefunded ?: throw IllegalStateException() - amountInputView.setText(item.amount.amountStr) - currencyView.text = item.amount.currency - abortButton.setOnClickListener { findNavController().navigateUp() } - refundButton.setOnClickListener { onRefundButtonClicked(item) } - - refundManager.refundResult.observe(viewLifecycleOwner, Observer { result -> - onRefundResultChanged(result) - }) - } - - private fun onRefundButtonClicked(item: OrderHistoryEntry) { - val inputAmount = try { - Amount.fromString(item.amount.currency, amountInputView.text.toString()) - } catch (e: AmountParserException) { - amountView.error = getString(R.string.refund_error_invalid_amount) - return - } - if (inputAmount > item.amount) { - amountView.error = getString(R.string.refund_error_max_amount, item.amount.amountStr) - return - } - if (inputAmount.isZero()) { - amountView.error = getString(R.string.refund_error_zero) - return - } - amountView.error = null - refundButton.fadeOut() - progressBar.fadeIn() - refundManager.refund(item, inputAmount, reasonInputView.text.toString()) - } - - private fun onRefundResultChanged(result: RefundResult?): Any = when (result) { - Error -> onError(R.string.refund_error_backend) - PastDeadline -> onError(R.string.refund_error_deadline) - AlreadyRefunded -> onError(R.string.refund_error_already_refunded) - is Success -> { - progressBar.fadeOut() - refundButton.fadeIn() - navigate(actionRefundFragmentToRefundUriFragment()) - } - null -> { // no-op - } - } - - private fun onError(@StringRes res: Int) { - Snackbar.make(requireView(), res, LENGTH_LONG).show() - progressBar.fadeOut() - refundButton.fadeIn() - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt deleted file mode 100644 index 7f9b4c5..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantpos.history - -import android.util.Log -import androidx.annotation.UiThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.android.volley.Request.Method.POST -import com.android.volley.RequestQueue -import com.android.volley.Response.Listener -import com.android.volley.VolleyError -import net.taler.common.Amount -import net.taler.merchantlib.OrderHistoryEntry -import net.taler.merchantpos.LogErrorListener -import net.taler.merchantpos.config.ConfigManager -import net.taler.merchantpos.config.MerchantRequest -import org.json.JSONObject - -sealed class RefundResult { - object Error : RefundResult() - object PastDeadline : RefundResult() - object AlreadyRefunded : RefundResult() - class Success( - val refundUri: String, - val item: OrderHistoryEntry, - val amount: Amount, - val reason: String - ) : RefundResult() -} - -class RefundManager( - private val configManager: ConfigManager, - private val queue: RequestQueue -) { - - companion object { - val TAG = RefundManager::class.java.simpleName - } - - var toBeRefunded: OrderHistoryEntry? = null - private set - - private val mRefundResult = MutableLiveData() - internal val refundResult: LiveData = mRefundResult - - @UiThread - internal fun startRefund(item: OrderHistoryEntry) { - toBeRefunded = item - mRefundResult.value = null - } - - @UiThread - internal fun abortRefund() { - toBeRefunded = null - mRefundResult.value = null - } - - @UiThread - internal fun refund(item: OrderHistoryEntry, amount: Amount, reason: String) { - val merchantConfig = configManager.merchantConfig!! - val refundRequest = mapOf( - "order_id" to item.orderId, - "refund" to amount.toJSONString(), - "reason" to reason - ) - val body = JSONObject(refundRequest) - Log.d(TAG, body.toString(4)) - val req = MerchantRequest(POST, merchantConfig, "refund", null, body, - Listener { onRefundResponse(it, item, amount, reason) }, - LogErrorListener { onRefundError(it) } - ) - queue.add(req) - } - - @UiThread - private fun onRefundResponse( - json: JSONObject, - item: OrderHistoryEntry, - amount: Amount, - reason: String - ) { - if (!json.has("contract_terms")) { - Log.e(TAG, "Contract terms missing: $json") - onRefundError() - return - } - - val contractTerms = json.getJSONObject("contract_terms") - val refundDeadline = if (contractTerms.has("refund_deadline")) { - contractTerms.getJSONObject("refund_deadline").getLong("t_ms") - } else null - val autoRefund = contractTerms.has("auto_refund") - val refundUri = json.getString("taler_refund_uri") - - Log.e("TEST", "refundDeadline: $refundDeadline") - if (refundDeadline != null) Log.e( - "TEST", - "refundDeadline passed: ${System.currentTimeMillis() > refundDeadline}" - ) - Log.e("TEST", "autoRefund: $autoRefund") - Log.e("TEST", "refundUri: $refundUri") - - mRefundResult.value = RefundResult.Success(refundUri, item, amount, reason) - } - - @UiThread - private fun onRefundError(error: VolleyError? = null) { - val data = error?.networkResponse?.data - if (data != null) { - val json = JSONObject(String(data)) - if (json.has("code") && json.getInt("code") == 2602) { - mRefundResult.value = RefundResult.AlreadyRefunded - return - } - } - mRefundResult.value = RefundResult.Error - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt deleted file mode 100644 index 1ea0959..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2020 Taler Systems S.A. - * - * GNU Taler is free software; you can redistribute it and/or modify it under the - * terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3, or (at your option) any later version. - * - * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.merchantpos.history - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import kotlinx.android.synthetic.main.fragment_refund_uri.* -import net.taler.common.NfcManager.Companion.hasNfc -import net.taler.common.QrCodeManager.makeQrCode -import net.taler.merchantpos.MainViewModel -import net.taler.merchantpos.R - -class RefundUriFragment : Fragment() { - - private val model: MainViewModel by activityViewModels() - private val refundManager by lazy { model.refundManager } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_refund_uri, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val result = refundManager.refundResult.value - if (result !is RefundResult.Success) throw IllegalStateException() - - refundQrcodeView.setImageBitmap(makeQrCode(result.refundUri)) - - val introRes = - if (hasNfc(requireContext())) R.string.refund_intro_nfc else R.string.refund_intro - refundIntroView.setText(introRes) - - refundAmountView.text = result.amount.toString() - - refundRefView.text = - getString(R.string.refund_order_ref, result.item.orderId, result.reason) - - cancelRefundButton.setOnClickListener { findNavController().navigateUp() } - completeButton.setOnClickListener { findNavController().navigateUp() } - } - - override fun onDestroy() { - super.onDestroy() - refundManager.abortRefund() - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt index e935d4f..4f8e5af 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt @@ -21,18 +21,14 @@ import android.view.LayoutInflater import android.view.View import android.view.View.INVISIBLE import android.view.ViewGroup -import android.widget.Button import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.Adapter import kotlinx.android.synthetic.main.fragment_categories.* import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R import net.taler.merchantpos.config.Category -import net.taler.merchantpos.order.CategoryAdapter.CategoryViewHolder interface CategorySelectionListener { fun onCategorySelected(category: Category) @@ -69,39 +65,3 @@ class CategoriesFragment : Fragment(), CategorySelectionListener { } } - -private class CategoryAdapter( - private val listener: CategorySelectionListener -) : Adapter() { - - private val categories = ArrayList() - - override fun getItemCount() = categories.size - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder { - val view = - LayoutInflater.from(parent.context).inflate(R.layout.list_item_category, parent, false) - return CategoryViewHolder(view) - } - - override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) { - holder.bind(categories[position]) - } - - fun setItems(items: List) { - categories.clear() - categories.addAll(items) - notifyDataSetChanged() - } - - private inner class CategoryViewHolder(v: View) : RecyclerView.ViewHolder(v) { - private val button: Button = v.findViewById(R.id.button) - - fun bind(category: Category) { - button.text = category.localizedName - button.isPressed = category.selected - button.setOnClickListener { listener.onCategorySelected(category) } - } - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoryAdapter.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoryAdapter.kt new file mode 100644 index 0000000..c690ec5 --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoryAdapter.kt @@ -0,0 +1,62 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantpos.order + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import net.taler.merchantpos.R +import net.taler.merchantpos.config.Category +import net.taler.merchantpos.order.CategoryAdapter.CategoryViewHolder + +internal class CategoryAdapter(private val listener: CategorySelectionListener) : + Adapter() { + + private val categories = ArrayList() + + override fun getItemCount() = categories.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_category, parent, false) + return CategoryViewHolder(view) + } + + override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) { + holder.bind(categories[position]) + } + + fun setItems(items: List) { + categories.clear() + categories.addAll(items) + notifyDataSetChanged() + } + + internal inner class CategoryViewHolder(v: View) : RecyclerView.ViewHolder(v) { + private val button: Button = v.findViewById(R.id.button) + + fun bind(category: Category) { + button.text = category.localizedName + button.isPressed = category.selected + button.setOnClickListener { listener.onCategorySelected(category) } + } + } + +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderAdapter.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderAdapter.kt new file mode 100644 index 0000000..2180ccb --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderAdapter.kt @@ -0,0 +1,114 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantpos.order + +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.selection.ItemDetailsLookup +import androidx.recyclerview.selection.ItemKeyProvider +import androidx.recyclerview.selection.SelectionTracker +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import net.taler.merchantpos.R +import net.taler.merchantpos.config.ConfigProduct +import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder + +internal class OrderAdapter : Adapter() { + + lateinit var tracker: SelectionTracker + val keyProvider = OrderKeyProvider() + private val itemCallback = object : ItemCallback() { + override fun areItemsTheSame(oldItem: ConfigProduct, newItem: ConfigProduct): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: ConfigProduct, newItem: ConfigProduct): Boolean { + return oldItem.quantity == newItem.quantity + } + } + private val differ = AsyncListDiffer(this, itemCallback) + + override fun getItemCount() = differ.currentList.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_order, parent, false) + return OrderViewHolder(view) + } + + override fun onBindViewHolder(holder: OrderViewHolder, position: Int) { + val item = getItem(position)!! + holder.bind(item, tracker.isSelected(item.id)) + } + + fun setItems(items: List, commitCallback: () -> Unit) { + // toMutableList() is needed for some reason, otherwise doesn't update adapter + differ.submitList(items.toMutableList(), commitCallback) + } + + fun getItem(position: Int): ConfigProduct? = differ.currentList[position] + + fun getItemByKey(key: String): ConfigProduct? { + return differ.currentList.find { it.id == key } + } + + fun findPosition(product: ConfigProduct): Int { + return differ.currentList.indexOf(product) + } + + internal inner class OrderViewHolder(private val v: View) : RecyclerView.ViewHolder(v) { + private val quantity: TextView = v.findViewById(R.id.quantity) + private val name: TextView = v.findViewById(R.id.name) + private val price: TextView = v.findViewById(R.id.price) + + fun bind(product: ConfigProduct, selected: Boolean) { + v.isActivated = selected + quantity.text = product.quantity.toString() + name.text = product.localizedDescription + price.text = product.totalPrice.amountStr + } + } + + internal inner class OrderKeyProvider : ItemKeyProvider(SCOPE_MAPPED) { + override fun getKey(position: Int) = getItem(position)!!.id + override fun getPosition(key: String): Int { + return differ.currentList.indexOfFirst { it.id == key } + } + } + + internal class OrderLineLookup(private val list: RecyclerView) : ItemDetailsLookup() { + override fun getItemDetails(e: MotionEvent): ItemDetails? { + list.findChildViewUnder(e.x, e.y)?.let { view -> + val holder = list.getChildViewHolder(view) + val adapter = list.adapter as OrderAdapter + val position = holder.adapterPosition + return object : ItemDetails() { + override fun getPosition(): Int = position + override fun getSelectionKey(): String = adapter.keyProvider.getKey(position) + override fun inSelectionHotspot(e: MotionEvent) = true + } + } + return null + } + } + +} \ No newline at end of file diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt index f792d7a..b60f3a5 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt @@ -18,32 +18,21 @@ package net.taler.merchantpos.order import android.os.Bundle import android.view.LayoutInflater -import android.view.MotionEvent import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer -import androidx.recyclerview.selection.ItemDetailsLookup -import androidx.recyclerview.selection.ItemKeyProvider import androidx.recyclerview.selection.SelectionPredicates import androidx.recyclerview.selection.SelectionTracker import androidx.recyclerview.selection.StorageStrategy -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.Adapter -import androidx.recyclerview.widget.RecyclerView.ViewHolder import kotlinx.android.synthetic.main.fragment_order_state.* import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R -import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.order.OrderAdapter.OrderLineLookup -import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder class OrderStateFragment : Fragment() { @@ -130,84 +119,3 @@ class OrderStateFragment : Fragment() { } } - -private class OrderAdapter : Adapter() { - - lateinit var tracker: SelectionTracker - val keyProvider = OrderKeyProvider() - private val itemCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ConfigProduct, newItem: ConfigProduct): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: ConfigProduct, newItem: ConfigProduct): Boolean { - return oldItem.quantity == newItem.quantity - } - } - private val differ = AsyncListDiffer(this, itemCallback) - - override fun getItemCount() = differ.currentList.size - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder { - val view = - LayoutInflater.from(parent.context).inflate(R.layout.list_item_order, parent, false) - return OrderViewHolder(view) - } - - override fun onBindViewHolder(holder: OrderViewHolder, position: Int) { - val item = getItem(position)!! - holder.bind(item, tracker.isSelected(item.id)) - } - - fun setItems(items: List, commitCallback: () -> Unit) { - // toMutableList() is needed for some reason, otherwise doesn't update adapter - differ.submitList(items.toMutableList(), commitCallback) - } - - fun getItem(position: Int): ConfigProduct? = differ.currentList[position] - - fun getItemByKey(key: String): ConfigProduct? { - return differ.currentList.find { it.id == key } - } - - fun findPosition(product: ConfigProduct): Int { - return differ.currentList.indexOf(product) - } - - private inner class OrderViewHolder(private val v: View) : ViewHolder(v) { - private val quantity: TextView = v.findViewById(R.id.quantity) - private val name: TextView = v.findViewById(R.id.name) - private val price: TextView = v.findViewById(R.id.price) - - fun bind(product: ConfigProduct, selected: Boolean) { - v.isActivated = selected - quantity.text = product.quantity.toString() - name.text = product.localizedDescription - price.text = product.totalPrice.amountStr - } - } - - private inner class OrderKeyProvider : ItemKeyProvider(SCOPE_MAPPED) { - override fun getKey(position: Int) = getItem(position)!!.id - override fun getPosition(key: String): Int { - return differ.currentList.indexOfFirst { it.id == key } - } - } - - internal class OrderLineLookup(private val list: RecyclerView) : ItemDetailsLookup() { - override fun getItemDetails(e: MotionEvent): ItemDetails? { - list.findChildViewUnder(e.x, e.y)?.let { view -> - val holder = list.getChildViewHolder(view) - val adapter = list.adapter as OrderAdapter - val position = holder.adapterPosition - return object : ItemDetails() { - override fun getPosition(): Int = position - override fun getSelectionKey(): String = adapter.keyProvider.getKey(position) - override fun inSelectionHotspot(e: MotionEvent) = true - } - } - return null - } - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt index bc1e35f..6bab0e6 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt @@ -110,8 +110,8 @@ class PaymentManager( // delete unpaid order val merchantConfig = configManager.merchantConfig!! mPayment.value?.let { payment -> - if (!payment.paid) payment.orderId?.let { orderId -> - Log.e(TAG, "Deleting cancelled and unpaid order $orderId") + if (!payment.paid && payment.error != null) payment.orderId?.let { orderId -> + Log.d(TAG, "Deleting cancelled and unpaid order $orderId") scope.launch(Dispatchers.IO) { api.deleteOrder(merchantConfig, orderId) } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundFragment.kt new file mode 100644 index 0000000..edb2758 --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundFragment.kt @@ -0,0 +1,108 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantpos.refund + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.fragment_refund.* +import net.taler.common.Amount +import net.taler.common.AmountParserException +import net.taler.common.fadeIn +import net.taler.common.fadeOut +import net.taler.common.navigate +import net.taler.merchantlib.OrderHistoryEntry +import net.taler.merchantpos.MainViewModel +import net.taler.merchantpos.R +import net.taler.merchantpos.refund.RefundFragmentDirections.Companion.actionRefundFragmentToRefundUriFragment +import net.taler.merchantpos.refund.RefundResult.AlreadyRefunded +import net.taler.merchantpos.refund.RefundResult.Error +import net.taler.merchantpos.refund.RefundResult.PastDeadline +import net.taler.merchantpos.refund.RefundResult.Success + +class RefundFragment : Fragment() { + + private val model: MainViewModel by activityViewModels() + private val refundManager by lazy { model.refundManager } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_refund, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val item = refundManager.toBeRefunded ?: throw IllegalStateException() + amountInputView.setText(item.amount.amountStr) + currencyView.text = item.amount.currency + abortButton.setOnClickListener { findNavController().navigateUp() } + refundButton.setOnClickListener { onRefundButtonClicked(item) } + + refundManager.refundResult.observe(viewLifecycleOwner, Observer { result -> + onRefundResultChanged(result) + }) + } + + private fun onRefundButtonClicked(item: OrderHistoryEntry) { + val inputAmount = try { + Amount.fromString(item.amount.currency, amountInputView.text.toString()) + } catch (e: AmountParserException) { + amountView.error = getString(R.string.refund_error_invalid_amount) + return + } + if (inputAmount > item.amount) { + amountView.error = getString(R.string.refund_error_max_amount, item.amount.amountStr) + return + } + if (inputAmount.isZero()) { + amountView.error = getString(R.string.refund_error_zero) + return + } + amountView.error = null + refundButton.fadeOut() + progressBar.fadeIn() + refundManager.refund(item, inputAmount, reasonInputView.text.toString()) + } + + private fun onRefundResultChanged(result: RefundResult?): Any = when (result) { + is Error -> onError(result.msg) + PastDeadline -> onError(getString(R.string.refund_error_deadline)) + AlreadyRefunded -> onError(getString(R.string.refund_error_already_refunded)) + is Success -> { + progressBar.fadeOut() + refundButton.fadeIn() + navigate(actionRefundFragmentToRefundUriFragment()) + } + null -> { // no-op + } + } + + private fun onError(msg: String) { + Snackbar.make(requireView(), msg, LENGTH_LONG).show() + progressBar.fadeOut() + refundButton.fadeIn() + } + +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt new file mode 100644 index 0000000..ea2d398 --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt @@ -0,0 +1,91 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantpos.refund + +import androidx.annotation.UiThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.taler.common.Amount +import net.taler.merchantlib.MerchantApi +import net.taler.merchantlib.OrderHistoryEntry +import net.taler.merchantlib.RefundRequest +import net.taler.merchantpos.config.ConfigManager + +sealed class RefundResult { + class Error(val msg: String) : RefundResult() + object PastDeadline : RefundResult() + object AlreadyRefunded : RefundResult() + class Success( + val refundUri: String, + val item: OrderHistoryEntry, + val amount: Amount, + val reason: String + ) : RefundResult() +} + +class RefundManager( + private val configManager: ConfigManager, + private val scope: CoroutineScope, + private val api: MerchantApi +) { + + var toBeRefunded: OrderHistoryEntry? = null + private set + + private val mRefundResult = MutableLiveData() + internal val refundResult: LiveData = mRefundResult + + @UiThread + internal fun startRefund(item: OrderHistoryEntry) { + toBeRefunded = item + mRefundResult.value = null + } + + @UiThread + internal fun abortRefund() { + toBeRefunded = null + mRefundResult.value = null + } + + @UiThread + internal fun refund(item: OrderHistoryEntry, amount: Amount, reason: String) { + val merchantConfig = configManager.merchantConfig!! + val request = RefundRequest(amount, reason) + scope.launch(Dispatchers.IO) { + api.giveRefund(merchantConfig, item.orderId, request).handle(::onRefundError) { + val result = RefundResult.Success( + refundUri = it.talerRefundUri, + item = item, + amount = amount, + reason = reason + ) + mRefundResult.postValue(result) + } + } + } + + @UiThread + private fun onRefundError(msg: String) { + if (msg.contains("2602")) { + mRefundResult.postValue(RefundResult.AlreadyRefunded) + } else mRefundResult.postValue(RefundResult.Error(msg)) + } + +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundUriFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundUriFragment.kt new file mode 100644 index 0000000..b8e8997 --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundUriFragment.kt @@ -0,0 +1,69 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.merchantpos.refund + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_refund_uri.* +import net.taler.common.NfcManager.Companion.hasNfc +import net.taler.common.QrCodeManager.makeQrCode +import net.taler.merchantpos.MainViewModel +import net.taler.merchantpos.R + +class RefundUriFragment : Fragment() { + + private val model: MainViewModel by activityViewModels() + private val refundManager by lazy { model.refundManager } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_refund_uri, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val result = refundManager.refundResult.value + if (result !is RefundResult.Success) throw IllegalStateException() + + refundQrcodeView.setImageBitmap(makeQrCode(result.refundUri)) + + val introRes = + if (hasNfc(requireContext())) R.string.refund_intro_nfc else R.string.refund_intro + refundIntroView.setText(introRes) + + refundAmountView.text = result.amount.toString() + + refundRefView.text = + getString(R.string.refund_order_ref, result.item.orderId, result.reason) + + cancelRefundButton.setOnClickListener { findNavController().navigateUp() } + completeButton.setOnClickListener { findNavController().navigateUp() } + } + + override fun onDestroy() { + super.onDestroy() + refundManager.abortRefund() + } + +} diff --git a/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml b/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml index b19f14c..0061a1c 100644 --- a/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml +++ b/merchant-terminal/src/main/res/layout/fragment_merchant_config.xml @@ -24,7 +24,7 @@ + tools:context=".config.ConfigFragment"> + tools:context=".refund.RefundFragment">