taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

OrderManager.kt (8693B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2020 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 package net.taler.merchantpos.order
     18 
     19 import android.content.Context
     20 import android.util.Log
     21 import androidx.annotation.UiThread
     22 import androidx.lifecycle.LiveData
     23 import androidx.lifecycle.MutableLiveData
     24 import androidx.lifecycle.map
     25 import net.taler.merchantpos.R
     26 import net.taler.merchantpos.config.Category
     27 import net.taler.merchantpos.config.ConfigProduct
     28 import net.taler.merchantpos.config.ConfigurationReceiver
     29 import net.taler.merchantpos.config.PosConfig
     30 import net.taler.merchantpos.order.RestartState.ENABLED
     31 
     32 class OrderManager(private val context: Context) : ConfigurationReceiver {
     33 
     34     companion object {
     35         val TAG: String = OrderManager::class.java.simpleName
     36         private const val ALL_PRODUCTS_CATEGORY_ID = -1
     37         private const val UNCATEGORIZED_CATEGORY_ID = -2
     38         private const val LEGACY_DEFAULT_CATEGORY_NAME = "Default"
     39     }
     40 
     41     private lateinit var currency: String
     42     private var orderCounter: Int = 0
     43     private val mCurrentOrderId = MutableLiveData<Int>()
     44     internal val currentOrderId: LiveData<Int> = mCurrentOrderId
     45 
     46     private val productsByCategory = HashMap<Category, ArrayList<ConfigProduct>>()
     47 
     48     private val orders = LinkedHashMap<Int, MutableLiveOrder>()
     49 
     50     private val mProducts = MutableLiveData<List<ConfigProduct>>()
     51     internal val products: LiveData<List<ConfigProduct>> = mProducts
     52 
     53     private val mCategories = MutableLiveData<List<Category>>()
     54     internal val categories: LiveData<List<Category>> = mCategories
     55 
     56     override suspend fun onConfigurationReceived(posConfig: PosConfig, currency: String): String? {
     57         // parse categories
     58         if (posConfig.categories.isEmpty()) {
     59             Log.e(TAG, "No valid category found.")
     60             return context.getString(R.string.config_error_category)
     61         }
     62         val allProductsCategory = Category(
     63             ALL_PRODUCTS_CATEGORY_ID,
     64             context.getString(R.string.product_category_all_objects)
     65         ).apply {
     66             selected = true
     67         }
     68         val uncategorizedCategory = Category(
     69             UNCATEGORIZED_CATEGORY_ID,
     70             context.getString(R.string.product_category_uncategorized)
     71         )
     72         val visibleCategories = posConfig.categories.filterNot(::isLegacyDefaultCategory)
     73         val legacyDefaultCategoryIds = posConfig.categories
     74             .filter(::isLegacyDefaultCategory)
     75             .map { it.id }
     76             .toSet()
     77 
     78         // group products by categories
     79         productsByCategory.clear()
     80         productsByCategory[allProductsCategory] = ArrayList()
     81         visibleCategories.forEach { category ->
     82             category.selected = false
     83             productsByCategory[category] = ArrayList()
     84         }
     85         productsByCategory[uncategorizedCategory] = ArrayList()
     86         posConfig.products.forEach { product ->
     87             val productCurrency = product.price.currency
     88             if (productCurrency != currency) {
     89                 Log.e(TAG, "Product $product has currency $productCurrency, $currency expected")
     90                 return context.getString(
     91                     R.string.config_error_currency, product.description, productCurrency, currency
     92                 )
     93             }
     94             productsByCategory.getValue(allProductsCategory).add(product)
     95             if (product.categories.isEmpty()) {
     96                 productsByCategory.getValue(uncategorizedCategory).add(product)
     97             }
     98             product.categories.forEach { categoryId ->
     99                 if (categoryId in legacyDefaultCategoryIds) {
    100                     productsByCategory.getValue(uncategorizedCategory).add(product)
    101                     return@forEach
    102                 }
    103                 val category = visibleCategories.find { it.id == categoryId }
    104                 if (category == null) {
    105                     Log.e(TAG, "Product $product has unknown category $categoryId")
    106                     productsByCategory.getValue(uncategorizedCategory).add(product)
    107                 } else {
    108                     productsByCategory.getValue(category).add(product)
    109                 }
    110             }
    111         }
    112         this.currency = currency
    113         val categoryList = buildList {
    114             add(allProductsCategory)
    115             addAll(visibleCategories)
    116             if (productsByCategory.getValue(uncategorizedCategory).isNotEmpty()) {
    117                 add(uncategorizedCategory)
    118             } else {
    119                 productsByCategory.remove(uncategorizedCategory)
    120             }
    121         }
    122         mCategories.postValue(categoryList)
    123         mProducts.postValue(productsByCategory[allProductsCategory] ?: emptyList())
    124         orders.clear()
    125         orderCounter = 0
    126         orders[0] = MutableLiveOrder(0, currency, productsByCategory)
    127         mCurrentOrderId.postValue(0)
    128         return null // success, no error string
    129     }
    130 
    131     @UiThread
    132     internal fun getOrder(orderId: Int): LiveOrder {
    133         return orders[orderId] ?: throw IllegalArgumentException("Order not found: $orderId")
    134     }
    135 
    136     @UiThread
    137     internal fun nextOrder() {
    138         val currentId = currentOrderId.value!!
    139         var foundCurrentOrder = false
    140         var nextId: Int? = null
    141         for (orderId in orders.keys) {
    142             if (foundCurrentOrder) {
    143                 nextId = orderId
    144                 break
    145             }
    146             if (orderId == currentId) foundCurrentOrder = true
    147         }
    148         if (nextId == null) {
    149             nextId = ++orderCounter
    150             orders[nextId] = MutableLiveOrder(nextId, currency, productsByCategory)
    151         }
    152         val currentOrder = order(currentId)
    153         if (currentOrder.isEmpty()) orders.remove(currentId)
    154         else currentOrder.lastAddedProduct = null  // not needed anymore and it would get selected
    155         mCurrentOrderId.value = requireNotNull(nextId)
    156     }
    157 
    158     @UiThread
    159     internal fun previousOrder() {
    160         val currentId = currentOrderId.value!!
    161         var previousId: Int? = null
    162         var foundCurrentOrder = false
    163         for (orderId in orders.keys) {
    164             if (orderId == currentId) {
    165                 foundCurrentOrder = true
    166                 break
    167             }
    168             previousId = orderId
    169         }
    170         if (previousId == null || !foundCurrentOrder) {
    171             throw AssertionError("Could not find previous order for $currentId")
    172         }
    173         val currentOrder = order(currentId)
    174         // remove current order if empty, or lastAddedProduct as it is not needed anymore
    175         // and would get selected when navigating back instead of last selection
    176         if (currentOrder.isEmpty()) orders.remove(currentId)
    177         else currentOrder.lastAddedProduct = null
    178         mCurrentOrderId.value = requireNotNull(previousId)
    179     }
    180 
    181     fun hasPreviousOrder(currentOrderId: Int): Boolean {
    182         return currentOrderId != orders.keys.first()
    183     }
    184 
    185     fun hasNextOrder(currentOrderId: Int) = order(currentOrderId).restartState.map { state ->
    186         state == ENABLED || currentOrderId != orders.keys.last()
    187     }
    188 
    189     internal fun setCurrentCategory(category: Category) {
    190         val newCategories = categories.value?.apply {
    191             forEach { if (it.selected) it.selected = false }
    192             category.selected = true
    193         }
    194         mCategories.postValue(newCategories ?: emptyList())
    195         mProducts.postValue(productsByCategory[category])
    196     }
    197 
    198     @UiThread
    199     internal fun addProduct(orderId: Int, product: ConfigProduct) {
    200         order(orderId).addProduct(product)
    201     }
    202 
    203     @UiThread
    204     internal fun onOrderPaid(orderId: Int) {
    205         if (currentOrderId.value == orderId) {
    206             if (hasPreviousOrder(orderId)) previousOrder()
    207             else nextOrder()
    208         }
    209         orders.remove(orderId)
    210     }
    211 
    212     private fun order(orderId: Int): MutableLiveOrder {
    213         return orders[orderId] ?: throw IllegalStateException()
    214     }
    215 
    216     private fun isLegacyDefaultCategory(category: Category): Boolean {
    217         return category.name.equals(LEGACY_DEFAULT_CATEGORY_NAME, ignoreCase = true)
    218     }
    219 
    220 }