summaryrefslogtreecommitdiff
path: root/merchant-terminal/src/main/java/net/taler/merchantpos/history
diff options
context:
space:
mode:
Diffstat (limited to 'merchant-terminal/src/main/java/net/taler/merchantpos/history')
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt106
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt160
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt99
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt111
-rw-r--r--merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt65
5 files changed, 541 insertions, 0 deletions
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
new file mode 100644
index 0000000..594e7cc
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
@@ -0,0 +1,106 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+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.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.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import net.taler.merchantpos.Amount
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.config.MerchantRequest
+import org.json.JSONObject
+
+@JsonInclude(NON_EMPTY)
+class Timestamp(
+ @JsonProperty("t_ms")
+ val ms: Long
+)
+
+data class HistoryItem(
+ @JsonProperty("order_id")
+ val orderId: String,
+ @JsonProperty("amount")
+ val amountStr: String,
+ val summary: String,
+ val timestamp: Timestamp
+) {
+ @get:JsonIgnore
+ val amount: Amount by lazy { Amount.fromString(amountStr) }
+
+ @get:JsonIgnore
+ val time = timestamp.ms
+}
+
+sealed class HistoryResult {
+ object Error : HistoryResult()
+ class Success(val items: List<HistoryItem>) : HistoryResult()
+}
+
+class HistoryManager(
+ private val configManager: ConfigManager,
+ private val queue: RequestQueue,
+ private val mapper: ObjectMapper
+) {
+
+ private val mIsLoading = MutableLiveData(false)
+ val isLoading: LiveData<Boolean> = mIsLoading
+
+ private val mItems = MutableLiveData<HistoryResult>()
+ val items: LiveData<HistoryResult> = mItems
+
+ @UiThread
+ internal fun fetchHistory() {
+ mIsLoading.value = true
+ val merchantConfig = configManager.merchantConfig!!
+ val params = mapOf("instance" to merchantConfig.instance)
+ val req = MerchantRequest(GET, merchantConfig, "history", params, null,
+ Listener { onHistoryResponse(it) },
+ ErrorListener { onHistoryError() })
+ queue.add(req)
+ }
+
+ @UiThread
+ private fun onHistoryResponse(body: JSONObject) {
+ mIsLoading.value = false
+ val items = arrayListOf<HistoryItem>()
+ val historyJson = body.getJSONArray("history")
+ for (i in 0 until historyJson.length()) {
+ val historyItem: HistoryItem = mapper.readValue(historyJson.getString(i))
+ items.add(historyItem)
+ }
+ mItems.value = HistoryResult.Success(items)
+ }
+
+ @UiThread
+ private fun onHistoryError() {
+ mIsLoading.value = false
+ mItems.value = HistoryResult.Error
+ }
+
+}
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
new file mode 100644
index 0000000..0c53f71
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
@@ -0,0 +1,160 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.history
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+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.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_merchant_history.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.exhaustive
+import net.taler.merchantpos.history.HistoryItemAdapter.HistoryItemViewHolder
+import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionGlobalMerchantSettings
+import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionNavHistoryToRefundFragment
+import net.taler.merchantpos.navigate
+import net.taler.merchantpos.toRelativeTime
+import java.util.*
+
+private interface RefundClickListener {
+ fun onRefundClicked(item: HistoryItem)
+}
+
+/**
+ * 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()
+ is HistoryResult.Success -> historyListAdapter.setData(result.items)
+ }.exhaustive
+ })
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (model.configManager.merchantConfig?.instance == null) {
+ navigate(actionGlobalMerchantSettings())
+ } else {
+ historyManager.fetchHistory()
+ }
+ }
+
+ private fun onError() {
+ Snackbar.make(view!!, R.string.error_network, LENGTH_SHORT).show()
+ }
+
+ override fun onRefundClicked(item: HistoryItem) {
+ refundManager.startRefund(item)
+ navigate(actionNavHistoryToRefundFragment())
+ }
+
+}
+
+private class HistoryItemAdapter(private val listener: RefundClickListener) :
+ Adapter<HistoryItemViewHolder>() {
+
+ private val items = ArrayList<HistoryItem>()
+
+ 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<HistoryItem>) {
+ 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: HistoryItem) {
+ orderSummaryView.text = item.summary
+ val amount = item.amount
+ @SuppressLint("SetTextI18n")
+ orderAmountView.text = "${amount.amount} ${amount.currency}"
+ orderIdView.text = v.context.getString(R.string.history_ref_no, item.orderId)
+ orderTimeView.text = item.time.toRelativeTime(v.context)
+ refundButton.setOnClickListener { listener.onRefundClicked(item) }
+ }
+
+ }
+
+}
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
new file mode 100644
index 0000000..1797cea
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt
@@ -0,0 +1,99 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+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.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.fadeIn
+import net.taler.merchantpos.fadeOut
+import net.taler.merchantpos.history.RefundFragmentDirections.Companion.actionRefundFragmentToRefundUriFragment
+import net.taler.merchantpos.history.RefundResult.Error
+import net.taler.merchantpos.history.RefundResult.PastDeadline
+import net.taler.merchantpos.history.RefundResult.Success
+import net.taler.merchantpos.navigate
+
+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.amount)
+ 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: HistoryItem) {
+ val inputAmount = amountInputView.text.toString().toDouble()
+ if (inputAmount > item.amount.amount.toDouble()) {
+ amountView.error = getString(R.string.refund_error_max_amount, item.amount.amount)
+ return
+ }
+ if (inputAmount <= 0.0) {
+ 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)
+ is Success -> {
+ progressBar.fadeOut()
+ refundButton.fadeIn()
+ navigate(actionRefundFragmentToRefundUriFragment())
+ }
+ null -> { // no-op
+ }
+ }
+
+ private fun onError(@StringRes res: Int) {
+ Snackbar.make(view!!, 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
new file mode 100644
index 0000000..270b3b8
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt
@@ -0,0 +1,111 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+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.ErrorListener
+import com.android.volley.Response.Listener
+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()
+ class Success(
+ val refundUri: String,
+ val item: HistoryItem,
+ val amount: Double,
+ val reason: String
+ ) : RefundResult()
+}
+
+class RefundManager(
+ private val configManager: ConfigManager,
+ private val queue: RequestQueue
+) {
+
+ var toBeRefunded: HistoryItem? = null
+ private set
+
+ private val mRefundResult = MutableLiveData<RefundResult>()
+ internal val refundResult: LiveData<RefundResult> = mRefundResult
+
+ @UiThread
+ internal fun startRefund(item: HistoryItem) {
+ toBeRefunded = item
+ mRefundResult.value = null
+ }
+
+ @UiThread
+ internal fun refund(item: HistoryItem, amount: Double, reason: String) {
+ val merchantConfig = configManager.merchantConfig!!
+ val refundRequest = mapOf(
+ "order_id" to item.orderId,
+ "refund" to "${item.amount.currency}:$amount",
+ "reason" to reason
+ )
+ val body = JSONObject(refundRequest)
+ val req = MerchantRequest(POST, merchantConfig, "refund", null, body,
+ Listener { onRefundResponse(it, item, amount, reason) },
+ ErrorListener { onRefundError() }
+ )
+ queue.add(req)
+ }
+
+ @UiThread
+ private fun onRefundResponse(
+ json: JSONObject,
+ item: HistoryItem,
+ amount: Double,
+ reason: String
+ ) {
+ if (!json.has("contract_terms")) {
+ Log.e("TEST", "json: $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() {
+ 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
new file mode 100644
index 0000000..f2bd569
--- /dev/null
+++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.merchantpos.history
+
+import android.annotation.SuppressLint
+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.merchantpos.MainViewModel
+import net.taler.merchantpos.NfcManager.Companion.hasNfc
+import net.taler.merchantpos.QrCodeManager.makeQrCode
+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)
+
+ @SuppressLint("SetTextI18n")
+ refundAmountView.text = "${result.amount} ${result.item.amount.currency}"
+
+ refundRefView.text =
+ getString(R.string.refund_order_ref, result.item.orderId, result.reason)
+
+ cancelRefundButton.setOnClickListener { findNavController().navigateUp() }
+ }
+
+}