From aa6dad91b20edd0a304423d1edc267cf4e8b5dbe Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 1 Apr 2020 15:02:15 -0300 Subject: [pos] Enable refunds --- build.gradle | 2 +- .../java/net/taler/merchantpos/MainActivity.kt | 4 + .../src/main/java/net/taler/merchantpos/Utils.kt | 15 ++ .../net/taler/merchantpos/config/ConfigManager.kt | 6 +- .../taler/merchantpos/config/MerchantRequest.kt | 3 +- .../taler/merchantpos/history/HistoryManager.kt | 10 +- .../taler/merchantpos/history/RefundFragment.kt | 4 +- .../net/taler/merchantpos/history/RefundManager.kt | 24 ++- .../taler/merchantpos/history/RefundUriFragment.kt | 6 + .../taler/merchantpos/payment/PaymentManager.kt | 20 +-- .../main/res/layout/fragment_process_payment.xml | 155 ++++++++++--------- .../src/main/res/layout/fragment_refund_uri.xml | 137 +++++++++-------- .../src/main/res/layout/list_item_history.xml | 1 - .../src/main/res/navigation/nav_graph.xml | 165 +++++++++++---------- merchant-terminal/src/main/res/values/strings.xml | 10 +- .../src/main/java/net/taler/common/TalerUtils.kt | 7 + 16 files changed, 316 insertions(+), 253 deletions(-) diff --git a/build.gradle b/build.gradle index 57a780b..59e7f30 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:3.6.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt index d6e3747..64f6ceb 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt @@ -47,6 +47,10 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { private var reallyExit = false + companion object { + val TAG = "taler-pos" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt index 578debf..9deb042 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt @@ -16,11 +16,15 @@ package net.taler.merchantpos +import android.util.Log import android.view.View import androidx.annotation.StringRes +import com.android.volley.Response +import com.android.volley.VolleyError import com.google.android.material.snackbar.BaseTransientBottomBar.ANIMATION_MODE_FADE import com.google.android.material.snackbar.BaseTransientBottomBar.Duration import com.google.android.material.snackbar.Snackbar.make +import net.taler.merchantpos.MainActivity.Companion.TAG fun topSnackbar(view: View, text: CharSequence, @Duration duration: Int) { make(view, text, duration) @@ -32,3 +36,14 @@ fun topSnackbar(view: View, text: CharSequence, @Duration duration: Int) { fun topSnackbar(view: View, @StringRes resId: Int, @Duration duration: Int) { topSnackbar(view, view.resources.getText(resId), duration) } + +class LogErrorListener(private val onError: (error: VolleyError) -> 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/ConfigManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt index edb8059..171cf28 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt @@ -26,7 +26,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.android.volley.Request.Method.GET import com.android.volley.RequestQueue -import com.android.volley.Response.ErrorListener import com.android.volley.Response.Listener import com.android.volley.VolleyError import com.android.volley.toolbox.JsonObjectRequest @@ -35,6 +34,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import net.taler.merchantpos.LogErrorListener import net.taler.merchantpos.R import org.json.JSONObject @@ -91,7 +91,7 @@ class ConfigManager( val stringRequest = object : JsonObjectRequest(GET, config.configUrl, null, Listener { onConfigReceived(it, configToSave) }, - ErrorListener { onNetworkError(it) } + LogErrorListener { onNetworkError(it) } ) { // send basic auth header override fun getHeaders(): MutableMap { @@ -117,7 +117,7 @@ class ConfigManager( val params = mapOf("instance" to merchantConfig.instance) val req = MerchantRequest(GET, merchantConfig, "config", params, null, Listener { onMerchantConfigReceived(config, json, merchantConfig, it) }, - ErrorListener { onNetworkError(it) } + LogErrorListener { onNetworkError(it) } ) queue.add(req) } 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 index 8d95378..862dd33 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantRequest.kt @@ -20,6 +20,7 @@ package net.taler.merchantpos.config import android.util.ArrayMap import com.android.volley.Response import com.android.volley.toolbox.JsonObjectRequest +import net.taler.merchantpos.LogErrorListener import org.json.JSONObject class MerchantRequest( @@ -29,7 +30,7 @@ class MerchantRequest( params: Map?, jsonRequest: JSONObject?, listener: Response.Listener, - errorListener: Response.ErrorListener + errorListener: LogErrorListener ) : JsonObjectRequest(method, merchantConfig.urlFor(endpoint, params), jsonRequest, listener, errorListener) { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt index 3aaf3a4..6b95e16 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.android.volley.Request.Method.GET import com.android.volley.RequestQueue -import com.android.volley.Response.ErrorListener import com.android.volley.Response.Listener import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty @@ -29,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import net.taler.common.Amount import net.taler.common.Timestamp +import net.taler.merchantpos.LogErrorListener import net.taler.merchantpos.config.ConfigManager import net.taler.merchantpos.config.MerchantRequest import org.json.JSONObject @@ -36,14 +36,10 @@ import org.json.JSONObject data class HistoryItem( @JsonProperty("order_id") val orderId: String, - @JsonProperty("amount") - val amountStr: String, + val amount: Amount, val summary: String, val timestamp: Timestamp ) { - @get:JsonIgnore - val amount: Amount by lazy { Amount.fromJSONString(amountStr) } - @get:JsonIgnore val time = timestamp.ms } @@ -72,7 +68,7 @@ class HistoryManager( val params = mapOf("instance" to merchantConfig.instance) val req = MerchantRequest(GET, merchantConfig, "history", params, null, Listener { onHistoryResponse(it) }, - ErrorListener { onHistoryError() }) + LogErrorListener { onHistoryError() }) queue.add(req) } 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 index 7652ca4..2b85add 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt @@ -36,6 +36,7 @@ import net.taler.common.navigate 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 @@ -72,7 +73,7 @@ class RefundFragment : Fragment() { return } if (inputAmount > item.amount) { - amountView.error = getString(R.string.refund_error_max_amount, item.amountStr) + amountView.error = getString(R.string.refund_error_max_amount, item.amount.amountStr) return } if (inputAmount.isZero()) { @@ -88,6 +89,7 @@ class RefundFragment : Fragment() { 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() 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 index 910116e..da642d6 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt @@ -22,9 +22,10 @@ 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.ErrorListener import com.android.volley.Response.Listener +import com.android.volley.VolleyError import net.taler.common.Amount +import net.taler.merchantpos.LogErrorListener import net.taler.merchantpos.config.ConfigManager import net.taler.merchantpos.config.MerchantRequest import org.json.JSONObject @@ -32,6 +33,7 @@ import org.json.JSONObject sealed class RefundResult { object Error : RefundResult() object PastDeadline : RefundResult() + object AlreadyRefunded : RefundResult() class Success( val refundUri: String, val item: HistoryItem, @@ -61,6 +63,12 @@ class RefundManager( mRefundResult.value = null } + @UiThread + internal fun abortRefund() { + toBeRefunded = null + mRefundResult.value = null + } + @UiThread internal fun refund(item: HistoryItem, amount: Amount, reason: String) { val merchantConfig = configManager.merchantConfig!! @@ -73,7 +81,7 @@ class RefundManager( Log.d(TAG, body.toString(4)) val req = MerchantRequest(POST, merchantConfig, "refund", null, body, Listener { onRefundResponse(it, item, amount, reason) }, - ErrorListener { onRefundError() } + LogErrorListener { onRefundError(it) } ) queue.add(req) } @@ -86,7 +94,7 @@ class RefundManager( reason: String ) { if (!json.has("contract_terms")) { - Log.e("TEST", "json: $json") + Log.e(TAG, "Contract terms missing: $json") onRefundError() return } @@ -110,7 +118,15 @@ class RefundManager( } @UiThread - private fun onRefundError() { + 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 index 1bc4002..1ea0959 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt @@ -58,6 +58,12 @@ class RefundUriFragment : Fragment() { 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/payment/PaymentManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt index 054d7cd..9138740 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 @@ -24,10 +24,11 @@ import androidx.lifecycle.MutableLiveData import com.android.volley.Request.Method.GET import com.android.volley.Request.Method.POST import com.android.volley.RequestQueue -import com.android.volley.Response.ErrorListener import com.android.volley.Response.Listener -import com.android.volley.VolleyError import com.fasterxml.jackson.databind.ObjectMapper +import net.taler.common.Timestamp +import net.taler.common.now +import net.taler.merchantpos.LogErrorListener import net.taler.merchantpos.config.ConfigManager import net.taler.merchantpos.config.MerchantRequest import net.taler.merchantpos.order.Order @@ -73,11 +74,12 @@ class PaymentManager( val currency = merchantConfig.currency!! val summary = order.summary val summaryI18n = order.summaryI18n -// val refundDeadline = Timestamp(System.currentTimeMillis() + HOURS.toMillis(2)) + val now = now() + val deadline = Timestamp(now + MINUTES.toMillis(120)) mPayment.value = Payment(order, summary, currency) - val fulfillmentId = "${System.currentTimeMillis()}-${order.hashCode()}" + val fulfillmentId = "${now}-${order.hashCode()}" val fulfillmentUrl = "${FULFILLMENT_PREFIX}${URLEncoder.encode(summary, "UTF-8")}#$fulfillmentId" val body = JSONObject().apply { @@ -88,7 +90,8 @@ class PaymentManager( // fulfillment_url needs to be unique per order put("fulfillment_url", fulfillmentUrl) put("instance", "default") -// put("refund_deadline", JSONObject(mapper.writeValueAsString(refundDeadline))) + put("wire_transfer_deadline", JSONObject(mapper.writeValueAsString(deadline))) + put("refund_deadline", JSONObject(mapper.writeValueAsString(deadline))) put("products", order.getProductsJson()) }) } @@ -97,7 +100,7 @@ class PaymentManager( val req = MerchantRequest(POST, merchantConfig, "order", null, body, Listener { onOrderCreated(it) }, - ErrorListener { onNetworkError(it) } + LogErrorListener { onNetworkError() } ) queue.add(req) } @@ -123,7 +126,7 @@ class PaymentManager( val req = MerchantRequest(GET, merchantConfig, "check-payment", params, null, Listener { onPaymentChecked(it) }, - ErrorListener { onNetworkError(it) }) + LogErrorListener { onNetworkError() }) queue.add(req) } @@ -141,8 +144,7 @@ class PaymentManager( } } - private fun onNetworkError(volleyError: VolleyError) { - Log.e(PaymentManager::class.java.simpleName, volleyError.toString()) + private fun onNetworkError() { cancelPayment() } diff --git a/merchant-terminal/src/main/res/layout/fragment_process_payment.xml b/merchant-terminal/src/main/res/layout/fragment_process_payment.xml index 6cd8ea1..cb69aa2 100644 --- a/merchant-terminal/src/main/res/layout/fragment_process_payment.xml +++ b/merchant-terminal/src/main/res/layout/fragment_process_payment.xml @@ -1,5 +1,4 @@ - - + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".payment.ProcessPaymentFragment"> + android:id="@+id/qrcodeView" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_margin="32dp" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/guideline" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="ContentDescription" + tools:src="@tools:sample/avatars" + tools:visibility="visible" /> + android:id="@+id/progressBar" + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="@+id/qrcodeView" + app:layout_constraintEnd_toEndOf="@+id/qrcodeView" + app:layout_constraintStart_toStartOf="@+id/qrcodeView" + app:layout_constraintTop_toTopOf="@+id/qrcodeView" /> + android:id="@+id/guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.54" /> + android:id="@+id/payIntroView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:text="@string/payment_intro_nfc" + android:textAlignment="center" + android:textSize="22sp" + android:visibility="invisible" + app:layout_constraintBottom_toTopOf="@+id/amountView" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/guideline" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="spread" + tools:visibility="visible" /> + android:id="@+id/amountView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:textAppearance="@style/TextAppearance.AppCompat.Headline" + app:layout_constraintBottom_toTopOf="@+id/orderRefView" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/guideline" + app:layout_constraintTop_toBottomOf="@+id/payIntroView" + tools:text="10.49 TESTKUDOS" /> + android:id="@+id/orderRefView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:textAlignment="center" + android:visibility="invisible" + app:layout_constraintBottom_toTopOf="@id/cancelPaymentButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/guideline" + app:layout_constraintTop_toBottomOf="@+id/amountView" + tools:text="@string/payment_order_ref" + tools:visibility="visible" />