From 5b1163311192e9adf15ef3d626c72812e638f90c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 3 Aug 2020 13:31:26 -0300 Subject: [pos] improve payment processing - cancel orders that have been abandoned and will not be paid - show unpaid orders in history (in case one makes it through) - set deadlines when creating orders in case it helps with enabling refunds --- .../main/java/net/taler/merchantlib/MerchantApi.kt | 5 +- .../java/net/taler/merchantlib/OrderHistory.kt | 3 + .../java/net/taler/merchantlib/PostOrderRequest.kt | 6 +- .../merchantpos/history/HistoryItemAdapter.kt | 88 ++++++++++++++++++++++ .../taler/merchantpos/history/HistoryManager.kt | 6 +- .../merchantpos/history/MerchantHistoryFragment.kt | 60 +-------------- .../main/java/net/taler/merchantpos/order/Order.kt | 7 +- .../taler/merchantpos/payment/PaymentManager.kt | 9 ++- .../merchantpos/payment/ProcessPaymentFragment.kt | 5 ++ merchant-terminal/src/main/res/values/strings.xml | 1 + .../main/java/net/taler/common/ContractTerms.kt | 8 +- 11 files changed, 128 insertions(+), 70 deletions(-) create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryItemAdapter.kt 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 db37586..96892f5 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt @@ -29,7 +29,6 @@ import io.ktor.http.HttpHeaders.Authorization import io.ktor.http.contentType import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration -import net.taler.common.ContractTerms import net.taler.merchantlib.Response.Companion.response class MerchantApi(private val httpClient: HttpClient) { @@ -40,12 +39,12 @@ class MerchantApi(private val httpClient: HttpClient) { suspend fun postOrder( merchantConfig: MerchantConfig, - contractTerms: ContractTerms + orderRequest: PostOrderRequest ): Response = response { httpClient.post(merchantConfig.urlFor("private/orders")) { header(Authorization, "ApiKey ${merchantConfig.apiKey}") contentType(Json) - body = PostOrderRequest(contractTerms) + body = orderRequest } as PostOrderResponse } diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/OrderHistory.kt b/merchant-lib/src/main/java/net/taler/merchantlib/OrderHistory.kt index 718bde5..b1ff5b1 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/OrderHistory.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/OrderHistory.kt @@ -41,6 +41,9 @@ data class OrderHistoryEntry( // the summary of the order val summary: String, + // if the order has been paid + val paid: Boolean, + // whether some part of the order is refundable val refundable: Boolean ) diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt b/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt index 4854a80..783dd19 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/PostOrderRequest.kt @@ -25,12 +25,14 @@ 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 - + val contractTerms: ContractTerms, + @SerialName("refund_delay") + val refundDelay: Duration? = null ) @Serializable diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryItemAdapter.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryItemAdapter.kt new file mode 100644 index 0000000..25e94fb --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryItemAdapter.kt @@ -0,0 +1,88 @@ +/* + * 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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import net.taler.common.toRelativeTime +import net.taler.merchantlib.OrderHistoryEntry +import net.taler.merchantpos.R +import net.taler.merchantpos.history.HistoryItemAdapter.HistoryItemViewHolder +import java.util.ArrayList + + +internal class HistoryItemAdapter(private val listener: RefundClickListener) : + Adapter() { + + private val items = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryItemViewHolder { + val v = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_history, parent, false) + return HistoryItemViewHolder(v) + } + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: HistoryItemViewHolder, position: Int) { + holder.bind(items[position]) + } + + fun setData(items: List) { + this.items.clear() + this.items.addAll(items) + this.notifyDataSetChanged() + } + + internal inner class HistoryItemViewHolder(private val v: View) : RecyclerView.ViewHolder(v) { + + private val orderSummaryView: TextView = v.findViewById(R.id.orderSummaryView) + private val orderAmountView: TextView = v.findViewById(R.id.orderAmountView) + private val orderTimeView: TextView = v.findViewById(R.id.orderTimeView) + private val orderIdView: TextView = v.findViewById(R.id.orderIdView) + private val refundButton: ImageButton = v.findViewById(R.id.refundButton) + + private val orderIdColor = orderIdView.currentTextColor + + fun bind(item: OrderHistoryEntry) { + orderSummaryView.text = item.summary + val amount = item.amount + orderAmountView.text = amount.toString() + orderTimeView.text = item.timestamp.ms.toRelativeTime(v.context) + if (item.paid) { + orderIdView.text = v.context.getString(R.string.history_ref_no, item.orderId) + orderIdView.setTextColor(orderIdColor) + } else { + orderIdView.text = v.context.getString(R.string.history_unpaid) + orderIdView.setTextColor(v.context.getColor(R.color.red)) + } + if (item.refundable) { + refundButton.visibility = View.VISIBLE + refundButton.setOnClickListener { listener.onRefundClicked(item) } + } else { + refundButton.visibility = View.GONE + } + } + + } + +} 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 cb77096..aabe4cc 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 @@ -55,9 +55,9 @@ class HistoryManager( } } - private fun onHistoryError(msg: String) = scope.launch(Dispatchers.Main) { - mIsLoading.value = false - mItems.value = HistoryResult.Error(msg) + private fun onHistoryError(msg: String) { + mIsLoading.postValue(false) + mItems.postValue(HistoryResult.Error(msg)) } } 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 index 25805dc..596b8b0 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt @@ -20,34 +20,25 @@ import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.TextView 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 androidx.recyclerview.widget.RecyclerView.Adapter -import androidx.recyclerview.widget.RecyclerView.ViewHolder 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.common.toRelativeTime import net.taler.merchantlib.OrderHistoryEntry import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R -import net.taler.merchantpos.history.HistoryItemAdapter.HistoryItemViewHolder import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionGlobalMerchantSettings import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionNavHistoryToRefundFragment -import java.util.ArrayList -private interface RefundClickListener { +internal interface RefundClickListener { fun onRefundClicked(item: OrderHistoryEntry) } @@ -115,52 +106,3 @@ class MerchantHistoryFragment : Fragment(), RefundClickListener { } } - -private class HistoryItemAdapter(private val listener: RefundClickListener) : - Adapter() { - - private val items = ArrayList() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryItemViewHolder { - val v = - LayoutInflater.from(parent.context).inflate(R.layout.list_item_history, parent, false) - return HistoryItemViewHolder(v) - } - - override fun getItemCount() = items.size - - override fun onBindViewHolder(holder: HistoryItemViewHolder, position: Int) { - holder.bind(items[position]) - } - - fun setData(items: List) { - this.items.clear() - this.items.addAll(items) - this.notifyDataSetChanged() - } - - private inner class HistoryItemViewHolder(private val v: View) : ViewHolder(v) { - - private val orderSummaryView: TextView = v.findViewById(R.id.orderSummaryView) - private val orderAmountView: TextView = v.findViewById(R.id.orderAmountView) - private val orderTimeView: TextView = v.findViewById(R.id.orderTimeView) - private val orderIdView: TextView = v.findViewById(R.id.orderIdView) - private val refundButton: ImageButton = v.findViewById(R.id.refundButton) - - fun bind(item: OrderHistoryEntry) { - orderSummaryView.text = item.summary - val amount = item.amount - orderAmountView.text = amount.toString() - orderIdView.text = v.context.getString(R.string.history_ref_no, item.orderId) - orderTimeView.text = item.timestamp.ms.toRelativeTime(v.context) - if (item.refundable) { - refundButton.visibility = VISIBLE - refundButton.setOnClickListener { listener.onRefundClicked(item) } - } else { - refundButton.visibility = GONE - } - } - - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt index bb75362..4053d4b 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt @@ -18,10 +18,12 @@ package net.taler.merchantpos.order import net.taler.common.Amount import net.taler.common.ContractTerms +import net.taler.common.Timestamp import net.taler.common.now import net.taler.merchantpos.config.Category import net.taler.merchantpos.config.ConfigProduct import java.net.URLEncoder +import java.util.concurrent.TimeUnit.HOURS private const val FULFILLMENT_PREFIX = "taler://fulfillment-success/" @@ -115,12 +117,15 @@ data class Order(val id: Int, val currency: String, val availableCategories: Map } fun toContractTerms(): ContractTerms { + val deadline = Timestamp(now() + HOURS.toMillis(1)) return ContractTerms( summary = summary, summaryI18n = summaryI18n, amount = total, fulfillmentUrl = fulfillmentUri, - products = products.map { it.toContractProduct() } + products = products.map { it.toContractProduct() }, + refundDeadline = deadline, + wireTransferDeadline = deadline ) } 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 fc4f642..bc1e35f 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 @@ -25,13 +25,16 @@ import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import net.taler.common.Duration import net.taler.merchantlib.CheckPaymentResponse import net.taler.merchantlib.MerchantApi +import net.taler.merchantlib.PostOrderRequest import net.taler.merchantlib.PostOrderResponse import net.taler.merchantpos.MainActivity.Companion.TAG import net.taler.merchantpos.R import net.taler.merchantpos.config.ConfigManager import net.taler.merchantpos.order.Order +import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS @@ -65,7 +68,11 @@ class PaymentManager( val merchantConfig = configManager.merchantConfig!! mPayment.value = Payment(order, order.summary, configManager.currency!!) scope.launch(Dispatchers.IO) { - val response = api.postOrder(merchantConfig, order.toContractTerms()) + val request = PostOrderRequest( + contractTerms = order.toContractTerms(), + refundDelay = Duration(HOURS.toMillis(1)) + ) + val response = api.postOrder(merchantConfig, request) response.handle(::onNetworkError, ::onOrderCreated) } } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt index 5278a03..27ef366 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt @@ -60,6 +60,11 @@ class ProcessPaymentFragment : Fragment() { } } + override fun onDestroy() { + super.onDestroy() + paymentManager.cancelPayment(getString(R.string.error_cancelled)) + } + private fun onPaymentStateChanged(payment: Payment) { if (payment.error != null) { topSnackbar(requireView(), payment.error, LENGTH_LONG) diff --git a/merchant-terminal/src/main/res/values/strings.xml b/merchant-terminal/src/main/res/values/strings.xml index 4c0ba5a..f7a62da 100644 --- a/merchant-terminal/src/main/res/values/strings.xml +++ b/merchant-terminal/src/main/res/values/strings.xml @@ -47,6 +47,7 @@ Payment history @string/payment_order_id + Unpaid Refund Amount Refund reason diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt index 013427f..ab442f2 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt @@ -35,7 +35,13 @@ data class ContractTerms( @SerialName("fulfillment_url") @get:JsonProperty("fulfillment_url") val fulfillmentUrl: String, - val products: List + val products: List, + @SerialName("wire_transfer_deadline") + @get:JsonProperty("wire_transfer_deadline") + val wireTransferDeadline: Timestamp? = null, + @SerialName("refund_deadline") + @get:JsonProperty("refund_deadline") + val refundDeadline: Timestamp? = null ) @JsonInclude(NON_NULL) -- cgit v1.2.3