/*
* 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 android.content.Context
import android.util.Log
import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations.map
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.config.PosConfig
import net.taler.merchantpos.order.RestartState.ENABLED
class OrderManager(private val context: Context) : ConfigurationReceiver {
companion object {
val TAG = OrderManager::class.java.simpleName
}
private lateinit var currency: String
private var orderCounter: Int = 0
private val mCurrentOrderId = MutableLiveData()
internal val currentOrderId: LiveData = mCurrentOrderId
private val productsByCategory = HashMap>()
private val orders = LinkedHashMap()
private val mProducts = MutableLiveData>()
internal val products: LiveData> = mProducts
private val mCategories = MutableLiveData>()
internal val categories: LiveData> = mCategories
override suspend fun onConfigurationReceived(posConfig: PosConfig, currency: String): String? {
// parse categories
if (posConfig.categories.isEmpty()) {
Log.e(TAG, "No valid category found.")
return context.getString(R.string.config_error_category)
}
// pre-select the first category
posConfig.categories[0].selected = true
// group products by categories
productsByCategory.clear()
posConfig.products.forEach { product ->
val productCurrency = product.price.currency
if (productCurrency != currency) {
Log.e(TAG, "Product $product has currency $productCurrency, $currency expected")
return context.getString(
R.string.config_error_currency, product.description, productCurrency, currency
)
}
product.categories.forEach { categoryId ->
val category = posConfig.categories.find { it.id == categoryId }
if (category == null) {
Log.e(TAG, "Product $product has unknown category $categoryId")
return context.getString(
R.string.config_error_product_category_id, product.description, categoryId
)
}
if (productsByCategory.containsKey(category)) {
productsByCategory[category]?.add(product)
} else {
productsByCategory[category] = ArrayList().apply { add(product) }
}
}
}
return if (productsByCategory.size > 0) {
this.currency = currency
mCategories.postValue(posConfig.categories)
mProducts.postValue(productsByCategory[posConfig.categories[0]])
// Initialize first empty order, note this won't work when updating config mid-flight
if (orders.isEmpty()) {
val id = orderCounter++
orders[id] = MutableLiveOrder(id, currency, productsByCategory)
mCurrentOrderId.postValue(id)
}
null // success, no error string
} else context.getString(R.string.config_error_product_zero)
}
@UiThread
internal fun getOrder(orderId: Int): LiveOrder {
return orders[orderId] ?: throw IllegalArgumentException("Order not found: $orderId")
}
@UiThread
internal fun nextOrder() {
val currentId = currentOrderId.value!!
var foundCurrentOrder = false
var nextId: Int? = null
for (orderId in orders.keys) {
if (foundCurrentOrder) {
nextId = orderId
break
}
if (orderId == currentId) foundCurrentOrder = true
}
if (nextId == null) {
nextId = orderCounter++
orders[nextId] = MutableLiveOrder(nextId, currency, productsByCategory)
}
val currentOrder = order(currentId)
if (currentOrder.isEmpty()) orders.remove(currentId)
else currentOrder.lastAddedProduct = null // not needed anymore and it would get selected
mCurrentOrderId.value = nextId
}
@UiThread
internal fun previousOrder() {
val currentId = currentOrderId.value!!
var previousId: Int? = null
var foundCurrentOrder = false
for (orderId in orders.keys) {
if (orderId == currentId) {
foundCurrentOrder = true
break
}
previousId = orderId
}
if (previousId == null || !foundCurrentOrder) {
throw AssertionError("Could not find previous order for $currentId")
}
val currentOrder = order(currentId)
// remove current order if empty, or lastAddedProduct as it is not needed anymore
// and would get selected when navigating back instead of last selection
if (currentOrder.isEmpty()) orders.remove(currentId)
else currentOrder.lastAddedProduct = null
mCurrentOrderId.value = previousId
}
fun hasPreviousOrder(currentOrderId: Int): Boolean {
return currentOrderId != orders.keys.first()
}
fun hasNextOrder(currentOrderId: Int) = map(order(currentOrderId).restartState) { state ->
state == ENABLED || currentOrderId != orders.keys.last()
}
internal fun setCurrentCategory(category: Category) {
val newCategories = categories.value?.apply {
forEach { if (it.selected) it.selected = false }
category.selected = true
}
mCategories.postValue(newCategories)
mProducts.postValue(productsByCategory[category])
}
@UiThread
internal fun addProduct(orderId: Int, product: ConfigProduct) {
order(orderId).addProduct(product)
}
@UiThread
internal fun onOrderPaid(orderId: Int) {
if (currentOrderId.value == orderId) {
if (hasPreviousOrder(orderId)) previousOrder()
else nextOrder()
}
orders.remove(orderId)
}
private fun order(orderId: Int): MutableLiveOrder {
return orders[orderId] ?: throw IllegalStateException()
}
}