taler-ios

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

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 }