From 82b8b57dc16112b859150696199774fcf06655e1 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 18 Mar 2020 17:24:02 -0300 Subject: Factor out code from merchant-terminal into common library --- .../src/main/java/net/taler/merchantpos/Amount.kt | 48 ----- .../java/net/taler/merchantpos/MainActivity.kt | 1 + .../main/java/net/taler/merchantpos/NfcManager.kt | 233 --------------------- .../java/net/taler/merchantpos/QrCodeManager.kt | 42 ---- .../src/main/java/net/taler/merchantpos/Utils.kt | 121 ----------- .../merchantpos/config/ConfigFetcherFragment.kt | 7 +- .../net/taler/merchantpos/config/MerchantConfig.kt | 45 ++++ .../merchantpos/config/MerchantConfigFragment.kt | 5 +- .../taler/merchantpos/history/HistoryManager.kt | 11 +- .../merchantpos/history/MerchantHistoryFragment.kt | 6 +- .../taler/merchantpos/history/RefundFragment.kt | 6 +- .../taler/merchantpos/history/RefundUriFragment.kt | 4 +- .../taler/merchantpos/order/CategoriesFragment.kt | 1 + .../net/taler/merchantpos/order/Definitions.kt | 205 ------------------ .../java/net/taler/merchantpos/order/LiveOrder.kt | 4 +- .../main/java/net/taler/merchantpos/order/Order.kt | 108 ++++++++++ .../net/taler/merchantpos/order/OrderFragment.kt | 9 +- .../net/taler/merchantpos/order/OrderManager.kt | 4 +- .../taler/merchantpos/order/OrderStateFragment.kt | 5 +- .../taler/merchantpos/order/ProductsFragment.kt | 1 + .../taler/merchantpos/payment/PaymentManager.kt | 3 +- .../merchantpos/payment/ProcessPaymentFragment.kt | 12 +- 22 files changed, 191 insertions(+), 690 deletions(-) delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt delete mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt create mode 100644 merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt (limited to 'merchant-terminal/src/main') diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt deleted file mode 100644 index 17ddd61..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 - -import org.json.JSONObject - -data class Amount(val currency: String, val amount: String) { - @Suppress("unused") - fun isZero(): Boolean { - return amount.toDouble() == 0.0 - } - - companion object { - private const val FRACTIONAL_BASE = 1e8 - - @Suppress("unused") - fun fromJson(jsonAmount: JSONObject): Amount { - val amountCurrency = jsonAmount.getString("currency") - val amountValue = jsonAmount.getString("value") - val amountFraction = jsonAmount.getString("fraction") - val amountIntValue = Integer.parseInt(amountValue) - val amountIntFraction = Integer.parseInt(amountFraction) - return Amount( - amountCurrency, - (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString() - ) - } - - fun fromString(strAmount: String): Amount { - val components = strAmount.split(":") - return Amount(components[0], components[1]) - } - } -} 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 0c6bdfa..d6e3747 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt @@ -36,6 +36,7 @@ import androidx.navigation.ui.setupWithNavController import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.app_bar_main.* +import net.taler.common.NfcManager class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt deleted file mode 100644 index 09c1470..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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 - -import android.app.Activity -import android.content.Context -import android.nfc.NfcAdapter -import android.nfc.NfcAdapter.FLAG_READER_NFC_A -import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK -import android.nfc.Tag -import android.nfc.tech.IsoDep -import android.util.Log -import net.taler.merchantpos.Utils.hexStringToByteArray -import org.json.JSONObject -import java.io.ByteArrayOutputStream -import java.net.URL -import javax.net.ssl.HttpsURLConnection - -@Suppress("unused") -private const val TALER_AID = "A0000002471001" - -class NfcManager : NfcAdapter.ReaderCallback { - - companion object { - const val TAG = "taler-merchant" - - /** - * Returns true if NFC is supported and false otherwise. - */ - fun hasNfc(context: Context): Boolean { - return getNfcAdapter(context) != null - } - - /** - * Enables NFC reader mode. Don't forget to call [stop] afterwards. - */ - fun start(activity: Activity, nfcManager: NfcManager) { - getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager, nfcManager.flags, null) - } - - /** - * Disables NFC reader mode. Call after [start]. - */ - fun stop(activity: Activity) { - getNfcAdapter(activity)?.disableReaderMode(activity) - } - - private fun getNfcAdapter(context: Context): NfcAdapter? { - return NfcAdapter.getDefaultAdapter(context) - } - } - - private val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK - - private var tagString: String? = null - private var currentTag: IsoDep? = null - - fun setTagString(tagString: String) { - this.tagString = tagString - } - - override fun onTagDiscovered(tag: Tag?) { - - Log.v(TAG, "tag discovered") - - val isoDep = IsoDep.get(tag) - isoDep.connect() - - currentTag = isoDep - - isoDep.transceive(apduSelectFile()) - - val tagString: String? = tagString - if (tagString != null) { - isoDep.transceive(apduPutTalerData(1, tagString.toByteArray())) - } - - // FIXME: use better pattern for sleeps in between requests - // -> start with fast polling, poll more slowly if no requests are coming - - while (true) { - try { - val reqFrame = isoDep.transceive(apduGetData()) - if (reqFrame.size < 2) { - Log.v(TAG, "request frame too small") - break - } - val req = ByteArray(reqFrame.size - 2) - if (req.isEmpty()) { - continue - } - reqFrame.copyInto(req, 0, 0, reqFrame.size - 2) - val jsonReq = JSONObject(req.toString(Charsets.UTF_8)) - val reqId = jsonReq.getInt("id") - Log.v(TAG, "got request $jsonReq") - val jsonInnerReq = jsonReq.getJSONObject("request") - val method = jsonInnerReq.getString("method") - val urlStr = jsonInnerReq.getString("url") - Log.v(TAG, "url '$urlStr'") - Log.v(TAG, "method '$method'") - val url = URL(urlStr) - val conn: HttpsURLConnection = url.openConnection() as HttpsURLConnection - conn.setRequestProperty("Accept", "application/json") - conn.connectTimeout = 5000 - conn.doInput = true - when (method) { - "get" -> { - conn.requestMethod = "GET" - } - "postJson" -> { - conn.requestMethod = "POST" - conn.doOutput = true - conn.setRequestProperty("Content-Type", "application/json; utf-8") - val body = jsonInnerReq.getString("body") - conn.outputStream.write(body.toByteArray(Charsets.UTF_8)) - } - else -> { - throw Exception("method not supported") - } - } - Log.v(TAG, "connecting") - conn.connect() - Log.v(TAG, "connected") - - val statusCode = conn.responseCode - val tunnelResp = JSONObject() - tunnelResp.put("id", reqId) - tunnelResp.put("status", conn.responseCode) - - if (statusCode == 200) { - val stream = conn.inputStream - val httpResp = stream.buffered().readBytes() - tunnelResp.put("responseJson", JSONObject(httpResp.toString(Charsets.UTF_8))) - } - - Log.v(TAG, "sending: $tunnelResp") - - isoDep.transceive(apduPutTalerData(2, tunnelResp.toString().toByteArray())) - } catch (e: Exception) { - Log.v(TAG, "exception during NFC loop: $e") - break - } - } - - isoDep.close() - } - - private fun writeApduLength(stream: ByteArrayOutputStream, size: Int) { - when { - size == 0 -> { - // No size field needed! - } - size <= 255 -> // One byte size field - stream.write(size) - size <= 65535 -> { - stream.write(0) - // FIXME: is this supposed to be little or big endian? - stream.write(size and 0xFF) - stream.write((size ushr 8) and 0xFF) - } - else -> throw Error("payload too big") - } - } - - private fun apduSelectFile(): ByteArray { - return hexStringToByteArray("00A4040007A0000002471001") - } - - private fun apduPutData(payload: ByteArray): ByteArray { - val stream = ByteArrayOutputStream() - - // Class - stream.write(0x00) - - // Instruction 0xDA = put data - stream.write(0xDA) - - // Instruction parameters - // (proprietary encoding) - stream.write(0x01) - stream.write(0x00) - - writeApduLength(stream, payload.size) - - stream.write(payload) - - return stream.toByteArray() - } - - private fun apduPutTalerData(talerInst: Int, payload: ByteArray): ByteArray { - val realPayload = ByteArrayOutputStream() - realPayload.write(talerInst) - realPayload.write(payload) - return apduPutData(realPayload.toByteArray()) - } - - private fun apduGetData(): ByteArray { - val stream = ByteArrayOutputStream() - - // Class - stream.write(0x00) - - // Instruction 0xCA = get data - stream.write(0xCA) - - // Instruction parameters - // (proprietary encoding) - stream.write(0x01) - stream.write(0x00) - - // Max expected response size, two - // zero bytes denotes 65536 - stream.write(0x0) - stream.write(0x0) - - return stream.toByteArray() - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt deleted file mode 100644 index 595e7ac..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 - -import android.graphics.Bitmap -import android.graphics.Bitmap.Config.RGB_565 -import android.graphics.Color.BLACK -import android.graphics.Color.WHITE -import com.google.zxing.BarcodeFormat.QR_CODE -import com.google.zxing.qrcode.QRCodeWriter - -object QrCodeManager { - - fun makeQrCode(text: String, size: Int = 256): Bitmap { - val qrCodeWriter = QRCodeWriter() - val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size) - val height = bitMatrix.height - val width = bitMatrix.width - val bmp = Bitmap.createBitmap(width, height, RGB_565) - for (x in 0 until width) { - for (y in 0 until height) { - bmp.setPixel(x, y, if (bitMatrix.get(x, y)) BLACK else WHITE) - } - } - return bmp - } - -} 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 a0c30d6..578debf 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt @@ -16,86 +16,12 @@ package net.taler.merchantpos -import android.content.Context -import android.text.format.DateUtils.DAY_IN_MILLIS -import android.text.format.DateUtils.FORMAT_ABBREV_MONTH -import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE -import android.text.format.DateUtils.FORMAT_NO_YEAR -import android.text.format.DateUtils.FORMAT_SHOW_DATE -import android.text.format.DateUtils.FORMAT_SHOW_TIME -import android.text.format.DateUtils.MINUTE_IN_MILLIS -import android.text.format.DateUtils.formatDateTime -import android.text.format.DateUtils.getRelativeTimeSpanString import android.view.View -import android.view.View.INVISIBLE -import android.view.View.VISIBLE import androidx.annotation.StringRes -import androidx.fragment.app.Fragment -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.Observer -import androidx.navigation.NavController -import androidx.navigation.NavDirections -import androidx.navigation.fragment.findNavController 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 -object Utils { - - private const val HEX_CHARS = "0123456789ABCDEF" - - fun hexStringToByteArray(data: String): ByteArray { - val result = ByteArray(data.length / 2) - - for (i in data.indices step 2) { - val firstIndex = HEX_CHARS.indexOf(data[i]) - val secondIndex = HEX_CHARS.indexOf(data[i + 1]) - - val octet = firstIndex.shl(4).or(secondIndex) - result[i.shr(1)] = octet.toByte() - } - return result - } - - - private val HEX_CHARS_ARRAY = HEX_CHARS.toCharArray() - - @Suppress("unused") - fun toHex(byteArray: ByteArray): String { - val result = StringBuffer() - - byteArray.forEach { - val octet = it.toInt() - val firstIndex = (octet and 0xF0).ushr(4) - val secondIndex = octet and 0x0F - result.append(HEX_CHARS_ARRAY[firstIndex]) - result.append(HEX_CHARS_ARRAY[secondIndex]) - } - return result.toString() - } - -} - -fun View.fadeIn(endAction: () -> Unit = {}) { - if (visibility == VISIBLE) return - alpha = 0f - visibility = VISIBLE - animate().alpha(1f).withEndAction { - if (context != null) endAction.invoke() - }.start() -} - -fun View.fadeOut(endAction: () -> Unit = {}) { - if (visibility == INVISIBLE) return - animate().alpha(0f).withEndAction { - if (context == null) return@withEndAction - visibility = INVISIBLE - alpha = 1f - endAction.invoke() - }.start() -} - fun topSnackbar(view: View, text: CharSequence, @Duration duration: Int) { make(view, text, duration) .setAnimationMode(ANIMATION_MODE_FADE) @@ -106,50 +32,3 @@ 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) } - -fun NavDirections.navigate(nav: NavController) = nav.navigate(this) - -fun Fragment.navigate(directions: NavDirections) = findNavController().navigate(directions) - -fun Long.toRelativeTime(context: Context): CharSequence { - val now = System.currentTimeMillis() - return if (now - this > DAY_IN_MILLIS * 2) { - val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR - formatDateTime(context, this, flags) - } else getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE) -} - -class CombinedLiveData( - source1: LiveData, - source2: LiveData, - private val combine: (data1: T?, data2: K?) -> S -) : MediatorLiveData() { - - private var data1: T? = null - private var data2: K? = null - - init { - super.addSource(source1) { t -> - data1 = t - value = combine(data1, data2) - } - super.addSource(source2) { k -> - data2 = k - value = combine(data1, data2) - } - } - - override fun addSource(source: LiveData, onChanged: Observer) { - throw UnsupportedOperationException() - } - - override fun removeSource(toRemote: LiveData) { - throw UnsupportedOperationException() - } -} - -/** - * Use this with 'when' expressions when you need it to handle all possibilities/branches. - */ -val T.exhaustive: T - get() = this diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt index c370e33..c0c87dc 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt @@ -23,14 +23,13 @@ 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.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT +import net.taler.common.navigate import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R import net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToMerchantSettings import net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToOrder -import net.taler.merchantpos.navigate class ConfigFetcherFragment : Fragment() { @@ -52,7 +51,7 @@ class ConfigFetcherFragment : Fragment() { null -> return@Observer is ConfigUpdateResult.Error -> onNetworkError(result.msg) is ConfigUpdateResult.Success -> { - actionConfigFetcherToOrder().navigate(findNavController()) + navigate(actionConfigFetcherToOrder()) } } }) @@ -60,7 +59,7 @@ class ConfigFetcherFragment : Fragment() { private fun onNetworkError(msg: String) { Snackbar.make(view!!, msg, LENGTH_SHORT).show() - actionConfigFetcherToMerchantSettings().navigate(findNavController()) + navigate(actionConfigFetcherToMerchantSettings()) } } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt index 2050e28..8141f0f 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt @@ -17,7 +17,13 @@ package net.taler.merchantpos.config import android.net.Uri +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty +import net.taler.common.Amount +import net.taler.common.ContractProduct +import net.taler.common.Product +import net.taler.common.TalerUtils +import java.util.* data class Config( val configUrl: String, @@ -45,3 +51,42 @@ data class MerchantConfig( return uriBuilder.toString() } } + +data class Category( + val id: Int, + val name: String, + @JsonProperty("name_i18n") + val nameI18n: Map? +) { + var selected: Boolean = false + val localizedName: String get() = TalerUtils.getLocalizedString(nameI18n, name) +} + +data class ConfigProduct( + @JsonIgnore + val id: String = UUID.randomUUID().toString(), + override val productId: String?, + override val description: String, + override val descriptionI18n: Map?, + override val price: String, + override val location: String?, + override val image: String?, + val categories: List, + @JsonIgnore + val quantity: Int = 0 +) : Product() { + val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() } + + fun toContractProduct() = ContractProduct( + productId = productId, + description = description, + descriptionI18n = descriptionI18n, + price = price, + location = location, + image = image, + quantity = quantity + ) + + override fun equals(other: Any?) = other is ConfigProduct && id == other.id + override fun hashCode() = id.hashCode() +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt index aad1c93..a584af8 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt @@ -28,14 +28,13 @@ 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 com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_merchant_config.* +import net.taler.common.navigate import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R import net.taler.merchantpos.config.MerchantConfigFragmentDirections.Companion.actionSettingsToOrder -import net.taler.merchantpos.navigate import net.taler.merchantpos.topSnackbar /** @@ -149,7 +148,7 @@ class MerchantConfigFragment : Fragment() { onResultReceived() updateView() topSnackbar(view!!, getString(R.string.config_changed, currency), LENGTH_LONG) - actionSettingsToOrder().navigate(findNavController()) + navigate(actionSettingsToOrder()) } private fun onError(msg: String) { 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 faee226..fc3f93a 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 @@ -24,22 +24,15 @@ 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.common.Amount +import net.taler.common.Timestamp 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, 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 0c53f71..afa925d 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 @@ -35,14 +35,14 @@ 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.common.exhaustive +import net.taler.common.navigate +import net.taler.common.toRelativeTime 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 { 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 1797cea..aa2489a 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 @@ -28,15 +28,15 @@ 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.common.fadeIn +import net.taler.common.fadeOut +import net.taler.common.navigate 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() { 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 f2bd569..6e5b96d 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 @@ -25,9 +25,9 @@ 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.common.NfcManager.Companion.hasNfc +import net.taler.common.QrCodeManager.makeQrCode 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() { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt index 34b97c0..e935d4f 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt @@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView.Adapter import kotlinx.android.synthetic.main.fragment_categories.* import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R +import net.taler.merchantpos.config.Category import net.taler.merchantpos.order.CategoryAdapter.CategoryViewHolder interface CategorySelectionListener { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt deleted file mode 100644 index 63eda17..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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.order - -import androidx.core.os.LocaleListCompat -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL -import com.fasterxml.jackson.annotation.JsonProperty -import net.taler.merchantpos.Amount -import java.util.* -import java.util.Locale.LanguageRange -import kotlin.collections.ArrayList -import kotlin.collections.HashMap - -data class Category( - val id: Int, - val name: String, - @JsonProperty("name_i18n") - val nameI18n: Map? -) { - var selected: Boolean = false - val localizedName: String get() = getLocalizedString(nameI18n, name) -} - -@JsonInclude(NON_NULL) -abstract class Product { - @get:JsonProperty("product_id") - abstract val productId: String? - abstract val description: String - @get:JsonProperty("description_i18n") - abstract val descriptionI18n: Map? - abstract val price: String - @get:JsonProperty("delivery_location") - abstract val location: String? - abstract val image: String? - @get:JsonIgnore - val localizedDescription: String - get() = getLocalizedString(descriptionI18n, description) -} - -data class ConfigProduct( - @JsonIgnore - val id: String = UUID.randomUUID().toString(), - override val productId: String?, - override val description: String, - override val descriptionI18n: Map?, - override val price: String, - override val location: String?, - override val image: String?, - val categories: List, - @JsonIgnore - val quantity: Int = 0 -) : Product() { - val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() } - - override fun equals(other: Any?) = other is ConfigProduct && id == other.id - override fun hashCode() = id.hashCode() -} - -data class ContractProduct( - override val productId: String?, - override val description: String, - override val descriptionI18n: Map?, - override val price: String, - override val location: String?, - override val image: String?, - val quantity: Int -) : Product() { - constructor(product: ConfigProduct) : this( - product.productId, - product.description, - product.descriptionI18n, - product.price, - product.location, - product.image, - product.quantity - ) -} - -private fun getLocalizedString(map: Map?, default: String): String { - // just return the default, if it is the only element - if (map == null) return default - // create a priority list of language ranges from system locales - val locales = LocaleListCompat.getDefault() - val priorityList = ArrayList(locales.size()) - for (i in 0 until locales.size()) { - priorityList.add(LanguageRange(locales[i].toLanguageTag())) - } - // create a list of locales available in the given map - val availableLocales = map.keys.mapNotNull { - if (it == "_") return@mapNotNull null - val list = it.split("_") - when (list.size) { - 1 -> Locale(list[0]) - 2 -> Locale(list[0], list[1]) - 3 -> Locale(list[0], list[1], list[2]) - else -> null - } - } - val match = Locale.lookup(priorityList, availableLocales) - return match?.toString()?.let { map[it] } ?: default -} - -data class Order(val id: Int, val availableCategories: Map) { - val products = ArrayList() - val title: String = id.toString() - val summary: String - get() { - if (products.size == 1) return products[0].description - return getCategoryQuantities().map { (category: Category, quantity: Int) -> - "$quantity x ${category.localizedName}" - }.joinToString() - } - val total: Double - get() { - var total = 0.0 - products.forEach { product -> - val price = product.priceAsDouble - total += price * product.quantity - } - return total - } - val totalAsString: String - get() = String.format("%.2f", total) - - operator fun plus(product: ConfigProduct): Order { - val i = products.indexOf(product) - if (i == -1) { - products.add(product.copy(quantity = 1)) - } else { - val quantity = products[i].quantity - products[i] = products[i].copy(quantity = quantity + 1) - } - return this - } - - operator fun minus(product: ConfigProduct): Order { - val i = products.indexOf(product) - if (i == -1) return this - val quantity = products[i].quantity - if (quantity <= 1) { - products.remove(product) - } else { - products[i] = products[i].copy(quantity = quantity - 1) - } - return this - } - - private fun getCategoryQuantities(): HashMap { - val categories = HashMap() - products.forEach { product -> - val categoryId = product.categories[0] - val category = availableCategories.getValue(categoryId) - val oldQuantity = categories[category] ?: 0 - categories[category] = oldQuantity + product.quantity - } - return categories - } - - /** - * Returns a map of i18n summaries for each locale present in *all* given [Category]s - * or null if there's no locale that fulfills this criteria. - */ - val summaryI18n: Map? - get() { - if (products.size == 1) return products[0].descriptionI18n - val categoryQuantities = getCategoryQuantities() - // get all available locales - val availableLocales = categoryQuantities.mapNotNull { (category, _) -> - val nameI18n = category.nameI18n - // if one category doesn't have locales, we can return null here already - nameI18n?.keys ?: return null - }.flatten().toHashSet() - // remove all locales not supported by all categories - categoryQuantities.forEach { (category, _) -> - // category.nameI18n should be non-null now - availableLocales.retainAll(category.nameI18n!!.keys) - if (availableLocales.isEmpty()) return null - } - return availableLocales.map { locale -> - Pair( - locale, categoryQuantities.map { (category, quantity) -> - // category.nameI18n should be non-null now - "$quantity x ${category.nameI18n!![locale]}" - }.joinToString() - ) - }.toMap() - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt index ff6061a..847326b 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt @@ -20,7 +20,9 @@ import androidx.annotation.UiThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations -import net.taler.merchantpos.CombinedLiveData +import net.taler.common.CombinedLiveData +import net.taler.merchantpos.config.Category +import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.order.RestartState.DISABLED import net.taler.merchantpos.order.RestartState.ENABLED import net.taler.merchantpos.order.RestartState.UNDO 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 new file mode 100644 index 0000000..5954e63 --- /dev/null +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt @@ -0,0 +1,108 @@ +/* + * 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.order + +import net.taler.merchantpos.config.Category +import net.taler.merchantpos.config.ConfigProduct + +data class Order(val id: Int, val availableCategories: Map) { + val products = ArrayList() + val title: String = id.toString() + val summary: String + get() { + if (products.size == 1) return products[0].description + return getCategoryQuantities().map { (category: Category, quantity: Int) -> + "$quantity x ${category.localizedName}" + }.joinToString() + } + val total: Double + get() { + var total = 0.0 + products.forEach { product -> + val price = product.priceAsDouble + total += price * product.quantity + } + return total + } + val totalAsString: String + get() = String.format("%.2f", total) + + operator fun plus(product: ConfigProduct): Order { + val i = products.indexOf(product) + if (i == -1) { + products.add(product.copy(quantity = 1)) + } else { + val quantity = products[i].quantity + products[i] = products[i].copy(quantity = quantity + 1) + } + return this + } + + operator fun minus(product: ConfigProduct): Order { + val i = products.indexOf(product) + if (i == -1) return this + val quantity = products[i].quantity + if (quantity <= 1) { + products.remove(product) + } else { + products[i] = products[i].copy(quantity = quantity - 1) + } + return this + } + + private fun getCategoryQuantities(): HashMap { + val categories = HashMap() + products.forEach { product -> + val categoryId = product.categories[0] + val category = availableCategories.getValue(categoryId) + val oldQuantity = categories[category] ?: 0 + categories[category] = oldQuantity + product.quantity + } + return categories + } + + /** + * Returns a map of i18n summaries for each locale present in *all* given [Category]s + * or null if there's no locale that fulfills this criteria. + */ + val summaryI18n: Map? + get() { + if (products.size == 1) return products[0].descriptionI18n + val categoryQuantities = getCategoryQuantities() + // get all available locales + val availableLocales = categoryQuantities.mapNotNull { (category, _) -> + val nameI18n = category.nameI18n + // if one category doesn't have locales, we can return null here already + nameI18n?.keys ?: return null + }.flatten().toHashSet() + // remove all locales not supported by all categories + categoryQuantities.forEach { (category, _) -> + // category.nameI18n should be non-null now + availableLocales.retainAll(category.nameI18n!!.keys) + if (availableLocales.isEmpty()) return null + } + return availableLocales.map { locale -> + Pair( + locale, categoryQuantities.map { (category, quantity) -> + // category.nameI18n should be non-null now + "$quantity x ${category.nameI18n!![locale]}" + }.joinToString() + ) + }.toMap() + } + +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt index 49f7cf2..ad6cd87 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt @@ -23,12 +23,11 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer -import androidx.navigation.fragment.findNavController import androidx.transition.TransitionManager.beginDelayedTransition import kotlinx.android.synthetic.main.fragment_order.* +import net.taler.common.navigate import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R -import net.taler.merchantpos.navigate import net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionGlobalConfigFetcher import net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionOrderToMerchantSettings import net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionOrderToProcessPayment @@ -65,9 +64,9 @@ class OrderFragment : Fragment() { override fun onStart() { super.onStart() if (!viewModel.configManager.config.isValid()) { - actionOrderToMerchantSettings().navigate(findNavController()) + navigate(actionOrderToMerchantSettings()) } else if (viewModel.configManager.merchantConfig?.currency == null) { - actionGlobalConfigFetcher().navigate(findNavController()) + navigate(actionGlobalConfigFetcher()) } } @@ -108,7 +107,7 @@ class OrderFragment : Fragment() { completeButton.setOnClickListener { val order = liveOrder.order.value ?: return@setOnClickListener paymentManager.createPayment(order) - actionOrderToProcessPayment().navigate(findNavController()) + navigate(actionOrderToProcessPayment()) } } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt index 48ddc57..a30c264 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt @@ -24,8 +24,10 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations.map import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper -import net.taler.merchantpos.Amount.Companion.fromString +import net.taler.common.Amount.Companion.fromString import net.taler.merchantpos.R +import net.taler.merchantpos.config.Category +import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.config.ConfigurationReceiver import net.taler.merchantpos.order.RestartState.ENABLED import org.json.JSONObject diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt index 1b70016..a90334b 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt @@ -37,10 +37,11 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.ViewHolder import kotlinx.android.synthetic.main.fragment_order_state.* +import net.taler.common.fadeIn +import net.taler.common.fadeOut import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R -import net.taler.merchantpos.fadeIn -import net.taler.merchantpos.fadeOut +import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.order.OrderAdapter.OrderLineLookup import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt index 4704ad0..d4da73f 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt @@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import kotlinx.android.synthetic.main.fragment_products.* import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R +import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.order.ProductAdapter.ProductViewHolder interface ProductSelectionListener { 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 7f15816..4cfb069 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 @@ -30,7 +30,6 @@ 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 @@ -103,7 +102,7 @@ class PaymentManager( } private fun Order.getProductsJson(): JSONArray { - val contractProducts = products.map { ContractProduct(it) } + val contractProducts = products.map { it.toContractProduct() } val productsStr = mapper.writeValueAsString(contractProducts) return JSONArray(productsStr) } 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 24f67f1..1d61894 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 @@ -27,13 +27,13 @@ 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.common.NfcManager.Companion.hasNfc +import net.taler.common.QrCodeManager.makeQrCode +import net.taler.common.fadeIn +import net.taler.common.fadeOut +import net.taler.common.navigate 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 @@ -69,7 +69,7 @@ class ProcessPaymentFragment : Fragment() { } if (payment.paid) { model.orderManager.onOrderPaid(payment.order.id) - actionProcessPaymentToPaymentSuccess().navigate(findNavController()) + navigate(actionProcessPaymentToPaymentSuccess()) return } payIntroView.fadeIn() -- cgit v1.2.3