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 }