taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }