HistoryManager.swift (6171B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2026 Bohdan, Volodymyr Potuzhnyi 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 import Foundation 18 import Combine 19 import os 20 21 private let logger = Logger(subsystem: "net.taler.pos", category: "HistoryManager") 22 private let pageSize = 20 23 private let loadMoreThreshold = 5 24 25 @MainActor 26 class HistoryManager: ObservableObject { 27 @Published var items: [OrderHistoryEntry] = [] 28 @Published var isLoading = false 29 @Published var isLoadingMore = false 30 @Published var errorMessage: String? 31 @Published var forceDeleteOrderId: String? 32 33 private let api: MerchantApi 34 private let configManager: ConfigManager 35 private var loadedItems: [OrderHistoryEntry] = [] 36 private var nextOffset: Int64? 37 private var reachedEnd = false 38 39 init(api: MerchantApi, configManager: ConfigManager) { 40 self.api = api 41 self.configManager = configManager 42 } 43 44 func fetchHistory() { 45 if ScreenshotController.isActive { return } 46 fetchHistoryPage(reset: true) 47 } 48 49 func loadMoreHistoryIfNeeded(lastVisibleIndex: Int) { 50 guard !isLoading, !isLoadingMore, !reachedEnd else { return } 51 if items.isEmpty { return } 52 if lastVisibleIndex < items.count - 1 - loadMoreThreshold { return } 53 fetchHistoryPage(reset: false) 54 } 55 56 func deleteOrder(orderId: String) { 57 guard let merchantConfig = configManager.merchantConfig else { return } 58 isLoading = true 59 Task { 60 do { 61 try await api.deleteOrder(merchantConfig: merchantConfig, orderId: orderId) 62 self.configManager.refreshInventory() 63 self.fetchHistory() 64 } catch { 65 self.isLoading = false 66 self.forceDeleteOrderId = orderId 67 } 68 } 69 } 70 71 func forceDeleteOrder(_ orderId: String) { 72 forceDeleteOrderId = nil 73 isLoading = true 74 guard let merchantConfig = configManager.merchantConfig else { return } 75 Task { 76 do { 77 try await api.deleteOrder(merchantConfig: merchantConfig, orderId: orderId, force: true) 78 self.configManager.refreshInventory() 79 self.fetchHistory() 80 } catch { 81 self.errorMessage = error.localizedDescription 82 self.isLoading = false 83 } 84 } 85 } 86 87 func clearForceDeletePrompt() { 88 forceDeleteOrderId = nil 89 } 90 91 func clearError() { 92 errorMessage = nil 93 } 94 95 private func fetchHistoryPage(reset: Bool) { 96 if ScreenshotController.isActive { return } 97 guard let merchantConfig = configManager.merchantConfig else { 98 logger.error("fetchHistoryPage: no merchantConfig, bailing") 99 isLoading = false 100 isLoadingMore = false 101 return 102 } 103 if reset { 104 isLoading = true 105 isLoadingMore = false 106 } else { 107 isLoadingMore = true 108 } 109 110 let offset = reset ? nil : nextOffset 111 Task { 112 do { 113 logger.warning("Fetching history reset=\(reset) offset=\(String(describing: offset))") 114 let history = try await api.getOrderHistory( 115 merchantConfig: merchantConfig, 116 limit: -pageSize, 117 offset: offset 118 ) 119 logger.warning("Got \(history.orders.count) orders") 120 self.isLoading = false 121 self.isLoadingMore = false 122 123 if reset { 124 self.loadedItems.removeAll() 125 self.reachedEnd = false 126 } 127 128 let existingIds = Set(self.loadedItems.map(\.orderId)) 129 let newItems = history.orders.filter { !existingIds.contains($0.orderId) } 130 131 if !newItems.isEmpty { 132 self.loadedItems += newItems 133 self.nextOffset = newItems.last?.rowId ?? self.loadedItems.last?.rowId 134 } 135 if history.orders.count < pageSize || newItems.isEmpty { 136 self.reachedEnd = true 137 } 138 self.items = self.loadedItems 139 logger.warning("Total items: \(self.items.count)") 140 141 await enrichOrders(newItems: newItems, merchantConfig: merchantConfig) 142 } catch { 143 logger.error("History fetch failed: \(error)") 144 self.isLoading = false 145 self.isLoadingMore = false 146 self.errorMessage = error.localizedDescription 147 } 148 } 149 } 150 151 private func enrichOrders(newItems: [OrderHistoryEntry], merchantConfig: MerchantConfig) async { 152 for item in newItems where item.paid { 153 do { 154 let response = try await api.checkOrder(merchantConfig: merchantConfig, orderId: item.orderId) 155 if case .paid(let refunded, let refundPending, let refundAmount) = response { 156 if let idx = self.loadedItems.firstIndex(where: { $0.orderId == item.orderId }) { 157 self.loadedItems[idx].refunded = refunded 158 self.loadedItems[idx].refundPending = refundPending 159 if let ra = refundAmount { 160 self.loadedItems[idx].refundAmount = ra 161 } 162 self.items = self.loadedItems 163 } 164 } 165 } catch { 166 continue 167 } 168 } 169 } 170 }