From f0a53c148c52eabcc178194859d3d7154c69bed3 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 4 Jan 2023 19:37:08 +0800 Subject: [PATCH 01/14] Fetch IPP orders from storage --- .../InPersonPaymentsMenuViewController.swift | 2 + .../InPersonPaymentsMenuViewModel.swift | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift index 57145d02737..04042b1754b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift @@ -76,6 +76,8 @@ final class InPersonPaymentsMenuViewController: UIViewController { runCardPresentPaymentsOnboarding() configureWebViewPresentation() viewModel.viewDidLoad() + // WIP: + viewModel.displayResults() } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index 127ca813808..ef66bb1e2a0 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -1,16 +1,20 @@ import Foundation import Yosemite import WooFoundation +import protocol Storage.StorageManagerType final class InPersonPaymentsMenuViewModel { // MARK: - Dependencies struct Dependencies { let stores: StoresManager let analytics: Analytics + let storage: StorageManagerType init(stores: StoresManager = ServiceLocator.stores, + storage: StorageManagerType = ServiceLocator.storageManager, analytics: Analytics = ServiceLocator.analytics) { self.stores = stores + self.storage = storage self.analytics = analytics } } @@ -21,10 +25,16 @@ final class InPersonPaymentsMenuViewModel { dependencies.stores } + private var storage: StorageManagerType { + dependencies.storage + } + private var analytics: Analytics { dependencies.analytics } + private lazy var resultsController = createIPPOrdersResultsController() + // MARK: - Output properties @Published var showWebView: AuthenticatedWebViewModel? = nil @@ -39,6 +49,8 @@ final class InPersonPaymentsMenuViewModel { cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration = CardPresentConfigurationLoader().configuration) { self.dependencies = dependencies self.cardPresentPaymentsConfiguration = cardPresentPaymentsConfiguration + + fetchIPPTransactions() } func viewDidLoad() { @@ -62,6 +74,35 @@ final class InPersonPaymentsMenuViewModel { self?.showWebView = nil }) } + + func displayResults() { + // Business logic: + let results = resultsController.fetchedObjects.count + print("IPP transactions within 30 days: \(results)") + if results < 10 { + // TODO: Select banner 1 + } else { + // TODO: Select banner 2 + } + } + + private func fetchIPPTransactions() { + do { + try resultsController.performFetch() + } catch { + DDLogError("Error fetching IPP transactions: \(error)") + } + } +} + +private extension InPersonPaymentsMenuViewModel { + /// Results controller that fetches IPP transactions + /// + func createIPPOrdersResultsController() -> ResultsController { + // TODO: Add further details to Query: Limit 30 days + let predicate = NSPredicate(format: "siteID == %lld AND paymentMethodID == %@", siteID ?? 0, "woocommerce_payments") + return ResultsController(storageManager: storage, matching: predicate, sortedBy: []) + } } private enum Constants { From 36e0ef468f0ebfa08b62633a800f340ccbcc5ac0 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 5 Jan 2023 12:18:17 +0800 Subject: [PATCH 02/14] Query only IPP orders within 30 days --- .../InPersonPaymentsMenuViewModel.swift | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index ef66bb1e2a0..d69ef4b304b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -76,13 +76,18 @@ final class InPersonPaymentsMenuViewModel { } func displayResults() { - // Business logic: - let results = resultsController.fetchedObjects.count - print("IPP transactions within 30 days: \(results)") - if results < 10 { + let results = resultsController.fetchedObjects + let resultsCount = results.count + // Debug: + print("IPP transactions within 30 days: \(resultsCount)") + print(results.map { ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") }) + + if resultsCount < 10 { // TODO: Select banner 1 + print("< 10 transactions. Banner 1 shown") } else { // TODO: Select banner 2 + print(">= 10 transactions. Banner 2 shown") } } @@ -99,8 +104,18 @@ private extension InPersonPaymentsMenuViewModel { /// Results controller that fetches IPP transactions /// func createIPPOrdersResultsController() -> ResultsController { - // TODO: Add further details to Query: Limit 30 days - let predicate = NSPredicate(format: "siteID == %lld AND paymentMethodID == %@", siteID ?? 0, "woocommerce_payments") + let today = Date() + let thirtyDaysBeforeToday = Calendar.current.date( + byAdding: .day, + value: -30, + to: today + )! + + let predicate = NSPredicate( + format: "siteID == %lld AND paymentMethodID == %@ AND datePaid >= %@", + argumentArray: [siteID ?? 0, "woocommerce_payments", thirtyDaysBeforeToday] + ) + return ResultsController(storageManager: storage, matching: predicate, sortedBy: []) } } From d2c14b8bfed61d554932e904357ec73912308c50 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 5 Jan 2023 12:27:23 +0800 Subject: [PATCH 03/14] Rename methods for clarity. Add internal comments --- .../InPersonPaymentsMenuViewController.swift | 3 +-- .../InPersonPaymentsMenuViewModel.swift | 16 +++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift index 04042b1754b..b8c35173109 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift @@ -76,8 +76,7 @@ final class InPersonPaymentsMenuViewController: UIViewController { runCardPresentPaymentsOnboarding() configureWebViewPresentation() viewModel.viewDidLoad() - // WIP: - viewModel.displayResults() + viewModel.displayIPPFeedbackBannerIfEligible() } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index d69ef4b304b..e7641a44d94 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -33,7 +33,7 @@ final class InPersonPaymentsMenuViewModel { dependencies.analytics } - private lazy var resultsController = createIPPOrdersResultsController() + private lazy var resultsController = createRecentIPPOrdersResultsController() // MARK: - Output properties @Published var showWebView: AuthenticatedWebViewModel? = nil @@ -75,18 +75,20 @@ final class InPersonPaymentsMenuViewModel { }) } - func displayResults() { + /// This method is just a helper for debugging, we may use it for populating different Banner content based on the fetched objects count + /// + func displayIPPFeedbackBannerIfEligible() { + // Debug: let results = resultsController.fetchedObjects let resultsCount = results.count - // Debug: print("IPP transactions within 30 days: \(resultsCount)") print(results.map { ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") }) if resultsCount < 10 { - // TODO: Select banner 1 + // TODO: Populate banner 1 print("< 10 transactions. Banner 1 shown") } else { - // TODO: Select banner 2 + // TODO: Populate banner 2 print(">= 10 transactions. Banner 2 shown") } } @@ -101,9 +103,9 @@ final class InPersonPaymentsMenuViewModel { } private extension InPersonPaymentsMenuViewModel { - /// Results controller that fetches IPP transactions + /// Results controller that fetches IPP transactions within the last 30 days /// - func createIPPOrdersResultsController() -> ResultsController { + func createRecentIPPOrdersResultsController() -> ResultsController { let today = Date() let thirtyDaysBeforeToday = Calendar.current.date( byAdding: .day, From 17ff5af7e16f6f341ff03c54477c5672374e2542 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 5 Jan 2023 12:43:34 +0800 Subject: [PATCH 04/14] Add additional case for no IPP transactions --- .../InPersonPaymentsMenuViewModel.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index e7641a44d94..f7e358cfc4d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -84,12 +84,17 @@ final class InPersonPaymentsMenuViewModel { print("IPP transactions within 30 days: \(resultsCount)") print(results.map { ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") }) - if resultsCount < 10 { + if resultsCount == 0 { // TODO: Populate banner 1 - print("< 10 transactions. Banner 1 shown") - } else { + // TODO: Should this option use a different results controller? We're looking for 0 orders historically, not within 30 days. + print("0 transactions. Banner 1 shown") + } + else if resultsCount < 10 { // TODO: Populate banner 2 - print(">= 10 transactions. Banner 2 shown") + print("< 10 transactions within 30 days. Banner 2 shown") + } else if resultsCount >= 10 { + // TODO: Populate banner 3 + print(">= 10 transactions within 30 days. Banner 3 shown") } } @@ -113,6 +118,7 @@ private extension InPersonPaymentsMenuViewModel { to: today )! + // TODO: Question. Are we looking for the paymentMethodID to be woocommerce_payments? Or COD? let predicate = NSPredicate( format: "siteID == %lld AND paymentMethodID == %@ AND datePaid >= %@", argumentArray: [siteID ?? 0, "woocommerce_payments", thirtyDaysBeforeToday] From 4f89f0334f0abe9ad98831644e155b8d606e05ed Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 5 Jan 2023 18:59:35 +0800 Subject: [PATCH 05/14] Add logic to check if COD is enabled --- .../InPersonPaymentsMenuViewModel.swift | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index f7e358cfc4d..d5bc1ff5b74 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -33,6 +33,14 @@ final class InPersonPaymentsMenuViewModel { dependencies.analytics } + private var isCODEnabled: Bool { + guard let siteID = siteID, + let codGateway = dependencies.storage.viewStorage.loadPaymentGateway(siteID: siteID, gatewayID: "cod")?.toReadOnly() else { + return false + } + return codGateway.enabled + } + private lazy var resultsController = createRecentIPPOrdersResultsController() // MARK: - Output properties @@ -78,23 +86,25 @@ final class InPersonPaymentsMenuViewModel { /// This method is just a helper for debugging, we may use it for populating different Banner content based on the fetched objects count /// func displayIPPFeedbackBannerIfEligible() { - // Debug: - let results = resultsController.fetchedObjects - let resultsCount = results.count - print("IPP transactions within 30 days: \(resultsCount)") - print(results.map { ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") }) - - if resultsCount == 0 { - // TODO: Populate banner 1 - // TODO: Should this option use a different results controller? We're looking for 0 orders historically, not within 30 days. - print("0 transactions. Banner 1 shown") - } - else if resultsCount < 10 { - // TODO: Populate banner 2 - print("< 10 transactions within 30 days. Banner 2 shown") - } else if resultsCount >= 10 { - // TODO: Populate banner 3 - print(">= 10 transactions within 30 days. Banner 3 shown") + if isCODEnabled { + // Debug: + let results = resultsController.fetchedObjects + let resultsCount = results.count + print("IPP transactions within 30 days: \(resultsCount)") + print(results.map { ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") }) + + if resultsCount == 0 { + // TODO: Should this option use a different results controller? We're looking for 0 orders historically, not within 30 days. + print("0 transactions. Banner 1 shown") + } + else if resultsCount < 10 { + print("< 10 transactions within 30 days. Banner 2 shown") + } else if resultsCount >= 10 { + print(">= 10 transactions within 30 days. Banner 3 shown") + } + } else { + print("COD not enabled.") + DDLogInfo("COD not enabled.") } } @@ -112,6 +122,7 @@ private extension InPersonPaymentsMenuViewModel { /// func createRecentIPPOrdersResultsController() -> ResultsController { let today = Date() + let paymentGateway = Constants.wcpay let thirtyDaysBeforeToday = Calendar.current.date( byAdding: .day, value: -30, @@ -121,7 +132,7 @@ private extension InPersonPaymentsMenuViewModel { // TODO: Question. Are we looking for the paymentMethodID to be woocommerce_payments? Or COD? let predicate = NSPredicate( format: "siteID == %lld AND paymentMethodID == %@ AND datePaid >= %@", - argumentArray: [siteID ?? 0, "woocommerce_payments", thirtyDaysBeforeToday] + argumentArray: [siteID ?? 0, paymentGateway, thirtyDaysBeforeToday] ) return ResultsController(storageManager: storage, matching: predicate, sortedBy: []) @@ -131,4 +142,5 @@ private extension InPersonPaymentsMenuViewModel { private enum Constants { static let utmCampaign = "payments_menu_item" static let utmSource = "payments_menu" + static let wcpay = "woocommerce_payments" } From 01394ebbfbe03fd009d899c04fcfc4c44d871a58 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 6 Jan 2023 14:05:41 +0800 Subject: [PATCH 06/14] Remove >160 char warning. Remove print statement --- .../InPersonPaymentsMenuViewModel.swift | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index d5bc1ff5b74..0f8bd2de10a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -7,8 +7,8 @@ final class InPersonPaymentsMenuViewModel { // MARK: - Dependencies struct Dependencies { let stores: StoresManager - let analytics: Analytics let storage: StorageManagerType + let analytics: Analytics init(stores: StoresManager = ServiceLocator.stores, storage: StorageManagerType = ServiceLocator.storageManager, @@ -87,24 +87,22 @@ final class InPersonPaymentsMenuViewModel { /// func displayIPPFeedbackBannerIfEligible() { if isCODEnabled { - // Debug: - let results = resultsController.fetchedObjects - let resultsCount = results.count + let resultsCount = resultsController.fetchedObjects.count + let numberOfTransactions = 10 + print("IPP transactions within 30 days: \(resultsCount)") - print(results.map { ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") }) + print(resultsController.fetchedObjects.map { + ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") + }) if resultsCount == 0 { - // TODO: Should this option use a different results controller? We're looking for 0 orders historically, not within 30 days. + // TODO: This option should use a different results controller or predicate, as we're looking for 0 orders historically, not within 30 days. print("0 transactions. Banner 1 shown") - } - else if resultsCount < 10 { + } else if resultsCount < numberOfTransactions { print("< 10 transactions within 30 days. Banner 2 shown") - } else if resultsCount >= 10 { + } else if resultsCount >= numberOfTransactions { print(">= 10 transactions within 30 days. Banner 3 shown") } - } else { - print("COD not enabled.") - DDLogInfo("COD not enabled.") } } @@ -129,7 +127,6 @@ private extension InPersonPaymentsMenuViewModel { to: today )! - // TODO: Question. Are we looking for the paymentMethodID to be woocommerce_payments? Or COD? let predicate = NSPredicate( format: "siteID == %lld AND paymentMethodID == %@ AND datePaid >= %@", argumentArray: [siteID ?? 0, paymentGateway, thirtyDaysBeforeToday] From 218ba06237ee33f4d365a1ef20182b50e4e308ce Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 9 Jan 2023 19:51:46 +0800 Subject: [PATCH 07/14] Add resultsController to fetch all wcpay Orders --- .../InPersonPaymentsMenuViewModel.swift | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index 0f8bd2de10a..095b0866092 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -41,7 +41,8 @@ final class InPersonPaymentsMenuViewModel { return codGateway.enabled } - private lazy var resultsController = createRecentIPPOrdersResultsController() + private lazy var resultsController = createIPPOrdersResultsController() + private lazy var recentIPPOrdersResultsController = createRecentIPPOrdersResultsController() // MARK: - Output properties @Published var showWebView: AuthenticatedWebViewModel? = nil @@ -87,16 +88,18 @@ final class InPersonPaymentsMenuViewModel { /// func displayIPPFeedbackBannerIfEligible() { if isCODEnabled { - let resultsCount = resultsController.fetchedObjects.count + let hasResults = resultsController.fetchedObjects.isEmpty ? false : true + let resultsCount = recentIPPOrdersResultsController.fetchedObjects.count let numberOfTransactions = 10 + // Debug: Remove before merging + print("hasResults? \(hasResults)") print("IPP transactions within 30 days: \(resultsCount)") - print(resultsController.fetchedObjects.map { + print(recentIPPOrdersResultsController.fetchedObjects.map { ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") }) - if resultsCount == 0 { - // TODO: This option should use a different results controller or predicate, as we're looking for 0 orders historically, not within 30 days. + if !hasResults { print("0 transactions. Banner 1 shown") } else if resultsCount < numberOfTransactions { print("< 10 transactions within 30 days. Banner 2 shown") @@ -109,6 +112,8 @@ final class InPersonPaymentsMenuViewModel { private func fetchIPPTransactions() { do { try resultsController.performFetch() + // TODO: Perform the 2nd fetch only if the first one was successful? + try recentIPPOrdersResultsController.performFetch() } catch { DDLogError("Error fetching IPP transactions: \(error)") } @@ -116,7 +121,18 @@ final class InPersonPaymentsMenuViewModel { } private extension InPersonPaymentsMenuViewModel { - /// Results controller that fetches IPP transactions within the last 30 days + /// Results controller that fetches any IPP transactions via WooCommerce Payments + /// + func createIPPOrdersResultsController() -> ResultsController { + let paymentGateway = Constants.wcpay + let predicate = NSPredicate( + format: "siteID == %lld AND paymentMethodID == %@", + argumentArray: [siteID ?? 0, paymentGateway] + ) + return ResultsController(storageManager: storage, matching: predicate, sortedBy: []) + } + + /// Results controller that fetches IPP transactions via WooCommerce Payments, within the last 30 days /// func createRecentIPPOrdersResultsController() -> ResultsController { let today = Date() From fe967c76c30c1ac9787ebd87cc1828ca4f684eb0 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 11 Jan 2023 13:21:27 +0800 Subject: [PATCH 08/14] Filter IPP orders by receipt_url metadata Previously we considered IPP orders just based on their `paymetMethodID` or `paymentMethodTitle`, but this would not differentiate between IPP orders processed through the website vs the app. In order to have a more reliable solution, we add a second check to the Order `meta_data`, as IPP Orders processed through the app should have a `receipt_url` field, unlike their website counterpart. --- .../InPersonPaymentsMenuViewModel.swift | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index 095b0866092..16b84e2a518 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -33,15 +33,9 @@ final class InPersonPaymentsMenuViewModel { dependencies.analytics } - private var isCODEnabled: Bool { - guard let siteID = siteID, - let codGateway = dependencies.storage.viewStorage.loadPaymentGateway(siteID: siteID, gatewayID: "cod")?.toReadOnly() else { - return false - } - return codGateway.enabled - } + // MARK: - ResultsControllers + private lazy var IPPOrdersResultsController = createIPPOrdersResultsController() - private lazy var resultsController = createIPPOrdersResultsController() private lazy var recentIPPOrdersResultsController = createRecentIPPOrdersResultsController() // MARK: - Output properties @@ -52,6 +46,14 @@ final class InPersonPaymentsMenuViewModel { return stores.sessionManager.defaultStoreID } + private var isCODEnabled: Bool { + guard let siteID = siteID, + let codGateway = dependencies.storage.viewStorage.loadPaymentGateway(siteID: siteID, gatewayID: "cod")?.toReadOnly() else { + return false + } + return codGateway.enabled + } + private let cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration init(dependencies: Dependencies = Dependencies(), @@ -88,22 +90,29 @@ final class InPersonPaymentsMenuViewModel { /// func displayIPPFeedbackBannerIfEligible() { if isCODEnabled { - let hasResults = resultsController.fetchedObjects.isEmpty ? false : true - let resultsCount = recentIPPOrdersResultsController.fetchedObjects.count - let numberOfTransactions = 10 - - // Debug: Remove before merging + let hasResults = IPPOrdersResultsController.fetchedObjects.isEmpty ? false : true + + /// In order to filter WCPay transactions processed through IPP within the last 30 days, + /// we check if these contain `receipt_url` in their metadata, unlike those processed through a website, + /// which doesn't + /// + let IPPTransactionsFound = recentIPPOrdersResultsController.fetchedObjects.filter({ + $0.customFields.contains(where: {$0.key == Constants.receiptURLKey }) && + $0.paymentMethodTitle == Constants.paymentMethodTitle}) + let IPPresultsCount = IPPTransactionsFound.count + + // TODO: Debug. Remove before merging print("hasResults? \(hasResults)") - print("IPP transactions within 30 days: \(resultsCount)") + print("IPP transactions within 30 days: \(IPPresultsCount)") print(recentIPPOrdersResultsController.fetchedObjects.map { - ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) - DatePaid: \(String(describing: $0.datePaid))") + ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) (\($0.paymentMethodTitle) - DatePaid: \(String(describing: $0.datePaid))") }) if !hasResults { print("0 transactions. Banner 1 shown") - } else if resultsCount < numberOfTransactions { + } else if IPPresultsCount < Constants.numberOfTransactions { print("< 10 transactions within 30 days. Banner 2 shown") - } else if resultsCount >= numberOfTransactions { + } else if IPPresultsCount >= Constants.numberOfTransactions { print(">= 10 transactions within 30 days. Banner 3 shown") } } @@ -111,8 +120,7 @@ final class InPersonPaymentsMenuViewModel { private func fetchIPPTransactions() { do { - try resultsController.performFetch() - // TODO: Perform the 2nd fetch only if the first one was successful? + try IPPOrdersResultsController.performFetch() try recentIPPOrdersResultsController.performFetch() } catch { DDLogError("Error fetching IPP transactions: \(error)") @@ -124,19 +132,19 @@ private extension InPersonPaymentsMenuViewModel { /// Results controller that fetches any IPP transactions via WooCommerce Payments /// func createIPPOrdersResultsController() -> ResultsController { - let paymentGateway = Constants.wcpay + let paymentGateway = Constants.paymentMethodID let predicate = NSPredicate( format: "siteID == %lld AND paymentMethodID == %@", argumentArray: [siteID ?? 0, paymentGateway] - ) + ) return ResultsController(storageManager: storage, matching: predicate, sortedBy: []) } - /// Results controller that fetches IPP transactions via WooCommerce Payments, within the last 30 days + /// Results controller that fetches IPP transactions via WooCommerce Payments, within the last 30 days /// func createRecentIPPOrdersResultsController() -> ResultsController { let today = Date() - let paymentGateway = Constants.wcpay + let paymentGateway = Constants.paymentMethodID let thirtyDaysBeforeToday = Calendar.current.date( byAdding: .day, value: -30, @@ -155,5 +163,8 @@ private extension InPersonPaymentsMenuViewModel { private enum Constants { static let utmCampaign = "payments_menu_item" static let utmSource = "payments_menu" - static let wcpay = "woocommerce_payments" + static let paymentMethodID = "woocommerce_payments" + static let paymentMethodTitle = "WooCommerce In-Person Payments" + static let receiptURLKey = "receipt_url" + static let numberOfTransactions = 10 } From 6f259628f291c605a86b2e15f59a228529b2c7dc Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 16 Jan 2023 09:39:58 +0800 Subject: [PATCH 09/14] Declare result controllers in Order List view --- .../Orders/OrderListViewModel.swift | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift index 089ce5fcf71..3d369a78e02 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift @@ -53,6 +53,36 @@ final class OrderListViewModel { /// private var isAppActive: Bool = true + /// Results controller that fetches any IPP transactions via WooCommerce Payments + /// + private lazy var IPPOrdersResultsController: ResultsController = { + let paymentGateway = Constants.paymentMethodID + let predicate = NSPredicate( + format: "siteID == %lld AND paymentMethodID == %@", + argumentArray: [siteID, paymentGateway] + ) + return ResultsController(storageManager: storageManager, matching: predicate, sortedBy: []) + }() + + /// Results controller that fetches IPP transactions via WooCommerce Payments, within the last 30 days + /// + private lazy var recentIPPOrdersResultsController: ResultsController = { + let today = Date() + let paymentGateway = Constants.paymentMethodID + let thirtyDaysBeforeToday = Calendar.current.date( + byAdding: .day, + value: -30, + to: today + )! // TODO: Remove force-unwrap + + let predicate = NSPredicate( + format: "siteID == %lld AND paymentMethodID == %@ AND datePaid >= %@", + argumentArray: [siteID, paymentGateway, thirtyDaysBeforeToday] + ) + + return ResultsController(storageManager: storageManager, matching: predicate, sortedBy: []) + }() + /// Used for looking up the `OrderStatus` to show in the `OrderTableViewCell`. /// /// The `OrderStatus` data is fetched from the API by `OrdersTabbedViewModel`. @@ -337,3 +367,13 @@ extension OrderListViewModel { case none } } + +// MARK: IPP feedback constants +private extension OrderListViewModel { + enum Constants { + static let paymentMethodID = "woocommerce_payments" + static let paymentMethodTitle = "WooCommerce In-Person Payments" + static let receiptURLKey = "receipt_url" + static let numberOfTransactions = 10 + } +} From c80f494ddd76088df43c1bd7b824c360cf3b4748 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 16 Jan 2023 09:46:52 +0800 Subject: [PATCH 10/14] Add eligibility rules to OrderList view model --- .../Orders/OrderListViewController.swift | 2 + .../Orders/OrderListViewModel.swift | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift index 0872f4ecf33..4b6d48b8854 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift @@ -156,6 +156,8 @@ final class OrderListViewController: UIViewController, GhostableViewController { configureViewModel() configureSyncingCoordinator() + + viewModel.displayIPPFeedbackBannerIfEligible() } override func viewWillAppear(_ animated: Bool) { diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift index 3d369a78e02..803b4a71224 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift @@ -53,6 +53,13 @@ final class OrderListViewModel { /// private var isAppActive: Bool = true + private var isCODEnabled: Bool { + guard let codGateway = storageManager.viewStorage.loadPaymentGateway(siteID: siteID, gatewayID: "cod")?.toReadOnly() else { + return false + } + return codGateway.enabled + } + /// Results controller that fetches any IPP transactions via WooCommerce Payments /// private lazy var IPPOrdersResultsController: ResultsController = { @@ -155,6 +162,7 @@ final class OrderListViewModel { observeForegroundRemoteNotifications() bindTopBannerState() loadOrdersBannerVisibility() + fetchIPPTransactions() } func dismissOrdersBanner() { @@ -222,6 +230,45 @@ final class OrderListViewModel { completionHandler: completionHandler) } + private func fetchIPPTransactions() { + do { + try IPPOrdersResultsController.performFetch() + try recentIPPOrdersResultsController.performFetch() + } catch { + DDLogError("Error fetching IPP transactions: \(error)") + } + } + + func displayIPPFeedbackBannerIfEligible() { + if isCODEnabled { + let hasResults = IPPOrdersResultsController.fetchedObjects.isEmpty ? false : true + + /// In order to filter WCPay transactions processed through IPP within the last 30 days, + /// we check if these contain `receipt_url` in their metadata, unlike those processed through a website, + /// which doesn't + /// + let IPPTransactionsFound = recentIPPOrdersResultsController.fetchedObjects.filter({ + $0.customFields.contains(where: {$0.key == Constants.receiptURLKey }) && + $0.paymentMethodTitle == Constants.paymentMethodTitle}) + let IPPresultsCount = IPPTransactionsFound.count + + // TODO: Debug. Remove before merging + print("hasResults? \(hasResults)") + print("IPP transactions within 30 days: \(IPPresultsCount)") + print(recentIPPOrdersResultsController.fetchedObjects.map { + ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) (\($0.paymentMethodTitle) - DatePaid: \(String(describing: $0.datePaid))") + }) + + if !hasResults { + print("0 transactions. Banner 1 shown") + } else if IPPresultsCount < Constants.numberOfTransactions { + print("< 10 transactions within 30 days. Banner 2 shown") + } else if IPPresultsCount >= Constants.numberOfTransactions { + print(">= 10 transactions within 30 days. Banner 3 shown") + } + } + } + private func createQuery() -> FetchResultSnapshotsProvider.Query { let predicateStatus: NSPredicate = { let excludeSearchCache = NSPredicate(format: "exclusiveForSearch = false") From 55d327963406cfb8597b20f09821aaa19abe057b Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 16 Jan 2023 09:50:19 +0800 Subject: [PATCH 11/14] Remove previous Payments View implementation --- .../InPersonPaymentsMenuViewController.swift | 1 - .../InPersonPaymentsMenuViewModel.swift | 100 ------------------ 2 files changed, 101 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift index 5ee8901c0a8..6e2fa5d1b69 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift @@ -67,7 +67,6 @@ final class InPersonPaymentsMenuViewController: UIViewController { runCardPresentPaymentsOnboardingIfPossible() configureWebViewPresentation() viewModel.viewDidLoad() - viewModel.displayIPPFeedbackBannerIfEligible() } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index 98cb8f88b46..6f9860b6526 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -1,20 +1,16 @@ import Foundation import Yosemite import WooFoundation -import protocol Storage.StorageManagerType final class InPersonPaymentsMenuViewModel { // MARK: - Dependencies struct Dependencies { let stores: StoresManager - let storage: StorageManagerType let analytics: Analytics init(stores: StoresManager = ServiceLocator.stores, - storage: StorageManagerType = ServiceLocator.storageManager, analytics: Analytics = ServiceLocator.analytics) { self.stores = stores - self.storage = storage self.analytics = analytics } } @@ -25,19 +21,10 @@ final class InPersonPaymentsMenuViewModel { dependencies.stores } - private var storage: StorageManagerType { - dependencies.storage - } - private var analytics: Analytics { dependencies.analytics } - // MARK: - ResultsControllers - private lazy var IPPOrdersResultsController = createIPPOrdersResultsController() - - private lazy var recentIPPOrdersResultsController = createRecentIPPOrdersResultsController() - // MARK: - Output properties @Published var showWebView: AuthenticatedWebViewModel? = nil @@ -46,14 +33,6 @@ final class InPersonPaymentsMenuViewModel { return stores.sessionManager.defaultStoreID } - private var isCODEnabled: Bool { - guard let siteID = siteID, - let codGateway = dependencies.storage.viewStorage.loadPaymentGateway(siteID: siteID, gatewayID: "cod")?.toReadOnly() else { - return false - } - return codGateway.enabled - } - var isEligibleForCardPresentPayments: Bool { cardPresentPaymentsConfiguration.isSupportedCountry } @@ -64,8 +43,6 @@ final class InPersonPaymentsMenuViewModel { cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration = CardPresentConfigurationLoader().configuration) { self.dependencies = dependencies self.cardPresentPaymentsConfiguration = cardPresentPaymentsConfiguration - - fetchIPPTransactions() } func viewDidLoad() { @@ -89,86 +66,9 @@ final class InPersonPaymentsMenuViewModel { self?.showWebView = nil }) } - - /// This method is just a helper for debugging, we may use it for populating different Banner content based on the fetched objects count - /// - func displayIPPFeedbackBannerIfEligible() { - if isCODEnabled { - let hasResults = IPPOrdersResultsController.fetchedObjects.isEmpty ? false : true - - /// In order to filter WCPay transactions processed through IPP within the last 30 days, - /// we check if these contain `receipt_url` in their metadata, unlike those processed through a website, - /// which doesn't - /// - let IPPTransactionsFound = recentIPPOrdersResultsController.fetchedObjects.filter({ - $0.customFields.contains(where: {$0.key == Constants.receiptURLKey }) && - $0.paymentMethodTitle == Constants.paymentMethodTitle}) - let IPPresultsCount = IPPTransactionsFound.count - - // TODO: Debug. Remove before merging - print("hasResults? \(hasResults)") - print("IPP transactions within 30 days: \(IPPresultsCount)") - print(recentIPPOrdersResultsController.fetchedObjects.map { - ("OrderID: \($0.orderID) - PaymentMethodID: \($0.paymentMethodID) (\($0.paymentMethodTitle) - DatePaid: \(String(describing: $0.datePaid))") - }) - - if !hasResults { - print("0 transactions. Banner 1 shown") - } else if IPPresultsCount < Constants.numberOfTransactions { - print("< 10 transactions within 30 days. Banner 2 shown") - } else if IPPresultsCount >= Constants.numberOfTransactions { - print(">= 10 transactions within 30 days. Banner 3 shown") - } - } - } - - private func fetchIPPTransactions() { - do { - try IPPOrdersResultsController.performFetch() - try recentIPPOrdersResultsController.performFetch() - } catch { - DDLogError("Error fetching IPP transactions: \(error)") - } - } -} - -private extension InPersonPaymentsMenuViewModel { - /// Results controller that fetches any IPP transactions via WooCommerce Payments - /// - func createIPPOrdersResultsController() -> ResultsController { - let paymentGateway = Constants.paymentMethodID - let predicate = NSPredicate( - format: "siteID == %lld AND paymentMethodID == %@", - argumentArray: [siteID ?? 0, paymentGateway] - ) - return ResultsController(storageManager: storage, matching: predicate, sortedBy: []) - } - - /// Results controller that fetches IPP transactions via WooCommerce Payments, within the last 30 days - /// - func createRecentIPPOrdersResultsController() -> ResultsController { - let today = Date() - let paymentGateway = Constants.paymentMethodID - let thirtyDaysBeforeToday = Calendar.current.date( - byAdding: .day, - value: -30, - to: today - )! - - let predicate = NSPredicate( - format: "siteID == %lld AND paymentMethodID == %@ AND datePaid >= %@", - argumentArray: [siteID ?? 0, paymentGateway, thirtyDaysBeforeToday] - ) - - return ResultsController(storageManager: storage, matching: predicate, sortedBy: []) - } } private enum Constants { static let utmCampaign = "payments_menu_item" static let utmSource = "payments_menu" - static let paymentMethodID = "woocommerce_payments" - static let paymentMethodTitle = "WooCommerce In-Person Payments" - static let receiptURLKey = "receipt_url" - static let numberOfTransactions = 10 } From 2539b07d86ddbabcbb50038d97c47135e62e213f Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 16 Jan 2023 09:53:50 +0800 Subject: [PATCH 12/14] Add isIPPSupportedCountry eligibility --- .../Classes/ViewRelated/Orders/OrderListViewModel.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift index 803b4a71224..df2392a3a76 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift @@ -60,6 +60,10 @@ final class OrderListViewModel { return codGateway.enabled } + private var isIPPSupportedCountry: Bool { + CardPresentConfigurationLoader().configuration.isSupportedCountry + } + /// Results controller that fetches any IPP transactions via WooCommerce Payments /// private lazy var IPPOrdersResultsController: ResultsController = { @@ -240,7 +244,7 @@ final class OrderListViewModel { } func displayIPPFeedbackBannerIfEligible() { - if isCODEnabled { + if isCODEnabled && isIPPSupportedCountry { let hasResults = IPPOrdersResultsController.fetchedObjects.isEmpty ? false : true /// In order to filter WCPay transactions processed through IPP within the last 30 days, @@ -253,6 +257,7 @@ final class OrderListViewModel { let IPPresultsCount = IPPTransactionsFound.count // TODO: Debug. Remove before merging + print("COD enabled? \(isCODEnabled) - Eligible Country? \(isIPPSupportedCountry)") print("hasResults? \(hasResults)") print("IPP transactions within 30 days: \(IPPresultsCount)") print(recentIPPOrdersResultsController.fetchedObjects.map { From 0394db4cb5507639019b3b3b84cb8859cf5020fa Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 16 Jan 2023 09:56:39 +0800 Subject: [PATCH 13/14] Add Date unwrap safety --- WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift index df2392a3a76..f931f9150c8 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift @@ -84,7 +84,7 @@ final class OrderListViewModel { byAdding: .day, value: -30, to: today - )! // TODO: Remove force-unwrap + ) ?? Date() let predicate = NSPredicate( format: "siteID == %lld AND paymentMethodID == %@ AND datePaid >= %@", From c3fef0d8bb63e57965574182fa5c691f563d5dbe Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 16 Jan 2023 10:16:11 +0800 Subject: [PATCH 14/14] Hide fetch behind feature flag --- .../Classes/ViewRelated/Orders/OrderListViewController.swift | 4 +++- .../Classes/ViewRelated/Orders/OrderListViewModel.swift | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift index 4b6d48b8854..59740f96c0f 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift @@ -157,7 +157,9 @@ final class OrderListViewController: UIViewController, GhostableViewController { configureViewModel() configureSyncingCoordinator() - viewModel.displayIPPFeedbackBannerIfEligible() + if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.IPPInAppFeedbackBanner) { + viewModel.displayIPPFeedbackBannerIfEligible() + } } override func viewWillAppear(_ animated: Bool) { diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift index f931f9150c8..f53dd24cdb1 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift @@ -166,7 +166,10 @@ final class OrderListViewModel { observeForegroundRemoteNotifications() bindTopBannerState() loadOrdersBannerVisibility() - fetchIPPTransactions() + + if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.IPPInAppFeedbackBanner) { + fetchIPPTransactions() + } } func dismissOrdersBanner() {