From a4796ec47d89a851b260b6fc195494547208a025 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 18 Mar 2020 14:24:41 -0300 Subject: Merge all three apps into one repository --- .../taler/merchantpos/history/HistoryManager.kt | 106 ++++++++++++++ .../merchantpos/history/MerchantHistoryFragment.kt | 160 +++++++++++++++++++++ .../taler/merchantpos/history/RefundFragment.kt | 99 +++++++++++++ .../net/taler/merchantpos/history/RefundManager.kt | 111 ++++++++++++++ .../taler/merchantpos/history/RefundUriFragment.kt | 65 +++++++++ 5 files changed, 541 insertions(+) create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundManager.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt (limited to 'merchant-terminal/src/main/java/net/taler/merchantpos/history') 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 + */ + +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) : HistoryResult() +} + +class HistoryManager( + private val configManager: ConfigManager, + private val queue: RequestQueue, + private val mapper: ObjectMapper +) { + + private val mIsLoading = MutableLiveData(false) + val isLoading: LiveData = mIsLoading + + private val mItems = MutableLiveData() + val items: LiveData = 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() + 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 + */ + +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() { + + 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: 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 + */ + +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 + */ + +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() + internal val refundResult: LiveData = 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 + */ + +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() } + } + +} -- cgit v1.2.3