diff options
Diffstat (limited to 'merchant-terminal/src/main/java/net/taler/merchantpos/payment')
4 files changed, 323 insertions, 0 deletions
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt new file mode 100644 index 0000000..b7e4a4b --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/Payment.kt @@ -0,0 +1,29 @@ +/* + * 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.payment + +import net.taler.merchantpos.order.Order + +data class Payment( + val order: Order, + val summary: String, + val currency: String, + val orderId: String? = null, + val talerPayUri: String? = null, + val paid: Boolean = false, + val error: Boolean = false +) 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 new file mode 100644 index 0000000..7f15816 --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt @@ -0,0 +1,154 @@ +/* + * 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.payment + +import android.os.CountDownTimer +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.android.volley.VolleyError +import com.fasterxml.jackson.databind.ObjectMapper +import net.taler.merchantpos.config.ConfigManager +import net.taler.merchantpos.config.MerchantRequest +import net.taler.merchantpos.order.ContractProduct +import net.taler.merchantpos.order.Order +import org.json.JSONArray +import org.json.JSONObject +import java.net.URLEncoder +import java.util.concurrent.TimeUnit.MINUTES +import java.util.concurrent.TimeUnit.SECONDS + +private val TIMEOUT = MINUTES.toMillis(2) +private val CHECK_INTERVAL = SECONDS.toMillis(1) +private const val FULFILLMENT_PREFIX = "taler://fulfillment-success/" + +class PaymentManager( + private val configManager: ConfigManager, + private val queue: RequestQueue, + private val mapper: ObjectMapper +) { + + companion object { + val TAG = PaymentManager::class.java.simpleName + } + + private val mPayment = MutableLiveData<Payment>() + val payment: LiveData<Payment> = mPayment + + private val checkTimer = object : CountDownTimer(TIMEOUT, CHECK_INTERVAL) { + override fun onTick(millisUntilFinished: Long) { + val orderId = payment.value?.orderId + if (orderId == null) cancel() + else checkPayment(orderId) + } + + override fun onFinish() { + payment.value?.copy(error = true)?.let { mPayment.value = it } + } + } + + @UiThread + fun createPayment(order: Order) { + val merchantConfig = configManager.merchantConfig!! + + val currency = merchantConfig.currency!! + val amount = "$currency:${order.totalAsString}" + val summary = order.summary + val summaryI18n = order.summaryI18n + + mPayment.value = Payment(order, summary, currency) + + val fulfillmentId = "${System.currentTimeMillis()}-${order.hashCode()}" + val fulfillmentUrl = + "${FULFILLMENT_PREFIX}${URLEncoder.encode(summary, "UTF-8")}#$fulfillmentId" + val body = JSONObject().apply { + put("order", JSONObject().apply { + put("amount", amount) + put("summary", summary) + if (summaryI18n != null) put("summary_i18n", order.summaryI18n) + // fulfillment_url needs to be unique per order + put("fulfillment_url", fulfillmentUrl) + put("instance", "default") + put("products", order.getProductsJson()) + }) + } + + Log.d(TAG, body.toString(4)) + + val req = MerchantRequest(POST, merchantConfig, "order", null, body, + Listener { onOrderCreated(it) }, + ErrorListener { onNetworkError(it) } + ) + queue.add(req) + } + + private fun Order.getProductsJson(): JSONArray { + val contractProducts = products.map { ContractProduct(it) } + val productsStr = mapper.writeValueAsString(contractProducts) + return JSONArray(productsStr) + } + + private fun onOrderCreated(orderResponse: JSONObject) { + val orderId = orderResponse.getString("order_id") + mPayment.value = mPayment.value!!.copy(orderId = orderId) + checkTimer.start() + } + + private fun checkPayment(orderId: String) { + val merchantConfig = configManager.merchantConfig!! + val params = mapOf( + "order_id" to orderId, + "instance" to merchantConfig.instance + ) + + val req = MerchantRequest(GET, merchantConfig, "check-payment", params, null, + Listener { onPaymentChecked(it) }, + ErrorListener { onNetworkError(it) }) + queue.add(req) + } + + /** + * Called when the /check-payment response gave a result. + */ + private fun onPaymentChecked(checkPaymentResponse: JSONObject) { + val currentValue = requireNotNull(mPayment.value) + if (checkPaymentResponse.getBoolean("paid")) { + mPayment.value = currentValue.copy(paid = true) + checkTimer.cancel() + } else if (currentValue.talerPayUri == null) { + val talerPayUri = checkPaymentResponse.getString("taler_pay_uri") + mPayment.value = currentValue.copy(talerPayUri = talerPayUri) + } + } + + private fun onNetworkError(volleyError: VolleyError) { + Log.e(PaymentManager::class.java.simpleName, volleyError.toString()) + cancelPayment() + } + + fun cancelPayment() { + mPayment.value = mPayment.value!!.copy(error = true) + checkTimer.cancel() + } + +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt new file mode 100644 index 0000000..10d538d --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentSuccessFragment.kt @@ -0,0 +1,44 @@ +/* + * 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.payment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_payment_success.* +import net.taler.merchantpos.R + +class PaymentSuccessFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_payment_success, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + paymentButton.setOnClickListener { + findNavController().navigateUp() + } + } + +} 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 new file mode 100644 index 0000000..24f67f1 --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt @@ -0,0 +1,96 @@ +/* + * 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.payment + +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.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG +import kotlinx.android.synthetic.main.fragment_process_payment.* +import net.taler.merchantpos.MainViewModel +import net.taler.merchantpos.NfcManager.Companion.hasNfc +import net.taler.merchantpos.QrCodeManager.makeQrCode +import net.taler.merchantpos.R +import net.taler.merchantpos.fadeIn +import net.taler.merchantpos.fadeOut +import net.taler.merchantpos.navigate +import net.taler.merchantpos.payment.ProcessPaymentFragmentDirections.Companion.actionProcessPaymentToPaymentSuccess +import net.taler.merchantpos.topSnackbar + +class ProcessPaymentFragment : Fragment() { + + private val model: MainViewModel by activityViewModels() + private val paymentManager by lazy { model.paymentManager } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_process_payment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val introRes = + if (hasNfc(requireContext())) R.string.payment_intro_nfc else R.string.payment_intro + payIntroView.setText(introRes) + paymentManager.payment.observe(viewLifecycleOwner, Observer { payment -> + onPaymentStateChanged(payment) + }) + cancelPaymentButton.setOnClickListener { + onPaymentCancel() + } + } + + private fun onPaymentStateChanged(payment: Payment) { + if (payment.error) { + topSnackbar(view!!, R.string.error_network, LENGTH_LONG) + findNavController().navigateUp() + return + } + if (payment.paid) { + model.orderManager.onOrderPaid(payment.order.id) + actionProcessPaymentToPaymentSuccess().navigate(findNavController()) + return + } + payIntroView.fadeIn() + @SuppressLint("SetTextI18n") + amountView.text = "${payment.order.totalAsString} ${payment.currency}" + payment.orderId?.let { + orderRefView.text = getString(R.string.payment_order_ref, it) + orderRefView.fadeIn() + } + payment.talerPayUri?.let { + val qrcodeBitmap = makeQrCode(it) + qrcodeView.setImageBitmap(qrcodeBitmap) + qrcodeView.fadeIn() + progressBar.fadeOut() + } + } + + private fun onPaymentCancel() { + paymentManager.cancelPayment() + findNavController().navigateUp() + topSnackbar(view!!, R.string.payment_canceled, LENGTH_LONG) + } + +} |