diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift new file mode 100644 index 00000000000..29b26057782 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift @@ -0,0 +1,526 @@ +import Foundation +import Combine +import Yosemite +import MessageUI +import WooFoundation +import protocol Storage.StorageManagerType + +/// Use case to collect payments from an order. +/// Orchestrates reader connection, payment, UI alerts, receipt handling and analytics. +/// +final class LegacyCollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol { + /// Currency Formatter + /// + private let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings) + + /// Store's ID. + /// + private let siteID: Int64 + + /// Order to collect. + /// + private let order: Order + + /// Order total in decimal number. It is lazy so we avoid multiple conversions. + /// It can be lazy because the order is a constant and never changes (this class is intended to be + /// fired and disposed, not reused for multiple payment flows). + /// + private lazy var orderTotal: NSDecimalNumber? = { + currencyFormatter.convertToDecimal(order.total) + }() + + /// Formatted amount to collect. + /// + private let formattedAmount: String + + /// Payment Gateway Account to use. + /// + private let paymentGatewayAccount: PaymentGatewayAccount + + /// Stores manager. + /// + private let stores: StoresManager + + /// Analytics manager. + /// + private let analytics: Analytics + + /// View Controller used to present alerts. + /// + private var rootViewController: UIViewController + + /// Stores the card reader listener subscription while trying to connect to one. + /// + private var readerSubscription: AnyCancellable? + + /// Stores the connected card reader for analytics. + private var connectedReader: CardReader? + + /// Alert manager to inform merchants about reader & card actions. + /// + private let alerts: OrderDetailsPaymentAlertsProtocol + + /// IPP Configuration. + /// + private let configuration: CardPresentPaymentsConfiguration + + /// IPP payments collector. + /// + private lazy var paymentOrchestrator = PaymentCaptureOrchestrator(stores: stores) + + /// Controller to connect a card reader. + /// + private lazy var connectionController = { + CardReaderConnectionController(forSiteID: siteID, + knownReaderProvider: CardReaderSettingsKnownReaderStorage(), + alertsProvider: CardReaderSettingsAlerts(), + configuration: configuration, + analyticsTracker: CardReaderConnectionAnalyticsTracker(configuration: configuration, + stores: stores, + analytics: analytics)) + }() + + /// Coordinates emailing a receipt after payment success. + private var receiptEmailCoordinator: CardPresentPaymentReceiptEmailCoordinator? + + init(siteID: Int64, + order: Order, + formattedAmount: String, + paymentGatewayAccount: PaymentGatewayAccount, + rootViewController: UIViewController, + alerts: OrderDetailsPaymentAlertsProtocol, + configuration: CardPresentPaymentsConfiguration, + stores: StoresManager = ServiceLocator.stores, + analytics: Analytics = ServiceLocator.analytics) { + self.siteID = siteID + self.order = order + self.formattedAmount = formattedAmount + self.paymentGatewayAccount = paymentGatewayAccount + self.rootViewController = rootViewController + self.alerts = alerts + self.configuration = configuration + self.stores = stores + self.analytics = analytics + } + + /// Starts the collect payment flow. + /// 1. Connects to a reader + /// 2. Collect payment from order + /// 3. If successful: prints or emails receipt + /// 4. If failure: Allows retry + /// + /// + /// - Parameter onCollect: Closure invoked after the collect process has finished. + /// - Parameter onCancel: Closure invoked after the flow is cancelled + /// - Parameter onCompleted: Closure invoked after the flow has been totally completed, currently after merchant has handled the receipt. + func collectPayment(onCollect: @escaping (Result) -> (), + onCancel: @escaping () -> (), + onCompleted: @escaping () -> ()) { + guard isTotalAmountValid() else { + let error = totalAmountInvalidError() + onCollect(.failure(error)) + return handleTotalAmountInvalidError(totalAmountInvalidError(), onCompleted: onCompleted) + } + + configureBackend() + observeConnectedReadersForAnalytics() + connectReader { [weak self] result in + guard let self = self else { return } + switch result { + case .success: + self.attemptPayment(onCompletion: { [weak self] result in + guard let self = self else { return } + // Inform about the collect payment state + switch result { + case .failure(CollectOrderPaymentUseCaseError.flowCanceledByUser): + self.rootViewController.presentedViewController?.dismiss(animated: true) + return onCancel() + default: + onCollect(result.map { _ in () }) // Transforms Result to Result + } + + // Handle payment receipt + guard let paymentData = try? result.get() else { + return onCompleted() + } + self.presentReceiptAlert(receiptParameters: paymentData.receiptParameters, onCompleted: onCompleted) + }) + case .failure(CollectOrderPaymentUseCaseError.flowCanceledByUser): + self.trackPaymentCancelation() + onCancel() + case .failure(let error): + onCollect(.failure(error)) + } + } + } +} + +// MARK: Private functions +private extension LegacyCollectOrderPaymentUseCase { + /// Checks whether the amount to be collected is valid: (not nil, convertible to decimal, higher than minimum amount ...) + /// + func isTotalAmountValid() -> Bool { + guard let orderTotal = orderTotal else { + return false + } + + /// Bail out if the order amount is below the minimum allowed: + /// https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts + return orderTotal as Decimal >= configuration.minimumAllowedChargeAmount as Decimal + } + + /// Determines and returns the error that provoked the amount being invalid + /// + func totalAmountInvalidError() -> Error { + let orderTotalAmountCanBeConverted = orderTotal != nil + + guard orderTotalAmountCanBeConverted, + let minimum = currencyFormatter.formatAmount(configuration.minimumAllowedChargeAmount, with: order.currency) else { + return NotValidAmountError.other + } + + return NotValidAmountError.belowMinimumAmount(amount: minimum) + } + + func handleTotalAmountInvalidError(_ error: Error, onCompleted: @escaping () -> ()) { + trackPaymentFailure(with: error) + DDLogError("💳 Error: failed to capture payment for order. Order amount is below minimum or not valid") + self.alerts.nonRetryableError(from: self.rootViewController, error: totalAmountInvalidError(), dismissCompletion: onCompleted) + } + + /// Configure the CardPresentPaymentStore to use the appropriate backend + /// + func configureBackend() { + let setAccount = CardPresentPaymentAction.use(paymentGatewayAccount: paymentGatewayAccount) + stores.dispatch(setAccount) + } + + /// Attempts to connect to a reader. + /// Finishes with success immediately if a reader is already connected. + /// + func connectReader(onCompletion: @escaping (Result) -> ()) { + // `checkCardReaderConnected` action will return a publisher that: + // - Sends one value if there is no reader connected. + // - Completes when a reader is connected. + let readerConnected = CardPresentPaymentAction.publishCardReaderConnections() { [weak self] connectPublisher in + guard let self = self else { return } + self.readerSubscription = connectPublisher + .sink(receiveValue: { [weak self] readers in + guard let self = self else { return } + + if readers.isNotEmpty { + // Dismiss the current connection alert before notifying the completion. + // If no presented controller is found(because the reader was already connected), just notify the completion. + if let connectionController = self.rootViewController.presentedViewController { + connectionController.dismiss(animated: true) { + onCompletion(.success(())) + } + } else { + onCompletion(.success(())) + } + + // Nil the subscription since we are done with the connection. + self.readerSubscription = nil + } else { + // Attempt reader connection + self.connectionController.searchAndConnect(from: self.rootViewController) { [weak self] result in + guard let self = self else { return } + switch result { + case let .success(connectionResult): + switch connectionResult { + case .canceled: + self.readerSubscription = nil + onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) + case .connected: + // Connected case will be handled in `if readers.isNotEmpty`. + break + } + case .failure(let error): + self.readerSubscription = nil + onCompletion(.failure(error)) + } + } + } + }) + } + stores.dispatch(readerConnected) + } + + /// Attempts to collect payment for an order. + /// + func attemptPayment(onCompletion: @escaping (Result) -> ()) { + guard let orderTotal = orderTotal else { + onCompletion(.failure(NotValidAmountError.other)) + + return + } + + // Show preparing reader alert + alerts.preparingReader(onCancel: { [weak self] in + self?.cancelPayment(onCompleted: { + onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) + }) + }) + + // Start collect payment process + paymentOrchestrator.collectPayment( + for: order, + orderTotal: orderTotal, + paymentGatewayAccount: paymentGatewayAccount, + paymentMethodTypes: configuration.paymentMethods.map(\.rawValue), + stripeSmallestCurrencyUnitMultiplier: configuration.stripeSmallestCurrencyUnitMultiplier, + onWaitingForInput: { [weak self] inputMethods in + guard let self = self else { return } + self.alerts.tapOrInsertCard(title: Localization.collectPaymentTitle(username: self.order.billingAddress?.firstName), + amount: self.formattedAmount, + inputMethods: inputMethods, + onCancel: { [weak self] in + self?.cancelPayment { + onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) + } + }) + + }, onProcessingMessage: { [weak self] in + // Waiting message + self?.alerts.processingPayment() + }, onDisplayMessage: { [weak self] message in + // Reader messages. EG: Remove Card + self?.alerts.displayReaderMessage(message: message) + }, onProcessingCompletion: { [weak self] intent in + self?.trackProcessingCompletion(intent: intent) + self?.markOrderAsPaidIfNeeded(intent: intent) + }, onCompletion: { [weak self] result in + switch result { + case .success(let capturedPaymentData): + self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion) + case .failure(let error): + self?.handlePaymentFailureAndRetryPayment(error, onCompletion: onCompletion) + } + } + ) + } + + /// Tracks the successful payments + /// + func handleSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData, + onCompletion: @escaping (Result) -> ()) { + // Record success + analytics.track(event: WooAnalyticsEvent.InPersonPayments + .collectPaymentSuccess(forGatewayID: paymentGatewayAccount.gatewayID, + countryCode: configuration.countryCode, + paymentMethod: capturedPaymentData.paymentMethod, + cardReaderModel: connectedReader?.readerType.model ?? "")) + + // Success Callback + onCompletion(.success(capturedPaymentData)) + } + + /// Log the failure reason, cancel the current payment and retry it if possible. + /// + func handlePaymentFailureAndRetryPayment(_ error: Error, onCompletion: @escaping (Result) -> ()) { + DDLogError("Failed to collect payment: \(error.localizedDescription)") + + trackPaymentFailure(with: error) + + // Inform about the error + alerts.error(error: error, + tryAgain: { [weak self] in + + // Cancel current payment + self?.paymentOrchestrator.cancelPayment { [weak self] result in + guard let self = self else { return } + + switch result { + case .success: + // Retry payment + self.attemptPayment(onCompletion: onCompletion) + + case .failure(let cancelError): + // Inform that payment can't be retried. + self.alerts.nonRetryableError(from: self.rootViewController, error: cancelError) { + onCompletion(.failure(error)) + } + } + } + }, dismissCompletion: { + onCompletion(.failure(error)) + }) + } + + private func trackPaymentFailure(with error: Error) { + // Record error + analytics.track(event: WooAnalyticsEvent.InPersonPayments.collectPaymentFailed(forGatewayID: paymentGatewayAccount.gatewayID, + error: error, + countryCode: configuration.countryCode, + cardReaderModel: connectedReader?.readerType.model)) + } + + /// Cancels payment and record analytics. + /// + func cancelPayment(onCompleted: @escaping () -> ()) { + paymentOrchestrator.cancelPayment { [weak self] _ in + self?.trackPaymentCancelation() + onCompleted() + } + } + + func trackPaymentCancelation() { + analytics.track(event: WooAnalyticsEvent.InPersonPayments.collectPaymentCanceled(forGatewayID: paymentGatewayAccount.gatewayID, + countryCode: configuration.countryCode, + cardReaderModel: connectedReader?.readerType.model ?? "")) + } + + /// Allow merchants to print or email the payment receipt. + /// + func presentReceiptAlert(receiptParameters: CardPresentReceiptParameters, onCompleted: @escaping () -> ()) { + // Present receipt alert + alerts.success(printReceipt: { [order, configuration, weak self] in + guard let self = self else { return } + + // Inform about flow completion. + onCompleted() + + // Delegate print action + ReceiptActionCoordinator.printReceipt(for: order, + params: receiptParameters, + countryCode: configuration.countryCode, + cardReaderModel: self.connectedReader?.readerType.model, + stores: self.stores, + analytics: self.analytics) + + }, emailReceipt: { [order, analytics, paymentOrchestrator, configuration, weak self] in + guard let self = self else { return } + + // Record button tapped + analytics.track(event: .InPersonPayments + .receiptEmailTapped(countryCode: configuration.countryCode, + cardReaderModel: self.connectedReader?.readerType.model ?? "")) + + // Request & present email + paymentOrchestrator.emailReceipt(for: order, params: receiptParameters) { [weak self] emailContent in + self?.presentEmailForm(content: emailContent, onCompleted: onCompleted) + } + }, noReceiptAction: { + // Inform about flow completion. + onCompleted() + }) + } + + /// Presents the native email client with the provided content. + /// + func presentEmailForm(content: String, onCompleted: @escaping () -> ()) { + let coordinator = CardPresentPaymentReceiptEmailCoordinator(analytics: analytics, + countryCode: configuration.countryCode, + cardReaderModel: connectedReader?.readerType.model) + receiptEmailCoordinator = coordinator + coordinator.presentEmailForm(data: .init(content: content, + order: order, + storeName: stores.sessionManager.defaultSite?.name), + from: rootViewController, + completion: onCompleted) + } +} + +// MARK: Interac handling +private extension LegacyCollectOrderPaymentUseCase { + /// For certain payment methods like Interac in Canada, the payment is captured on the client side (customer is charged). + /// To prevent the order from multiple charges after the first client side success, the order is marked as paid locally in case of any + /// potential failures until the next order refresh. + func markOrderAsPaidIfNeeded(intent: PaymentIntent) { + guard let paymentMethod = intent.paymentMethod() else { + return + } + switch paymentMethod { + case .interacPresent: + let action = OrderAction.markOrderAsPaidLocally(siteID: order.siteID, orderID: order.orderID, datePaid: Date()) { _ in } + stores.dispatch(action) + default: + return + } + } +} + +// MARK: Analytics +private extension LegacyCollectOrderPaymentUseCase { + func observeConnectedReadersForAnalytics() { + let action = CardPresentPaymentAction.observeConnectedReaders() { [weak self] readers in + self?.connectedReader = readers.first + } + stores.dispatch(action) + } + + func trackProcessingCompletion(intent: PaymentIntent) { + guard let paymentMethod = intent.paymentMethod() else { + return + } + switch paymentMethod { + case .interacPresent: + analytics.track(event: .InPersonPayments + .collectInteracPaymentSuccess(gatewayID: paymentGatewayAccount.gatewayID, + countryCode: configuration.countryCode, + cardReaderModel: connectedReader?.readerType.model ?? "")) + default: + return + } + } +} + +// MARK: Definitions +private extension LegacyCollectOrderPaymentUseCase { + /// Mailing a receipt failed but the SDK didn't return a more specific error + /// + struct UnknownEmailError: Error {} + + + enum Localization { + private static let emailSubjectWithStoreName = NSLocalizedString("Your receipt from %1$@", + comment: "Subject of email sent with a card present payment receipt") + private static let emailSubjectWithoutStoreName = NSLocalizedString("Your receipt", + comment: "Subject of email sent with a card present payment receipt") + static func emailSubject(storeName: String?) -> String { + guard let storeName = storeName, storeName.isNotEmpty else { + return emailSubjectWithoutStoreName + } + return .localizedStringWithFormat(emailSubjectWithStoreName, storeName) + } + + private static let collectPaymentWithoutName = NSLocalizedString("Collect payment", + comment: "Alert title when starting the collect payment flow without a user name.") + private static let collectPaymentWithName = NSLocalizedString("Collect payment from %1$@", + comment: "Alert title when starting the collect payment flow with a user name.") + static func collectPaymentTitle(username: String?) -> String { + guard let username = username, username.isNotEmpty else { + return collectPaymentWithoutName + } + return .localizedStringWithFormat(collectPaymentWithName, username) + } + } +} + +extension LegacyCollectOrderPaymentUseCase { + enum NotValidAmountError: Error, LocalizedError { + case belowMinimumAmount(amount: String) + case other + + var errorDescription: String? { + switch self { + case .belowMinimumAmount(let amount): + return String.localizedStringWithFormat(Localization.belowMinimumAmount, amount) + case .other: + return Localization.defaultMessage + } + } + + private enum Localization { + static let defaultMessage = NSLocalizedString( + "Unable to process payment. Order total amount is not valid.", + comment: "Error message when the order amount is not valid." + ) + + static let belowMinimumAmount = NSLocalizedString( + "Unable to process payment. Order total amount is below the minimum amount you can charge, which is %1$@", + comment: "Error message when the order amount is below the minimum amount allowed." + ) + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift index 1b47902ed9e..7dec515a8ab 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift @@ -99,7 +99,7 @@ final class PaymentMethodsViewModel: ObservableObject { /// Retains the use-case so it can perform all of its async tasks. /// - private var collectPaymentsUseCase: CollectOrderPaymentProtocol? + private var legacyCollectPaymentsUseCase: CollectOrderPaymentProtocol? private let cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration @@ -220,7 +220,7 @@ final class PaymentMethodsViewModel: ObservableObject { return DDLogError("⛔️ Payment Gateway not found, can't collect payment.") } - self.collectPaymentsUseCase = useCase ?? CollectOrderPaymentUseCase( + self.legacyCollectPaymentsUseCase = useCase ?? LegacyCollectOrderPaymentUseCase( siteID: self.siteID, order: order, formattedAmount: self.formattedTotal, @@ -230,7 +230,7 @@ final class PaymentMethodsViewModel: ObservableObject { presentingController: rootViewController), configuration: CardPresentConfigurationLoader().configuration) - self.collectPaymentsUseCase?.collectPayment( + self.legacyCollectPaymentsUseCase?.collectPayment( onCollect: { [weak self] result in guard result.isFailure else { return } self?.trackFlowFailed() @@ -249,7 +249,7 @@ final class PaymentMethodsViewModel: ObservableObject { self?.presentNoticeSubject.send(.completed) // Make sure we free all the resources - self?.collectPaymentsUseCase = nil + self?.legacyCollectPaymentsUseCase = nil // Tracks completion self?.trackFlowCompleted(method: .card) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 20aff889f70..8cf9ac7df00 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -241,7 +241,7 @@ 026B3C57249A046E00F7823C /* TextFieldTextAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026B3C56249A046E00F7823C /* TextFieldTextAlignment.swift */; }; 026CF63A237E9ABE009563D4 /* ProductVariationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026CF638237E9ABE009563D4 /* ProductVariationsViewController.swift */; }; 026CF63B237E9ABE009563D4 /* ProductVariationsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 026CF639237E9ABE009563D4 /* ProductVariationsViewController.xib */; }; - 026D4A24280461960090164F /* CollectOrderPaymentUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026D4A23280461960090164F /* CollectOrderPaymentUseCaseTests.swift */; }; + 026D4A24280461960090164F /* LegacyCollectOrderPaymentUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026D4A23280461960090164F /* LegacyCollectOrderPaymentUseCaseTests.swift */; }; 0270F47624D005B00005210A /* ProductFormViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0270F47524D005B00005210A /* ProductFormViewModelProtocol.swift */; }; 0270F47824D006F60005210A /* ProductFormPresentationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0270F47724D006F60005210A /* ProductFormPresentationStyle.swift */; }; 027111422913B9FC00F5269A /* AccountCreationFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027111412913B9FC00F5269A /* AccountCreationFormViewModelTests.swift */; }; @@ -461,6 +461,7 @@ 031B10E3274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */; }; 035BA3A8291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */; }; 035C6DEB273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */; }; + 035DBA45292D0164003E5125 /* CollectOrderPaymentUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA44292D0163003E5125 /* CollectOrderPaymentUseCase.swift */; }; 035F2308275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035F2307275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift */; }; 0366EAE12909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */; }; 036CA6B9291E8D4B00E4DF4F /* CardPresentModalPreparingReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036CA6B8291E8D4B00E4DF4F /* CardPresentModalPreparingReader.swift */; }; @@ -597,7 +598,7 @@ 268EC45F26CEA50C00716F5C /* EditCustomerNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268EC45E26CEA50C00716F5C /* EditCustomerNote.swift */; }; 268EC46126D3F67800716F5C /* EditCustomerNoteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268EC46026D3F67800716F5C /* EditCustomerNoteViewModel.swift */; }; 268EC46426D3F9C100716F5C /* EditCustomerNoteViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268EC46326D3F9C100716F5C /* EditCustomerNoteViewModelTests.swift */; }; - 268FD44727580A81008FDF9B /* CollectOrderPaymentUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268FD44627580A81008FDF9B /* CollectOrderPaymentUseCase.swift */; }; + 268FD44727580A81008FDF9B /* LegacyCollectOrderPaymentUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268FD44627580A81008FDF9B /* LegacyCollectOrderPaymentUseCase.swift */; }; 269098B427D2BBFC001FEB07 /* ShippingInputTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269098B327D2BBFC001FEB07 /* ShippingInputTransformer.swift */; }; 269098B627D2C09D001FEB07 /* ShippingInputTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269098B527D2C09D001FEB07 /* ShippingInputTransformerTests.swift */; }; 269098B827D68CCD001FEB07 /* FeesInputTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269098B727D68CCD001FEB07 /* FeesInputTransformer.swift */; }; @@ -2208,7 +2209,7 @@ 026B3C56249A046E00F7823C /* TextFieldTextAlignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldTextAlignment.swift; sourceTree = ""; }; 026CF638237E9ABE009563D4 /* ProductVariationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationsViewController.swift; sourceTree = ""; }; 026CF639237E9ABE009563D4 /* ProductVariationsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProductVariationsViewController.xib; sourceTree = ""; }; - 026D4A23280461960090164F /* CollectOrderPaymentUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectOrderPaymentUseCaseTests.swift; sourceTree = ""; }; + 026D4A23280461960090164F /* LegacyCollectOrderPaymentUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCollectOrderPaymentUseCaseTests.swift; sourceTree = ""; }; 0270C0A827069BEF00FC799F /* Experiments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Experiments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0270F47524D005B00005210A /* ProductFormViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormViewModelProtocol.swift; sourceTree = ""; }; 0270F47724D006F60005210A /* ProductFormPresentationStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormPresentationStyle.swift; sourceTree = ""; }; @@ -2431,6 +2432,7 @@ 031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalConnectionFailedUpdateAddress.swift; sourceTree = ""; }; 035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageAnnouncementCardViewModelTests.swift; sourceTree = ""; }; 035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdateTypeProperty.swift; sourceTree = ""; }; + 035DBA44292D0163003E5125 /* CollectOrderPaymentUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectOrderPaymentUseCase.swift; sourceTree = ""; }; 035F2307275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalConnectingFailedUpdatePostalCode.swift; sourceTree = ""; }; 0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageAnnouncementCardViewModel.swift; sourceTree = ""; }; 036CA6B8291E8D4B00E4DF4F /* CardPresentModalPreparingReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalPreparingReader.swift; sourceTree = ""; }; @@ -2560,7 +2562,7 @@ 268EC45E26CEA50C00716F5C /* EditCustomerNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomerNote.swift; sourceTree = ""; }; 268EC46026D3F67800716F5C /* EditCustomerNoteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomerNoteViewModel.swift; sourceTree = ""; }; 268EC46326D3F9C100716F5C /* EditCustomerNoteViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomerNoteViewModelTests.swift; sourceTree = ""; }; - 268FD44627580A81008FDF9B /* CollectOrderPaymentUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectOrderPaymentUseCase.swift; sourceTree = ""; }; + 268FD44627580A81008FDF9B /* LegacyCollectOrderPaymentUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCollectOrderPaymentUseCase.swift; sourceTree = ""; }; 269098B327D2BBFC001FEB07 /* ShippingInputTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingInputTransformer.swift; sourceTree = ""; }; 269098B527D2C09D001FEB07 /* ShippingInputTransformerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingInputTransformerTests.swift; sourceTree = ""; }; 269098B727D68CCD001FEB07 /* FeesInputTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeesInputTransformer.swift; sourceTree = ""; }; @@ -5331,7 +5333,8 @@ 268FD44827580A92008FDF9B /* Collect Payments */ = { isa = PBXGroup; children = ( - 268FD44627580A81008FDF9B /* CollectOrderPaymentUseCase.swift */, + 035DBA44292D0163003E5125 /* CollectOrderPaymentUseCase.swift */, + 268FD44627580A81008FDF9B /* LegacyCollectOrderPaymentUseCase.swift */, 0375799A28227EDE0083F2E1 /* CardPresentPaymentsOnboardingPresenter.swift */, 02C27BCD282CB52F0065471A /* CardPresentPaymentReceiptEmailCoordinator.swift */, ); @@ -8308,7 +8311,7 @@ E17E3BF8266917C10009D977 /* CardPresentModalScanningFailedTests.swift */, 3190D61C26D6E97B00EF364D /* CardPresentModalRetryableErrorTests.swift */, 31AD0B1226E95998000B6391 /* CardPresentModalConnectingFailedTests.swift */, - 026D4A23280461960090164F /* CollectOrderPaymentUseCaseTests.swift */, + 026D4A23280461960090164F /* LegacyCollectOrderPaymentUseCaseTests.swift */, 028E19BB2805BD22001C36E0 /* RefundSubmissionUseCaseTests.swift */, B9C4AB28280031AB007008B8 /* PaymentReceiptEmailParameterDeterminerTests.swift */, 036F6EA5281847D5006D84F8 /* PaymentCaptureOrchestratorTests.swift */, @@ -9966,7 +9969,7 @@ DEC51A9D274F8528009F3DF4 /* JetpackInstallStepsViewModel.swift in Sources */, 455DC3A327393C7E00D4644C /* OrderDatesFilterViewController.swift in Sources */, 45B6F4EF27592A4000C18782 /* ReviewsView.swift in Sources */, - 268FD44727580A81008FDF9B /* CollectOrderPaymentUseCase.swift in Sources */, + 268FD44727580A81008FDF9B /* LegacyCollectOrderPaymentUseCase.swift in Sources */, 269098B427D2BBFC001FEB07 /* ShippingInputTransformer.swift in Sources */, 740987B321B87760000E4C80 /* FancyAnimatedButton+Woo.swift in Sources */, 45F627B6253603AE00894B86 /* Product+DownloadSettingsViewModels.swift in Sources */, @@ -10585,6 +10588,7 @@ AE3AA889290C303B00BE422D /* WebKitViewController.swift in Sources */, 4535EE7A281ADD56004212B4 /* CouponCodeInputFormatter.swift in Sources */, DEDB2D262845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift in Sources */, + 035DBA45292D0164003E5125 /* CollectOrderPaymentUseCase.swift in Sources */, 0282DD96233C960C006A5FDB /* SearchResultCell.swift in Sources */, 260C32BE2527A2DE00157BC2 /* IssueRefundViewModel.swift in Sources */, 2678897C270E6E8B00BD249E /* SimplePaymentsAmount.swift in Sources */, @@ -10912,7 +10916,7 @@ 02ADC7CE23978EAA008D4BED /* PaginatedProductShippingClassListSelectorDataSourceTests.swift in Sources */, 45C8B25B231521510002FA77 /* CustomerNoteTableViewCellTests.swift in Sources */, B5980A6721AC91AA00EBF596 /* BundleWooTests.swift in Sources */, - 026D4A24280461960090164F /* CollectOrderPaymentUseCaseTests.swift in Sources */, + 026D4A24280461960090164F /* LegacyCollectOrderPaymentUseCaseTests.swift in Sources */, D88D5A3D230B5E85007B6E01 /* ServiceLocatorTests.swift in Sources */, 269098BA27D6922E001FEB07 /* FeesInputTransformerTests.swift in Sources */, 02A275C223FE590A005C560F /* MockKingfisherImageDownloader.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewModels/CardPresentPayments/CollectOrderPaymentUseCaseTests.swift b/WooCommerce/WooCommerceTests/ViewModels/CardPresentPayments/LegacyCollectOrderPaymentUseCaseTests.swift similarity index 96% rename from WooCommerce/WooCommerceTests/ViewModels/CardPresentPayments/CollectOrderPaymentUseCaseTests.swift rename to WooCommerce/WooCommerceTests/ViewModels/CardPresentPayments/LegacyCollectOrderPaymentUseCaseTests.swift index 89ea69b8089..2629650f24c 100644 --- a/WooCommerce/WooCommerceTests/ViewModels/CardPresentPayments/CollectOrderPaymentUseCaseTests.swift +++ b/WooCommerce/WooCommerceTests/ViewModels/CardPresentPayments/LegacyCollectOrderPaymentUseCaseTests.swift @@ -5,7 +5,7 @@ import XCTest import Yosemite @testable import WooCommerce -final class CollectOrderPaymentUseCaseTests: XCTestCase { +final class LegacyCollectOrderPaymentUseCaseTests: XCTestCase { private let defaultSiteID: Int64 = 122 private let defaultOrderID: Int64 = 322 @@ -13,7 +13,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase { private var analyticsProvider: MockAnalyticsProvider! private var analytics: WooAnalytics! private var alerts: MockOrderDetailsPaymentAlerts! - private var useCase: CollectOrderPaymentUseCase! + private var useCase: LegacyCollectOrderPaymentUseCase! override func setUp() { super.setUp() @@ -23,7 +23,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase { analytics = WooAnalytics(analyticsProvider: analyticsProvider) alerts = MockOrderDetailsPaymentAlerts() - useCase = CollectOrderPaymentUseCase(siteID: defaultSiteID, + useCase = LegacyCollectOrderPaymentUseCase(siteID: defaultSiteID, order: .fake().copy(siteID: defaultSiteID, orderID: defaultOrderID, total: "1.5"), formattedAmount: "1.5", paymentGatewayAccount: .fake().copy(gatewayID: Mocks.paymentGatewayAccount), @@ -154,7 +154,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase { func test_collectPayment_with_below_minimum_amount_results_in_failure_and_tracks_collectPaymentFailed_event() throws { // Given - let useCase = CollectOrderPaymentUseCase(siteID: 122, + let useCase = LegacyCollectOrderPaymentUseCase(siteID: 122, order: .fake().copy(total: "0.49"), formattedAmount: "0.49", paymentGatewayAccount: .fake().copy(gatewayID: Mocks.paymentGatewayAccount), @@ -179,7 +179,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase { } // Then - XCTAssertNotNil(result?.failure as? CollectOrderPaymentUseCase.NotValidAmountError) + XCTAssertNotNil(result?.failure as? LegacyCollectOrderPaymentUseCase.NotValidAmountError) let indexOfEvent = try XCTUnwrap(analyticsProvider.receivedEvents.firstIndex(where: { $0 == "card_present_collect_payment_failed"})) let eventProperties = try XCTUnwrap(analyticsProvider.receivedProperties[indexOfEvent]) @@ -234,7 +234,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase { } } -private extension CollectOrderPaymentUseCaseTests { +private extension LegacyCollectOrderPaymentUseCaseTests { func mockCardPresentPaymentActions() { stores.whenReceivingAction(ofType: CardPresentPaymentAction.self) { action in if case let .publishCardReaderConnections(completion) = action { @@ -272,7 +272,7 @@ private extension CollectOrderPaymentUseCaseTests { } } -private extension CollectOrderPaymentUseCaseTests { +private extension LegacyCollectOrderPaymentUseCaseTests { enum Mocks { static let configuration = CardPresentPaymentsConfiguration(country: "US") static let cardReaderModel: String = "WISEPAD_3"