Skip to content
2 changes: 2 additions & 0 deletions Modules/Sources/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return buildConfig == .localDeveloper || buildConfig == .alpha
case .ciabBookings:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .ciab:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .pointOfSaleSurveys:
return true
case .pointOfSaleCatalogAPI:
Expand Down
5 changes: 5 additions & 0 deletions Modules/Sources/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ public enum FeatureFlag: Int {
///
case ciabBookings

/// Represents CIAB environment availability overall
/// Has same underlying logic as `ciabBookings` flag.
///
case ciab

/// Enables surveys for potential and current POS merchants
///
case pointOfSaleSurveys
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import Foundation
import protocol Storage.StorageManagerType
import protocol NetworkingCore.Network
import protocol WooFoundation.CrashLogger
import enum WooFoundation.SeverityLevel

/// Determines whether an order is eligible for card present payment or not
///
public final class OrderCardPresentPaymentEligibilityStore: Store {
private let currentSite: () -> Site?
private let isCIABEnvironmentSupported: () -> Bool
private let crashLogger: CrashLogger
private lazy var siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker(
currentSite: currentSite
)
Expand All @@ -14,9 +18,13 @@ public final class OrderCardPresentPaymentEligibilityStore: Store {
dispatcher: Dispatcher,
storageManager: StorageManagerType,
network: Network,
crashLogger: CrashLogger,
isCIABEnvironmentSupported: @escaping () -> Bool,
currentSite: @escaping () -> Site?
) {
self.currentSite = currentSite
self.isCIABEnvironmentSupported = isCIABEnvironmentSupported
self.crashLogger = crashLogger
super.init(
dispatcher: dispatcher,
storageManager: storageManager,
Expand Down Expand Up @@ -56,20 +64,38 @@ private extension OrderCardPresentPaymentEligibilityStore {
onCompletion: (Result<Bool, Error>) -> Void) {
let storage = storageManager.viewStorage

guard let site = storage.loadSite(siteID: siteID)?.toReadOnly() else {
return onCompletion(
.failure(
OrderIsEligibleForCardPresentPaymentError.siteNotFoundInStorage
)
)
}
/// The following checks are only relevant if CIAB is rolled out.
if isCIABEnvironmentSupported() {
let storageSite = storage.loadSite(siteID: siteID)?.toReadOnly()

guard siteCIABEligibilityChecker.isFeatureSupported(.cardReader, for: site) else {
return onCompletion(
.failure(
OrderIsEligibleForCardPresentPaymentError.cardReaderPaymentOptionIsNotSupportedForCIABSites
let site: Site?
if let storageSite {
site = storageSite
} else {
/// Non - fatal fallback to `currentSite` when a storage site is missing
site = currentSite()

logFailedStorageSiteRead(
siteID: siteID,
currentSiteFallbackValue: site
)
)
}

if let site {
guard siteCIABEligibilityChecker.isFeatureSupported(.cardReader, for: site) else {
return onCompletion(
.failure(
OrderIsEligibleForCardPresentPaymentError.cardReaderPaymentOptionIsNotSupportedForCIABSites
)
)
}
} else {
/// Don't interrupt the flow if the `site` is not found
/// Making the assumption that it's not a CIAB site and skipping those checks
///
/// Log an error
logFailedDefaultSiteRead(siteID: siteID)
}
}

guard let order = storage.loadOrder(siteID: siteID, orderID: orderID)?.toReadOnly() else {
Expand All @@ -83,10 +109,41 @@ private extension OrderCardPresentPaymentEligibilityStore {
}
}

/// Error logging
private extension OrderCardPresentPaymentEligibilityStore {
func logFailedStorageSiteRead(siteID: Int64, currentSiteFallbackValue: Site?) {
let message = "OrderCardPresentPaymentEligibilityStore: Storage site missing, falling back to currentSite."

DDLogError(message)

crashLogger.logMessage(
message,
properties: [
"siteID": siteID,
"currentSiteID": currentSiteFallbackValue?.siteID ?? "empty",
],
level: .error
)
}

func logFailedDefaultSiteRead(siteID: Int64) {
let message = "OrderCardPresentPaymentEligibilityStore: Current default site missing."

DDLogError(message)

crashLogger.logMessage(
"OrderCardPresentPaymentEligibilityStore: Current default site missing.",
properties: [
"requestedSiteID": siteID
],
level: .error
)
}
}

extension OrderCardPresentPaymentEligibilityStore {
enum OrderIsEligibleForCardPresentPaymentError: Error {
case orderNotFoundInStorage
case siteNotFoundInStorage
case cardReaderPaymentOptionIsNotSupportedForCIABSites
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class MockFeatureFlagService: POSFeatureFlagProviding {
var isProductImageOptimizedHandlingEnabled: Bool
var isFeatureFlagEnabledReturnValue: [FeatureFlag: Bool] = [:]
var isCIABBookingsEnabled: Bool
var isCIABEnabled: Bool

init(isInboxOn: Bool = false,
isShowInboxCTAEnabled: Bool = false,
Expand All @@ -47,7 +48,8 @@ final class MockFeatureFlagService: POSFeatureFlagProviding {
notificationSettings: Bool = false,
allowMerchantAIAPIKey: Bool = false,
isProductImageOptimizedHandlingEnabled: Bool = false,
isCIABBookingsEnabled: Bool = false) {
isCIABBookingsEnabled: Bool = false,
isCIABEnabled: Bool = false) {
self.isInboxOn = isInboxOn
self.isShowInboxCTAEnabled = isShowInboxCTAEnabled
self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn
Expand All @@ -70,6 +72,7 @@ final class MockFeatureFlagService: POSFeatureFlagProviding {
self.allowMerchantAIAPIKey = allowMerchantAIAPIKey
self.isProductImageOptimizedHandlingEnabled = isProductImageOptimizedHandlingEnabled
self.isCIABBookingsEnabled = isCIABBookingsEnabled
self.isCIABEnabled = isCIABEnabled
}

func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool {
Expand Down Expand Up @@ -124,6 +127,8 @@ final class MockFeatureFlagService: POSFeatureFlagProviding {
return isProductImageOptimizedHandlingEnabled
case .ciabBookings:
return isCIABBookingsEnabled
case .ciab:
return isCIABEnabled
default:
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import XCTest

@testable import Yosemite
@testable import Networking
@testable import WooFoundation

final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase {

Expand All @@ -28,6 +29,7 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase {
private var store: OrderCardPresentPaymentEligibilityStore!

private var currentSite: Site?
private var isCIABSupported = true

override func setUp() {
super.setUp()
Expand All @@ -38,6 +40,10 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase {
dispatcher: dispatcher,
storageManager: storageManager,
network: network,
crashLogger: MockCrashLogger(),
isCIABEnvironmentSupported: { [weak self] in
return self?.isCIABSupported ?? false
},
currentSite: { [weak self] in
return self?.currentSite
}
Expand All @@ -46,6 +52,7 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase {

override func tearDown() {
currentSite = nil
isCIABSupported = true
super.tearDown()
}

Expand Down Expand Up @@ -98,6 +105,53 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase {
XCTAssertTrue(eligibility)
}

func test_orderIsEligibleForCardPresentPayment_returns_true_for_eligible_order_and_none_stored_site() throws {
// Given
let orderItem = OrderItem.fake().copy(itemID: 1234,
name: "Chocolate cake",
productID: 678,
quantity: 1.0)
let cppEligibleOrder = Order.fake().copy(siteID: sampleSiteID,
orderID: 111,
status: .pending,
currency: "USD",
datePaid: nil,
total: "5.00",
paymentMethodID: "woocommerce_payments",
items: [orderItem])
let nonSubscriptionProduct = Product.fake().copy(siteID: sampleSiteID,
productID: 678,
name: "Chocolate cake",
productTypeKey: "simple")

let regularSite = Site.fake().copy(
siteID: sampleSiteID,
isGarden: false,
gardenName: nil
)
self.currentSite = regularSite

storageManager.insertSampleProduct(readOnlyProduct: nonSubscriptionProduct)
storageManager.insertSampleOrder(readOnlyOrder: cppEligibleOrder)

let configuration = CardPresentPaymentsConfiguration(country: .US)

// When
let result = waitFor { promise in
let action = OrderCardPresentPaymentEligibilityAction
.orderIsEligibleForCardPresentPayment(orderID: 111,
siteID: self.sampleSiteID,
cardPresentPaymentsConfiguration: configuration) { result in
promise(result)
}
self.store.onAction(action)
}

// Then
let eligibility = try XCTUnwrap(result.get())
XCTAssertTrue(eligibility)
}

func test_orderIsEligibleForCardPresentPayment_returns_failure_for_CIAB_sites() throws {
// Given
let orderItem = OrderItem.fake().copy(itemID: 1234,
Expand Down Expand Up @@ -147,4 +201,96 @@ final class OrderCardPresentPaymentEligibilityStoreTests: XCTestCase {
.cardReaderPaymentOptionIsNotSupportedForCIABSites)
}
}

func test_orderIsEligibleForCardPresentPayment_returns_success_when_site_is_CIAB_and_CIAB_not_supported() throws {
// Given

/// Simulate that the CIAB environment support is not yet rolled out
isCIABSupported = false

let orderItem = OrderItem.fake().copy(itemID: 1234,
name: "Chocolate cake",
productID: 678,
quantity: 1.0)
let cppEligibleOrder = Order.fake().copy(siteID: sampleSiteID,
orderID: 111,
status: .pending,
currency: "USD",
datePaid: nil,
total: "5.00",
paymentMethodID: "woocommerce_payments",
items: [orderItem])
let nonSubscriptionProduct = Product.fake().copy(siteID: sampleSiteID,
productID: 678,
name: "Chocolate cake",
productTypeKey: "simple")

let ciabSite = Site.fake().copy(
siteID: sampleSiteID,
isGarden: true,
gardenName: "commerce"
)
self.currentSite = ciabSite

storageManager.insertSampleSite(readOnlySite: ciabSite)
storageManager.insertSampleProduct(readOnlyProduct: nonSubscriptionProduct)
storageManager.insertSampleOrder(readOnlyOrder: cppEligibleOrder)

let configuration = CardPresentPaymentsConfiguration(country: .US)

// When
let result = waitFor { promise in
let action = OrderCardPresentPaymentEligibilityAction
.orderIsEligibleForCardPresentPayment(orderID: 111,
siteID: self.sampleSiteID,
cardPresentPaymentsConfiguration: configuration) { result in
promise(result)
}
self.store.onAction(action)
}

// Then
let eligibility = try XCTUnwrap(result.get())
XCTAssertTrue(eligibility)
}

func test_orderIsEligibleForCardPresentPayment_returns_success_when_site_is_not_obtained_and_CIAB_supported() throws {
// Given
let orderItem = OrderItem.fake().copy(itemID: 1234,
name: "Chocolate cake",
productID: 678,
quantity: 1.0)
let cppEligibleOrder = Order.fake().copy(siteID: sampleSiteID,
orderID: 111,
status: .pending,
currency: "USD",
datePaid: nil,
total: "5.00",
paymentMethodID: "woocommerce_payments",
items: [orderItem])
let nonSubscriptionProduct = Product.fake().copy(siteID: sampleSiteID,
productID: 678,
name: "Chocolate cake",
productTypeKey: "simple")

storageManager.insertSampleProduct(readOnlyProduct: nonSubscriptionProduct)
storageManager.insertSampleOrder(readOnlyOrder: cppEligibleOrder)

let configuration = CardPresentPaymentsConfiguration(country: .US)

// When
let result = waitFor { promise in
let action = OrderCardPresentPaymentEligibilityAction
.orderIsEligibleForCardPresentPayment(orderID: 111,
siteID: self.sampleSiteID,
cardPresentPaymentsConfiguration: configuration) { result in
promise(result)
}
self.store.onAction(action)
}

// Then
let eligibility = try XCTUnwrap(result.get())
XCTAssertTrue(eligibility)
}
}
6 changes: 5 additions & 1 deletion WooCommerce/Classes/Yosemite/AuthenticatedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,12 @@ class AuthenticatedState: StoresManagerState {
dispatcher: dispatcher,
storageManager: storageManager,
network: network,
crashLogger: ServiceLocator.crashLogging,
isCIABEnvironmentSupported: {
ServiceLocator.featureFlagService.isFeatureFlagEnabled(.ciab)
},
currentSite: {
ServiceLocator.stores.sessionManager.defaultSite
sessionManager.defaultSite
}
),
OrderNoteStore(dispatcher: dispatcher, storageManager: storageManager, network: network),
Expand Down
Loading