From 2f295b09e269a4bd99f4523697659f08e74b02b3 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 15:44:10 +0700 Subject: [PATCH 01/14] check and delete auto-drafts on exit POS --- .../POS/Controllers/PointOfSaleOrderController.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift index 98031bd8b6c..92a827e15e0 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift @@ -144,10 +144,20 @@ protocol PointOfSaleOrderControllerProtocol { } func clearOrder() { + clearAutoDraftIfNeeded(for: order) order = nil orderState = .idle } + private func clearAutoDraftIfNeeded(for order: Order?) { + if let order, order.status == .autoDraft { + DispatchQueue.main.async { [weak self] in + let action = OrderAction.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in } + self?.stores.dispatch(action) + } + } + } + private func celebrate() { celebration.celebrate() } From 7ea9b21d6e67787d762e0b1b8e35475e30b1dfd4 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 15:51:56 +0700 Subject: [PATCH 02/14] update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e823639d18c..b8d93b67904 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -8,6 +8,7 @@ - [*] Shipping Labels: Made HS tariff number field required in customs form for EU destinations [https://github.com/woocommerce/woocommerce-ios/pull/15946] - [*] Order Details > Edit Shipping/Billing Address: Added map-based address lookup support for iOS 17+. [https://github.com/woocommerce/woocommerce-ios/pull/15964] - [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960] +- [*] Point of Sale: Remove temporary orders from storage on exiting POS mode [https://github.com/woocommerce/woocommerce-ios/pull/15975] - [internal] Replace COTS_DEVICE reader model name with TAP_TO_PAY_DEVICE. [https://github.com/woocommerce/woocommerce-ios/pull/15961] 22.9 From 4c4ed833d803a6c30c792bbb7be4781213aac0d8 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 16:43:47 +0700 Subject: [PATCH 03/14] add tests --- .../PointOfSaleOrderControllerTests.swift | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index b3a0baae6ea..79b0b52b016 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -572,6 +572,91 @@ struct PointOfSaleOrderControllerTests { } } + @MainActor + @available(iOS 17.0, *) + @Test func clearOrder_when_no_order_then_does_not_dispatch_delete_action() async throws { + // Given + let mockStores = MockStoresManager(sessionManager: .testingInstance) + let sut = PointOfSaleOrderController(orderService: mockOrderService, + receiptService: mockReceiptService, + stores: mockStores) + + var deleteActionWasDispatched = false + mockStores.whenReceivingAction(ofType: OrderAction.self) { action in + if case .deleteOrder = action { + deleteActionWasDispatched = true + } + } + + // When + sut.clearOrder() + + // Then + #expect(deleteActionWasDispatched == false) + #expect(sut.orderState == .idle) + } + + @MainActor + @available(iOS 17.0, *) + @Test func clearOrder_when_autodraft_order_then_dispatches_delete_action() async throws { + // Given + let mockStores = MockStoresManager(sessionManager: .testingInstance) + let sut = PointOfSaleOrderController(orderService: mockOrderService, + receiptService: mockReceiptService, + stores: mockStores) + + let autoDraftOrder = Order.fake().copy(siteID: 123, status: .autoDraft) + mockOrderService.orderToReturn = autoDraftOrder + await sut.syncOrder(for: .init(purchasableItems: [makeItem()]), retryHandler: {}) + + var deleteActionWasDispatched = false + var dispatchedOrder: Order? + mockStores.whenReceivingAction(ofType: OrderAction.self) { action in + if case let .deleteOrder(_, order, deletePermanently, _) = action { + deleteActionWasDispatched = true + dispatchedOrder = order + #expect(deletePermanently == true) + } + } + + // When + sut.clearOrder() + + // Then + #expect(deleteActionWasDispatched == true) + #expect(dispatchedOrder?.orderID == autoDraftOrder.orderID) + #expect(dispatchedOrder?.status == .autoDraft) + #expect(sut.orderState == .idle) + } + + @MainActor + @available(iOS 17.0, *) + @Test func clearOrder_when_completed_order_then_does_not_dispatch_delete_action() async throws { + // Given + let mockStores = MockStoresManager(sessionManager: .testingInstance) + let sut = PointOfSaleOrderController(orderService: mockOrderService, + receiptService: mockReceiptService, + stores: mockStores) + + let completedOrder = Order.fake().copy(status: .completed) + mockOrderService.orderToReturn = completedOrder + await sut.syncOrder(for: .init(purchasableItems: [makeItem()]), retryHandler: {}) + + var deleteActionWasDispatched = false + mockStores.whenReceivingAction(ofType: OrderAction.self) { action in + if case .deleteOrder = action { + deleteActionWasDispatched = true + } + } + + // When + sut.clearOrder() + + // Then + #expect(deleteActionWasDispatched == false) + #expect(sut.orderState == .idle) + } + @MainActor struct AnalyticsTests { private let analytics: WooAnalytics From f4d7c91ef9b6968d4d4e3c7c39ad28521e23630a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 17:11:11 +0700 Subject: [PATCH 04/14] add delay for testing the dispatch --- .../POS/Controllers/PointOfSaleOrderControllerTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index 79b0b52b016..53544590e8d 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -621,6 +621,7 @@ struct PointOfSaleOrderControllerTests { // When sut.clearOrder() + try await Task.sleep(nanoseconds: 100_000_000) // Then #expect(deleteActionWasDispatched == true) From fc8199744306bc9699c824bb1062645899c67f7a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 17:30:57 +0700 Subject: [PATCH 05/14] make clearOrder() async --- .../PointOfSaleOrderController.swift | 18 +++++++++++------- .../POS/Models/PointOfSaleAggregateModel.swift | 8 ++++++-- .../PointOfSalePreviewOrderController.swift | 2 +- .../PointOfSaleOrderControllerTests.swift | 7 +++---- .../Mocks/MockPointOfSaleOrderController.swift | 2 +- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift index 92a827e15e0..ca27ab54dee 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift @@ -35,7 +35,7 @@ protocol PointOfSaleOrderControllerProtocol { @discardableResult func syncOrder(for cart: Cart, retryHandler: @escaping () async -> Void) async -> Result func sendReceipt(recipientEmail: String) async throws - func clearOrder() + func clearOrder() async func collectCashPayment(changeDueAmount: String?) async throws } @@ -143,16 +143,20 @@ protocol PointOfSaleOrderControllerProtocol { } } - func clearOrder() { - clearAutoDraftIfNeeded(for: order) + func clearOrder() async { + await clearAutoDraftIfNeeded(for: order) order = nil orderState = .idle } - private func clearAutoDraftIfNeeded(for order: Order?) { - if let order, order.status == .autoDraft { - DispatchQueue.main.async { [weak self] in - let action = OrderAction.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in } + private func clearAutoDraftIfNeeded(for order: Order?) async { + guard let order, order.status == .autoDraft else { return } + + await withCheckedContinuation { continuation in + Task { @MainActor [weak self] in + let action = OrderAction.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in + continuation.resume() + } self?.stores.dispatch(action) } } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index e40ffa2a39f..1b144cdf8bb 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -173,7 +173,9 @@ extension PointOfSaleAggregateModel { func startNewCart() { removeAllItemsFromCart() - orderController.clearOrder() + Task { + await orderController.clearOrder() + } setStateForEditing() viewStateCoordinator.reset() } @@ -621,7 +623,9 @@ extension PointOfSaleAggregateModel { // Before exiting Point of Sale, we warn the merchant about losing their in-progress order. // We need to clear it down as any accidental retention can cause issues especially when reconnecting card readers. - orderController.clearOrder() + Task { + await orderController.clearOrder() + } // Ideally, we could rely on the POS being deallocated to cancel all these. Since we have memory leak issues, // cancelling them explicitly helps reduce the risk of user-visible bugs while we work on the memory leaks. diff --git a/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift b/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift index 8945ddf0d33..7ba75466005 100644 --- a/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift +++ b/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift @@ -18,7 +18,7 @@ class PointOfSalePreviewOrderController: PointOfSaleOrderControllerProtocol { func sendReceipt(recipientEmail: String) async throws { } - func clearOrder() { } + func clearOrder() async { } func collectCashPayment(changeDueAmount: String?) async throws {} } diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index 53544590e8d..62658f57d1e 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -589,7 +589,7 @@ struct PointOfSaleOrderControllerTests { } // When - sut.clearOrder() + await sut.clearOrder() // Then #expect(deleteActionWasDispatched == false) @@ -620,8 +620,7 @@ struct PointOfSaleOrderControllerTests { } // When - sut.clearOrder() - try await Task.sleep(nanoseconds: 100_000_000) + await sut.clearOrder() // Then #expect(deleteActionWasDispatched == true) @@ -651,7 +650,7 @@ struct PointOfSaleOrderControllerTests { } // When - sut.clearOrder() + await sut.clearOrder() // Then #expect(deleteActionWasDispatched == false) diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift index 575935eb212..133c0e7ca8e 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift @@ -37,7 +37,7 @@ final class MockPointOfSaleOrderController: PointOfSaleOrderControllerProtocol { } var clearOrderWasCalled: Bool = false - func clearOrder() { + func clearOrder() async { clearOrderWasCalled = true } From dad2062d941fe36d16fb926e44fb1c9a7b5bce54 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 30 Sep 2025 11:21:23 +0700 Subject: [PATCH 06/14] inject stores to dispatch order deletion --- RELEASE-NOTES.txt | 2 +- .../PointOfSaleOrderController.swift | 4 + .../POS/TabBar/POSTabCoordinator.swift | 3 +- .../PointOfSaleOrderControllerTests.swift | 90 +++++++++++++------ 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index f7b08b16e44..a7d7c793e8c 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -4,6 +4,7 @@ 23.4 ----- - [*] Order details: Fixes broken country selector navigation and "Non editable banner" width. [https://github.com/woocommerce/woocommerce-ios/pull/16171] +- [*] Point of Sale: Remove temporary orders from storage on exiting POS mode [https://github.com/woocommerce/woocommerce-ios/pull/15975] 23.3 ----- @@ -45,7 +46,6 @@ - [*] Product List: Load list with simplified product objects to improve performance. [https://teamkiwip2.wordpress.com/2025/08/01/hack-week-improving-performance-when-loading-cached-products/] - [*] Order Details > Edit Shipping/Billing Address: Added map-based address lookup support for iOS 17+. [https://github.com/woocommerce/woocommerce-ios/pull/15964] - [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960] -- [*] Point of Sale: Remove temporary orders from storage on exiting POS mode [https://github.com/woocommerce/woocommerce-ios/pull/15975] - [internal] Replace COTS_DEVICE reader model name with TAP_TO_PAY_DEVICE. [https://github.com/woocommerce/woocommerce-ios/pull/15961] [**] POS: Added a guided setup flow for barcode scanning, and improved the scanning experience with better error handling and support for starting a new order on scan. [https://github.com/woocommerce/woocommerce-ios/pull/15979] diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift index 70527d83c23..524cd07502e 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift @@ -20,6 +20,7 @@ import enum WooFoundation.CurrencyCode import protocol WooFoundation.Analytics import enum Alamofire.AFError import class Yosemite.OrderTotalsCalculator +import protocol Yosemite.StoresManager enum SyncOrderState { case newOrder @@ -45,11 +46,13 @@ protocol PointOfSaleOrderControllerProtocol { receiptSender: POSReceiptSending, currencySettingsProvider: POSCurrencySettingsProviding, analytics: POSAnalyticsProviding, + stores: StoresManager, celebration: PaymentCaptureCelebrationProtocol = PaymentCaptureCelebration()) { self.orderService = orderService self.receiptSender = receiptSender self.currencySettingsProvider = currencySettingsProvider self.analytics = analytics + self.stores = stores self.celebration = celebration } @@ -58,6 +61,7 @@ protocol PointOfSaleOrderControllerProtocol { private let currencySettingsProvider: POSCurrencySettingsProviding private let celebration: PaymentCaptureCelebrationProtocol private let analytics: POSAnalyticsProviding + private let stores: StoresManager private(set) var orderState: PointOfSaleInternalOrderState = .idle private var order: Order? = nil diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 5878826b9f2..8847974316c 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -223,7 +223,8 @@ private extension POSTabCoordinator { orderController: PointOfSaleOrderController(orderService: orderService, receiptSender: receiptSender, currencySettingsProvider: serviceAdaptor.currency, - analytics: serviceAdaptor.analytics), + analytics: serviceAdaptor.analytics, + stores: storesManager), receiptSender: receiptSender, settingsController: PointOfSaleSettingsController(siteID: siteID, settingsService: settingsService, diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index fb640dad4eb..5fba107f970 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -14,6 +14,7 @@ import protocol WooFoundation.Analytics import enum Networking.DotcomError import enum Networking.NetworkError +@MainActor struct PointOfSaleOrderControllerTests { let mockOrderService = MockPOSOrderService() let mockReceiptSender = MockPOSReceiptSender() @@ -23,7 +24,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) // When await sut.syncOrder(for: Cart(), retryHandler: {}) @@ -37,7 +39,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) let cartItem = makeItem(orderItemsToMatch: [orderItem]) @@ -58,7 +61,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) mockOrderService.simulateSyncing = true Task { await sut.syncOrder(for: Cart(purchasableItems: [makeItem(quantity: 1)]), retryHandler: {}) @@ -85,7 +89,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(currencySettings: currencySettings), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) // When await sut.syncOrder(for: Cart(purchasableItems: [makeItem()]), retryHandler: {}) @@ -99,7 +104,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let cartItem = makeItem(quantity: 1) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) @@ -121,7 +127,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let fakeOrder = Order.fake() mockOrderService.orderToReturn = fakeOrder var orderStates: [PointOfSaleInternalOrderState] = [sut.orderState] @@ -160,7 +167,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) mockOrderService.orderToReturn = nil var orderStates: [PointOfSaleInternalOrderState] = [sut.orderState] @@ -198,7 +206,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let email = "test@example.com" // When @@ -216,7 +225,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let order = Order.fake() let recipientEmail = "test@fake.com" mockOrderService.orderToReturn = order @@ -240,6 +250,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance), celebration: MockPaymentCaptureCelebration()) try await sut.collectCashPayment(changeDueAmount: nil) } catch let error as PointOfSaleOrderController.PointOfSaleOrderControllerError { @@ -256,6 +267,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance), celebration: mockPaymentCelebration) let orderItem = OrderItem.fake() @@ -278,6 +290,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance), celebration: MockPaymentCaptureCelebration()) let orderItem = OrderItem.fake() @@ -299,7 +312,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let fakeOrderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake() let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) @@ -321,7 +335,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let fakeOrder = Order.fake() mockOrderService.orderToReturn = fakeOrder @@ -345,7 +360,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) let cartItem = makeItem(orderItemsToMatch: [orderItem]) @@ -370,7 +386,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let cartItem = makeItem(quantity: 1) // When @@ -390,7 +407,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let orderItem = OrderItem.fake().copy(quantity: 1) let couponCode = "SAVE10" let coupon = OrderCouponLine.fake().copy(code: couponCode) @@ -415,7 +433,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let orderItem = OrderItem.fake().copy(quantity: 1) let initialCouponCode = "SAVE10" let initialCoupon = OrderCouponLine.fake().copy(code: initialCouponCode) @@ -440,7 +459,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let orderItem = OrderItem.fake().copy(quantity: 1) let couponCode = "SAVE10" let coupon = OrderCouponLine.fake().copy(code: couponCode) @@ -465,7 +485,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let errorMessage = "Invalid coupon code" mockOrderService.errorToReturn = DotcomError.unknown(code: "woocommerce_rest_invalid_coupon", message: errorMessage) @@ -506,7 +527,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) let errorMessage = "Coupon INVALID does not exist" let errorJSON = """ { @@ -554,7 +576,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics()) + analytics: MockPOSAnalytics(), + stores: MockStoresManager(sessionManager: .testingInstance)) // First create a successful order let orderItem = OrderItem.fake().copy(quantity: 1) @@ -596,12 +619,13 @@ struct PointOfSaleOrderControllerTests { } @MainActor - @available(iOS 17.0, *) @Test func clearOrder_when_no_order_then_does_not_dispatch_delete_action() async throws { // Given let mockStores = MockStoresManager(sessionManager: .testingInstance) let sut = PointOfSaleOrderController(orderService: mockOrderService, - receiptService: mockReceiptService, + receiptSender: mockReceiptSender, + currencySettingsProvider: MockCurrencySettingsProvider(), + analytics: MockPOSAnalytics(), stores: mockStores) var deleteActionWasDispatched = false @@ -620,17 +644,21 @@ struct PointOfSaleOrderControllerTests { } @MainActor - @available(iOS 17.0, *) @Test func clearOrder_when_autodraft_order_then_dispatches_delete_action() async throws { // Given let mockStores = MockStoresManager(sessionManager: .testingInstance) let sut = PointOfSaleOrderController(orderService: mockOrderService, - receiptService: mockReceiptService, + receiptSender: mockReceiptSender, + currencySettingsProvider: MockCurrencySettingsProvider(), + analytics: MockPOSAnalytics(), stores: mockStores) - let autoDraftOrder = Order.fake().copy(siteID: 123, status: .autoDraft) + let fakeOrderItem = OrderItem.fake().copy(quantity: 1) + let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) + let autoDraftOrder = Order.fake().copy(status: .autoDraft) mockOrderService.orderToReturn = autoDraftOrder - await sut.syncOrder(for: .init(purchasableItems: [makeItem()]), retryHandler: {}) + + await sut.syncOrder(for: .init(purchasableItems: [fakeCartItem]), retryHandler: { }) var deleteActionWasDispatched = false var dispatchedOrder: Order? @@ -653,12 +681,13 @@ struct PointOfSaleOrderControllerTests { } @MainActor - @available(iOS 17.0, *) @Test func clearOrder_when_completed_order_then_does_not_dispatch_delete_action() async throws { // Given let mockStores = MockStoresManager(sessionManager: .testingInstance) let sut = PointOfSaleOrderController(orderService: mockOrderService, - receiptService: mockReceiptService, + receiptSender: mockReceiptSender, + currencySettingsProvider: MockCurrencySettingsProvider(), + analytics: MockPOSAnalytics(), stores: mockStores) let completedOrder = Order.fake().copy(status: .completed) @@ -692,7 +721,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: orderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: analytics) + analytics: analytics, + stores: MockStoresManager(sessionManager: .testingInstance)) let fakeOrderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake() let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) @@ -710,7 +740,8 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: orderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: analytics) + analytics: analytics, + stores: MockStoresManager(sessionManager: .testingInstance)) orderService.orderToReturn = nil // When @@ -727,6 +758,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: analytics, + stores: MockStoresManager(sessionManager: .testingInstance), celebration: MockPaymentCaptureCelebration()) // In order to test the order controller failure we need to succeed first in creating a successful order: From b07e1387372da08337c44dbe42481f325a301c33 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 30 Sep 2025 19:13:15 +0700 Subject: [PATCH 07/14] temp DI stores --- .../Classes/POS/Presentation/PointOfSaleEntryPointView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index c318bfd124b..7670e363f58 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -93,7 +93,8 @@ public struct PointOfSaleEntryPointView: View { self.orderController = PointOfSaleOrderController(orderService: orderService, receiptSender: receiptSender, currencySettingsProvider: services.currency, - analytics: services.analytics) + analytics: services.analytics, + stores: ServiceLocator.stores) // Temporary, to resolve merge conflict self.settingsController = PointOfSaleSettingsController(siteID: siteID, settingsService: settingsService, cardPresentPaymentService: cardPresentPaymentService, From a54af56baa9883f2447ded5453a3c27668243e3c Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 30 Sep 2025 20:30:07 +0700 Subject: [PATCH 08/14] make OrderStoreMethodsProtocol to share delete func between app and pos --- .../Stores/Helpers/OrderStoreMethods.swift | 90 +++++++++++++++++++ .../Sources/Yosemite/Stores/OrderStore.swift | 13 ++- .../Tools/POS/POSOrderManagementService.swift | 44 +++++++++ .../Adaptors/POSServiceLocatorAdaptor.swift | 14 +++ .../PointOfSaleOrderController.swift | 13 ++- .../PointOfSaleEntryPointView.swift | 2 +- .../Protocols/POSDependencyProviding.swift | 2 + .../Classes/POS/Utils/PreviewHelpers.swift | 8 ++ 8 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift create mode 100644 Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift diff --git a/Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift b/Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift new file mode 100644 index 00000000000..7da033f74f8 --- /dev/null +++ b/Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift @@ -0,0 +1,90 @@ +import Foundation +import Networking +import Storage + +/// OrderStoreMethods extracts functionality of OrderStore that needs be reused within Yosemite +/// OrderStoreMethods is intentionally internal not to be exposed outside the module +/// +internal protocol OrderStoreMethodsProtocol { + func deleteOrder(siteID: Int64, + order: Order, + deletePermanently: Bool, + onCompletion: @escaping (Result) -> Void) +} + +internal class OrderStoreMethods: OrderStoreMethodsProtocol { + private let remote: OrdersRemote + private let storageManager: StorageManagerType + + init( + storageManager: StorageManagerType, + remote: OrdersRemote + ) { + self.remote = remote + self.storageManager = storageManager + } + + /// Deletes a given order. + /// Extracted from OrderStore.deleteOrder() implementation. + /// + func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { + // Optimistically delete the order from storage + deleteStoredOrder(siteID: siteID, orderID: order.orderID) + + remote.deleteOrder(for: siteID, orderID: order.orderID, force: deletePermanently) { [weak self] result in + switch result { + case .success: + onCompletion(result) + case .failure: + // Revert optimistic deletion unless the order is an auto-draft (shouldn't be stored) + guard order.status != .autoDraft else { + return onCompletion(result) + } + self?.upsertStoredOrdersInBackground(readOnlyOrders: [order], onCompletion: { + onCompletion(result) + }) + } + } + } +} + +// MARK: - Storage Methods + +private extension OrderStoreMethods { + /// Deletes any Storage.Order with the specified OrderID + /// Extracted from OrderStore.deleteStoredOrder() + /// + func deleteStoredOrder(siteID: Int64, orderID: Int64, onCompletion: (() -> Void)? = nil) { + storageManager.performAndSave({ storage in + guard let order = storage.loadOrder(siteID: siteID, orderID: orderID) else { + return + } + storage.deleteObject(order) + }, completion: onCompletion, on: .main) + } + + /// Updates (OR Inserts) the specified ReadOnly Order Entities *in a background thread*. + /// Extracted from OrderStore.upsertStoredOrdersInBackground() + /// + func upsertStoredOrdersInBackground(readOnlyOrders: [Networking.Order], + removeAllStoredOrders: Bool = false, + onCompletion: (() -> Void)? = nil) { + storageManager.performAndSave({ [weak self] derivedStorage in + guard let self else { return } + if removeAllStoredOrders { + derivedStorage.deleteAllObjects(ofType: Storage.Order.self) + } + upsertStoredOrders(readOnlyOrders: readOnlyOrders, in: derivedStorage) + }, completion: onCompletion, on: .main) + } + + /// Updates (OR Inserts) the specified ReadOnly Order Entities into the Storage Layer. + /// Extracted from OrderStore.upsertStoredOrders() + /// + func upsertStoredOrders(readOnlyOrders: [Networking.Order], + insertingSearchResults: Bool = false, + in storage: StorageType) { + let useCase = OrdersUpsertUseCase(storage: storage) + useCase.upsert(readOnlyOrders, insertingSearchResults: insertingSearchResults) + } +} diff --git a/Modules/Sources/Yosemite/Stores/OrderStore.swift b/Modules/Sources/Yosemite/Stores/OrderStore.swift index a4093adbfaa..f660ba152b3 100644 --- a/Modules/Sources/Yosemite/Stores/OrderStore.swift +++ b/Modules/Sources/Yosemite/Stores/OrderStore.swift @@ -8,9 +8,20 @@ import Storage // public class OrderStore: Store { private let remote: OrdersRemote + private let methods: OrderStoreMethods + + init(dispatcher: Dispatcher, + storageManager: StorageManagerType, + network: Network, + remote: OrdersRemote) { + self.remote = remote + self.methods = OrderStoreMethods(storageManager: storageManager, remote: remote) + super.init(dispatcher: dispatcher, storageManager: storageManager, network: network) + } public override init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) { self.remote = OrdersRemote(network: network) + self.methods = OrderStoreMethods(storageManager: storageManager, remote: self.remote) super.init(dispatcher: dispatcher, storageManager: storageManager, network: network) } @@ -85,7 +96,7 @@ public class OrderStore: Store { case let .markOrderAsPaidLocally(siteID, orderID, datePaid, onCompletion): markOrderAsPaidLocally(siteID: siteID, orderID: orderID, datePaid: datePaid, onCompletion: onCompletion) case let .deleteOrder(siteID, order, deletePermanently, onCompletion): - deleteOrder(siteID: siteID, order: order, deletePermanently: deletePermanently, onCompletion: onCompletion) + methods.deleteOrder(siteID: siteID, order: order, deletePermanently: deletePermanently, onCompletion: onCompletion) case let .observeInsertedOrders(siteID, completion): observeInsertedOrders(siteID: siteID, completion: completion) case let .checkIfStoreHasOrders(siteID, completion): diff --git a/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift b/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift new file mode 100644 index 00000000000..cb9cae2c4b2 --- /dev/null +++ b/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift @@ -0,0 +1,44 @@ +import Foundation +import Networking +import protocol Storage.StorageManagerType +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite + +public protocol POSOrderManagementServiceProtocol { + /// Deletes an order permanently or moves it to trash + /// - Parameters: + /// - siteID: The site ID where the order belongs + /// - order: The order to delete + /// - deletePermanently: Whether to delete permanently or move to trash + /// - onCompletion: Completion handler with the result + func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) +} + +public final class POSOrderManagementService: POSOrderManagementServiceProtocol { + private let orderStoreMethods: OrderStoreMethodsProtocol + + public convenience init?(siteID: Int64, + credentials: Credentials?, + selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, + storageManager: StorageManagerType) { + guard let credentials else { + DDLogError("⛔️ Could not create POSOrderManagementService due to not finding credentials") + return nil + } + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) + let remote = OrdersRemote(network: network) + self.init(storageManager: storageManager, remote: remote) + } + + public init(storageManager: StorageManagerType, remote: OrdersRemote) { + self.orderStoreMethods = OrderStoreMethods(storageManager: storageManager, remote: remote) + } + + // MARK: - Protocol conformance + public func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { + orderStoreMethods.deleteOrder(siteID: siteID, order: order, deletePermanently: deletePermanently, onCompletion: onCompletion) + } +} diff --git a/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift b/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift index 25fa3d2f068..2a0f446a2e4 100644 --- a/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift +++ b/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift @@ -5,6 +5,7 @@ import Yosemite import protocol Experiments.FeatureFlagService import enum Experiments.FeatureFlag import protocol Storage.StorageManagerType + final class POSServiceLocatorAdaptor: POSDependencyProviding { var analytics: POSAnalyticsProviding { POSAnalyticsAdaptor() @@ -29,6 +30,10 @@ final class POSServiceLocatorAdaptor: POSDependencyProviding { var externalViews: POSExternalViewProviding { POSExternalViewAdaptor() } + + var orderManagement: POSOrderManagementServiceProtocol { + POSOrderManagementServiceAdaptor() + } } // MARK: - Individual Service Adaptors @@ -117,3 +122,12 @@ private struct POSExternalViewAdaptor: POSExternalViewProviding { )) } } + +private struct POSOrderManagementServiceAdaptor: POSOrderManagementServiceProtocol { + func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { + let action = OrderAction.deleteOrder(siteID: siteID, order: order, deletePermanently: deletePermanently, onCompletion: onCompletion) + Task { @MainActor in + ServiceLocator.stores.dispatch(action) + } + } +} diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift index 524cd07502e..16baab45599 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift @@ -20,7 +20,7 @@ import enum WooFoundation.CurrencyCode import protocol WooFoundation.Analytics import enum Alamofire.AFError import class Yosemite.OrderTotalsCalculator -import protocol Yosemite.StoresManager +import protocol Yosemite.POSOrderManagementServiceProtocol enum SyncOrderState { case newOrder @@ -46,13 +46,13 @@ protocol PointOfSaleOrderControllerProtocol { receiptSender: POSReceiptSending, currencySettingsProvider: POSCurrencySettingsProviding, analytics: POSAnalyticsProviding, - stores: StoresManager, + orderManagement: POSOrderManagementServiceProtocol, celebration: PaymentCaptureCelebrationProtocol = PaymentCaptureCelebration()) { self.orderService = orderService self.receiptSender = receiptSender self.currencySettingsProvider = currencySettingsProvider self.analytics = analytics - self.stores = stores + self.orderManagement = orderManagement self.celebration = celebration } @@ -61,7 +61,7 @@ protocol PointOfSaleOrderControllerProtocol { private let currencySettingsProvider: POSCurrencySettingsProviding private let celebration: PaymentCaptureCelebrationProtocol private let analytics: POSAnalyticsProviding - private let stores: StoresManager + private let orderManagement: POSOrderManagementServiceProtocol private(set) var orderState: PointOfSaleInternalOrderState = .idle private var order: Order? = nil @@ -128,11 +128,10 @@ protocol PointOfSaleOrderControllerProtocol { guard let order, order.status == .autoDraft else { return } await withCheckedContinuation { continuation in - Task { @MainActor [weak self] in - let action = OrderAction.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in + Task { @MainActor in + orderManagement.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in continuation.resume() } - self?.stores.dispatch(action) } } } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 7670e363f58..b61f3d655f0 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -94,7 +94,7 @@ public struct PointOfSaleEntryPointView: View { receiptSender: receiptSender, currencySettingsProvider: services.currency, analytics: services.analytics, - stores: ServiceLocator.stores) // Temporary, to resolve merge conflict + orderManagement: services.orderManagement) self.settingsController = PointOfSaleSettingsController(siteID: siteID, settingsService: settingsService, cardPresentPaymentService: cardPresentPaymentService, diff --git a/WooCommerce/Classes/POS/Protocols/POSDependencyProviding.swift b/WooCommerce/Classes/POS/Protocols/POSDependencyProviding.swift index d5428a429d3..651db2083c0 100644 --- a/WooCommerce/Classes/POS/Protocols/POSDependencyProviding.swift +++ b/WooCommerce/Classes/POS/Protocols/POSDependencyProviding.swift @@ -5,6 +5,7 @@ import enum Experiments.FeatureFlag import struct Yosemite.Coupon import enum Yosemite.CouponDiscountType import enum Yosemite.POSItem +import protocol Yosemite.POSOrderManagementServiceProtocol /// POSDepenencyProviding is part of the POS entry point that defines the external dependencies from the Woo app that POS depends on @@ -72,4 +73,5 @@ public protocol POSDependencyProviding { var connectivity: POSConnectivityProviding { get } var externalNavigation: POSExternalNavigationProviding { get } var externalViews: POSExternalViewProviding { get } + var orderManagement: POSOrderManagementServiceProtocol { get } } diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index 30a1cff82d5..90c2062a785 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -45,6 +45,7 @@ import protocol Yosemite.POSItemFetchAnalyticsTracking import protocol Yosemite.POSOrderListFetchStrategyFactoryProtocol import protocol Yosemite.POSOrderListFetchStrategy import protocol Yosemite.PointOfSaleCouponFetchStrategyFactoryProtocol +import protocol Yosemite.POSOrderManagementServiceProtocol // MARK: - PreviewProvider helpers // @@ -537,6 +538,13 @@ final class POSPreviewServices: POSDependencyProviding { var connectivity: POSConnectivityProviding = EmptyPOSConnectivityProvider() var externalNavigation: POSExternalNavigationProviding = EmptyPOSExternalNavigation() var externalViews: POSExternalViewProviding = EmptyPOSExternalView() + var orderManagement: POSOrderManagementServiceProtocol = POSPreviewOrderManagementService() +} + +final class POSPreviewOrderManagementService: POSOrderManagementServiceProtocol { + func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { + onCompletion(.success(order)) + } } // MARK: - Preview Catalog Services From b96908b70501e38fc807a843b0c19a7f0c33ce18 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 30 Sep 2025 20:42:22 +0700 Subject: [PATCH 09/14] update tests --- .../PointOfSaleOrderControllerTests.swift | 109 ++++++++---------- 1 file changed, 49 insertions(+), 60 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index 5fba107f970..d0849b54f9d 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -13,11 +13,13 @@ import class WooFoundation.CurrencySettings import protocol WooFoundation.Analytics import enum Networking.DotcomError import enum Networking.NetworkError +import protocol Yosemite.POSOrderManagementServiceProtocol @MainActor struct PointOfSaleOrderControllerTests { let mockOrderService = MockPOSOrderService() let mockReceiptSender = MockPOSReceiptSender() + let mockOrderManagement = MockPOSOrderManagementService() @Test func syncOrder_without_items_doesnt_call_orderService() async throws { // Given @@ -25,7 +27,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) // When await sut.syncOrder(for: Cart(), retryHandler: {}) @@ -40,7 +42,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) let cartItem = makeItem(orderItemsToMatch: [orderItem]) @@ -62,7 +64,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) mockOrderService.simulateSyncing = true Task { await sut.syncOrder(for: Cart(purchasableItems: [makeItem(quantity: 1)]), retryHandler: {}) @@ -90,7 +92,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(currencySettings: currencySettings), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) // When await sut.syncOrder(for: Cart(purchasableItems: [makeItem()]), retryHandler: {}) @@ -105,7 +107,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let cartItem = makeItem(quantity: 1) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) @@ -128,7 +130,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let fakeOrder = Order.fake() mockOrderService.orderToReturn = fakeOrder var orderStates: [PointOfSaleInternalOrderState] = [sut.orderState] @@ -168,7 +170,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) mockOrderService.orderToReturn = nil var orderStates: [PointOfSaleInternalOrderState] = [sut.orderState] @@ -207,7 +209,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let email = "test@example.com" // When @@ -226,7 +228,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let order = Order.fake() let recipientEmail = "test@fake.com" mockOrderService.orderToReturn = order @@ -250,7 +252,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance), + orderManagement: mockOrderManagement, celebration: MockPaymentCaptureCelebration()) try await sut.collectCashPayment(changeDueAmount: nil) } catch let error as PointOfSaleOrderController.PointOfSaleOrderControllerError { @@ -267,7 +269,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance), + orderManagement: mockOrderManagement, celebration: mockPaymentCelebration) let orderItem = OrderItem.fake() @@ -290,7 +292,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance), + orderManagement: mockOrderManagement, celebration: MockPaymentCaptureCelebration()) let orderItem = OrderItem.fake() @@ -313,7 +315,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let fakeOrderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake() let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) @@ -336,7 +338,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let fakeOrder = Order.fake() mockOrderService.orderToReturn = fakeOrder @@ -361,7 +363,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) let cartItem = makeItem(orderItemsToMatch: [orderItem]) @@ -387,7 +389,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let cartItem = makeItem(quantity: 1) // When @@ -408,7 +410,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let orderItem = OrderItem.fake().copy(quantity: 1) let couponCode = "SAVE10" let coupon = OrderCouponLine.fake().copy(code: couponCode) @@ -434,7 +436,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let orderItem = OrderItem.fake().copy(quantity: 1) let initialCouponCode = "SAVE10" let initialCoupon = OrderCouponLine.fake().copy(code: initialCouponCode) @@ -460,7 +462,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let orderItem = OrderItem.fake().copy(quantity: 1) let couponCode = "SAVE10" let coupon = OrderCouponLine.fake().copy(code: couponCode) @@ -486,7 +488,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let errorMessage = "Invalid coupon code" mockOrderService.errorToReturn = DotcomError.unknown(code: "woocommerce_rest_invalid_coupon", message: errorMessage) @@ -528,7 +530,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) let errorMessage = "Coupon INVALID does not exist" let errorJSON = """ { @@ -577,7 +579,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: mockOrderManagement) // First create a successful order let orderItem = OrderItem.fake().copy(quantity: 1) @@ -621,37 +623,28 @@ struct PointOfSaleOrderControllerTests { @MainActor @Test func clearOrder_when_no_order_then_does_not_dispatch_delete_action() async throws { // Given - let mockStores = MockStoresManager(sessionManager: .testingInstance) let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: mockStores) - - var deleteActionWasDispatched = false - mockStores.whenReceivingAction(ofType: OrderAction.self) { action in - if case .deleteOrder = action { - deleteActionWasDispatched = true - } - } + orderManagement: mockOrderManagement) // When await sut.clearOrder() // Then - #expect(deleteActionWasDispatched == false) + #expect(mockOrderManagement.deleteOrderWasCalled == false) #expect(sut.orderState == .idle) } @MainActor @Test func clearOrder_when_autodraft_order_then_dispatches_delete_action() async throws { // Given - let mockStores = MockStoresManager(sessionManager: .testingInstance) let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: mockStores) + orderManagement: mockOrderManagement) let fakeOrderItem = OrderItem.fake().copy(quantity: 1) let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) @@ -660,52 +653,34 @@ struct PointOfSaleOrderControllerTests { await sut.syncOrder(for: .init(purchasableItems: [fakeCartItem]), retryHandler: { }) - var deleteActionWasDispatched = false - var dispatchedOrder: Order? - mockStores.whenReceivingAction(ofType: OrderAction.self) { action in - if case let .deleteOrder(_, order, deletePermanently, _) = action { - deleteActionWasDispatched = true - dispatchedOrder = order - #expect(deletePermanently == true) - } - } - // When await sut.clearOrder() // Then - #expect(deleteActionWasDispatched == true) - #expect(dispatchedOrder?.orderID == autoDraftOrder.orderID) - #expect(dispatchedOrder?.status == .autoDraft) + #expect(mockOrderManagement.deleteOrderWasCalled == true) + #expect(mockOrderManagement.deletedOrder?.orderID == autoDraftOrder.orderID) + #expect(mockOrderManagement.deletedOrder?.status == .autoDraft) #expect(sut.orderState == .idle) } @MainActor @Test func clearOrder_when_completed_order_then_does_not_dispatch_delete_action() async throws { // Given - let mockStores = MockStoresManager(sessionManager: .testingInstance) let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - stores: mockStores) + orderManagement: mockOrderManagement) let completedOrder = Order.fake().copy(status: .completed) mockOrderService.orderToReturn = completedOrder await sut.syncOrder(for: .init(purchasableItems: [makeItem()]), retryHandler: {}) - var deleteActionWasDispatched = false - mockStores.whenReceivingAction(ofType: OrderAction.self) { action in - if case .deleteOrder = action { - deleteActionWasDispatched = true - } - } - // When await sut.clearOrder() // Then - #expect(deleteActionWasDispatched == false) + #expect(mockOrderManagement.deleteOrderWasCalled == false) #expect(sut.orderState == .idle) } @@ -722,7 +697,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: analytics, - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: MockPOSOrderManagementService()) let fakeOrderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake() let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) @@ -741,7 +716,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: analytics, - stores: MockStoresManager(sessionManager: .testingInstance)) + orderManagement: MockPOSOrderManagementService()) orderService.orderToReturn = nil // When @@ -758,7 +733,7 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: analytics, - stores: MockStoresManager(sessionManager: .testingInstance), + orderManagement: MockPOSOrderManagementService(), celebration: MockPaymentCaptureCelebration()) // In order to test the order controller failure we need to succeed first in creating a successful order: @@ -795,6 +770,20 @@ private func makeItem(name: String = "", quantity: quantity) } +// MARK: - Mock POS Order Management Service + +final class MockPOSOrderManagementService: POSOrderManagementServiceProtocol { + var deleteOrderWasCalled = false + var deletedOrder: Order? + var deleteOrderResult: Result = .success(Order.fake()) + + func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { + deleteOrderWasCalled = true + deletedOrder = order + onCompletion(deleteOrderResult) + } +} + // MARK: - Mock Currency Settings Provider final class MockCurrencySettingsProvider: POSCurrencySettingsProviding { From f1ed6e52a44feed9d9b46ccc0666d3495ff12347 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 30 Sep 2025 20:46:49 +0700 Subject: [PATCH 10/14] periphery ignores --- Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift | 1 + Modules/Sources/Yosemite/Stores/OrderStore.swift | 2 ++ .../Sources/Yosemite/Tools/POS/POSOrderManagementService.swift | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift b/Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift index 7da033f74f8..16b924221fb 100644 --- a/Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift +++ b/Modules/Sources/Yosemite/Stores/Helpers/OrderStoreMethods.swift @@ -5,6 +5,7 @@ import Storage /// OrderStoreMethods extracts functionality of OrderStore that needs be reused within Yosemite /// OrderStoreMethods is intentionally internal not to be exposed outside the module /// +/// periphery: ignore internal protocol OrderStoreMethodsProtocol { func deleteOrder(siteID: Int64, order: Order, diff --git a/Modules/Sources/Yosemite/Stores/OrderStore.swift b/Modules/Sources/Yosemite/Stores/OrderStore.swift index f660ba152b3..db86604dea9 100644 --- a/Modules/Sources/Yosemite/Stores/OrderStore.swift +++ b/Modules/Sources/Yosemite/Stores/OrderStore.swift @@ -6,6 +6,7 @@ import Storage // MARK: - OrderStore // +/// periphery: ignore public class OrderStore: Store { private let remote: OrdersRemote private let methods: OrderStoreMethods @@ -108,6 +109,7 @@ public class OrderStore: Store { // MARK: - Services! // +/// periphery: ignore private extension OrderStore { /// Nukes all of the Stored Orders. diff --git a/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift b/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift index cb9cae2c4b2..c459911e791 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift @@ -13,7 +13,7 @@ public protocol POSOrderManagementServiceProtocol { /// - onCompletion: Completion handler with the result func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) } - +/// periphery: ignore public final class POSOrderManagementService: POSOrderManagementServiceProtocol { private let orderStoreMethods: OrderStoreMethodsProtocol From b259f516060eccb2bc79d0acfa80adca685d1edc Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 1 Oct 2025 10:06:01 +0700 Subject: [PATCH 11/14] handle clear order being async in tests --- .../POS/Mocks/MockPointOfSaleOrderController.swift | 13 +++++++++++++ .../POS/Models/PointOfSaleAggregateModelTests.swift | 2 ++ 2 files changed, 15 insertions(+) diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift index e6b48db153b..856ca4ad657 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift @@ -33,8 +33,21 @@ final class MockPointOfSaleOrderController: PointOfSaleOrderControllerProtocol { } var clearOrderWasCalled: Bool = false + private var clearOrderContinuation: CheckedContinuation? + func clearOrder() async { clearOrderWasCalled = true + await withCheckedContinuation { continuation in + clearOrderContinuation = continuation + // Resume immediately to simulate completion + continuation.resume() + } + } + + func waitForClearOrder() async { + while !clearOrderWasCalled { + try? await Task.sleep(nanoseconds: 1_000_000) + } } var sendReceiptErrorToThrow: Error? diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 4907c11771d..0c7e05a71c7 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -220,6 +220,7 @@ struct PointOfSaleAggregateModelTests { sut.startNewCart() // Then + await orderController.waitForClearOrder() #expect(orderController.clearOrderWasCalled == true) } @@ -329,6 +330,7 @@ struct PointOfSaleAggregateModelTests { sut.pointOfSaleClosed() // Then + await orderController.waitForClearOrder() #expect(orderController.clearOrderWasCalled == true) #expect(cardPresentPaymentService.cancelPaymentCalled == true) } From bfcb35970ad029e654d2bcb05f7490dee1cfa27c Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 3 Oct 2025 14:32:18 +0700 Subject: [PATCH 12/14] update tests to get rid of timeout --- .../MockPointOfSaleOrderController.swift | 14 ++--------- .../PointOfSaleAggregateModelTests.swift | 24 ++++++++++++------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift index 856ca4ad657..b79606a5945 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift @@ -33,21 +33,11 @@ final class MockPointOfSaleOrderController: PointOfSaleOrderControllerProtocol { } var clearOrderWasCalled: Bool = false - private var clearOrderContinuation: CheckedContinuation? + var onClearOrderCalled: () -> Void = {} func clearOrder() async { clearOrderWasCalled = true - await withCheckedContinuation { continuation in - clearOrderContinuation = continuation - // Resume immediately to simulate completion - continuation.resume() - } - } - - func waitForClearOrder() async { - while !clearOrderWasCalled { - try? await Task.sleep(nanoseconds: 1_000_000) - } + onClearOrderCalled() } var sendReceiptErrorToThrow: Error? diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 0c7e05a71c7..23ff8bc208d 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -213,14 +213,18 @@ struct PointOfSaleAggregateModelTests { cardPresentPaymentService: cardPresentPaymentService, orderController: orderController, ) - sut.addToCart(makePurchasableItem()) - // When - sut.startNewCart() + await withCheckedContinuation { continuation in + orderController.onClearOrderCalled = { + continuation.resume() + } + + // When + sut.startNewCart() + } // Then - await orderController.waitForClearOrder() #expect(orderController.clearOrderWasCalled == true) } @@ -323,14 +327,18 @@ struct PointOfSaleAggregateModelTests { itemsController: itemsController, cardPresentPaymentService: cardPresentPaymentService, orderController: orderController) - sut.addToCart(makePurchasableItem()) - // When - sut.pointOfSaleClosed() + await withCheckedContinuation { continuation in + orderController.onClearOrderCalled = { + continuation.resume() + } + + // When + sut.pointOfSaleClosed() + } // Then - await orderController.waitForClearOrder() #expect(orderController.clearOrderWasCalled == true) #expect(cardPresentPaymentService.cancelPaymentCalled == true) } From 040be92156189d2a4a3c92da285631ce71302a7e Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 3 Oct 2025 15:27:23 +0700 Subject: [PATCH 13/14] Move deleteOrder to existing POSOrderService --- .../Tools/POS/POSOrderManagementService.swift | 44 -------- .../Yosemite/Tools/POS/POSOrderService.swift | 20 +++- .../Mocks/MockOrderStoreMethods.swift | 22 ++++ .../Tools/POS/POSOrderServiceTests.swift | 5 +- .../Adaptors/POSServiceLocatorAdaptor.swift | 12 --- .../PointOfSaleOrderController.swift | 6 +- .../PointOfSaleEntryPointView.swift | 3 +- .../Protocols/POSDependencyProviding.swift | 2 - .../POS/TabBar/POSTabCoordinator.swift | 3 +- .../Classes/POS/Utils/PreviewHelpers.swift | 11 +- .../PointOfSaleOrderControllerTests.swift | 102 +++++------------- .../POS/Mocks/MockPOSOrderService.swift | 10 ++ 12 files changed, 88 insertions(+), 152 deletions(-) delete mode 100644 Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift create mode 100644 Modules/Tests/YosemiteTests/Mocks/MockOrderStoreMethods.swift diff --git a/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift b/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift deleted file mode 100644 index c459911e791..00000000000 --- a/Modules/Sources/Yosemite/Tools/POS/POSOrderManagementService.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -import Networking -import protocol Storage.StorageManagerType -import struct Combine.AnyPublisher -import struct NetworkingCore.JetpackSite - -public protocol POSOrderManagementServiceProtocol { - /// Deletes an order permanently or moves it to trash - /// - Parameters: - /// - siteID: The site ID where the order belongs - /// - order: The order to delete - /// - deletePermanently: Whether to delete permanently or move to trash - /// - onCompletion: Completion handler with the result - func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) -} -/// periphery: ignore -public final class POSOrderManagementService: POSOrderManagementServiceProtocol { - private let orderStoreMethods: OrderStoreMethodsProtocol - - public convenience init?(siteID: Int64, - credentials: Credentials?, - selectedSite: AnyPublisher, - appPasswordSupportState: AnyPublisher, - storageManager: StorageManagerType) { - guard let credentials else { - DDLogError("⛔️ Could not create POSOrderManagementService due to not finding credentials") - return nil - } - let network = AlamofireNetwork(credentials: credentials, - selectedSite: selectedSite, - appPasswordSupportState: appPasswordSupportState) - let remote = OrdersRemote(network: network) - self.init(storageManager: storageManager, remote: remote) - } - - public init(storageManager: StorageManagerType, remote: OrdersRemote) { - self.orderStoreMethods = OrderStoreMethods(storageManager: storageManager, remote: remote) - } - - // MARK: - Protocol conformance - public func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { - orderStoreMethods.deleteOrder(siteID: siteID, order: order, deletePermanently: deletePermanently, onCompletion: onCompletion) - } -} diff --git a/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift b/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift index 270e2436dbb..e49c1370310 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift @@ -4,6 +4,7 @@ import class WooFoundation.CurrencyFormatter import enum WooFoundation.CurrencyCode import struct Combine.AnyPublisher import struct NetworkingCore.JetpackSite +import protocol Storage.StorageManagerType public protocol POSOrderServiceProtocol { /// Syncs order based on the cart. @@ -13,16 +14,19 @@ public protocol POSOrderServiceProtocol { func syncOrder(cart: POSCart, currency: CurrencyCode) async throws -> Order func updatePOSOrder(orderID: Int64, recipientEmail: String) async throws func markOrderAsCompletedWithCashPayment(order: Order, changeDueAmount: String?) async throws + func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) } public final class POSOrderService: POSOrderServiceProtocol { private let siteID: Int64 private let ordersRemote: POSOrdersRemoteProtocol + private let orderStoreMethods: OrderStoreMethodsProtocol public convenience init?(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher, - appPasswordSupportState: AnyPublisher) { + appPasswordSupportState: AnyPublisher, + storageManager: StorageManagerType) { guard let credentials else { DDLogError("⛔️ Could not create POSOrderService due to not finding credentials") return nil @@ -30,14 +34,18 @@ public final class POSOrderService: POSOrderServiceProtocol { let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite, appPasswordSupportState: appPasswordSupportState) + let remote = OrdersRemote(network: network) self.init(siteID: siteID, - ordersRemote: OrdersRemote(network: network)) + ordersRemote: remote, + orderStoreMethods: OrderStoreMethods(storageManager: storageManager, remote: remote)) } - public init(siteID: Int64, - ordersRemote: POSOrdersRemoteProtocol) { + internal init(siteID: Int64, + ordersRemote: POSOrdersRemoteProtocol, + orderStoreMethods: OrderStoreMethodsProtocol) { self.siteID = siteID self.ordersRemote = ordersRemote + self.orderStoreMethods = orderStoreMethods } // MARK: - Protocol conformance @@ -81,6 +89,10 @@ public final class POSOrderService: POSOrderServiceProtocol { throw POSOrderServiceError.updateOrderFailed } } + + public func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { + orderStoreMethods.deleteOrder(siteID: siteID, order: order, deletePermanently: deletePermanently, onCompletion: onCompletion) + } } private extension Order { diff --git a/Modules/Tests/YosemiteTests/Mocks/MockOrderStoreMethods.swift b/Modules/Tests/YosemiteTests/Mocks/MockOrderStoreMethods.swift new file mode 100644 index 00000000000..e5b083d3713 --- /dev/null +++ b/Modules/Tests/YosemiteTests/Mocks/MockOrderStoreMethods.swift @@ -0,0 +1,22 @@ +import Foundation +@testable import Yosemite +import Networking + +final class MockOrderStoreMethods: OrderStoreMethodsProtocol { + var deleteOrderCalled = false + var deletedOrder: Order? + var deletedSiteID: Int64? + var deletedPermanently: Bool? + var deleteOrderResult: Result = .success(Order.fake()) + + func deleteOrder(siteID: Int64, + order: Order, + deletePermanently: Bool, + onCompletion: @escaping (Result) -> Void) { + deleteOrderCalled = true + deletedOrder = order + deletedSiteID = siteID + deletedPermanently = deletePermanently + onCompletion(deleteOrderResult) + } +} diff --git a/Modules/Tests/YosemiteTests/Tools/POS/POSOrderServiceTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSOrderServiceTests.swift index e7e09015375..ee041508b31 100644 --- a/Modules/Tests/YosemiteTests/Tools/POS/POSOrderServiceTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSOrderServiceTests.swift @@ -5,11 +5,14 @@ import Testing struct POSOrderServiceTests { let sut: POSOrderService let mockOrdersRemote: MockPOSOrdersRemote + let mockOrderStoreMethods: MockOrderStoreMethods init() { let mockOrdersRemote = MockPOSOrdersRemote() + let mockOrderStoreMethods = MockOrderStoreMethods() self.mockOrdersRemote = mockOrdersRemote - self.sut = POSOrderService(siteID: 123, ordersRemote: mockOrdersRemote) + self.mockOrderStoreMethods = mockOrderStoreMethods + self.sut = POSOrderService(siteID: 123, ordersRemote: mockOrdersRemote, orderStoreMethods: mockOrderStoreMethods) } @Test diff --git a/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift b/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift index 2a0f446a2e4..0caf3f2e3a8 100644 --- a/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift +++ b/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift @@ -30,10 +30,6 @@ final class POSServiceLocatorAdaptor: POSDependencyProviding { var externalViews: POSExternalViewProviding { POSExternalViewAdaptor() } - - var orderManagement: POSOrderManagementServiceProtocol { - POSOrderManagementServiceAdaptor() - } } // MARK: - Individual Service Adaptors @@ -123,11 +119,3 @@ private struct POSExternalViewAdaptor: POSExternalViewProviding { } } -private struct POSOrderManagementServiceAdaptor: POSOrderManagementServiceProtocol { - func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { - let action = OrderAction.deleteOrder(siteID: siteID, order: order, deletePermanently: deletePermanently, onCompletion: onCompletion) - Task { @MainActor in - ServiceLocator.stores.dispatch(action) - } - } -} diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift index 16baab45599..f73800adbff 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift @@ -20,7 +20,6 @@ import enum WooFoundation.CurrencyCode import protocol WooFoundation.Analytics import enum Alamofire.AFError import class Yosemite.OrderTotalsCalculator -import protocol Yosemite.POSOrderManagementServiceProtocol enum SyncOrderState { case newOrder @@ -46,13 +45,11 @@ protocol PointOfSaleOrderControllerProtocol { receiptSender: POSReceiptSending, currencySettingsProvider: POSCurrencySettingsProviding, analytics: POSAnalyticsProviding, - orderManagement: POSOrderManagementServiceProtocol, celebration: PaymentCaptureCelebrationProtocol = PaymentCaptureCelebration()) { self.orderService = orderService self.receiptSender = receiptSender self.currencySettingsProvider = currencySettingsProvider self.analytics = analytics - self.orderManagement = orderManagement self.celebration = celebration } @@ -61,7 +58,6 @@ protocol PointOfSaleOrderControllerProtocol { private let currencySettingsProvider: POSCurrencySettingsProviding private let celebration: PaymentCaptureCelebrationProtocol private let analytics: POSAnalyticsProviding - private let orderManagement: POSOrderManagementServiceProtocol private(set) var orderState: PointOfSaleInternalOrderState = .idle private var order: Order? = nil @@ -129,7 +125,7 @@ protocol PointOfSaleOrderControllerProtocol { await withCheckedContinuation { continuation in Task { @MainActor in - orderManagement.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in + orderService.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in continuation.resume() } } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index b61f3d655f0..c318bfd124b 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -93,8 +93,7 @@ public struct PointOfSaleEntryPointView: View { self.orderController = PointOfSaleOrderController(orderService: orderService, receiptSender: receiptSender, currencySettingsProvider: services.currency, - analytics: services.analytics, - orderManagement: services.orderManagement) + analytics: services.analytics) self.settingsController = PointOfSaleSettingsController(siteID: siteID, settingsService: settingsService, cardPresentPaymentService: cardPresentPaymentService, diff --git a/WooCommerce/Classes/POS/Protocols/POSDependencyProviding.swift b/WooCommerce/Classes/POS/Protocols/POSDependencyProviding.swift index 651db2083c0..d5428a429d3 100644 --- a/WooCommerce/Classes/POS/Protocols/POSDependencyProviding.swift +++ b/WooCommerce/Classes/POS/Protocols/POSDependencyProviding.swift @@ -5,7 +5,6 @@ import enum Experiments.FeatureFlag import struct Yosemite.Coupon import enum Yosemite.CouponDiscountType import enum Yosemite.POSItem -import protocol Yosemite.POSOrderManagementServiceProtocol /// POSDepenencyProviding is part of the POS entry point that defines the external dependencies from the Woo app that POS depends on @@ -73,5 +72,4 @@ public protocol POSDependencyProviding { var connectivity: POSConnectivityProviding { get } var externalNavigation: POSExternalNavigationProviding { get } var externalViews: POSExternalViewProviding { get } - var orderManagement: POSOrderManagementServiceProtocol { get } } diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index c1c67969475..e40a1082b14 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -146,7 +146,8 @@ private extension POSTabCoordinator { let orderService = POSOrderService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: isAppPasswordSupported), + appPasswordSupportState: isAppPasswordSupported, + storageManager: storageManager), #available(iOS 17.0, *) { let posView = PointOfSaleEntryPointView( siteID: siteID, diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index 90c2062a785..c6a8b8f6504 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -45,7 +45,6 @@ import protocol Yosemite.POSItemFetchAnalyticsTracking import protocol Yosemite.POSOrderListFetchStrategyFactoryProtocol import protocol Yosemite.POSOrderListFetchStrategy import protocol Yosemite.PointOfSaleCouponFetchStrategyFactoryProtocol -import protocol Yosemite.POSOrderManagementServiceProtocol // MARK: - PreviewProvider helpers // @@ -442,6 +441,7 @@ final class POSCollectOrderPaymentPreviewAnalytics: POSCollectOrderPaymentAnalyt } final class POSOrderServicePreview: POSOrderServiceProtocol { + func syncOrder(cart: POSCart, currency: CurrencyCode) async throws -> Order { .empty } @@ -449,6 +449,8 @@ final class POSOrderServicePreview: POSOrderServiceProtocol { func updatePOSOrder(orderID: Int64, recipientEmail: String) async throws {} func markOrderAsCompletedWithCashPayment(order: Yosemite.Order, changeDueAmount: String?) async throws {} + + func deleteOrder(siteID: Int64, order: Yosemite.Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) {} } final class POSReceiptServicePreview: POSReceiptServiceProtocol { @@ -538,13 +540,6 @@ final class POSPreviewServices: POSDependencyProviding { var connectivity: POSConnectivityProviding = EmptyPOSConnectivityProvider() var externalNavigation: POSExternalNavigationProviding = EmptyPOSExternalNavigation() var externalViews: POSExternalViewProviding = EmptyPOSExternalView() - var orderManagement: POSOrderManagementServiceProtocol = POSPreviewOrderManagementService() -} - -final class POSPreviewOrderManagementService: POSOrderManagementServiceProtocol { - func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { - onCompletion(.success(order)) - } } // MARK: - Preview Catalog Services diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index d0849b54f9d..c97b7c50702 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -13,21 +13,18 @@ import class WooFoundation.CurrencySettings import protocol WooFoundation.Analytics import enum Networking.DotcomError import enum Networking.NetworkError -import protocol Yosemite.POSOrderManagementServiceProtocol @MainActor struct PointOfSaleOrderControllerTests { let mockOrderService = MockPOSOrderService() let mockReceiptSender = MockPOSReceiptSender() - let mockOrderManagement = MockPOSOrderManagementService() @Test func syncOrder_without_items_doesnt_call_orderService() async throws { // Given let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) // When await sut.syncOrder(for: Cart(), retryHandler: {}) @@ -41,8 +38,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) let cartItem = makeItem(orderItemsToMatch: [orderItem]) @@ -63,8 +59,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) mockOrderService.simulateSyncing = true Task { await sut.syncOrder(for: Cart(purchasableItems: [makeItem(quantity: 1)]), retryHandler: {}) @@ -91,8 +86,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(currencySettings: currencySettings), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) // When await sut.syncOrder(for: Cart(purchasableItems: [makeItem()]), retryHandler: {}) @@ -106,8 +100,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let cartItem = makeItem(quantity: 1) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) @@ -129,8 +122,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let fakeOrder = Order.fake() mockOrderService.orderToReturn = fakeOrder var orderStates: [PointOfSaleInternalOrderState] = [sut.orderState] @@ -169,8 +161,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) mockOrderService.orderToReturn = nil var orderStates: [PointOfSaleInternalOrderState] = [sut.orderState] @@ -208,8 +199,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let email = "test@example.com" // When @@ -227,8 +217,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let order = Order.fake() let recipientEmail = "test@fake.com" mockOrderService.orderToReturn = order @@ -252,7 +241,6 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement, celebration: MockPaymentCaptureCelebration()) try await sut.collectCashPayment(changeDueAmount: nil) } catch let error as PointOfSaleOrderController.PointOfSaleOrderControllerError { @@ -269,7 +257,6 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement, celebration: mockPaymentCelebration) let orderItem = OrderItem.fake() @@ -292,7 +279,6 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement, celebration: MockPaymentCaptureCelebration()) let orderItem = OrderItem.fake() @@ -314,8 +300,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let fakeOrderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake() let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) @@ -337,8 +322,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let fakeOrder = Order.fake() mockOrderService.orderToReturn = fakeOrder @@ -362,8 +346,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let orderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake().copy(items: [orderItem]) let cartItem = makeItem(orderItemsToMatch: [orderItem]) @@ -388,8 +371,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let cartItem = makeItem(quantity: 1) // When @@ -409,8 +391,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let orderItem = OrderItem.fake().copy(quantity: 1) let couponCode = "SAVE10" let coupon = OrderCouponLine.fake().copy(code: couponCode) @@ -435,8 +416,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let orderItem = OrderItem.fake().copy(quantity: 1) let initialCouponCode = "SAVE10" let initialCoupon = OrderCouponLine.fake().copy(code: initialCouponCode) @@ -461,8 +441,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let orderItem = OrderItem.fake().copy(quantity: 1) let couponCode = "SAVE10" let coupon = OrderCouponLine.fake().copy(code: couponCode) @@ -487,8 +466,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let errorMessage = "Invalid coupon code" mockOrderService.errorToReturn = DotcomError.unknown(code: "woocommerce_rest_invalid_coupon", message: errorMessage) @@ -529,8 +507,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let errorMessage = "Coupon INVALID does not exist" let errorJSON = """ { @@ -578,8 +555,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) // First create a successful order let orderItem = OrderItem.fake().copy(quantity: 1) @@ -626,14 +602,13 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) // When await sut.clearOrder() // Then - #expect(mockOrderManagement.deleteOrderWasCalled == false) + #expect(mockOrderService.deleteOrderWasCalled == false) #expect(sut.orderState == .idle) } @@ -643,8 +618,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let fakeOrderItem = OrderItem.fake().copy(quantity: 1) let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) @@ -657,9 +631,9 @@ struct PointOfSaleOrderControllerTests { await sut.clearOrder() // Then - #expect(mockOrderManagement.deleteOrderWasCalled == true) - #expect(mockOrderManagement.deletedOrder?.orderID == autoDraftOrder.orderID) - #expect(mockOrderManagement.deletedOrder?.status == .autoDraft) + #expect(mockOrderService.deleteOrderWasCalled == true) + #expect(mockOrderService.deletedOrder?.orderID == autoDraftOrder.orderID) + #expect(mockOrderService.deletedOrder?.status == .autoDraft) #expect(sut.orderState == .idle) } @@ -669,8 +643,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: mockOrderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: MockPOSAnalytics(), - orderManagement: mockOrderManagement) + analytics: MockPOSAnalytics()) let completedOrder = Order.fake().copy(status: .completed) mockOrderService.orderToReturn = completedOrder @@ -680,7 +653,7 @@ struct PointOfSaleOrderControllerTests { await sut.clearOrder() // Then - #expect(mockOrderManagement.deleteOrderWasCalled == false) + #expect(mockOrderService.deleteOrderWasCalled == false) #expect(sut.orderState == .idle) } @@ -696,8 +669,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: orderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: analytics, - orderManagement: MockPOSOrderManagementService()) + analytics: analytics) let fakeOrderItem = OrderItem.fake().copy(quantity: 1) let fakeOrder = Order.fake() let fakeCartItem = makeItem(orderItemsToMatch: [fakeOrderItem]) @@ -715,8 +687,7 @@ struct PointOfSaleOrderControllerTests { let sut = PointOfSaleOrderController(orderService: orderService, receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), - analytics: analytics, - orderManagement: MockPOSOrderManagementService()) + analytics: analytics) orderService.orderToReturn = nil // When @@ -733,7 +704,6 @@ struct PointOfSaleOrderControllerTests { receiptSender: mockReceiptSender, currencySettingsProvider: MockCurrencySettingsProvider(), analytics: analytics, - orderManagement: MockPOSOrderManagementService(), celebration: MockPaymentCaptureCelebration()) // In order to test the order controller failure we need to succeed first in creating a successful order: @@ -770,20 +740,6 @@ private func makeItem(name: String = "", quantity: quantity) } -// MARK: - Mock POS Order Management Service - -final class MockPOSOrderManagementService: POSOrderManagementServiceProtocol { - var deleteOrderWasCalled = false - var deletedOrder: Order? - var deleteOrderResult: Result = .success(Order.fake()) - - func deleteOrder(siteID: Int64, order: Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { - deleteOrderWasCalled = true - deletedOrder = order - onCompletion(deleteOrderResult) - } -} - // MARK: - Mock Currency Settings Provider final class MockCurrencySettingsProvider: POSCurrencySettingsProviding { diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderService.swift index 6c33349ebed..627f08635cb 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderService.swift @@ -14,6 +14,10 @@ class MockPOSOrderService: POSOrderServiceProtocol { var spySyncOrderCurrency: CurrencyCode? var spyCashPaymentChangeDueAmount: String? + var deleteOrderWasCalled = false + var deletedOrder: Order? + var deleteOrderResult: Result = .success(Order.fake()) + func syncOrder(cart: Yosemite.POSCart, currency: CurrencyCode) async throws -> Yosemite.Order { syncOrderWasCalled = true @@ -60,6 +64,12 @@ class MockPOSOrderService: POSOrderServiceProtocol { throw error } } + + func deleteOrder(siteID: Int64, order: Yosemite.Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { + deleteOrderWasCalled = true + deletedOrder = order + onCompletion(deleteOrderResult) + } } enum MockPOSOrderServiceError: Error { From dcc23c634857a76646e3cd3a9c90118008c45643 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 3 Oct 2025 15:28:13 +0700 Subject: [PATCH 14/14] lint --- .../Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift | 1 - WooCommerce/Classes/POS/Utils/PreviewHelpers.swift | 3 +-- .../WooCommerceTests/POS/Mocks/MockPOSOrderService.swift | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift b/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift index 0caf3f2e3a8..a0bd64e3f74 100644 --- a/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift +++ b/WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift @@ -118,4 +118,3 @@ private struct POSExternalViewAdaptor: POSExternalViewProviding { )) } } - diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index c6a8b8f6504..e8f7802ed63 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -441,7 +441,6 @@ final class POSCollectOrderPaymentPreviewAnalytics: POSCollectOrderPaymentAnalyt } final class POSOrderServicePreview: POSOrderServiceProtocol { - func syncOrder(cart: POSCart, currency: CurrencyCode) async throws -> Order { .empty } @@ -449,7 +448,7 @@ final class POSOrderServicePreview: POSOrderServiceProtocol { func updatePOSOrder(orderID: Int64, recipientEmail: String) async throws {} func markOrderAsCompletedWithCashPayment(order: Yosemite.Order, changeDueAmount: String?) async throws {} - + func deleteOrder(siteID: Int64, order: Yosemite.Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) {} } diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderService.swift index 627f08635cb..012b7c8b540 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderService.swift @@ -64,7 +64,7 @@ class MockPOSOrderService: POSOrderServiceProtocol { throw error } } - + func deleteOrder(siteID: Int64, order: Yosemite.Order, deletePermanently: Bool, onCompletion: @escaping (Result) -> Void) { deleteOrderWasCalled = true deletedOrder = order