OrderManager.kt (12794B)
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.common.CurrencySpecification 26 import net.taler.merchantpos.R 27 import net.taler.merchantpos.config.Category 28 import net.taler.merchantpos.config.ConfigProduct 29 import net.taler.merchantpos.config.ConfigurationReceiver 30 import net.taler.merchantpos.config.PosConfig 31 import net.taler.merchantpos.order.RestartState.ENABLED 32 33 class OrderManager(private val context: Context) : ConfigurationReceiver { 34 35 companion object { 36 val TAG: String = OrderManager::class.java.simpleName 37 private const val ALL_PRODUCTS_CATEGORY_ID = -1 38 private const val UNCATEGORIZED_CATEGORY_ID = -2 39 private const val LEGACY_DEFAULT_CATEGORY_NAME = "Default" 40 } 41 42 private lateinit var currency: String 43 private var currencySpec: CurrencySpecification? = null 44 private var orderCounter: Int = 0 45 private val mCurrentOrderId = MutableLiveData<Int>() 46 internal val currentOrderId: LiveData<Int> = mCurrentOrderId 47 48 private val productsByCategory = HashMap<Category, ArrayList<ConfigProduct>>() 49 private val productsById = HashMap<String, ConfigProduct>() 50 private val orders = LinkedHashMap<Int, MutableLiveOrder>() 51 52 private val mProducts = MutableLiveData<List<ConfigProduct>>() 53 internal val products: LiveData<List<ConfigProduct>> = mProducts 54 55 private val mCategories = MutableLiveData<List<Category>>() 56 internal val categories: LiveData<List<Category>> = mCategories 57 private var currentCategory: Category? = null 58 59 override suspend fun onConfigurationReceived( 60 posConfig: PosConfig, 61 currency: String, 62 currencySpec: CurrencySpecification?, 63 ): String? = applyConfiguration(posConfig, currency, currencySpec, resetOrders = true) 64 65 override suspend fun onInventoryUpdated( 66 posConfig: PosConfig, 67 currency: String, 68 currencySpec: CurrencySpecification?, 69 ): String? = applyConfiguration(posConfig, currency, currencySpec, resetOrders = false) 70 71 private fun applyConfiguration( 72 posConfig: PosConfig, 73 currency: String, 74 currencySpec: CurrencySpecification?, 75 resetOrders: Boolean, 76 ): String? { 77 val existingProductsByStableKey = productsById.values.associateBy { it.stableKey } 78 if (posConfig.categories.isEmpty()) { 79 Log.e(TAG, "No valid category found.") 80 return context.getString(R.string.config_error_category) 81 } 82 83 val selectedCategoryId = if (resetOrders) ALL_PRODUCTS_CATEGORY_ID else currentCategory?.id 84 val allProductsCategory = Category( 85 ALL_PRODUCTS_CATEGORY_ID, 86 context.getString(R.string.product_category_all_objects) 87 ) 88 val uncategorizedCategory = Category( 89 UNCATEGORIZED_CATEGORY_ID, 90 context.getString(R.string.product_category_uncategorized) 91 ) 92 val visibleCategories = posConfig.categories.filterNot(::isLegacyDefaultCategory) 93 val legacyDefaultCategoryIds = posConfig.categories 94 .filter(::isLegacyDefaultCategory) 95 .map { it.id } 96 .toSet() 97 98 productsByCategory.clear() 99 productsById.clear() 100 productsByCategory[allProductsCategory] = ArrayList() 101 visibleCategories.forEach { category -> 102 category.selected = false 103 productsByCategory[category] = ArrayList() 104 } 105 productsByCategory[uncategorizedCategory] = ArrayList() 106 107 posConfig.products.forEach { product -> 108 val productCurrency = product.price.currency 109 if (productCurrency != currency) { 110 Log.e(TAG, "Product $product has currency $productCurrency, $currency expected") 111 return context.getString( 112 R.string.config_error_currency, product.description, productCurrency, currency 113 ) 114 } 115 val remainingStock = product.stockLimit 116 val productWithSpec = product.copy( 117 id = existingProductsByStableKey[product.stableKey]?.id ?: product.id, 118 price = product.price.withSpec(currencySpec), 119 availableToSell = remainingStock == null || remainingStock > 0, 120 remainingStock = remainingStock, 121 ) 122 productsById[productWithSpec.id] = productWithSpec 123 productsByCategory.getValue(allProductsCategory).add(productWithSpec) 124 if (product.categories.isEmpty()) { 125 productsByCategory.getValue(uncategorizedCategory).add(productWithSpec) 126 } 127 product.categories.forEach { categoryId -> 128 if (categoryId in legacyDefaultCategoryIds) { 129 productsByCategory.getValue(uncategorizedCategory).add(productWithSpec) 130 return@forEach 131 } 132 val category = visibleCategories.find { it.id == categoryId } 133 if (category == null) { 134 Log.e(TAG, "Product $product has unknown category $categoryId") 135 productsByCategory.getValue(uncategorizedCategory).add(productWithSpec) 136 } else { 137 productsByCategory.getValue(category).add(productWithSpec) 138 } 139 } 140 } 141 142 this.currency = currency 143 this.currencySpec = currencySpec 144 val categoryList = buildList { 145 add(allProductsCategory) 146 addAll(visibleCategories) 147 if (productsByCategory.getValue(uncategorizedCategory).isNotEmpty()) { 148 add(uncategorizedCategory) 149 } else { 150 productsByCategory.remove(uncategorizedCategory) 151 } 152 } 153 val selectedCategory = 154 categoryList.firstOrNull { it.id == selectedCategoryId } ?: allProductsCategory 155 categoryList.forEach { it.selected = it.id == selectedCategory.id } 156 currentCategory = selectedCategory 157 mCategories.postValue(categoryList) 158 mProducts.postValue(getVisibleProducts()) 159 160 if (resetOrders) { 161 orders.clear() 162 orderCounter = 0 163 orders[0] = createOrder(0) 164 mCurrentOrderId.postValue(0) 165 } 166 return null 167 } 168 169 @UiThread 170 internal fun getOrder(orderId: Int): LiveOrder { 171 return orders[orderId] ?: throw IllegalArgumentException("Order not found: $orderId") 172 } 173 174 @UiThread 175 internal fun nextOrder() { 176 val currentId = currentOrderId.value!! 177 var foundCurrentOrder = false 178 var nextId: Int? = null 179 for (orderId in orders.keys) { 180 if (foundCurrentOrder) { 181 nextId = orderId 182 break 183 } 184 if (orderId == currentId) foundCurrentOrder = true 185 } 186 if (nextId == null) { 187 nextId = ++orderCounter 188 orders[nextId] = createOrder(nextId) 189 } 190 val currentOrder = order(currentId) 191 if (currentOrder.isEmpty()) orders.remove(currentId) 192 else currentOrder.lastAddedProduct = null 193 mCurrentOrderId.value = requireNotNull(nextId) 194 updateVisibleProducts() 195 } 196 197 @UiThread 198 internal fun previousOrder() { 199 val currentId = currentOrderId.value!! 200 var previousId: Int? = null 201 var foundCurrentOrder = false 202 for (orderId in orders.keys) { 203 if (orderId == currentId) { 204 foundCurrentOrder = true 205 break 206 } 207 previousId = orderId 208 } 209 if (previousId == null || !foundCurrentOrder) { 210 throw AssertionError("Could not find previous order for $currentId") 211 } 212 val currentOrder = order(currentId) 213 if (currentOrder.isEmpty()) orders.remove(currentId) 214 else currentOrder.lastAddedProduct = null 215 mCurrentOrderId.value = requireNotNull(previousId) 216 updateVisibleProducts() 217 } 218 219 fun hasPreviousOrder(currentOrderId: Int): Boolean { 220 return currentOrderId != orders.keys.first() 221 } 222 223 fun hasNextOrder(currentOrderId: Int) = order(currentOrderId).restartState.map { state -> 224 state == ENABLED || currentOrderId != orders.keys.last() 225 } 226 227 internal fun setCurrentCategory(category: Category) { 228 currentCategory = category 229 val currentCategories = categories.value.orEmpty() 230 val newCategories = currentCategories.map { existing -> 231 existing.copy().also { copied -> 232 copied.selected = existing.id == category.id 233 } 234 } 235 currentCategory = newCategories.firstOrNull { it.id == category.id } ?: category 236 mCategories.postValue(newCategories) 237 updateVisibleProducts() 238 } 239 240 @UiThread 241 internal fun addProduct(orderId: Int, product: ConfigProduct) { 242 order(orderId).addProduct(product) 243 } 244 245 @UiThread 246 internal fun debugSeedCurrentOrder(productIds: List<String>) { 247 val orderId = currentOrderId.value ?: return 248 val liveOrder = order(orderId) 249 productIds.forEach { productId -> 250 productsById[productId]?.let(liveOrder::addProduct) 251 } 252 updateVisibleProducts() 253 } 254 255 @UiThread 256 internal fun onOrderPaid(orderId: Int) { 257 if (currentOrderId.value == orderId) { 258 if (hasPreviousOrder(orderId)) previousOrder() 259 else nextOrder() 260 } 261 orders.remove(orderId) 262 updateVisibleProducts() 263 } 264 265 @UiThread 266 internal fun deleteCurrentOrder() { 267 val currentId = currentOrderId.value ?: return 268 val orderIds = orders.keys.toList() 269 val currentIndex = orderIds.indexOf(currentId) 270 if (currentIndex == -1) return 271 272 orders.remove(currentId) 273 val replacementId = when { 274 orders.isEmpty() -> { 275 orders[currentId] = createOrder(currentId) 276 currentId 277 } 278 currentIndex < orderIds.lastIndex -> orderIds[currentIndex + 1] 279 else -> orderIds[currentIndex - 1] 280 } 281 mCurrentOrderId.value = replacementId 282 updateVisibleProducts() 283 } 284 285 private fun order(orderId: Int): MutableLiveOrder { 286 return orders[orderId] ?: throw IllegalStateException() 287 } 288 289 private fun isLegacyDefaultCategory(category: Category): Boolean { 290 return category.name.equals(LEGACY_DEFAULT_CATEGORY_NAME, ignoreCase = true) 291 } 292 293 private fun createOrder(orderId: Int): MutableLiveOrder { 294 return MutableLiveOrder( 295 orderId, 296 currency, 297 currencySpec, 298 productsByCategory, 299 ::canAddProduct, 300 ::updateVisibleProducts, 301 ) 302 } 303 304 private fun getVisibleProducts(): List<ConfigProduct> { 305 val category = currentCategory ?: return emptyList() 306 return productsByCategory[category].orEmpty().map(::decorateProduct) 307 } 308 309 private fun updateVisibleProducts() { 310 mProducts.postValue(getVisibleProducts()) 311 } 312 313 private fun decorateProduct(product: ConfigProduct): ConfigProduct { 314 val remainingStock = remainingStock(product) 315 return product.copy( 316 availableToSell = remainingStock == null || remainingStock > 0, 317 remainingStock = remainingStock, 318 ) 319 } 320 321 private fun canAddProduct(product: ConfigProduct): Boolean { 322 return remainingStock(product)?.let { it > 0 } ?: true 323 } 324 325 private fun remainingStock(product: ConfigProduct): Int? { 326 val stockLimit = productsById[product.id]?.stockLimit ?: product.stockLimit ?: return null 327 val reserved = orders.values.sumOf { liveOrder -> 328 liveOrder.order.value 329 ?.products 330 ?.find { it.id == product.id } 331 ?.quantity 332 ?: 0 333 } 334 return (stockLimit - reserved).coerceAtLeast(0) 335 } 336 }