HistoryManager.kt (7179B)
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.history 18 19 import androidx.annotation.StringRes 20 import androidx.annotation.UiThread 21 import androidx.lifecycle.LiveData 22 import androidx.lifecycle.MutableLiveData 23 import kotlinx.coroutines.CoroutineScope 24 import kotlinx.coroutines.launch 25 import net.taler.lib.android.assertUiThread 26 import net.taler.merchantlib.CheckPaymentResponse 27 import net.taler.merchantlib.MerchantApi 28 import net.taler.merchantlib.OrderHistoryEntry 29 import net.taler.merchantpos.R 30 import net.taler.merchantpos.config.ConfigManager 31 32 sealed class HistoryResult { 33 class Success(val items: List<OrderHistoryEntry>) : HistoryResult() 34 } 35 36 class HistoryError( 37 @StringRes val mainResId: Int, 38 val msg: String, 39 ) 40 41 class HistoryManager( 42 private val configManager: ConfigManager, 43 private val scope: CoroutineScope, 44 private val api: MerchantApi 45 ) { 46 companion object { 47 private const val PAGE_SIZE = 20 48 private const val LOAD_MORE_THRESHOLD = 5 49 } 50 51 private val mIsLoading = MutableLiveData(false) 52 val isLoading: LiveData<Boolean> = mIsLoading 53 private val mIsLoadingMore = MutableLiveData(false) 54 val isLoadingMore: LiveData<Boolean> = mIsLoadingMore 55 56 private val mItems = MutableLiveData<HistoryResult>() 57 val items: LiveData<HistoryResult> = mItems 58 private val mError = MutableLiveData<HistoryError?>(null) 59 val error: LiveData<HistoryError?> = mError 60 61 private val loadedItems = mutableListOf<OrderHistoryEntry>() 62 private var nextOffset: Long? = null 63 private var reachedEnd = false 64 65 @UiThread 66 internal fun fetchHistory() = fetchHistoryPage(reset = true) 67 68 @UiThread 69 internal fun loadMoreHistoryIfNeeded(lastVisibleIndex: Int) { 70 val currentItems = (mItems.value as? HistoryResult.Success)?.items ?: return 71 if (mIsLoading.value == true || mIsLoadingMore.value == true || reachedEnd) return 72 if (currentItems.isEmpty()) return 73 if (lastVisibleIndex < currentItems.lastIndex - LOAD_MORE_THRESHOLD) return 74 fetchHistoryPage(reset = false) 75 } 76 77 @UiThread 78 private fun fetchHistoryPage(reset: Boolean) = scope.launch { 79 if (reset) { 80 mIsLoading.value = true 81 mIsLoadingMore.value = false 82 } else { 83 mIsLoadingMore.value = true 84 } 85 val merchantConfig = configManager.merchantConfig!! 86 val offset = if (reset) null else nextOffset 87 api.getOrderHistory( 88 merchantConfig = merchantConfig, 89 limit = -PAGE_SIZE, 90 offset = offset, 91 ).handle({ onError(R.string.error_history, it) }) { response -> 92 assertUiThread() 93 mIsLoading.value = false 94 mIsLoadingMore.value = false 95 if (reset) { 96 loadedItems.clear() 97 reachedEnd = false 98 } 99 val page = response.orders 100 val existingOrderIds = loadedItems.mapTo(mutableSetOf()) { it.orderId } 101 val newItems = page.filterNot { it.orderId in existingOrderIds } 102 if (newItems.isNotEmpty()) { 103 loadedItems += newItems 104 nextOffset = newItems.lastOrNull()?.rowId ?: loadedItems.lastOrNull()?.rowId 105 } 106 if (page.size < PAGE_SIZE || newItems.isEmpty()) { 107 reachedEnd = true 108 } 109 publishItems() 110 enrichOrders(newItems) 111 } 112 } 113 114 private val mForceDeleteOrderId = MutableLiveData<String?>(null) 115 val forceDeleteOrderId: LiveData<String?> = mForceDeleteOrderId 116 117 @UiThread 118 internal fun deleteOrder(orderId: String) = scope.launch { 119 mIsLoading.value = true 120 mIsLoadingMore.value = false 121 val merchantConfig = configManager.merchantConfig!! 122 api.deleteOrder(merchantConfig, orderId).handle({ errorMsg -> 123 assertUiThread() 124 mIsLoading.value = false 125 mForceDeleteOrderId.value = orderId 126 }) { 127 assertUiThread() 128 configManager.refreshInventory() 129 fetchHistory() 130 } 131 } 132 133 @UiThread 134 internal fun forceDeleteOrder(orderId: String) = scope.launch { 135 mForceDeleteOrderId.postValue(null) 136 mIsLoading.value = true 137 val merchantConfig = configManager.merchantConfig!! 138 api.deleteOrder(merchantConfig, orderId, force = true).handle({ errorMsg -> 139 onError(R.string.error_delete_order, errorMsg) 140 }) { 141 assertUiThread() 142 configManager.refreshInventory() 143 fetchHistory() 144 } 145 } 146 147 @UiThread 148 internal fun clearForceDeletePrompt() { 149 mForceDeleteOrderId.value = null 150 } 151 152 @UiThread 153 internal fun clearError() { 154 mError.value = null 155 } 156 157 private fun publishItems() { 158 mItems.value = HistoryResult.Success(loadedItems.toList()) 159 } 160 161 @UiThread 162 internal fun debugSetHistory(items: List<OrderHistoryEntry>) { 163 loadedItems.clear() 164 loadedItems.addAll(items) 165 publishItems() 166 } 167 168 private fun enrichOrders(items: List<OrderHistoryEntry>) = scope.launch { 169 val merchantConfig = configManager.merchantConfig ?: return@launch 170 items.filter { it.paid }.forEach { item -> 171 api.checkOrder(merchantConfig, item.orderId).handle(null) { response -> 172 assertUiThread() 173 val paidResponse = response as? CheckPaymentResponse.Paid ?: return@handle 174 updateItem( 175 orderId = item.orderId, 176 transform = { current -> 177 current.copy( 178 refunded = paidResponse.refunded, 179 refundAmount = paidResponse.refundAmount ?: current.refundAmount, 180 refundPending = paidResponse.refundPending, 181 ) 182 } 183 ) 184 } 185 } 186 } 187 188 private fun updateItem( 189 orderId: String, 190 transform: (OrderHistoryEntry) -> OrderHistoryEntry, 191 ) { 192 val index = loadedItems.indexOfFirst { it.orderId == orderId } 193 if (index == -1) return 194 loadedItems[index] = transform(loadedItems[index]) 195 publishItems() 196 } 197 198 private fun onError(@StringRes mainResId: Int, msg: String) { 199 assertUiThread() 200 mIsLoading.value = false 201 mIsLoadingMore.value = false 202 mError.value = HistoryError(mainResId, msg) 203 } 204 }