RefundManager.swift (5084B)
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 20 enum RefundResult: Equatable { 21 case error(String) 22 case pastDeadline 23 case alreadyRefunded 24 case success(refundUri: String, item: OrderHistoryEntry, amount: Amount, reason: String) 25 26 static func == (lhs: RefundResult, rhs: RefundResult) -> Bool { 27 switch (lhs, rhs) { 28 case (.error(let a), .error(let b)): return a == b 29 case (.pastDeadline, .pastDeadline): return true 30 case (.alreadyRefunded, .alreadyRefunded): return true 31 case (.success(let a1, let a2, let a3, let a4), .success(let b1, let b2, let b3, let b4)): 32 return a1 == b1 && a2.orderId == b2.orderId && a3 == b3 && a4 == b4 33 default: return false 34 } 35 } 36 } 37 38 @MainActor 39 class RefundManager: ObservableObject { 40 @Published var toBeRefunded: OrderHistoryEntry? 41 @Published var refundResult: RefundResult? 42 @Published var pendingRefundOrderId: String? 43 @Published var refundReceived = false 44 45 private let api: MerchantApi 46 private let configManager: ConfigManager 47 private var statusTask: Task<Void, Never>? 48 49 init(api: MerchantApi, configManager: ConfigManager) { 50 self.api = api 51 self.configManager = configManager 52 } 53 54 func startRefund(item: OrderHistoryEntry) { 55 statusTask?.cancel() 56 toBeRefunded = item 57 refundResult = nil 58 pendingRefundOrderId = nil 59 refundReceived = false 60 } 61 62 func abortRefund() { 63 statusTask?.cancel() 64 toBeRefunded = nil 65 refundResult = nil 66 pendingRefundOrderId = nil 67 refundReceived = false 68 } 69 70 func completeRefund() { 71 statusTask?.cancel() 72 toBeRefunded = nil 73 refundResult = nil 74 pendingRefundOrderId = nil 75 refundReceived = false 76 } 77 78 func resumeRefund(item: OrderHistoryEntry) -> Bool { 79 guard let current = refundResult, 80 case .success(_, let successItem, _, _) = current, 81 successItem.orderId == item.orderId else { 82 return false 83 } 84 toBeRefunded = item 85 pendingRefundOrderId = item.orderId 86 if !refundReceived { 87 observeRefundStatus(orderId: item.orderId) 88 } 89 return true 90 } 91 92 func refund(item: OrderHistoryEntry, amount: Amount, reason: String) { 93 guard let merchantConfig = configManager.merchantConfig else { return } 94 let request = RefundRequest(refund: amount, reason: reason) 95 96 Task { 97 do { 98 let response = try await api.giveRefund( 99 merchantConfig: merchantConfig, 100 orderId: item.orderId, 101 request: request 102 ) 103 self.refundResult = .success( 104 refundUri: response.talerRefundUri, 105 item: item, 106 amount: amount, 107 reason: reason 108 ) 109 self.pendingRefundOrderId = item.orderId 110 self.refundReceived = false 111 self.observeRefundStatus(orderId: item.orderId) 112 } catch { 113 self.statusTask?.cancel() 114 self.pendingRefundOrderId = nil 115 self.refundReceived = false 116 if let apiError = error as? ApiError, apiError.backendCode == 2602 { 117 self.refundResult = .alreadyRefunded 118 } else { 119 self.refundResult = .error(error.localizedDescription) 120 } 121 } 122 } 123 } 124 125 private func observeRefundStatus(orderId: String) { 126 statusTask?.cancel() 127 statusTask = Task { 128 guard let merchantConfig = configManager.merchantConfig else { return } 129 while !Task.isCancelled { 130 do { 131 let response = try await api.checkOrder(merchantConfig: merchantConfig, orderId: orderId) 132 if case .paid(let refunded, let refundPending, let refundAmount) = response { 133 if refunded && !refundPending && refundAmount?.isZero == false { 134 self.refundReceived = true 135 break 136 } 137 } 138 } catch {} 139 try? await Task.sleep(nanoseconds: 2_000_000_000) 140 } 141 } 142 } 143 }