From 51e7e97d3cbbeda6b4f82b493ce66b460292e034 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:11:46 +0300 Subject: [PATCH 01/18] Add loadPOSOrders to OrdersRemote --- .../Sources/NetworkingCore/Remote/OrdersRemote.swift | 10 ++++++++++ .../Remote/POSOrdersRemoteProtocol.swift | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift b/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift index 142a622178e..391e681e2ee 100644 --- a/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift +++ b/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift @@ -450,6 +450,16 @@ extension OrdersRemote: POSOrdersRemoteProtocol { } } } + + public func loadPOSOrders(siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> PagedItems { + let orders = try await loadAllOrders(for: siteID, + createdVia: "pos-rest-api", + pageNumber: pageNumber, + pageSize: pageSize) + + let hasMorePages = orders.count == pageSize + return PagedItems(items: orders, hasMorePages: hasMorePages, totalItems: nil) + } } diff --git a/Modules/Sources/NetworkingCore/Remote/POSOrdersRemoteProtocol.swift b/Modules/Sources/NetworkingCore/Remote/POSOrdersRemoteProtocol.swift index a0f16ef522f..60e7f8dff5e 100644 --- a/Modules/Sources/NetworkingCore/Remote/POSOrdersRemoteProtocol.swift +++ b/Modules/Sources/NetworkingCore/Remote/POSOrdersRemoteProtocol.swift @@ -14,4 +14,8 @@ public protocol POSOrdersRemoteProtocol { func createPOSOrder(siteID: Int64, order: Order, fields: [OrdersRemote.CreateOrderField]) async throws -> Order + + func loadPOSOrders(siteID: Int64, + pageNumber: Int, + pageSize: Int) async throws -> PagedItems } From 7b82d7efe4da64197f34c0a24c02ced0f2c87f5a Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:12:51 +0300 Subject: [PATCH 02/18] Create PointOfSaleOrderService for providing orders on POS --- .../Orders/PointOfSaleOrderService.swift | 35 +++++ .../PointOfSaleOrderServiceProtocol.swift | 13 ++ .../Mocks/MockPOSOrdersRemote.swift | 20 +++ .../PointOfSaleOrderServiceTests.swift | 146 ++++++++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift create mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift create mode 100644 Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift new file mode 100644 index 00000000000..69e60fc5cd7 --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift @@ -0,0 +1,35 @@ +import Foundation +import enum Alamofire.AFError +import struct NetworkingCore.PagedItems +import struct NetworkingCore.Order +import protocol NetworkingCore.POSOrdersRemoteProtocol + +public final class PointOfSaleOrderService: PointOfSaleOrderServiceProtocol { + private let ordersRemote: POSOrdersRemoteProtocol + private let siteID: Int64 + + public init(siteID: Int64, ordersRemote: POSOrdersRemoteProtocol) { + self.siteID = siteID + self.ordersRemote = ordersRemote + } + + public func providePointOfSaleOrders(pageNumber: Int = 1) async throws -> PagedItems { + do { + let pagedOrders = try await ordersRemote.loadPOSOrders( + siteID: siteID, + pageNumber: pageNumber, + pageSize: 25 + ) + + if pageNumber != 1 && pagedOrders.items.count == 0 { + return .init(items: [], hasMorePages: false, totalItems: 0) + } + + return pagedOrders + } catch AFError.explicitlyCancelled { + throw PointOfSaleOrderServiceError.requestCancelled + } catch { + throw PointOfSaleOrderServiceError.requestFailed + } + } +} diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift new file mode 100644 index 00000000000..07ed3d82d37 --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift @@ -0,0 +1,13 @@ +import Foundation +import struct NetworkingCore.PagedItems +import struct NetworkingCore.Order + +public enum PointOfSaleOrderServiceError: Error, Equatable { + case requestFailed + case requestCancelled + case unknown +} + +public protocol PointOfSaleOrderServiceProtocol { + func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems +} diff --git a/Modules/Tests/YosemiteTests/Mocks/MockPOSOrdersRemote.swift b/Modules/Tests/YosemiteTests/Mocks/MockPOSOrdersRemote.swift index 0170cb4ddc2..64927dddf4b 100644 --- a/Modules/Tests/YosemiteTests/Mocks/MockPOSOrdersRemote.swift +++ b/Modules/Tests/YosemiteTests/Mocks/MockPOSOrdersRemote.swift @@ -33,4 +33,24 @@ final class MockPOSOrdersRemote: POSOrdersRemoteProtocol { spyCreatePOSOrderFields = fields return Order.fake() } + + var mockPagedOrdersResult: Result, Error> = .success(PagedItems(items: [], hasMorePages: false, totalItems: 0)) + var loadPOSOrdersCalled = false + var spySiteID: Int64? + var spyPageNumber: Int? + var spyPageSize: Int? + + func loadPOSOrders(siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> PagedItems { + loadPOSOrdersCalled = true + spySiteID = siteID + spyPageNumber = pageNumber + spyPageSize = pageSize + + switch mockPagedOrdersResult { + case .success(let pagedOrders): + return pagedOrders + case .failure(let error): + throw error + } + } } diff --git a/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift b/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift new file mode 100644 index 00000000000..14c71b8abb9 --- /dev/null +++ b/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift @@ -0,0 +1,146 @@ +import XCTest +@testable import Yosemite +import struct NetworkingCore.PagedItems +import struct NetworkingCore.Order +import enum NetworkingCore.OrderStatusEnum + +final class PointOfSaleOrderServiceTests: XCTestCase { + private let siteID: Int64 = 13092 + private var orderProvider: PointOfSaleOrderServiceProtocol! + private var mockOrdersRemote: MockPOSOrdersRemote! + + override func setUp() { + super.setUp() + mockOrdersRemote = MockPOSOrdersRemote() + orderProvider = PointOfSaleOrderService(siteID: siteID, ordersRemote: mockOrdersRemote) + } + + override func tearDown() { + orderProvider = nil + mockOrdersRemote = nil + super.tearDown() + } + + func test_PointOfSaleOrderServiceProtocol_when_fails_request_with_requestFailed_then_throws_error() async throws { + let expectedError = PointOfSaleOrderServiceError.requestFailed + mockOrdersRemote.mockPagedOrdersResult = .failure(expectedError) + + do { + _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) + XCTFail("Expected an error, but got success.") + } catch { + XCTAssertEqual(error as? PointOfSaleOrderServiceError, expectedError) + } + } + + func test_PointOfSaleOrderServiceProtocol_when_empty_data_for_non_first_page_of_orders_then_returns_empty_orders_and_no_next_page() async throws { + mockOrdersRemote.mockPagedOrdersResult = .success(PagedItems(items: [], hasMorePages: false, totalItems: 0)) + + let pagedOrders = try await orderProvider.providePointOfSaleOrders(pageNumber: 2) + + XCTAssertTrue(pagedOrders.items.isEmpty) + XCTAssertFalse(pagedOrders.hasMorePages) + } + + func test_PointOfSaleOrderServiceProtocol_provides_no_orders_when_store_has_no_orders() async throws { + mockOrdersRemote.mockPagedOrdersResult = .success(PagedItems(items: [], hasMorePages: false, totalItems: 0)) + + let pagedOrders = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) + + XCTAssertTrue(pagedOrders.items.isEmpty) + XCTAssertTrue(mockOrdersRemote.loadPOSOrdersCalled) + XCTAssertEqual(mockOrdersRemote.spyPageNumber, 1) + XCTAssertEqual(mockOrdersRemote.spyPageSize, 25) + } + + func test_PointOfSaleOrderServiceProtocol_provides_orders_when_store_has_orders() async throws { + let mockOrder = Order.fake().copy( + orderID: 1001, + number: "1001", + status: .completed, + total: "25.99" + ) + mockOrdersRemote.mockPagedOrdersResult = .success(PagedItems(items: [mockOrder], + hasMorePages: false, + totalItems: 1)) + + let pagedOrders = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) + + XCTAssertEqual(pagedOrders.items.count, 1) + XCTAssertTrue(mockOrdersRemote.loadPOSOrdersCalled) + XCTAssertEqual(mockOrdersRemote.spyPageNumber, 1) + XCTAssertEqual(mockOrdersRemote.spyPageSize, 25) + XCTAssertEqual(pagedOrders.items.first?.orderID, 1001) + } + + func test_PointOfSaleOrderServiceProtocol_returns_correct_pagination_when_more_pages_available() async throws { + let mockOrders = [ + Order.fake().copy(orderID: 1001, number: "1001", status: .completed, total: "25.99"), + Order.fake().copy(orderID: 1002, number: "1002", status: .completed, total: "15.50") + ] + mockOrdersRemote.mockPagedOrdersResult = .success(PagedItems(items: mockOrders, + hasMorePages: true, + totalItems: 10)) + + let pagedOrders = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) + + XCTAssertEqual(pagedOrders.items.count, 2) + XCTAssertTrue(pagedOrders.hasMorePages) + XCTAssertEqual(pagedOrders.totalItems, 10) + XCTAssertTrue(mockOrdersRemote.loadPOSOrdersCalled) + } + + func test_providePointOfSaleOrders_uses_passed_page_number() async throws { + mockOrdersRemote.mockPagedOrdersResult = .success(PagedItems(items: [], hasMorePages: false, totalItems: 0)) + + _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 5) + + XCTAssertTrue(mockOrdersRemote.loadPOSOrdersCalled) + XCTAssertEqual(mockOrdersRemote.spyPageNumber, 5) + XCTAssertEqual(mockOrdersRemote.spyPageSize, 25) + } + + func test_providePointOfSaleOrders_handles_fetch_error() async { + mockOrdersRemote.mockPagedOrdersResult = .failure(TestError.expectedError) + + do { + _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) + XCTFail("Expected error to be thrown") + } catch PointOfSaleOrderServiceError.requestFailed { + // Expected + } catch { + XCTFail("Unexpected error occurred: \(error)") + } + } + + func test_providePointOfSaleOrders_handles_empty_results() async throws { + mockOrdersRemote.mockPagedOrdersResult = .success(PagedItems(items: [], hasMorePages: false, totalItems: 0)) + + let pagedOrders = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) + + XCTAssertEqual(pagedOrders.items.count, 0) + XCTAssertTrue(mockOrdersRemote.loadPOSOrdersCalled) + } + + func test_providePointOfSaleOrders_passes_correct_site_id() async throws { + mockOrdersRemote.mockPagedOrdersResult = .success(PagedItems(items: [], hasMorePages: false, totalItems: 0)) + + _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) + + XCTAssertEqual(mockOrdersRemote.spySiteID, siteID) + } + + func test_providePointOfSaleOrders_uses_default_page_size() async throws { + mockOrdersRemote.mockPagedOrdersResult = .success(PagedItems(items: [], hasMorePages: false, totalItems: 0)) + + _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 0) + + XCTAssertEqual(mockOrdersRemote.spyPageSize, 25) + } +} + +private extension PointOfSaleOrderServiceTests { + enum TestError: Error { + case expectedError + } +} From aa2642f7ac64a52b516f660348b124a05b8f40ca Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:14:30 +0300 Subject: [PATCH 03/18] Create PointOfSaleOrdersController for managing loading, refresh, and pagination of POS orders --- .../PointOfSaleOrdersController.swift | 110 +++++++++ .../Classes/POS/Models/OrdersViewState.swift | 35 +++ .../POS/Models/PointOfSaleErrorState.swift | 29 +++ .../WooCommerce.xcodeproj/project.pbxproj | 16 ++ .../PointOfSaleOrdersControllerTests.swift | 217 ++++++++++++++++++ .../Mocks/MockPointOfSaleOrderService.swift | 97 ++++++++ 6 files changed, 504 insertions(+) create mode 100644 WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift create mode 100644 WooCommerce/Classes/POS/Models/OrdersViewState.swift create mode 100644 WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift create mode 100644 WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift new file mode 100644 index 00000000000..138211ffe98 --- /dev/null +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift @@ -0,0 +1,110 @@ +import Foundation +import Observation +import enum Yosemite.PointOfSaleOrderServiceError +import protocol Yosemite.PointOfSaleOrderServiceProtocol +import struct NetworkingCore.Order +import class Yosemite.Store + +protocol PointOfSaleOrdersControllerProtocol { + var ordersViewState: OrdersViewState { get } + func loadOrders() async + func refreshOrders() async + func loadNextOrders() async +} + +@Observable final class PointOfSaleOrdersController: PointOfSaleOrdersControllerProtocol { + var ordersViewState: OrdersViewState + private let paginationTracker: AsyncPaginationTracker + private var orderProvider: PointOfSaleOrderServiceProtocol + + init(orderProvider: PointOfSaleOrderServiceProtocol, + initialState: OrdersViewState = OrdersViewState(containerState: .loading, + ordersState: .loading([]))) { + self.orderProvider = orderProvider + self.ordersViewState = initialState + self.paginationTracker = .init() + } + + @MainActor + func loadOrders() async { + setLoadingState() + await loadFirstPage() + } + + @MainActor + func refreshOrders() async { + await loadFirstPage() + } + + @MainActor + func loadNextOrders() async { + guard paginationTracker.hasNextPage else { + return + } + let currentOrders = ordersViewState.ordersState.orders + ordersViewState.containerState = .content + ordersViewState.ordersState = .loading(currentOrders) + do { + _ = try await paginationTracker.ensureNextPageIsSynced { [weak self] pageNumber in + guard let self else { return true } + return try await fetchOrders(pageNumber: pageNumber) + } + } catch { + ordersViewState.containerState = .content + ordersViewState.ordersState = .inlineError(currentOrders, + error: .errorOnLoadingOrdersNextPage(error: error), + context: OrderListState.InlineErrorContext.pagination) + } + } + + @MainActor + private func loadFirstPage() async { + do { + try await paginationTracker.resync { [weak self] pageNumber in + guard let self else { return true } + return try await fetchOrders(pageNumber: pageNumber, appendToExistingOrders: false) + } + } catch { + let orders = ordersViewState.ordersState.orders + if orders.isEmpty { + ordersViewState = OrdersViewState(containerState: .content, ordersState: .error(.errorOnLoadingOrders(error: error))) + } else { + ordersViewState = OrdersViewState(containerState: .content, + ordersState: .inlineError(orders, error: .errorOnLoadingOrders(error: error), context: OrderListState.InlineErrorContext.refresh)) + } + } + } + + private func setLoadingState() { + let orders = ordersViewState.ordersState.orders + let isInitialState = ordersViewState.containerState == .loading + if !isInitialState { + ordersViewState.ordersState = .loading(orders) + } + } + + @MainActor + private func fetchOrders(pageNumber: Int, appendToExistingOrders: Bool = true) async throws -> Bool { + do { + let pagedOrders = try await orderProvider.providePointOfSaleOrders(pageNumber: pageNumber) + + let newOrders = pagedOrders.items + var allOrders = appendToExistingOrders ? ordersViewState.ordersState.orders : [] + let uniqueNewOrders = newOrders.filter { newOrder in + !allOrders.contains(where: { $0.orderID == newOrder.orderID }) + } + allOrders.append(contentsOf: uniqueNewOrders) + + if allOrders.isEmpty { + ordersViewState.containerState = .content + ordersViewState.ordersState = .empty + } else { + ordersViewState.containerState = .content + ordersViewState.ordersState = .loaded(allOrders, hasMoreItems: pagedOrders.hasMorePages) + } + return pagedOrders.hasMorePages + } catch PointOfSaleOrderServiceError.requestCancelled { + return true + } + } +} diff --git a/WooCommerce/Classes/POS/Models/OrdersViewState.swift b/WooCommerce/Classes/POS/Models/OrdersViewState.swift new file mode 100644 index 00000000000..78259710ec2 --- /dev/null +++ b/WooCommerce/Classes/POS/Models/OrdersViewState.swift @@ -0,0 +1,35 @@ +import Foundation +import struct NetworkingCore.Order + +struct OrdersViewState: Equatable { + var containerState: ItemsContainerState + var ordersState: OrderListState +} + +enum OrderListState: Equatable { + case loading([Order]) + case loaded([Order], hasMoreItems: Bool) + case empty + case error(PointOfSaleErrorState) + case inlineError([Order], error: PointOfSaleErrorState, context: InlineErrorContext) + + enum InlineErrorContext { + case refresh + case pagination + } + + var orders: [Order] { + switch self { + case .loading(let orders): + return orders + case .loaded(let orders, _): + return orders + case .empty: + return [] + case .error: + return [] + case .inlineError(let orders, _, _): + return orders + } + } +} diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleErrorState.swift b/WooCommerce/Classes/POS/Models/PointOfSaleErrorState.swift index 04d72b4df6b..7d4c7a3d352 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleErrorState.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleErrorState.swift @@ -12,6 +12,8 @@ struct PointOfSaleErrorState: Equatable { case couponsDisabled case couponsNextPageError case couponsRefreshError + case ordersLoadError + case ordersNextPageError } let errorType: ErrorType @@ -91,6 +93,22 @@ struct PointOfSaleErrorState: Equatable { buttonText: Constants.retryButtonTitle) } + static func errorOnLoadingOrders(error: Error? = nil) -> Self { + PointOfSaleErrorState( + errorType: .ordersLoadError, + title: Constants.failedToLoadOrdersTitle, + subtitle: subtitle(for: error), + buttonText: Constants.retryButtonTitle) + } + + static func errorOnLoadingOrdersNextPage(error: Error? = nil) -> Self { + PointOfSaleErrorState( + errorType: .ordersNextPageError, + title: Constants.failedToLoadOrdersNextPageTitle, + subtitle: subtitle(for: error), + buttonText: Constants.retryButtonTitle) + } + private static func subtitle(for error: Error?) -> String { if let error, error.isConnectivityError { return Constants.connectivityErrorSubtitle @@ -173,5 +191,16 @@ struct PointOfSaleErrorState: Equatable { value: "Please check your internet connection and try again.", comment: "Subtitle appearing on error screens when there is a network connectivity error." ) + static let failedToLoadOrdersTitle = NSLocalizedString( + "pos.orderList.failedToLoadOrdersTitle", + value: "Unable to load orders", + comment: "Text appearing on the order list screen when there's an error loading orders." + ) + static let failedToLoadOrdersNextPageTitle = NSLocalizedString( + "pos.orderList.failedToLoadOrdersNextPageTitle", + value: "Unable to load more orders", + comment: "Text appearing on the order list screen when there's an error loading a page of orders after " + + "the first. Shown inline with the previously loaded orders above." + ) } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index f3d7a142ae6..2ad5e875526 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -31,6 +31,10 @@ 011D7A352CEC87B70007C187 /* CardPresentModalErrorEmailSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011D7A342CEC87B60007C187 /* CardPresentModalErrorEmailSent.swift */; }; 011DF3442C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DF3432C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift */; }; 011DF3462C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DF3452C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift */; }; + 012ACB742E5C830500A49458 /* PointOfSaleOrdersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB732E5C830500A49458 /* PointOfSaleOrdersController.swift */; }; + 012ACB762E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB752E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift */; }; + 012ACB782E5C84A200A49458 /* OrdersViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB772E5C84A200A49458 /* OrdersViewState.swift */; }; + 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */; }; 01309A7F2DC4F39E00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */; }; 01309A812DC4F45300B77527 /* CardPresentModalCardInserted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */; }; 01309A832DC4F89D00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */; }; @@ -3225,6 +3229,10 @@ 011D7A342CEC87B60007C187 /* CardPresentModalErrorEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalErrorEmailSent.swift; sourceTree = ""; }; 011DF3432C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift; sourceTree = ""; }; 011DF3452C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift; sourceTree = ""; }; + 012ACB732E5C830500A49458 /* PointOfSaleOrdersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersController.swift; sourceTree = ""; }; + 012ACB752E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersControllerTests.swift; sourceTree = ""; }; + 012ACB772E5C84A200A49458 /* OrdersViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersViewState.swift; sourceTree = ""; }; + 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderService.swift; sourceTree = ""; }; 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift; sourceTree = ""; }; 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalCardInserted.swift; sourceTree = ""; }; 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageView.swift; sourceTree = ""; }; @@ -7749,6 +7757,7 @@ 02CD3BFC2C35D01600E575C4 /* Mocks */ = { isa = PBXGroup; children = ( + 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */, 01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */, 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */, 686A71B72DC9EB6D0006E835 /* MockPOSSearchHistoryService.swift */, @@ -8172,6 +8181,7 @@ 68B681152D92577F0098D5CD /* PointOfSaleCouponsController.swift */, 200BA1582CF092280006DC5B /* PointOfSaleItemsController.swift */, 20CF75B92CF4E69000ACCF4A /* PointOfSaleOrderController.swift */, + 012ACB732E5C830500A49458 /* PointOfSaleOrdersController.swift */, ); path = Controllers; sourceTree = ""; @@ -8183,6 +8193,7 @@ 20DB185C2CF5E7560018D3E1 /* PointOfSaleOrderControllerTests.swift */, 200BA15D2CF0A9EB0006DC5B /* PointOfSaleItemsControllerTests.swift */, 02E4F26F2E0F2C75003A31E7 /* POSEntryPointControllerTests.swift */, + 012ACB752E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift */, ); path = Controllers; sourceTree = ""; @@ -9980,6 +9991,7 @@ 204415902CE622BA0070BF54 /* PointOfSaleOrderTotals.swift */, 209B7A672CEB6742003BDEF0 /* PointOfSalePaymentState.swift */, 209566242D4CF00100977124 /* PointOfSalePaymentMethod.swift */, + 012ACB772E5C84A200A49458 /* OrdersViewState.swift */, ); path = Models; sourceTree = ""; @@ -15192,6 +15204,7 @@ 02D681AB29C3F8AC00348510 /* StoreOnboardingPaymentsSetupCoordinator.swift in Sources */, B59D49CD219B587E006BF0AD /* UILabel+OrderStatus.swift in Sources */, 265BCA0C2430E741004E53EE /* ProductCategoryTableViewCell.swift in Sources */, + 012ACB742E5C830500A49458 /* PointOfSaleOrdersController.swift in Sources */, 02ACD25A2852E11700EC928E /* CloseAccountCoordinator.swift in Sources */, 0191301B2CF4E782008C0C88 /* TapToPayEducationStepViewModel.swift in Sources */, 0258D9492B68E7FE00D280D0 /* ProductsSplitViewWrapperController.swift in Sources */, @@ -15710,6 +15723,7 @@ B58B4AC02108FF6100076FDD /* Array+Helpers.swift in Sources */, B90C65CD29ACE2D6004CAB9E /* CardPresentPaymentOnboardingStateCache.swift in Sources */, 028AFFB32484ED2800693C09 /* Dictionary+Logging.swift in Sources */, + 012ACB782E5C84A200A49458 /* OrdersViewState.swift in Sources */, 026CAF802AC2B7FF002D23BB /* ConfigurableBundleProductView.swift in Sources */, 45BBFBC1274FD94300213001 /* HubMenuCoordinator.swift in Sources */, 01E62EC82DFADF56003A6D9E /* Cart+BarcodeScanError.swift in Sources */, @@ -17175,6 +17189,7 @@ DEF657AA2C8AC25C00ACD61E /* BlazeCampaignObjectivePickerViewModelTests.swift in Sources */, EE2EDFE12987A189004E702B /* MockABTestVariationProvider.swift in Sources */, 0273707E24C0047800167204 /* SequenceHelpersTests.swift in Sources */, + 012ACB762E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift in Sources */, DE9A02A32A44441200193ABF /* RequirementsCheckerTests.swift in Sources */, D802547326551D0F001B2CC1 /* CardPresentModalTapCardTests.swift in Sources */, B55BC1F321A8790F0011A0C0 /* StringHTMLTests.swift in Sources */, @@ -17619,6 +17634,7 @@ 02A275C023FE58F6005C560F /* MockImageCache.swift in Sources */, EE6C6B6E2C65DC4100632BDA /* WordPressMediaLibraryPickerDataSourceTests.swift in Sources */, 20BCF6F02B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift in Sources */, + 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderService.swift in Sources */, 261AA30E275506DE009530FE /* PaymentMethodsViewModelTests.swift in Sources */, 4524CDA1242D045C00B2F20A /* ProductStatusSettingListSelectorCommandTests.swift in Sources */, 26A280D72B46027A00ACEE87 /* OrderNotificationView.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift new file mode 100644 index 00000000000..1dac12099ef --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift @@ -0,0 +1,217 @@ +import Testing +import Foundation +@testable import WooCommerce +import enum Yosemite.PointOfSaleOrderServiceError +import struct NetworkingCore.Order +import Observation + +final class PointOfSaleOrdersControllerTests { + @Test func loadOrders_requests_first_page_after_loading_two_pages() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + try #require(sut.ordersViewState.containerState == .loading) + orderProvider.shouldSimulateTwoPages = true + await sut.loadOrders() + + await sut.loadNextOrders() + try #require(orderProvider.spyLastRequestedPageNumber == 2) + + await sut.loadOrders() + + #expect(orderProvider.spyLastRequestedPageNumber == 1) + } + + @Test func loadOrders_results_in_loaded_state() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() + orderProvider.orderPages = [expectedOrders] + try #require(sut.ordersViewState.containerState == .loading) + + await sut.loadOrders() + + #expect(sut.ordersViewState == OrdersViewState(containerState: .content, + ordersState: .loaded(expectedOrders, hasMoreItems: false))) + } + + @Test func loadOrders_with_more_pages_sets_hasMoreItems() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() + try #require(sut.ordersViewState.containerState == .loading) + orderProvider.shouldSimulateTwoPages = true + + await sut.loadOrders() + + #expect(sut.ordersViewState == OrdersViewState(containerState: .content, + ordersState: .loaded(expectedOrders, hasMoreItems: true))) + } + + @Test func loadOrders_when_called_multiple_times_then_orders_are_not_duplicated() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + try #require(sut.ordersViewState.containerState == .loading) + let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() + orderProvider.orderPages = [expectedOrders] + + await sut.loadOrders() + await sut.loadOrders() + await sut.loadOrders() + + guard case .loaded(let orders, _) = sut.ordersViewState.ordersState else { + Issue.record("Expected loaded OrderList state, but got \(sut.ordersViewState)") + return + } + #expect(orders.count == expectedOrders.count) + } + + @Test func container_state_starts_as_loading() { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + #expect(sut.ordersViewState.containerState == .loading) + } + + @Test func loadNextOrders_when_initial_orders_empty_then_container_state_is_content_and_orders_state_is_empty() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + orderProvider.shouldReturnZeroOrders = true + + try #require(sut.ordersViewState.containerState == .loading) + + await sut.loadNextOrders() + + #expect(sut.ordersViewState.containerState == .content) + #expect(sut.ordersViewState.ordersState == .empty) + } + + @Test func loadOrders_when_initial_orders_has_orders_but_no_more_pages_then_state_is_loaded_with_initial_orders() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() + orderProvider.orderPages = [initialOrders] + + try #require(sut.ordersViewState.containerState == .loading) + + await sut.loadNextOrders() + + #expect(sut.ordersViewState == OrdersViewState(containerState: .content, + ordersState: .loaded(initialOrders, hasMoreItems: false))) + } + + @Test func loadNextOrders_when_simulateFetchNextPage_then_state_is_loaded_with_expected_orders() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + orderProvider.shouldSimulateTwoPages = true + await sut.loadOrders() + + await sut.loadNextOrders() + + guard case .loaded(let orders, _) = sut.ordersViewState.ordersState else { + Issue.record("Expected loaded OrderList state, but got \(sut.ordersViewState)") + return + } + #expect(orders.count == 4) + } + + @Test func loadNextOrders_requests_second_page() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + try #require(sut.ordersViewState.containerState == .loading) + orderProvider.shouldSimulateTwoPages = true + await sut.loadOrders() + + await sut.loadNextOrders() + + #expect(orderProvider.spyLastRequestedPageNumber == 2) + } + + @Test func loadNextOrders_when_simulateFetchNextPage_then_state_is_loaded_with_hasMoreItems() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + orderProvider.shouldSimulateTwoPages = true + orderProvider.shouldSimulateThreePages = true + await sut.loadOrders() + + await sut.loadNextOrders() + + guard case .loaded(let orders, let hasMoreItems) = sut.ordersViewState.ordersState else { + Issue.record("Expected loaded OrderList state, but got \(sut.ordersViewState)") + return + } + #expect(orders.count == 4) + #expect(hasMoreItems == true) + } + + @Test func loadNextOrders_when_hasNextPage_is_false_then_does_not_fetch_next_page() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() + orderProvider.orderPages = [expectedOrders] + await sut.loadOrders() + + let spyCallCountBeforeLoadNext = orderProvider.spyCallCount + await sut.loadNextOrders() + + #expect(orderProvider.spyCallCount == spyCallCountBeforeLoadNext) + } + + @Test func refreshOrders_requests_first_page() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + orderProvider.shouldSimulateTwoPages = true + await sut.loadOrders() + await sut.loadNextOrders() + + try #require(orderProvider.spyLastRequestedPageNumber == 2) + + await sut.refreshOrders() + + #expect(orderProvider.spyLastRequestedPageNumber == 1) + } + + @Test func loadOrders_when_error_occurs_then_shows_error_state() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + orderProvider.shouldThrowError = true + + await sut.loadOrders() + + guard case .error = sut.ordersViewState.ordersState else { + Issue.record("Expected error OrderList state, but got \(sut.ordersViewState)") + return + } + #expect(sut.ordersViewState.containerState == .content) + } + + @Test func loadOrders_when_error_occurs_with_existing_orders_then_shows_inline_error() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() + orderProvider.orderPages = [initialOrders] + await sut.loadOrders() + + orderProvider.shouldThrowError = true + await sut.refreshOrders() + + guard case .inlineError(let orders, _, let context) = sut.ordersViewState.ordersState else { + Issue.record("Expected inlineError OrderList state, but got \(sut.ordersViewState)") + return + } + #expect(orders == initialOrders) + #expect(context == .refresh) + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift new file mode 100644 index 00000000000..7247726f02a --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift @@ -0,0 +1,97 @@ +import Foundation +@testable import Yosemite +import struct NetworkingCore.PagedItems +import struct NetworkingCore.Order +import enum NetworkingCore.OrderStatusEnum +import WooFoundation + +final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { + var orderPages: [[Order]] = [] + var errorToThrow: Error? + var shouldReturnZeroOrders = false + var shouldSimulateTwoPages = false + var shouldSimulateThreePages = false + var shouldThrowError = false + + var spyLastRequestedPageNumber: Int? + var spyCallCount = 0 + + func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems { + spyLastRequestedPageNumber = pageNumber + spyCallCount += 1 + + if shouldThrowError { + throw PointOfSaleOrderServiceError.requestFailed + } + + if let errorToThrow { + throw errorToThrow + } + + if shouldReturnZeroOrders { + return .init(items: [], hasMorePages: false, totalItems: 0) + } + + if shouldSimulateTwoPages { + if shouldSimulateThreePages && pageNumber > 1 { + return .init(items: MockPointOfSaleOrderService.makeSecondPageOrders(), hasMorePages: true, totalItems: 6) + } else if pageNumber > 1 { + return .init(items: MockPointOfSaleOrderService.makeSecondPageOrders(), hasMorePages: false, totalItems: 4) + } else { + return .init(items: MockPointOfSaleOrderService.makeInitialOrders(), hasMorePages: shouldSimulateTwoPages, totalItems: 4) + } + } + + return .init(items: (orderPages[safe: pageNumber - 1] ?? []), hasMorePages: orderPages.count > pageNumber, totalItems: 2) + } +} + +extension MockPointOfSaleOrderService { + static func makeInitialOrders() -> [Order] { + let baseDate = Date(timeIntervalSince1970: 1672531200) // Fixed date: Jan 1, 2023 + + let order1 = Order.fake().copy( + orderID: 1001, + number: "1001", + status: .completed, + dateCreated: baseDate, + dateModified: baseDate, + total: "25.99" + ) + + let order2 = Order.fake().copy( + orderID: 1002, + number: "1002", + status: .completed, + dateCreated: baseDate.addingTimeInterval(3600), + dateModified: baseDate.addingTimeInterval(3600), + total: "15.50" + ) + + return [order1, order2] + } + + static func makeSecondPageOrders() -> [Order] { + let baseDate = Date(timeIntervalSince1970: 1672531200) // Fixed date: Jan 1, 2023 + + let order3 = Order.fake().copy( + orderID: 1003, + number: "1003", + status: .completed, + dateCreated: baseDate.addingTimeInterval(7200), + dateModified: baseDate.addingTimeInterval(7200), + total: "42.75" + ) + + let order4 = Order.fake().copy( + orderID: 1004, + number: "1004", + status: .refunded, + dateCreated: baseDate.addingTimeInterval(10800), + dateModified: baseDate.addingTimeInterval(10800), + total: "12.00" + ) + + return [order3, order4] + } +} From 7057b9c925d43d703594f5a767af8f446f93bc2d Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:26:14 +0300 Subject: [PATCH 04/18] Add a simple in-memory cache for PointOfSaleOrdersController --- .../PointOfSaleOrdersController.swift | 17 ++++ .../PointOfSaleOrdersControllerTests.swift | 81 +++++++++++++++++++ .../Mocks/MockPointOfSaleOrderService.swift | 4 +- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift index 138211ffe98..c962678225e 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift @@ -16,6 +16,7 @@ protocol PointOfSaleOrdersControllerProtocol { var ordersViewState: OrdersViewState private let paginationTracker: AsyncPaginationTracker private var orderProvider: PointOfSaleOrderServiceProtocol + private var cachedOrders: [Order] = [] init(orderProvider: PointOfSaleOrderServiceProtocol, initialState: OrdersViewState = OrdersViewState(containerState: .loading, @@ -27,6 +28,7 @@ protocol PointOfSaleOrdersControllerProtocol { @MainActor func loadOrders() async { + setCachedData() setLoadingState() await loadFirstPage() } @@ -101,10 +103,25 @@ protocol PointOfSaleOrdersControllerProtocol { } else { ordersViewState.containerState = .content ordersViewState.ordersState = .loaded(allOrders, hasMoreItems: pagedOrders.hasMorePages) + + // Cache the orders if this is the first page + if pageNumber == 1 && !appendToExistingOrders { + cachedOrders = allOrders + } } return pagedOrders.hasMorePages } catch PointOfSaleOrderServiceError.requestCancelled { return true } } + + @MainActor + private func setCachedData() { + guard !ordersViewState.ordersState.orders.isEmpty, !cachedOrders.isEmpty else { + return + } + + ordersViewState.containerState = .content + ordersViewState.ordersState = .loading(cachedOrders) + } } diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift index 1dac12099ef..49ba40ec877 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift @@ -214,4 +214,85 @@ final class PointOfSaleOrdersControllerTests { #expect(orders == initialOrders) #expect(context == .refresh) } + + @Test func loadOrders_when_cached_data_available_then_shows_cached_data_with_loading_state() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() + orderProvider.orderPages = [initialOrders] + + // First load - should cache the data + await sut.loadOrders() + + guard case .loaded(let firstLoadOrders, _) = sut.ordersViewState.ordersState else { + Issue.record("Expected loaded state after first load, but got \(sut.ordersViewState.ordersState)") + return + } + #expect(firstLoadOrders == initialOrders) + + // Second load - should show cached data immediately with loading state + await sut.loadOrders() + + // Should show cached data in loading state, then switch to loaded + guard case .loaded(let cachedOrders, _) = sut.ordersViewState.ordersState else { + Issue.record("Expected loaded state with cached data, but got \(sut.ordersViewState.ordersState)") + return + } + #expect(cachedOrders == initialOrders) + } + + @Test func loadOrders_when_no_cached_data_then_starts_with_empty_loading_state() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() + orderProvider.orderPages = [initialOrders] + + // Initial state should be loading with empty orders + try #require(sut.ordersViewState.containerState == .loading) + guard case .loading(let orders) = sut.ordersViewState.ordersState else { + Issue.record("Expected loading state with empty orders, but got \(sut.ordersViewState.ordersState)") + return + } + #expect(orders.isEmpty) + + await sut.loadOrders() + + // Should end up in loaded state + guard case .loaded(let loadedOrders, _) = sut.ordersViewState.ordersState else { + Issue.record("Expected loaded state, but got \(sut.ordersViewState.ordersState)") + return + } + #expect(loadedOrders == initialOrders) + } + + @Test func loadOrders_cached_data_is_replaced_with_fresh_data() async throws { + let orderProvider = MockPointOfSaleOrderService() + let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + + let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() + let freshOrders = MockPointOfSaleOrderService.makeSecondPageOrders() + + // First load + orderProvider.orderPages = [initialOrders] + await sut.loadOrders() + + guard case .loaded(let firstLoadOrders, _) = sut.ordersViewState.ordersState else { + Issue.record("Expected loaded state after first load, but got \(sut.ordersViewState.ordersState)") + return + } + #expect(firstLoadOrders == initialOrders) + + // Second load with different data + orderProvider.orderPages = [freshOrders] + await sut.loadOrders() + + // Should end up showing fresh data, not cached data + guard case .loaded(let finalOrders, _) = sut.ordersViewState.ordersState else { + Issue.record("Expected loaded state with fresh data, but got \(sut.ordersViewState.ordersState)") + return + } + #expect(finalOrders == freshOrders) + } } diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift index 7247726f02a..f1f8069ccb2 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift @@ -49,7 +49,7 @@ final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { extension MockPointOfSaleOrderService { static func makeInitialOrders() -> [Order] { let baseDate = Date(timeIntervalSince1970: 1672531200) // Fixed date: Jan 1, 2023 - + let order1 = Order.fake().copy( orderID: 1001, number: "1001", @@ -73,7 +73,7 @@ extension MockPointOfSaleOrderService { static func makeSecondPageOrders() -> [Order] { let baseDate = Date(timeIntervalSince1970: 1672531200) // Fixed date: Jan 1, 2023 - + let order3 = Order.fake().copy( orderID: 1003, number: "1003", From 1dfec4a18ecd452ef68aafd6e0995970da755797 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:17:36 +0300 Subject: [PATCH 05/18] Create POSOrder, POSOrderItem, POSOrderRefund to have separate POS models --- .../NetworkingCore/Remote/OrdersRemote.swift | 27 ++++++- .../PointOfSale/Orders/POSOrder.swift | 73 ++++++++++++++++++ .../PointOfSale/Orders/POSOrderItem.swift | 35 +++++++++ .../PointOfSale/Orders/POSOrderRefund.swift | 27 +++++++ .../Orders/PointOfSaleOrderService.swift | 9 ++- .../PointOfSaleOrderServiceProtocol.swift | 3 +- .../PointOfSaleOrderServiceTests.swift | 2 +- .../PointOfSaleOrdersController.swift | 11 ++- .../Classes/POS/Models/OrdersViewState.swift | 12 +-- .../Mocks/MockPointOfSaleOrderService.swift | 77 +++++++++++++------ 10 files changed, 234 insertions(+), 42 deletions(-) create mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/POSOrder.swift create mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift create mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderRefund.swift diff --git a/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift b/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift index 391e681e2ee..5c124e3e06a 100644 --- a/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift +++ b/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift @@ -452,11 +452,25 @@ extension OrdersRemote: POSOrdersRemoteProtocol { } public func loadPOSOrders(siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> PagedItems { - let orders = try await loadAllOrders(for: siteID, - createdVia: "pos-rest-api", - pageNumber: pageNumber, - pageSize: pageSize) + let parameters: [String: Any] = [ + ParameterKeys.page: String(pageNumber), + ParameterKeys.perPage: String(pageSize), + ParameterKeys.statusKey: Defaults.statusAny, + ParameterKeys.usesGMTDates: true, + ParameterKeys.fields: ParameterValues.posOrderFieldValues, + ParameterKeys.createdVia: "pos-rest-api" + ] + let path = Constants.ordersPath + let request = JetpackRequest(wooApiVersion: .mark3, + method: .get, + siteID: siteID, + path: path, + parameters: parameters, + availableAsRESTRequest: true) + let mapper = OrderListMapper(siteID: siteID) + + let orders: [Order] = try await enqueue(request, mapper: mapper) let hasMorePages = orders.count == pageSize return PagedItems(items: orders, hasMorePages: hasMorePages, totalItems: nil) } @@ -507,6 +521,11 @@ public extension OrdersRemote { "payment_url", "line_items", "shipping", "billing", "coupon_lines", "shipping_lines", "refunds", "fee_lines", "order_key", "tax_lines", "meta_data", "is_editable", "needs_payment", "needs_processing", "gift_cards", "created_via" ] + static let posOrderFieldValues: String = posOrderFields.joined(separator: ",") + private static let posOrderFields = [ + "id", "number", "date_created_gmt", "status", "total", "billing", "payment_method_title", + "line_items", "refunds", "meta_data", "currency", "currency_symbol" + ] static let dateModifiedField = "date_modified_gmt" } diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrder.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrder.swift new file mode 100644 index 00000000000..43076d28b52 --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrder.swift @@ -0,0 +1,73 @@ +import Foundation +import struct NetworkingCore.Address +import struct NetworkingCore.OrderItem +import struct NetworkingCore.OrderRefundCondensed +import struct NetworkingCore.MetaData +import enum NetworkingCore.OrderStatusEnum +import struct NetworkingCore.Order + +public struct POSOrder: Equatable, Hashable { + public let id: Int64 + public let number: String + public let dateCreated: Date + public let status: OrderStatusEnum + public let total: String + public let customerEmail: String? + public let paymentMethodTitle: String + public let lineItems: [POSOrderItem] + public let refunds: [POSOrderRefund] + public let currency: String + public let currencySymbol: String + + public init(id: Int64, + number: String, + dateCreated: Date, + status: OrderStatusEnum, + total: String, + customerEmail: String? = nil, + paymentMethodTitle: String, + lineItems: [POSOrderItem] = [], + refunds: [POSOrderRefund] = [], + currency: String, + currencySymbol: String) { + self.id = id + self.number = number + self.dateCreated = dateCreated + self.status = status + self.total = total + self.customerEmail = customerEmail + self.paymentMethodTitle = paymentMethodTitle + self.lineItems = lineItems + self.refunds = refunds + self.currency = currency + self.currencySymbol = currencySymbol + } +} + +// MARK: - Conversion from NetworkingCore.Order +public extension POSOrder { + init(from order: NetworkingCore.Order) { + // Extract customer email from billing address + let customerEmail = order.billingAddress?.email + + // Convert line items to POS format + let posLineItems = order.items.map { POSOrderItem(from: $0) } + + // Convert refunds to POS format + let posRefunds = order.refunds.map { POSOrderRefund(from: $0) } + + self.init( + id: order.orderID, + number: order.number, + dateCreated: order.dateCreated, + status: order.status, + total: order.total, + customerEmail: customerEmail, + paymentMethodTitle: order.paymentMethodTitle, + lineItems: posLineItems, + refunds: posRefunds, + currency: order.currency, + currencySymbol: order.currencySymbol + ) + } +} diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift new file mode 100644 index 00000000000..0320de1b862 --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift @@ -0,0 +1,35 @@ +import Foundation +import struct NetworkingCore.OrderItem + +public struct POSOrderItem: Equatable, Hashable { + public let itemID: Int64 + public let name: String + public let quantity: Decimal + public let total: String + public let sku: String? + + public init(itemID: Int64, + name: String, + quantity: Decimal, + total: String, + sku: String? = nil) { + self.itemID = itemID + self.name = name + self.quantity = quantity + self.total = total + self.sku = sku + } +} + +// MARK: - Conversion from NetworkingCore.OrderItem +public extension POSOrderItem { + init(from orderItem: OrderItem) { + self.init( + itemID: orderItem.itemID, + name: orderItem.name, + quantity: orderItem.quantity, + total: orderItem.total, + sku: orderItem.sku + ) + } +} diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderRefund.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderRefund.swift new file mode 100644 index 00000000000..53c2fd1d9dc --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderRefund.swift @@ -0,0 +1,27 @@ +import Foundation +import struct NetworkingCore.OrderRefundCondensed + +public struct POSOrderRefund: Equatable, Hashable { + public let refundID: Int64 + public let total: String + public let reason: String? + + public init(refundID: Int64, + total: String, + reason: String? = nil) { + self.refundID = refundID + self.total = total + self.reason = reason + } +} + +// MARK: - Conversion from NetworkingCore.OrderRefundCondensed +public extension POSOrderRefund { + init(from refund: OrderRefundCondensed) { + self.init( + refundID: refund.refundID, + total: refund.total, + reason: refund.reason + ) + } +} diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift index 69e60fc5cd7..6701fb0d1ab 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift @@ -13,7 +13,7 @@ public final class PointOfSaleOrderService: PointOfSaleOrderServiceProtocol { self.ordersRemote = ordersRemote } - public func providePointOfSaleOrders(pageNumber: Int = 1) async throws -> PagedItems { + public func providePointOfSaleOrders(pageNumber: Int = 1) async throws -> PagedItems { do { let pagedOrders = try await ordersRemote.loadPOSOrders( siteID: siteID, @@ -25,7 +25,12 @@ public final class PointOfSaleOrderService: PointOfSaleOrderServiceProtocol { return .init(items: [], hasMorePages: false, totalItems: 0) } - return pagedOrders + // Convert Order objects to POSOrder objects + let posOrders = pagedOrders.items.map { POSOrder(from: $0) } + + return .init(items: posOrders, + hasMorePages: pagedOrders.hasMorePages, + totalItems: pagedOrders.totalItems) } catch AFError.explicitlyCancelled { throw PointOfSaleOrderServiceError.requestCancelled } catch { diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift index 07ed3d82d37..f419c63e0f7 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift @@ -1,6 +1,5 @@ import Foundation import struct NetworkingCore.PagedItems -import struct NetworkingCore.Order public enum PointOfSaleOrderServiceError: Error, Equatable { case requestFailed @@ -9,5 +8,5 @@ public enum PointOfSaleOrderServiceError: Error, Equatable { } public protocol PointOfSaleOrderServiceProtocol { - func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems + func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems } diff --git a/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift b/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift index 14c71b8abb9..9d3d5c66dd1 100644 --- a/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift +++ b/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift @@ -70,7 +70,7 @@ final class PointOfSaleOrderServiceTests: XCTestCase { XCTAssertTrue(mockOrdersRemote.loadPOSOrdersCalled) XCTAssertEqual(mockOrdersRemote.spyPageNumber, 1) XCTAssertEqual(mockOrdersRemote.spyPageSize, 25) - XCTAssertEqual(pagedOrders.items.first?.orderID, 1001) + XCTAssertEqual(pagedOrders.items.first?.id, 1001) } func test_PointOfSaleOrderServiceProtocol_returns_correct_pagination_when_more_pages_available() async throws { diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift index c962678225e..b3dab185bff 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift @@ -2,7 +2,9 @@ import Foundation import Observation import enum Yosemite.PointOfSaleOrderServiceError import protocol Yosemite.PointOfSaleOrderServiceProtocol -import struct NetworkingCore.Order +import struct Yosemite.POSOrder +import struct Yosemite.POSOrderItem +import struct Yosemite.POSOrderRefund import class Yosemite.Store protocol PointOfSaleOrdersControllerProtocol { @@ -16,7 +18,7 @@ protocol PointOfSaleOrdersControllerProtocol { var ordersViewState: OrdersViewState private let paginationTracker: AsyncPaginationTracker private var orderProvider: PointOfSaleOrderServiceProtocol - private var cachedOrders: [Order] = [] + private var cachedOrders: [POSOrder] = [] init(orderProvider: PointOfSaleOrderServiceProtocol, initialState: OrdersViewState = OrdersViewState(containerState: .loading, @@ -72,7 +74,8 @@ protocol PointOfSaleOrdersControllerProtocol { ordersViewState = OrdersViewState(containerState: .content, ordersState: .error(.errorOnLoadingOrders(error: error))) } else { ordersViewState = OrdersViewState(containerState: .content, - ordersState: .inlineError(orders, error: .errorOnLoadingOrders(error: error), context: OrderListState.InlineErrorContext.refresh)) + ordersState: .inlineError(orders, error: .errorOnLoadingOrders(error: error), + context: OrderListState.InlineErrorContext.refresh)) } } } @@ -93,7 +96,7 @@ protocol PointOfSaleOrdersControllerProtocol { let newOrders = pagedOrders.items var allOrders = appendToExistingOrders ? ordersViewState.ordersState.orders : [] let uniqueNewOrders = newOrders.filter { newOrder in - !allOrders.contains(where: { $0.orderID == newOrder.orderID }) + !allOrders.contains(where: { $0.id == newOrder.id }) } allOrders.append(contentsOf: uniqueNewOrders) diff --git a/WooCommerce/Classes/POS/Models/OrdersViewState.swift b/WooCommerce/Classes/POS/Models/OrdersViewState.swift index 78259710ec2..7e9fa77fec6 100644 --- a/WooCommerce/Classes/POS/Models/OrdersViewState.swift +++ b/WooCommerce/Classes/POS/Models/OrdersViewState.swift @@ -1,5 +1,7 @@ import Foundation -import struct NetworkingCore.Order +import struct Yosemite.POSOrder +import struct Yosemite.POSOrderItem +import struct Yosemite.POSOrderRefund struct OrdersViewState: Equatable { var containerState: ItemsContainerState @@ -7,18 +9,18 @@ struct OrdersViewState: Equatable { } enum OrderListState: Equatable { - case loading([Order]) - case loaded([Order], hasMoreItems: Bool) + case loading([POSOrder]) + case loaded([POSOrder], hasMoreItems: Bool) case empty case error(PointOfSaleErrorState) - case inlineError([Order], error: PointOfSaleErrorState, context: InlineErrorContext) + case inlineError([POSOrder], error: PointOfSaleErrorState, context: InlineErrorContext) enum InlineErrorContext { case refresh case pagination } - var orders: [Order] { + var orders: [POSOrder] { switch self { case .loading(let orders): return orders diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift index f1f8069ccb2..279fa2804e6 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift @@ -6,7 +6,7 @@ import enum NetworkingCore.OrderStatusEnum import WooFoundation final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { - var orderPages: [[Order]] = [] + var orderPages: [[POSOrder]] = [] var errorToThrow: Error? var shouldReturnZeroOrders = false var shouldSimulateTwoPages = false @@ -16,7 +16,7 @@ final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { var spyLastRequestedPageNumber: Int? var spyCallCount = 0 - func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems { + func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems { spyLastRequestedPageNumber = pageNumber spyCallCount += 1 @@ -47,49 +47,78 @@ final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { } extension MockPointOfSaleOrderService { - static func makeInitialOrders() -> [Order] { + static func makeInitialOrders() -> [POSOrder] { let baseDate = Date(timeIntervalSince1970: 1672531200) // Fixed date: Jan 1, 2023 - let order1 = Order.fake().copy( - orderID: 1001, + let order1 = POSOrder( + id: 1001, number: "1001", - status: .completed, dateCreated: baseDate, - dateModified: baseDate, - total: "25.99" + status: .completed, + total: "25.99", + customerEmail: "customer1@example.com", + paymentMethodTitle: "Cash", + lineItems: [ + POSOrderItem(itemID: 1, name: "Coffee", quantity: 2, total: "20.00"), + POSOrderItem(itemID: 2, name: "Muffin", quantity: 1, total: "5.99") + ], + currency: "USD", + currencySymbol: "$" ) - let order2 = Order.fake().copy( - orderID: 1002, + let order2 = POSOrder( + id: 1002, number: "1002", - status: .completed, dateCreated: baseDate.addingTimeInterval(3600), - dateModified: baseDate.addingTimeInterval(3600), - total: "15.50" + status: .completed, + total: "15.50", + customerEmail: "customer2@example.com", + paymentMethodTitle: "Card", + lineItems: [ + POSOrderItem(itemID: 3, name: "Tea", quantity: 1, total: "15.50") + ], + currency: "USD", + currencySymbol: "$" ) return [order1, order2] } - static func makeSecondPageOrders() -> [Order] { + static func makeSecondPageOrders() -> [POSOrder] { let baseDate = Date(timeIntervalSince1970: 1672531200) // Fixed date: Jan 1, 2023 - let order3 = Order.fake().copy( - orderID: 1003, + let order3 = POSOrder( + id: 1003, number: "1003", - status: .completed, dateCreated: baseDate.addingTimeInterval(7200), - dateModified: baseDate.addingTimeInterval(7200), - total: "42.75" + status: .completed, + total: "42.75", + customerEmail: "customer3@example.com", + paymentMethodTitle: "Cash", + lineItems: [ + POSOrderItem(itemID: 4, name: "Sandwich", quantity: 1, total: "12.00"), + POSOrderItem(itemID: 5, name: "Soup", quantity: 2, total: "30.75") + ], + currency: "USD", + currencySymbol: "$" ) - let order4 = Order.fake().copy( - orderID: 1004, + let order4 = POSOrder( + id: 1004, number: "1004", - status: .refunded, dateCreated: baseDate.addingTimeInterval(10800), - dateModified: baseDate.addingTimeInterval(10800), - total: "12.00" + status: .refunded, + total: "12.00", + customerEmail: "customer4@example.com", + paymentMethodTitle: "Card", + lineItems: [ + POSOrderItem(itemID: 6, name: "Cookies", quantity: 1, total: "12.00") + ], + refunds: [ + POSOrderRefund(refundID: 1001, total: "12.00", reason: "Customer request") + ], + currency: "USD", + currencySymbol: "$" ) return [order3, order4] From 7ecdfb9cd16ce2db344ffce565209b27df69f360 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:33:26 +0300 Subject: [PATCH 06/18] Create PointOfSaleOrderFetchStrategy --- .../PointOfSaleOrderFetchStrategy.swift | 18 ++++++++++ ...PointOfSaleOrderFetchStrategyFactory.swift | 35 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift create mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift new file mode 100644 index 00000000000..634d96db058 --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift @@ -0,0 +1,18 @@ +import Foundation +import struct NetworkingCore.PagedItems + +public protocol PointOfSaleOrderFetchStrategy { + func fetchOrders(pageNumber: Int) async throws -> PagedItems +} + +public struct PointOfSaleDefaultOrderFetchStrategy: PointOfSaleOrderFetchStrategy { + private let orderService: PointOfSaleOrderServiceProtocol + + init(orderService: PointOfSaleOrderServiceProtocol) { + self.orderService = orderService + } + + public func fetchOrders(pageNumber: Int) async throws -> PagedItems { + try await orderService.providePointOfSaleOrders(pageNumber: pageNumber) + } +} diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift new file mode 100644 index 00000000000..f757e2c7913 --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift @@ -0,0 +1,35 @@ +import Foundation +import class Networking.AlamofireNetwork +import class Networking.OrdersRemote + +public protocol PointOfSaleOrderFetchStrategyFactoryProtocol { + func defaultStrategy() -> PointOfSaleOrderFetchStrategy +} + +public final class PointOfSaleOrderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol { + private let siteID: Int64 + private let ordersRemote: OrdersRemote + + public init(siteID: Int64, + credentials: Credentials?) { + self.siteID = siteID + let network = AlamofireNetwork(credentials: credentials) + self.ordersRemote = OrdersRemote(network: network) + } + + public func defaultStrategy() -> PointOfSaleOrderFetchStrategy { + PointOfSaleDefaultOrderFetchStrategy(orderService: PointOfSaleOrderService(siteID: siteID, ordersRemote: ordersRemote)) + } +} + +public final class PointOfSaleFixedOrderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol { + private let fixedStrategy: PointOfSaleOrderFetchStrategy + + public init(fixedStrategy: PointOfSaleOrderFetchStrategy) { + self.fixedStrategy = fixedStrategy + } + + public func defaultStrategy() -> PointOfSaleOrderFetchStrategy { + fixedStrategy + } +} From 53e56c33a97b51b95e69f67f1cf0170a5f0a4973 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:33:49 +0300 Subject: [PATCH 07/18] Use common fields in OrdersRemote since Order model expects all the common values --- Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift b/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift index 5c124e3e06a..709ceddeb71 100644 --- a/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift +++ b/Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift @@ -457,7 +457,7 @@ extension OrdersRemote: POSOrdersRemoteProtocol { ParameterKeys.perPage: String(pageSize), ParameterKeys.statusKey: Defaults.statusAny, ParameterKeys.usesGMTDates: true, - ParameterKeys.fields: ParameterValues.posOrderFieldValues, + ParameterKeys.fields: ParameterValues.fieldValues, ParameterKeys.createdVia: "pos-rest-api" ] @@ -521,11 +521,6 @@ public extension OrdersRemote { "payment_url", "line_items", "shipping", "billing", "coupon_lines", "shipping_lines", "refunds", "fee_lines", "order_key", "tax_lines", "meta_data", "is_editable", "needs_payment", "needs_processing", "gift_cards", "created_via" ] - static let posOrderFieldValues: String = posOrderFields.joined(separator: ",") - private static let posOrderFields = [ - "id", "number", "date_created_gmt", "status", "total", "billing", "payment_method_title", - "line_items", "refunds", "meta_data", "currency", "currency_symbol" - ] static let dateModifiedField = "date_modified_gmt" } From 97d5d0883d8278947d39e367ffd7dbb35db6ed33 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:58:06 +0300 Subject: [PATCH 08/18] Inject PointOfSaleOrdersModel through entry point --- .../Classes/POS/Presentation/PointOfSaleEntryPointView.swift | 5 +++++ WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 4ddf855e72b..a53572eb47e 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -15,6 +15,7 @@ struct PointOfSaleEntryPointView: View { private let purchasableItemsSearchController: PointOfSaleSearchingItemsControllerProtocol private let couponsController: PointOfSaleCouponsControllerProtocol private let couponsSearchController: PointOfSaleSearchingItemsControllerProtocol + private let ordersController: PointOfSaleOrdersControllerProtocol private let cardPresentPaymentService: CardPresentPaymentFacade private let orderController: PointOfSaleOrderControllerProtocol private let collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking @@ -26,6 +27,7 @@ struct PointOfSaleEntryPointView: View { purchasableItemsSearchController: PointOfSaleSearchingItemsControllerProtocol, couponsController: PointOfSaleCouponsControllerProtocol, couponsSearchController: PointOfSaleSearchingItemsControllerProtocol, + ordersController: PointOfSaleOrdersControllerProtocol, onPointOfSaleModeActiveStateChange: @escaping ((Bool) -> Void), cardPresentPaymentService: CardPresentPaymentFacade, orderController: PointOfSaleOrderControllerProtocol, @@ -45,6 +47,7 @@ struct PointOfSaleEntryPointView: View { self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker self.searchHistoryService = searchHistoryService self.popularPurchasableItemsController = popularPurchasableItemsController + self.ordersController = ordersController self.barcodeScanService = barcodeScanService self.posEntryPointController = POSEntryPointController(eligibilityChecker: posEligibilityChecker) } @@ -78,6 +81,7 @@ struct PointOfSaleEntryPointView: View { .environmentObject(posModalManager) .environmentObject(posSheetManager) .environmentObject(posCoverManager) + .environment(PointOfSaleOrdersModel(ordersController: ordersController)) .injectKeyboardObserver() .onAppear { onPointOfSaleModeActiveStateChange(true) @@ -96,6 +100,7 @@ struct PointOfSaleEntryPointView: View { purchasableItemsSearchController: PointOfSalePreviewItemsController(), couponsController: PointOfSalePreviewCouponsController(), couponsSearchController: PointOfSalePreviewCouponsController(), + ordersController: POSPreviewHelpers.makePreviewOrdersModel().ordersController, onPointOfSaleModeActiveStateChange: { _ in }, cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController(), diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 23492bdb61b..5e10f85978d 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -112,6 +112,11 @@ private extension POSTabCoordinator { fetchStrategyFactory: posCouponFetchStrategyFactory), couponsSearchController: PointOfSaleCouponsController(itemProvider: posCouponProvider, fetchStrategyFactory: posCouponFetchStrategyFactory), + ordersController: PointOfSaleOrdersController( + orderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactory(siteID: siteID, + credentials: credentials) + ), + onPointOfSaleModeActiveStateChange: { [weak self] isEnabled in self?.updateDefaultConfigurationForPointOfSale(isEnabled) }, From 54e6f75fed163e0870496ac04266264c2b2332e2 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:58:25 +0300 Subject: [PATCH 09/18] Create PointOfSaleOrdersModel for managing order list state --- .../POS/Models/PointOfSaleOrdersModel.swift | 14 ++++++++++++++ WooCommerce/WooCommerce.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 18 insertions(+) create mode 100644 WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift new file mode 100644 index 00000000000..771a4356176 --- /dev/null +++ b/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift @@ -0,0 +1,14 @@ +import Foundation +import Observation + +protocol PointOfSaleOrdersModelProtocol { + var ordersController: PointOfSaleOrdersControllerProtocol { get } +} + +@Observable final class PointOfSaleOrdersModel: PointOfSaleOrdersModelProtocol { + let ordersController: PointOfSaleOrdersControllerProtocol + + init(ordersController: PointOfSaleOrdersControllerProtocol) { + self.ordersController = ordersController + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 2ad5e875526..6c43eae4644 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 012ACB762E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB752E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift */; }; 012ACB782E5C84A200A49458 /* OrdersViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB772E5C84A200A49458 /* OrdersViewState.swift */; }; 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */; }; + 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift */; }; 01309A7F2DC4F39E00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */; }; 01309A812DC4F45300B77527 /* CardPresentModalCardInserted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */; }; 01309A832DC4F89D00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */; }; @@ -3233,6 +3234,7 @@ 012ACB752E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersControllerTests.swift; sourceTree = ""; }; 012ACB772E5C84A200A49458 /* OrdersViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersViewState.swift; sourceTree = ""; }; 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderService.swift; sourceTree = ""; }; + 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersModel.swift; sourceTree = ""; }; 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift; sourceTree = ""; }; 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalCardInserted.swift; sourceTree = ""; }; 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageView.swift; sourceTree = ""; }; @@ -9975,6 +9977,7 @@ 68F151DF2C0DA7800082AEC8 /* Models */ = { isa = PBXGroup; children = ( + 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift */, 01B3A1F12DB6D48800286B7F /* ItemListType.swift */, 20FCBCDC2CE223340082DCA3 /* PointOfSaleAggregateModel.swift */, 209ECA802DB8FC280089F3D2 /* PointOfSaleViewStateCoordinator.swift */, @@ -15402,6 +15405,7 @@ CCD2F51C26D697860010E679 /* ShippingLabelServicePackageListViewModel.swift in Sources */, 03076D38290C223E008EE839 /* WooNavigationSheet.swift in Sources */, 022CE91A29BB143000F210E0 /* ProductSelectorNavigationView.swift in Sources */, + 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift in Sources */, B99686E02A13C8CC00D1AF62 /* ScanToPayView.swift in Sources */, 02B191502CCF27F300CF38C9 /* PointOfSaleCardPresentPaymentOnboardingView.swift in Sources */, CE070A3E2BBC608A00017578 /* GiftCardsReportCardViewModel.swift in Sources */, From ca2713f1a00d2d38f59394eec54393bf11f0b24c Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:59:21 +0300 Subject: [PATCH 10/18] Flatten OrderViewState --- .../PointOfSaleOrdersController.swift | 68 ++++++++++--------- .../Classes/POS/Models/OrdersViewState.swift | 66 ++++++++++++++---- 2 files changed, 86 insertions(+), 48 deletions(-) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift index b3dab185bff..657e79fcdb9 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift @@ -2,30 +2,33 @@ import Foundation import Observation import enum Yosemite.PointOfSaleOrderServiceError import protocol Yosemite.PointOfSaleOrderServiceProtocol +import protocol Yosemite.PointOfSaleOrderFetchStrategyFactoryProtocol +import protocol Yosemite.PointOfSaleOrderFetchStrategy import struct Yosemite.POSOrder import struct Yosemite.POSOrderItem import struct Yosemite.POSOrderRefund import class Yosemite.Store protocol PointOfSaleOrdersControllerProtocol { - var ordersViewState: OrdersViewState { get } + var ordersViewState: OrderListState { get } func loadOrders() async func refreshOrders() async func loadNextOrders() async } @Observable final class PointOfSaleOrdersController: PointOfSaleOrdersControllerProtocol { - var ordersViewState: OrdersViewState + var ordersViewState: OrderListState private let paginationTracker: AsyncPaginationTracker - private var orderProvider: PointOfSaleOrderServiceProtocol + private let orderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol + private var fetchStrategy: PointOfSaleOrderFetchStrategy private var cachedOrders: [POSOrder] = [] - init(orderProvider: PointOfSaleOrderServiceProtocol, - initialState: OrdersViewState = OrdersViewState(containerState: .loading, - ordersState: .loading([]))) { - self.orderProvider = orderProvider + init(orderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol, + initialState: OrderListState = .loading([])) { + self.orderFetchStrategyFactory = orderFetchStrategyFactory self.ordersViewState = initialState self.paginationTracker = .init() + self.fetchStrategy = orderFetchStrategyFactory.defaultStrategy() } @MainActor @@ -45,19 +48,17 @@ protocol PointOfSaleOrdersControllerProtocol { guard paginationTracker.hasNextPage else { return } - let currentOrders = ordersViewState.ordersState.orders - ordersViewState.containerState = .content - ordersViewState.ordersState = .loading(currentOrders) + let currentOrders = ordersViewState.orders + ordersViewState = .loading(currentOrders) do { _ = try await paginationTracker.ensureNextPageIsSynced { [weak self] pageNumber in guard let self else { return true } return try await fetchOrders(pageNumber: pageNumber) } } catch { - ordersViewState.containerState = .content - ordersViewState.ordersState = .inlineError(currentOrders, - error: .errorOnLoadingOrdersNextPage(error: error), - context: OrderListState.InlineErrorContext.pagination) + ordersViewState = .inlineError(currentOrders, + error: .errorOnLoadingOrdersNextPage(error: error), + context: OrderListState.InlineErrorContext.pagination) } } @@ -69,45 +70,47 @@ protocol PointOfSaleOrdersControllerProtocol { return try await fetchOrders(pageNumber: pageNumber, appendToExistingOrders: false) } } catch { - let orders = ordersViewState.ordersState.orders + let orders = ordersViewState.orders if orders.isEmpty { - ordersViewState = OrdersViewState(containerState: .content, ordersState: .error(.errorOnLoadingOrders(error: error))) + ordersViewState = .error(.errorOnLoadingOrders(error: error)) } else { - ordersViewState = OrdersViewState(containerState: .content, - ordersState: .inlineError(orders, error: .errorOnLoadingOrders(error: error), - context: OrderListState.InlineErrorContext.refresh)) + ordersViewState = .inlineError(orders, + error: .errorOnLoadingOrders(error: error), + context: OrderListState.InlineErrorContext.refresh) } } } private func setLoadingState() { - let orders = ordersViewState.ordersState.orders - let isInitialState = ordersViewState.containerState == .loading + let orders = ordersViewState.orders + let isInitialState = ordersViewState.isLoading && orders.isEmpty if !isInitialState { - ordersViewState.ordersState = .loading(orders) + ordersViewState = .loading(orders) } } @MainActor private func fetchOrders(pageNumber: Int, appendToExistingOrders: Bool = true) async throws -> Bool { do { - let pagedOrders = try await orderProvider.providePointOfSaleOrders(pageNumber: pageNumber) + let pagedOrders = try await fetchStrategy.fetchOrders(pageNumber: pageNumber) let newOrders = pagedOrders.items - var allOrders = appendToExistingOrders ? ordersViewState.ordersState.orders : [] + var allOrders = appendToExistingOrders ? ordersViewState.orders : [] let uniqueNewOrders = newOrders.filter { newOrder in !allOrders.contains(where: { $0.id == newOrder.id }) } - allOrders.append(contentsOf: uniqueNewOrders) + + if appendToExistingOrders && !uniqueNewOrders.isEmpty { + allOrders.append(contentsOf: uniqueNewOrders) + } else if !appendToExistingOrders { + allOrders = uniqueNewOrders + } if allOrders.isEmpty { - ordersViewState.containerState = .content - ordersViewState.ordersState = .empty + ordersViewState = .empty } else { - ordersViewState.containerState = .content - ordersViewState.ordersState = .loaded(allOrders, hasMoreItems: pagedOrders.hasMorePages) + ordersViewState = .loaded(allOrders, hasMoreItems: pagedOrders.hasMorePages) - // Cache the orders if this is the first page if pageNumber == 1 && !appendToExistingOrders { cachedOrders = allOrders } @@ -120,11 +123,10 @@ protocol PointOfSaleOrdersControllerProtocol { @MainActor private func setCachedData() { - guard !ordersViewState.ordersState.orders.isEmpty, !cachedOrders.isEmpty else { + guard !ordersViewState.orders.isEmpty || !cachedOrders.isEmpty else { return } - ordersViewState.containerState = .content - ordersViewState.ordersState = .loading(cachedOrders) + ordersViewState = .loading(cachedOrders) } } diff --git a/WooCommerce/Classes/POS/Models/OrdersViewState.swift b/WooCommerce/Classes/POS/Models/OrdersViewState.swift index 7e9fa77fec6..3348d8c846e 100644 --- a/WooCommerce/Classes/POS/Models/OrdersViewState.swift +++ b/WooCommerce/Classes/POS/Models/OrdersViewState.swift @@ -3,35 +3,71 @@ import struct Yosemite.POSOrder import struct Yosemite.POSOrderItem import struct Yosemite.POSOrderRefund -struct OrdersViewState: Equatable { - var containerState: ItemsContainerState - var ordersState: OrderListState -} - enum OrderListState: Equatable { case loading([POSOrder]) case loaded([POSOrder], hasMoreItems: Bool) - case empty - case error(PointOfSaleErrorState) case inlineError([POSOrder], error: PointOfSaleErrorState, context: InlineErrorContext) + case error(PointOfSaleErrorState) + case empty enum InlineErrorContext { case refresh case pagination } + var isLoading: Bool { + switch self { + case .loading: + return true + default: + return false + } + } + + var isLoaded: Bool { + switch self { + case .loaded: + return true + default: + return false + } + } + + var isInlineError: Bool { + switch self { + case .inlineError: + return true + default: + return false + } + } + + var isError: Bool { + switch self { + case .error: + return true + default: + return false + } + } + + var isEmpty: Bool { + switch self { + case .empty: + return true + default: + return false + } + } + var orders: [POSOrder] { switch self { - case .loading(let orders): - return orders - case .loaded(let orders, _): + case .loading(let orders), + .loaded(let orders, _), + .inlineError(let orders, _, _): return orders - case .empty: - return [] - case .error: + case .error, .empty: return [] - case .inlineError(let orders, _, _): - return orders } } } From b26a0016f16e78636ca5be6e540ed38eb4b33494 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:59:36 +0300 Subject: [PATCH 11/18] Update PointOfSaleOrdersControllerTests --- .../Classes/POS/Utils/PreviewHelpers.swift | 61 ++++++++++ .../PointOfSaleOrdersControllerTests.swift | 109 +++++++++--------- 2 files changed, 118 insertions(+), 52 deletions(-) diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index 75b9da9a015..8e9d4e5f039 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -22,6 +22,9 @@ import protocol Yosemite.PointOfSaleBarcodeScanServiceProtocol import enum Yosemite.PointOfSaleBarcodeScanError import Combine import struct Yosemite.PaymentIntent +import struct Yosemite.POSOrder +import class Yosemite.PointOfSaleOrderService +import class Yosemite.PointOfSaleOrderFetchStrategyFactory // MARK: - PreviewProvider helpers // @@ -228,6 +231,64 @@ struct POSPreviewHelpers { barcodeScanService: barcodeScanService ) } + + static func makePreviewOrdersModel() -> PointOfSaleOrdersModel { + return PointOfSaleOrdersModel(ordersController: PointOfSalePreviewOrdersController()) + } +} + +// MARK: - Preview Orders Controller +final class PointOfSalePreviewOrdersController: PointOfSaleOrdersControllerProtocol { + var ordersViewState: OrderListState { + .loaded( + [ + POSOrder( + id: 1, + number: "1001", + dateCreated: Date(), + status: .completed, + total: "25.00", + customerEmail: "customer1@example.com", + paymentMethodTitle: "Credit Card", + lineItems: [], + refunds: [], + currency: "USD", + currencySymbol: "$" + ), + POSOrder( + id: 2, + number: "1002", + dateCreated: Date().addingTimeInterval(-3600), + status: .processing, + total: "45.50", + customerEmail: "customer2@example.com", + paymentMethodTitle: "Cash", + lineItems: [], + refunds: [], + currency: "USD", + currencySymbol: "$" + ), + POSOrder( + id: 3, + number: "1003", + dateCreated: Date().addingTimeInterval(-7200), + status: .completed, + total: "12.75", + customerEmail: nil, + paymentMethodTitle: "Credit Card", + lineItems: [], + refunds: [], + currency: "USD", + currencySymbol: "$" + ) + ], + hasMoreItems: false + ) + } + + func loadOrders() async {} + func loadNextOrders() async {} + func refreshOrders() async { } } // MARK: - Barcode Scan Service diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift index 49ba40ec877..c358ece6a4f 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift @@ -3,14 +3,22 @@ import Foundation @testable import WooCommerce import enum Yosemite.PointOfSaleOrderServiceError import struct NetworkingCore.Order +import class Yosemite.PointOfSaleFixedOrderFetchStrategyFactory +import struct Yosemite.PointOfSaleDefaultOrderFetchStrategy import Observation final class PointOfSaleOrdersControllerTests { + private func makePointOfSaleOrdersController(orderProvider: MockPointOfSaleOrderService) -> PointOfSaleOrdersController { + let fetchStrategy = PointOfSaleDefaultOrderFetchStrategy(orderService: orderProvider) + let fetchStrategyFactory = PointOfSaleFixedOrderFetchStrategyFactory(fixedStrategy: fetchStrategy) + return PointOfSaleOrdersController(orderFetchStrategyFactory: fetchStrategyFactory) + } + @Test func loadOrders_requests_first_page_after_loading_two_pages() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - try #require(sut.ordersViewState.containerState == .loading) + try #require(sut.ordersViewState.isLoading) orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() @@ -24,37 +32,35 @@ final class PointOfSaleOrdersControllerTests { @Test func loadOrders_results_in_loaded_state() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [expectedOrders] - try #require(sut.ordersViewState.containerState == .loading) + try #require(sut.ordersViewState.isLoading) await sut.loadOrders() - #expect(sut.ordersViewState == OrdersViewState(containerState: .content, - ordersState: .loaded(expectedOrders, hasMoreItems: false))) + #expect(sut.ordersViewState == .loaded(expectedOrders, hasMoreItems: false)) } @Test func loadOrders_with_more_pages_sets_hasMoreItems() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() - try #require(sut.ordersViewState.containerState == .loading) + try #require(sut.ordersViewState.isLoading) orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() - #expect(sut.ordersViewState == OrdersViewState(containerState: .content, - ordersState: .loaded(expectedOrders, hasMoreItems: true))) + #expect(sut.ordersViewState == .loaded(expectedOrders, hasMoreItems: true)) } @Test func loadOrders_when_called_multiple_times_then_orders_are_not_duplicated() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - try #require(sut.ordersViewState.containerState == .loading) + try #require(sut.ordersViewState.isLoading) let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [expectedOrders] @@ -62,7 +68,7 @@ final class PointOfSaleOrdersControllerTests { await sut.loadOrders() await sut.loadOrders() - guard case .loaded(let orders, _) = sut.ordersViewState.ordersState else { + guard case .loaded(let orders, _) = sut.ordersViewState else { Issue.record("Expected loaded OrderList state, but got \(sut.ordersViewState)") return } @@ -71,50 +77,49 @@ final class PointOfSaleOrdersControllerTests { @Test func container_state_starts_as_loading() { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - #expect(sut.ordersViewState.containerState == .loading) + #expect(sut.ordersViewState.isLoading) } @Test func loadNextOrders_when_initial_orders_empty_then_container_state_is_content_and_orders_state_is_empty() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) orderProvider.shouldReturnZeroOrders = true - try #require(sut.ordersViewState.containerState == .loading) + try #require(sut.ordersViewState.isLoading) await sut.loadNextOrders() - #expect(sut.ordersViewState.containerState == .content) - #expect(sut.ordersViewState.ordersState == .empty) + #expect(!sut.ordersViewState.isLoading) + #expect(sut.ordersViewState == .empty) } @Test func loadOrders_when_initial_orders_has_orders_but_no_more_pages_then_state_is_loaded_with_initial_orders() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [initialOrders] - try #require(sut.ordersViewState.containerState == .loading) + try #require(sut.ordersViewState.isLoading) await sut.loadNextOrders() - #expect(sut.ordersViewState == OrdersViewState(containerState: .content, - ordersState: .loaded(initialOrders, hasMoreItems: false))) + #expect(sut.ordersViewState == .loaded(initialOrders, hasMoreItems: false)) } @Test func loadNextOrders_when_simulateFetchNextPage_then_state_is_loaded_with_expected_orders() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() await sut.loadNextOrders() - guard case .loaded(let orders, _) = sut.ordersViewState.ordersState else { + guard case .loaded(let orders, _) = sut.ordersViewState else { Issue.record("Expected loaded OrderList state, but got \(sut.ordersViewState)") return } @@ -123,9 +128,9 @@ final class PointOfSaleOrdersControllerTests { @Test func loadNextOrders_requests_second_page() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - try #require(sut.ordersViewState.containerState == .loading) + try #require(sut.ordersViewState.isLoading) orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() @@ -136,7 +141,7 @@ final class PointOfSaleOrdersControllerTests { @Test func loadNextOrders_when_simulateFetchNextPage_then_state_is_loaded_with_hasMoreItems() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) orderProvider.shouldSimulateTwoPages = true orderProvider.shouldSimulateThreePages = true @@ -144,7 +149,7 @@ final class PointOfSaleOrdersControllerTests { await sut.loadNextOrders() - guard case .loaded(let orders, let hasMoreItems) = sut.ordersViewState.ordersState else { + guard case .loaded(let orders, let hasMoreItems) = sut.ordersViewState else { Issue.record("Expected loaded OrderList state, but got \(sut.ordersViewState)") return } @@ -154,7 +159,7 @@ final class PointOfSaleOrdersControllerTests { @Test func loadNextOrders_when_hasNextPage_is_false_then_does_not_fetch_next_page() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [expectedOrders] @@ -168,7 +173,7 @@ final class PointOfSaleOrdersControllerTests { @Test func refreshOrders_requests_first_page() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() @@ -183,22 +188,22 @@ final class PointOfSaleOrdersControllerTests { @Test func loadOrders_when_error_occurs_then_shows_error_state() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) orderProvider.shouldThrowError = true await sut.loadOrders() - guard case .error = sut.ordersViewState.ordersState else { + guard case .error = sut.ordersViewState else { Issue.record("Expected error OrderList state, but got \(sut.ordersViewState)") return } - #expect(sut.ordersViewState.containerState == .content) + #expect(!sut.ordersViewState.isLoading) } @Test func loadOrders_when_error_occurs_with_existing_orders_then_shows_inline_error() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [initialOrders] @@ -207,7 +212,7 @@ final class PointOfSaleOrdersControllerTests { orderProvider.shouldThrowError = true await sut.refreshOrders() - guard case .inlineError(let orders, _, let context) = sut.ordersViewState.ordersState else { + guard case .inlineError(let orders, _, let context) = sut.ordersViewState else { Issue.record("Expected inlineError OrderList state, but got \(sut.ordersViewState)") return } @@ -217,7 +222,7 @@ final class PointOfSaleOrdersControllerTests { @Test func loadOrders_when_cached_data_available_then_shows_cached_data_with_loading_state() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [initialOrders] @@ -225,8 +230,8 @@ final class PointOfSaleOrdersControllerTests { // First load - should cache the data await sut.loadOrders() - guard case .loaded(let firstLoadOrders, _) = sut.ordersViewState.ordersState else { - Issue.record("Expected loaded state after first load, but got \(sut.ordersViewState.ordersState)") + guard case .loaded(let firstLoadOrders, _) = sut.ordersViewState else { + Issue.record("Expected loaded state after first load, but got \(sut.ordersViewState)") return } #expect(firstLoadOrders == initialOrders) @@ -235,8 +240,8 @@ final class PointOfSaleOrdersControllerTests { await sut.loadOrders() // Should show cached data in loading state, then switch to loaded - guard case .loaded(let cachedOrders, _) = sut.ordersViewState.ordersState else { - Issue.record("Expected loaded state with cached data, but got \(sut.ordersViewState.ordersState)") + guard case .loaded(let cachedOrders, _) = sut.ordersViewState else { + Issue.record("Expected loaded state with cached data, but got \(sut.ordersViewState)") return } #expect(cachedOrders == initialOrders) @@ -244,15 +249,15 @@ final class PointOfSaleOrdersControllerTests { @Test func loadOrders_when_no_cached_data_then_starts_with_empty_loading_state() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [initialOrders] // Initial state should be loading with empty orders - try #require(sut.ordersViewState.containerState == .loading) - guard case .loading(let orders) = sut.ordersViewState.ordersState else { - Issue.record("Expected loading state with empty orders, but got \(sut.ordersViewState.ordersState)") + try #require(sut.ordersViewState.isLoading) + guard case .loading(let orders) = sut.ordersViewState else { + Issue.record("Expected loading state with empty orders, but got \(sut.ordersViewState)") return } #expect(orders.isEmpty) @@ -260,8 +265,8 @@ final class PointOfSaleOrdersControllerTests { await sut.loadOrders() // Should end up in loaded state - guard case .loaded(let loadedOrders, _) = sut.ordersViewState.ordersState else { - Issue.record("Expected loaded state, but got \(sut.ordersViewState.ordersState)") + guard case .loaded(let loadedOrders, _) = sut.ordersViewState else { + Issue.record("Expected loaded state, but got \(sut.ordersViewState)") return } #expect(loadedOrders == initialOrders) @@ -269,7 +274,7 @@ final class PointOfSaleOrdersControllerTests { @Test func loadOrders_cached_data_is_replaced_with_fresh_data() async throws { let orderProvider = MockPointOfSaleOrderService() - let sut = PointOfSaleOrdersController(orderProvider: orderProvider) + let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() let freshOrders = MockPointOfSaleOrderService.makeSecondPageOrders() @@ -278,8 +283,8 @@ final class PointOfSaleOrdersControllerTests { orderProvider.orderPages = [initialOrders] await sut.loadOrders() - guard case .loaded(let firstLoadOrders, _) = sut.ordersViewState.ordersState else { - Issue.record("Expected loaded state after first load, but got \(sut.ordersViewState.ordersState)") + guard case .loaded(let firstLoadOrders, _) = sut.ordersViewState else { + Issue.record("Expected loaded state after first load, but got \(sut.ordersViewState)") return } #expect(firstLoadOrders == initialOrders) @@ -289,8 +294,8 @@ final class PointOfSaleOrdersControllerTests { await sut.loadOrders() // Should end up showing fresh data, not cached data - guard case .loaded(let finalOrders, _) = sut.ordersViewState.ordersState else { - Issue.record("Expected loaded state with fresh data, but got \(sut.ordersViewState.ordersState)") + guard case .loaded(let finalOrders, _) = sut.ordersViewState else { + Issue.record("Expected loaded state with fresh data, but got \(sut.ordersViewState)") return } #expect(finalOrders == freshOrders) From 87384754b1a60f4ea1fbdc099aead52ab994f049 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:00:13 +0300 Subject: [PATCH 12/18] Create dummy UI for Order List and Details view for viewing loaded orders --- .../Orders/PointOfSaleOrderDetailsView.swift | 201 +++++++++++++++--- .../Orders/PointOfSaleOrdersListView.swift | 122 +++++++++-- .../Orders/PointOfSaleOrdersView.swift | 21 +- 3 files changed, 300 insertions(+), 44 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift index 3470c85ee4e..40a148df952 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift @@ -1,23 +1,23 @@ import SwiftUI +import struct Yosemite.POSOrder +import struct Yosemite.POSOrderItem +import struct Yosemite.POSOrderRefund struct PointOfSaleOrderDetailsView: View { - let orderID: String + let orderID: String? let onBack: () -> Void - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - private var orderTitle: String { - switch orderID { - case "order1": - return "Order 1" - case "order2": - return "Order 2" - default: - return "Unknown Order" - } + @Environment(PointOfSaleOrdersModel.self) private var ordersModel + + private var order: POSOrder? { + guard let orderID = orderID, + let orderIDInt = Int64(orderID) else { return nil } + return ordersModel.ordersController.ordersViewState.orders.first { $0.id == orderIDInt } } // Show back button when in compact mode (phone) where the detail view // is presented as a pushed view, not when in regular mode (tablet split view) + @Environment(\.horizontalSizeClass) private var horizontalSizeClass private var shouldShowBackButton: Bool { horizontalSizeClass == .compact } @@ -25,35 +25,178 @@ struct PointOfSaleOrderDetailsView: View { var body: some View { VStack(spacing: 0) { POSPageHeaderView( - title: orderTitle, - backButtonConfiguration: shouldShowBackButton ? - .init(state: .enabled, action: onBack) : nil + title: order.map { "Order #\($0.number)" } ?? "Order Details", + backButtonConfiguration: shouldShowBackButton ? .init(state: .enabled, action: onBack) : nil ) - VStack(alignment: .leading, spacing: 20) { - Text("Order details will be displayed here") - .foregroundColor(.secondary) + if let order = order { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + orderSummarySection(order) + productsSection(order) + totalsSection(order) + } + .padding() + } + } else { + VStack { + Spacer() + Text("Order not found") + Spacer() + } + } + } + .background(Color.posSurface) + .navigationBarHidden(true) + } + + + @ViewBuilder + private func orderSummarySection(_ order: POSOrder) -> some View { + VStack(spacing: 12) { + HStack { + Text("Order #\(order.number)") + .font(.headline) Spacer() + Text(order.status.rawValue) + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.gray.opacity(0.2)) + .cornerRadius(4) + } + + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Date & Time") + .font(.caption) + .foregroundColor(.secondary) + Text(order.dateCreated, style: .date) + Text(order.dateCreated, style: .time) + } + Spacer() + VStack(alignment: .trailing, spacing: 4) { + Text("Total") + .font(.caption) + .foregroundColor(.secondary) + Text("\(order.currencySymbol)\(order.total)") + .font(.title2) + .fontWeight(.semibold) + } + } + + if let customerEmail = order.customerEmail { + HStack { + Text("Customer Email") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text(customerEmail) + } + } + + HStack { + Text("Payment Method") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text(order.paymentMethodTitle) } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } + .padding() .background(Color.posSurface) - .navigationBarHidden(true) + .cornerRadius(12) } -} -#if DEBUG -#Preview("Details - Order 1") { - NavigationStack { - PointOfSaleOrderDetailsView(orderID: "order1", onBack: {}) + @ViewBuilder + private func productsSection(_ order: POSOrder) -> some View { + VStack(alignment: .leading, spacing: 12) { + Text("Products") + .font(.headline) + + VStack(spacing: 8) { + ForEach(order.lineItems, id: \.itemID) { item in + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(item.name) + .fontWeight(.medium) + Text("Qty: \(item.quantity.intValue)") + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + Text("\(order.currencySymbol)\(item.total)") + .fontWeight(.semibold) + } + .padding() + .background(Color.posSurface) + .cornerRadius(8) + } + } + } } -} -#Preview("Details - Order 2") { - NavigationStack { - PointOfSaleOrderDetailsView(orderID: "order2", onBack: {}) + @ViewBuilder + private func totalsSection(_ order: POSOrder) -> some View { + VStack(alignment: .leading, spacing: 12) { + Text("Totals") + .font(.headline) + + VStack(spacing: 8) { + HStack { + Text("Subtotal") + Spacer() + Text("\(order.currencySymbol)\(order.total)") + } + + Divider() + + HStack { + Text("Total") + .fontWeight(.semibold) + Spacer() + Text("\(order.currencySymbol)\(order.total)") + .fontWeight(.semibold) + } + + HStack { + Text("Paid") + Spacer() + Text("\(order.currencySymbol)\(order.total)") + } + + if !order.refunds.isEmpty { + let refundedTotal = order.refunds.reduce(0.0) { $0 + (Double($1.total) ?? 0.0) } + HStack { + Text("Refunded") + Spacer() + Text("-\(order.currencySymbol)\(String(format: "%.2f", refundedTotal))") + .foregroundColor(.red) + } + + let netPayment = (Double(order.total) ?? 0.0) - refundedTotal + HStack { + Text("Net Payment") + .fontWeight(.semibold) + Spacer() + Text("\(order.currencySymbol)\(String(format: "%.2f", netPayment))") + .fontWeight(.semibold) + } + } + } + .padding() + .background(Color.posSurface) + .cornerRadius(8) + } } + +} + + +#if DEBUG +#Preview("Order Details") { + PointOfSaleOrderDetailsView(orderID: "1", onBack: {}) + .environment(POSPreviewHelpers.makePreviewOrdersModel()) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift index 587e904e04c..fc01ea962ea 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift @@ -1,13 +1,16 @@ import SwiftUI +import struct Yosemite.POSOrder struct PointOfSaleOrdersListView: View { @Binding var selectedOrderID: String? let onClose: () -> Void - private let orders = [ - Order(id: "order1", title: "Order 1"), - Order(id: "order2", title: "Order 2") - ] + @Environment(PointOfSaleOrdersModel.self) private var ordersModel + @StateObject private var infiniteScrollTriggerDeterminer = ThresholdInfiniteScrollTriggerDeterminer() + + private var ordersViewState: OrderListState { + ordersModel.ordersController.ordersViewState + } var body: some View { VStack(spacing: 0) { @@ -16,28 +19,119 @@ struct PointOfSaleOrdersListView: View { backButtonConfiguration: .init(state: .enabled, action: onClose, buttonIcon: "xmark") ) - List(orders, id: \.id, selection: $selectedOrderID) { order in - NavigationLink(value: order.id) { - Text(order.title) - .padding(.vertical, 8) + InfiniteScrollView( + triggerDeterminer: infiniteScrollTriggerDeterminer, + loadMore: { + guard case .loaded(_, let hasMoreItems) = ordersViewState, hasMoreItems else { return } + await ordersModel.ordersController.loadNextOrders() + }, + content: { + LazyVStack(spacing: 8) { + headerRows + + switch ordersViewState { + case .empty: + Text("No orders") + case .error(let errorState): + ItemListErrorCardView(errorState: errorState) { + Task { @MainActor in + await ordersModel.ordersController.loadOrders() + } + } + default: + let orders = ordersViewState.orders + ForEach(orders, id: \.id) { order in + Button(action: { + selectedOrderID = String(order.id) + }) { + OrderRowView(order: order, isSelected: selectedOrderID == String(order.id)) + } + .buttonStyle(PlainButtonStyle()) + } + } + + footerRows + } + .padding(.horizontal) } - } - .listStyle(.plain) + ) } .background(Color.posSurfaceBright) .navigationBarHidden(true) + .refreshable { + await ordersModel.ordersController.refreshOrders() + } + .task { + await ordersModel.ordersController.loadOrders() + } + } + + @ViewBuilder + private var headerRows: some View { + switch ordersViewState { + case .inlineError(_, let errorState, .refresh): + ItemListErrorCardView(errorState: errorState) { + Task { @MainActor in + await ordersModel.ordersController.loadOrders() + } + } + default: + EmptyView() + } + } + + @ViewBuilder + private var footerRows: some View { + switch ordersViewState { + case .loading(let orders): + if orders.isEmpty { + ForEach(0..<8, id: \.self) { _ in + GhostItemCardView() + } + } else { + GhostItemCardView() + } + case .inlineError(_, let errorState, .pagination): + ItemListErrorCardView(errorState: errorState) { + Task { @MainActor in + await ordersModel.ordersController.loadNextOrders() + } + } + default: + EmptyView() + } } } -private struct Order { - let id: String - let title: String +private struct OrderRowView: View { + let order: POSOrder + let isSelected: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Order #\(order.number)") + Spacer() + Text("\(order.currencySymbol)\(order.total)") + } + + Text(order.dateCreated, style: .date) + } + .padding() + .background(isSelected ? Color.accentColor.opacity(0.1) : Color.posSurface) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 2) + ) + } } #if DEBUG #Preview("List") { NavigationSplitView { - PointOfSaleOrdersListView(selectedOrderID: .constant("order1"), onClose: {}) + PointOfSaleOrdersListView(selectedOrderID: .constant("1"), onClose: {}) + .environment(POSPreviewHelpers.makePreviewOrdersModel()) } detail: { Text("Detail View") } diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift index 7ff5c4ceb62..9c5e1cbc792 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift @@ -4,6 +4,8 @@ import UIKit struct PointOfSaleOrdersView: View { @Binding var isPresented: Bool @State private var selectedOrderID: String? + @Environment(PointOfSaleOrdersModel.self) private var ordersModel + @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { CustomNavigationSplitView(selection: $selectedOrderID) { _ in @@ -18,7 +20,23 @@ struct PointOfSaleOrdersView: View { } ) } setDefaultValue: { - selectedOrderID = "order1" + if selectedOrderID == nil, + let firstOrder = ordersModel.ordersController.ordersViewState.orders.first { + selectedOrderID = String(firstOrder.id) + } + } + .onChange(of: ordersModel.ordersController.ordersViewState.orders) { oldOrders, newOrders in + guard horizontalSizeClass == .regular else { return } + + guard let firstOrder = newOrders.first else { + return + } + + if let selectedOrderID, newOrders.map(\.number).contains(selectedOrderID) { + return + } + + self.selectedOrderID = String(firstOrder.id) } } } @@ -92,5 +110,6 @@ private enum Constants { #if DEBUG #Preview("Orders View") { PointOfSaleOrdersView(isPresented: .constant(true)) + .environment(POSPreviewHelpers.makePreviewOrdersModel()) } #endif From 34c7e0cb7c762d9f7e7f9ae167c08366639117ab Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:14:13 +0300 Subject: [PATCH 13/18] Update PointOfSaleOrderFetchStrategy.swift --- .../PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift index 634d96db058..8eb40e8afce 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift @@ -8,7 +8,7 @@ public protocol PointOfSaleOrderFetchStrategy { public struct PointOfSaleDefaultOrderFetchStrategy: PointOfSaleOrderFetchStrategy { private let orderService: PointOfSaleOrderServiceProtocol - init(orderService: PointOfSaleOrderServiceProtocol) { + public init(orderService: PointOfSaleOrderServiceProtocol) { self.orderService = orderService } From b6545a53adcbe17ad41b0ad4657045f34fc93375 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:52:55 +0300 Subject: [PATCH 14/18] Remove unused code --- .../PointOfSale/Orders/POSOrderItem.swift | 8 +-- .../PointOfSaleOrderFetchStrategy.swift | 6 +- ...PointOfSaleOrderFetchStrategyFactory.swift | 12 ---- .../PointOfSaleOrderServiceProtocol.swift | 1 - .../PointOfSaleOrdersController.swift | 2 - .../Classes/POS/Models/OrdersViewState.swift | 35 ----------- .../POS/Models/PointOfSaleOrdersModel.swift | 6 +- .../Orders/PointOfSaleOrderDetailsView.swift | 26 ++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 4 ++ .../PointOfSaleOrdersControllerTests.swift | 61 +------------------ ...PointOfSaleOrderFetchStrategyFactory.swift | 27 ++++++++ 11 files changed, 66 insertions(+), 122 deletions(-) create mode 100644 WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderFetchStrategyFactory.swift diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift index 0320de1b862..6913fc821f4 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift @@ -6,18 +6,15 @@ public struct POSOrderItem: Equatable, Hashable { public let name: String public let quantity: Decimal public let total: String - public let sku: String? public init(itemID: Int64, name: String, quantity: Decimal, - total: String, - sku: String? = nil) { + total: String) { self.itemID = itemID self.name = name self.quantity = quantity self.total = total - self.sku = sku } } @@ -28,8 +25,7 @@ public extension POSOrderItem { itemID: orderItem.itemID, name: orderItem.name, quantity: orderItem.quantity, - total: orderItem.total, - sku: orderItem.sku + total: orderItem.total ) } } diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift index 8eb40e8afce..b9b0888fd69 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift @@ -5,14 +5,14 @@ public protocol PointOfSaleOrderFetchStrategy { func fetchOrders(pageNumber: Int) async throws -> PagedItems } -public struct PointOfSaleDefaultOrderFetchStrategy: PointOfSaleOrderFetchStrategy { +struct PointOfSaleDefaultOrderFetchStrategy: PointOfSaleOrderFetchStrategy { private let orderService: PointOfSaleOrderServiceProtocol - public init(orderService: PointOfSaleOrderServiceProtocol) { + init(orderService: PointOfSaleOrderServiceProtocol) { self.orderService = orderService } - public func fetchOrders(pageNumber: Int) async throws -> PagedItems { + func fetchOrders(pageNumber: Int) async throws -> PagedItems { try await orderService.providePointOfSaleOrders(pageNumber: pageNumber) } } diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift index f757e2c7913..06b4aa6d1f5 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift @@ -21,15 +21,3 @@ public final class PointOfSaleOrderFetchStrategyFactory: PointOfSaleOrderFetchSt PointOfSaleDefaultOrderFetchStrategy(orderService: PointOfSaleOrderService(siteID: siteID, ordersRemote: ordersRemote)) } } - -public final class PointOfSaleFixedOrderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol { - private let fixedStrategy: PointOfSaleOrderFetchStrategy - - public init(fixedStrategy: PointOfSaleOrderFetchStrategy) { - self.fixedStrategy = fixedStrategy - } - - public func defaultStrategy() -> PointOfSaleOrderFetchStrategy { - fixedStrategy - } -} diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift index f419c63e0f7..a7e1e0b6c04 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift @@ -4,7 +4,6 @@ import struct NetworkingCore.PagedItems public enum PointOfSaleOrderServiceError: Error, Equatable { case requestFailed case requestCancelled - case unknown } public protocol PointOfSaleOrderServiceProtocol { diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift index 657e79fcdb9..f6257922dce 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift @@ -19,13 +19,11 @@ protocol PointOfSaleOrdersControllerProtocol { @Observable final class PointOfSaleOrdersController: PointOfSaleOrdersControllerProtocol { var ordersViewState: OrderListState private let paginationTracker: AsyncPaginationTracker - private let orderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol private var fetchStrategy: PointOfSaleOrderFetchStrategy private var cachedOrders: [POSOrder] = [] init(orderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol, initialState: OrderListState = .loading([])) { - self.orderFetchStrategyFactory = orderFetchStrategyFactory self.ordersViewState = initialState self.paginationTracker = .init() self.fetchStrategy = orderFetchStrategyFactory.defaultStrategy() diff --git a/WooCommerce/Classes/POS/Models/OrdersViewState.swift b/WooCommerce/Classes/POS/Models/OrdersViewState.swift index 3348d8c846e..a60bb7b2260 100644 --- a/WooCommerce/Classes/POS/Models/OrdersViewState.swift +++ b/WooCommerce/Classes/POS/Models/OrdersViewState.swift @@ -24,41 +24,6 @@ enum OrderListState: Equatable { } } - var isLoaded: Bool { - switch self { - case .loaded: - return true - default: - return false - } - } - - var isInlineError: Bool { - switch self { - case .inlineError: - return true - default: - return false - } - } - - var isError: Bool { - switch self { - case .error: - return true - default: - return false - } - } - - var isEmpty: Bool { - switch self { - case .empty: - return true - default: - return false - } - } var orders: [POSOrder] { switch self { diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift index 771a4356176..95e6cdb332b 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift @@ -1,11 +1,7 @@ import Foundation import Observation -protocol PointOfSaleOrdersModelProtocol { - var ordersController: PointOfSaleOrdersControllerProtocol { get } -} - -@Observable final class PointOfSaleOrdersModel: PointOfSaleOrdersModelProtocol { +@Observable final class PointOfSaleOrdersModel { let ordersController: PointOfSaleOrdersControllerProtocol init(ordersController: PointOfSaleOrdersControllerProtocol) { diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift index 40a148df952..b91c9fe3258 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift @@ -103,6 +103,14 @@ struct PointOfSaleOrderDetailsView: View { Spacer() Text(order.paymentMethodTitle) } + + HStack { + Text("Currency") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text(order.currency) + } } .padding() .background(Color.posSurface) @@ -175,6 +183,24 @@ struct PointOfSaleOrderDetailsView: View { .foregroundColor(.red) } + ForEach(order.refunds, id: \.refundID) { refund in + VStack(alignment: .leading, spacing: 2) { + HStack { + Text("Refund #\(refund.refundID)") + .font(.caption) + Spacer() + Text("-\(order.currencySymbol)\(refund.total)") + .font(.caption) + .foregroundColor(.red) + } + if let reason = refund.reason, !reason.isEmpty { + Text("Reason: \(reason)") + .font(.caption2) + .foregroundColor(.secondary) + } + } + } + let netPayment = (Double(order.total) ?? 0.0) - refundedTotal HStack { Text("Net Payment") diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 6c43eae4644..f72a8de86f0 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 012ACB782E5C84A200A49458 /* OrdersViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB772E5C84A200A49458 /* OrdersViewState.swift */; }; 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */; }; 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift */; }; + 012ACB822E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift */; }; 01309A7F2DC4F39E00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */; }; 01309A812DC4F45300B77527 /* CardPresentModalCardInserted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */; }; 01309A832DC4F89D00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */; }; @@ -3235,6 +3236,7 @@ 012ACB772E5C84A200A49458 /* OrdersViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersViewState.swift; sourceTree = ""; }; 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderService.swift; sourceTree = ""; }; 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersModel.swift; sourceTree = ""; }; + 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderFetchStrategyFactory.swift; sourceTree = ""; }; 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift; sourceTree = ""; }; 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalCardInserted.swift; sourceTree = ""; }; 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageView.swift; sourceTree = ""; }; @@ -7759,6 +7761,7 @@ 02CD3BFC2C35D01600E575C4 /* Mocks */ = { isa = PBXGroup; children = ( + 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift */, 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */, 01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */, 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */, @@ -17090,6 +17093,7 @@ 3198A1E82694DC7200597213 /* MockKnownReadersProvider.swift in Sources */, DEC51B04276B30F6009F3DF4 /* SystemStatusReportViewModelTests.swift in Sources */, 26B119C224D1CD3500FED5C7 /* WooConstantsTests.swift in Sources */, + 012ACB822E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift in Sources */, DE96844D2A332CC2000FBF4E /* ShareProductCoordinatorTests.swift in Sources */, 26100B202722FCAD00473045 /* MockCardPresentPaymentsOnboardingUseCase.swift in Sources */, 263491D5299C923400594566 /* SupportFormViewModelTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift index c358ece6a4f..7a047d8c66d 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift @@ -3,21 +3,14 @@ import Foundation @testable import WooCommerce import enum Yosemite.PointOfSaleOrderServiceError import struct NetworkingCore.Order -import class Yosemite.PointOfSaleFixedOrderFetchStrategyFactory -import struct Yosemite.PointOfSaleDefaultOrderFetchStrategy import Observation final class PointOfSaleOrdersControllerTests { - private func makePointOfSaleOrdersController(orderProvider: MockPointOfSaleOrderService) -> PointOfSaleOrdersController { - let fetchStrategy = PointOfSaleDefaultOrderFetchStrategy(orderService: orderProvider) - let fetchStrategyFactory = PointOfSaleFixedOrderFetchStrategyFactory(fixedStrategy: fetchStrategy) - return PointOfSaleOrdersController(orderFetchStrategyFactory: fetchStrategyFactory) - } + private let orderProvider = MockPointOfSaleOrderService() + private lazy var fetchStrategyFactory = MockPointOfSaleOrderFetchStrategyFactory(orderService: orderProvider) + private lazy var sut = PointOfSaleOrdersController(orderFetchStrategyFactory: fetchStrategyFactory) @Test func loadOrders_requests_first_page_after_loading_two_pages() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - try #require(sut.ordersViewState.isLoading) orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() @@ -31,9 +24,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_results_in_loaded_state() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [expectedOrders] try #require(sut.ordersViewState.isLoading) @@ -44,9 +34,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_with_more_pages_sets_hasMoreItems() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() try #require(sut.ordersViewState.isLoading) orderProvider.shouldSimulateTwoPages = true @@ -57,9 +44,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_called_multiple_times_then_orders_are_not_duplicated() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - try #require(sut.ordersViewState.isLoading) let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [expectedOrders] @@ -76,16 +60,10 @@ final class PointOfSaleOrdersControllerTests { } @Test func container_state_starts_as_loading() { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - #expect(sut.ordersViewState.isLoading) } @Test func loadNextOrders_when_initial_orders_empty_then_container_state_is_content_and_orders_state_is_empty() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - orderProvider.shouldReturnZeroOrders = true try #require(sut.ordersViewState.isLoading) @@ -97,9 +75,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_initial_orders_has_orders_but_no_more_pages_then_state_is_loaded_with_initial_orders() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [initialOrders] @@ -111,9 +86,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadNextOrders_when_simulateFetchNextPage_then_state_is_loaded_with_expected_orders() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() @@ -127,9 +99,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadNextOrders_requests_second_page() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - try #require(sut.ordersViewState.isLoading) orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() @@ -140,9 +109,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadNextOrders_when_simulateFetchNextPage_then_state_is_loaded_with_hasMoreItems() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - orderProvider.shouldSimulateTwoPages = true orderProvider.shouldSimulateThreePages = true await sut.loadOrders() @@ -158,9 +124,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadNextOrders_when_hasNextPage_is_false_then_does_not_fetch_next_page() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [expectedOrders] await sut.loadOrders() @@ -172,9 +135,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func refreshOrders_requests_first_page() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - orderProvider.shouldSimulateTwoPages = true await sut.loadOrders() await sut.loadNextOrders() @@ -187,9 +147,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_error_occurs_then_shows_error_state() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - orderProvider.shouldThrowError = true await sut.loadOrders() @@ -202,9 +159,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_error_occurs_with_existing_orders_then_shows_inline_error() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [initialOrders] await sut.loadOrders() @@ -221,9 +175,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_cached_data_available_then_shows_cached_data_with_loading_state() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [initialOrders] @@ -248,9 +199,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_no_cached_data_then_starts_with_empty_loading_state() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() orderProvider.orderPages = [initialOrders] @@ -273,9 +221,6 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_cached_data_is_replaced_with_fresh_data() async throws { - let orderProvider = MockPointOfSaleOrderService() - let sut = makePointOfSaleOrdersController(orderProvider: orderProvider) - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() let freshOrders = MockPointOfSaleOrderService.makeSecondPageOrders() diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderFetchStrategyFactory.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderFetchStrategyFactory.swift new file mode 100644 index 00000000000..e2baf5f5ebf --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderFetchStrategyFactory.swift @@ -0,0 +1,27 @@ +import Foundation +@testable import WooCommerce +import protocol Yosemite.PointOfSaleOrderFetchStrategyFactoryProtocol +import protocol Yosemite.PointOfSaleOrderFetchStrategy +import protocol Yosemite.PointOfSaleOrderServiceProtocol +import struct NetworkingCore.PagedItems +import struct Yosemite.POSOrder + +final class MockPointOfSaleOrderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol { + private let orderService: PointOfSaleOrderServiceProtocol + + init(orderService: PointOfSaleOrderServiceProtocol) { + self.orderService = orderService + } + + func defaultStrategy() -> PointOfSaleOrderFetchStrategy { + MockPointOfSaleOrderFetchStrategy(orderService: orderService) + } +} + +private struct MockPointOfSaleOrderFetchStrategy: PointOfSaleOrderFetchStrategy { + let orderService: PointOfSaleOrderServiceProtocol + + func fetchOrders(pageNumber: Int) async throws -> PagedItems { + try await orderService.providePointOfSaleOrders(pageNumber: pageNumber) + } +} From 73434abf787b2ed331ef665240da51923983d8ef Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:30:57 +0300 Subject: [PATCH 15/18] Simplify new orders appending logic --- .../PointOfSaleOrdersController.swift | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift index f6257922dce..5bc7c04c9f4 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift @@ -92,27 +92,18 @@ protocol PointOfSaleOrdersControllerProtocol { do { let pagedOrders = try await fetchStrategy.fetchOrders(pageNumber: pageNumber) - let newOrders = pagedOrders.items - var allOrders = appendToExistingOrders ? ordersViewState.orders : [] - let uniqueNewOrders = newOrders.filter { newOrder in - !allOrders.contains(where: { $0.id == newOrder.id }) + let existingOrders = appendToExistingOrders ? ordersViewState.orders : [] + let uniqueNewOrders = pagedOrders.items.filter { newOrder in + !existingOrders.contains(where: { $0.id == newOrder.id }) } + let allOrders = appendToExistingOrders ? existingOrders + uniqueNewOrders : uniqueNewOrders - if appendToExistingOrders && !uniqueNewOrders.isEmpty { - allOrders.append(contentsOf: uniqueNewOrders) - } else if !appendToExistingOrders { - allOrders = uniqueNewOrders - } - - if allOrders.isEmpty { - ordersViewState = .empty - } else { - ordersViewState = .loaded(allOrders, hasMoreItems: pagedOrders.hasMorePages) + ordersViewState = allOrders.isEmpty ? .empty : .loaded(allOrders, hasMoreItems: pagedOrders.hasMorePages) - if pageNumber == 1 && !appendToExistingOrders { - cachedOrders = allOrders - } + if pageNumber == 1 && !appendToExistingOrders { + cachedOrders = allOrders } + return pagedOrders.hasMorePages } catch PointOfSaleOrderServiceError.requestCancelled { return true From b8423db9960a20c427492396322c6f4fa8a6aa17 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:10:31 +0300 Subject: [PATCH 16/18] Add a loading indicator to POSPageHeaderView --- .../Reusable Views/POSPageHeaderView.swift | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSPageHeaderView.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSPageHeaderView.swift index 9403eae9faa..35218b1c616 100644 --- a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSPageHeaderView.swift +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSPageHeaderView.swift @@ -24,12 +24,14 @@ struct POSPageHeaderItem: Identifiable { let title: String let subtitle: String? let isSelected: Bool + let isLoading: Bool let action: (() -> Void)? - init(title: String, subtitle: String? = nil, isSelected: Bool, action: (() -> Void)? = nil) { + init(title: String, subtitle: String? = nil, isSelected: Bool, isLoading: Bool = false, action: (() -> Void)? = nil) { self.title = title self.subtitle = subtitle self.isSelected = isSelected + self.isLoading = isLoading self.action = action } } @@ -52,10 +54,11 @@ struct POSPageHeaderView: View { init( title: String, subtitle: String? = nil, + isLoading: Bool = false, backButtonConfiguration: POSPageHeaderBackButtonConfiguration? = nil, @ViewBuilder trailingContent: () -> TrailingContent = { EmptyView() } ) { - self.items = [.init(title: title, subtitle: subtitle, isSelected: true)] + self.items = [.init(title: title, subtitle: subtitle, isSelected: true, isLoading: isLoading)] self.backButtonConfiguration = backButtonConfiguration self.trailingContent = trailingContent() } @@ -79,20 +82,29 @@ struct POSPageHeaderView: View { HStack(alignment: hStackAlignment, spacing: POSSpacing.large) { ForEach(0.. Date: Tue, 26 Aug 2025 16:10:48 +0300 Subject: [PATCH 17/18] Display a small loading indicator when orders are cached but a new data is loading --- .../POS/Presentation/Orders/PointOfSaleOrdersListView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift index fc01ea962ea..b9651fac5cc 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift @@ -16,6 +16,12 @@ struct PointOfSaleOrdersListView: View { VStack(spacing: 0) { POSPageHeaderView( title: "Orders", + isLoading: { + if case .loading(let orders) = ordersViewState { + return !orders.isEmpty + } + return false + }(), backButtonConfiguration: .init(state: .enabled, action: onClose, buttonIcon: "xmark") ) From 6f5e548410d44bddef97cb521126654a990be55e Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 27 Aug 2025 10:53:29 +0300 Subject: [PATCH 18/18] Rename PointOfSaleOrders to PointOfSaleOrderList --- .../{Orders => OrderList}/POSOrder.swift | 0 .../{Orders => OrderList}/POSOrderItem.swift | 0 .../POSOrderRefund.swift | 0 .../PointOfSaleOrderListFetchStrategy.swift | 18 ++++ ...tOfSaleOrderListFetchStrategyFactory.swift | 24 ++++++ .../PointOfSaleOrderListService.swift} | 6 +- ...PointOfSaleOrderListServiceProtocol.swift} | 4 +- .../PointOfSaleOrderFetchStrategy.swift | 18 ---- ...PointOfSaleOrderFetchStrategyFactory.swift | 23 ------ .../PointOfSaleOrderServiceTests.swift | 10 +-- ...t => PointOfSaleOrderListController.swift} | 20 ++--- .../Models/PointOfSaleOrderListModel.swift | 10 +++ .../POS/Models/PointOfSaleOrdersModel.swift | 10 --- .../Orders/PointOfSaleOrderDetailsView.swift | 4 +- ...w.swift => PointOfSaleOrderListView.swift} | 20 ++--- .../Orders/PointOfSaleOrdersView.swift | 8 +- .../PointOfSaleEntryPointView.swift | 6 +- .../POS/TabBar/POSTabCoordinator.swift | 4 +- .../Classes/POS/Utils/PreviewHelpers.swift | 10 +-- .../WooCommerce.xcodeproj/project.pbxproj | 56 ++++++------- ...PointOfSaleOrderListControllerTests.swift} | 82 +++++++++---------- ...PointOfSaleOrderFetchStrategyFactory.swift | 27 ------ ... MockPointOfSaleOrderListController.swift} | 2 +- ...tOfSaleOrderListFetchStrategyFactory.swift | 27 ++++++ ... => MockPointOfSaleOrderListService.swift} | 12 +-- .../PointOfSaleAggregateModelTests.swift | 12 +-- .../POSItemActionHandlerTests.swift | 2 +- 27 files changed, 208 insertions(+), 207 deletions(-) rename Modules/Sources/Yosemite/PointOfSale/{Orders => OrderList}/POSOrder.swift (100%) rename Modules/Sources/Yosemite/PointOfSale/{Orders => OrderList}/POSOrderItem.swift (100%) rename Modules/Sources/Yosemite/PointOfSale/{Orders => OrderList}/POSOrderRefund.swift (100%) create mode 100644 Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategy.swift create mode 100644 Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift rename Modules/Sources/Yosemite/PointOfSale/{Orders/PointOfSaleOrderService.swift => OrderList/PointOfSaleOrderListService.swift} (85%) rename Modules/Sources/Yosemite/PointOfSale/{Orders/PointOfSaleOrderServiceProtocol.swift => OrderList/PointOfSaleOrderListServiceProtocol.swift} (62%) delete mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift delete mode 100644 Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift rename WooCommerce/Classes/POS/Controllers/{PointOfSaleOrdersController.swift => PointOfSaleOrderListController.swift} (84%) create mode 100644 WooCommerce/Classes/POS/Models/PointOfSaleOrderListModel.swift delete mode 100644 WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift rename WooCommerce/Classes/POS/Presentation/Orders/{PointOfSaleOrdersListView.swift => PointOfSaleOrderListView.swift} (85%) rename WooCommerce/WooCommerceTests/POS/Controllers/{PointOfSaleOrdersControllerTests.swift => PointOfSaleOrderListControllerTests.swift} (72%) delete mode 100644 WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderFetchStrategyFactory.swift rename WooCommerce/WooCommerceTests/POS/Mocks/{MockPointOfSaleOrderController.swift => MockPointOfSaleOrderListController.swift} (94%) create mode 100644 WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListFetchStrategyFactory.swift rename WooCommerce/WooCommerceTests/POS/Mocks/{MockPointOfSaleOrderService.swift => MockPointOfSaleOrderListService.swift} (86%) diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrder.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrder.swift similarity index 100% rename from Modules/Sources/Yosemite/PointOfSale/Orders/POSOrder.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrder.swift diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderItem.swift similarity index 100% rename from Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderItem.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderItem.swift diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderRefund.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderRefund.swift similarity index 100% rename from Modules/Sources/Yosemite/PointOfSale/Orders/POSOrderRefund.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderRefund.swift diff --git a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategy.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategy.swift new file mode 100644 index 00000000000..f59b1931f39 --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategy.swift @@ -0,0 +1,18 @@ +import Foundation +import struct NetworkingCore.PagedItems + +public protocol PointOfSaleOrderListFetchStrategy { + func fetchOrders(pageNumber: Int) async throws -> PagedItems +} + +struct PointOfSaleDefaultOrderListFetchStrategy: PointOfSaleOrderListFetchStrategy { + private let orderListService: PointOfSaleOrderListServiceProtocol + + init(orderListService: PointOfSaleOrderListServiceProtocol) { + self.orderListService = orderListService + } + + func fetchOrders(pageNumber: Int) async throws -> PagedItems { + try await orderListService.providePointOfSaleOrders(pageNumber: pageNumber) + } +} diff --git a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift new file mode 100644 index 00000000000..51df3241284 --- /dev/null +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift @@ -0,0 +1,24 @@ +import Foundation +import class Networking.AlamofireNetwork +import class Networking.OrdersRemote + +public protocol PointOfSaleOrderListFetchStrategyFactoryProtocol { + func defaultStrategy() -> PointOfSaleOrderListFetchStrategy +} + +public final class PointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol { + private let siteID: Int64 + private let ordersRemote: OrdersRemote + + public init(siteID: Int64, + credentials: Credentials?) { + self.siteID = siteID + let network = AlamofireNetwork(credentials: credentials) + self.ordersRemote = OrdersRemote(network: network) + } + + public func defaultStrategy() -> PointOfSaleOrderListFetchStrategy { + PointOfSaleDefaultOrderListFetchStrategy(orderListService: PointOfSaleOrderListService(siteID: siteID, + ordersRemote: ordersRemote)) + } +} diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListService.swift similarity index 85% rename from Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListService.swift index 6701fb0d1ab..9b8ad7ff7a9 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListService.swift @@ -4,7 +4,7 @@ import struct NetworkingCore.PagedItems import struct NetworkingCore.Order import protocol NetworkingCore.POSOrdersRemoteProtocol -public final class PointOfSaleOrderService: PointOfSaleOrderServiceProtocol { +public final class PointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol { private let ordersRemote: POSOrdersRemoteProtocol private let siteID: Int64 @@ -32,9 +32,9 @@ public final class PointOfSaleOrderService: PointOfSaleOrderServiceProtocol { hasMorePages: pagedOrders.hasMorePages, totalItems: pagedOrders.totalItems) } catch AFError.explicitlyCancelled { - throw PointOfSaleOrderServiceError.requestCancelled + throw PointOfSaleOrderListServiceError.requestCancelled } catch { - throw PointOfSaleOrderServiceError.requestFailed + throw PointOfSaleOrderListServiceError.requestFailed } } } diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListServiceProtocol.swift similarity index 62% rename from Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListServiceProtocol.swift index a7e1e0b6c04..c80cfe9a0e3 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderServiceProtocol.swift +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListServiceProtocol.swift @@ -1,11 +1,11 @@ import Foundation import struct NetworkingCore.PagedItems -public enum PointOfSaleOrderServiceError: Error, Equatable { +public enum PointOfSaleOrderListServiceError: Error, Equatable { case requestFailed case requestCancelled } -public protocol PointOfSaleOrderServiceProtocol { +public protocol PointOfSaleOrderListServiceProtocol { func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems } diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift deleted file mode 100644 index b9b0888fd69..00000000000 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategy.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import struct NetworkingCore.PagedItems - -public protocol PointOfSaleOrderFetchStrategy { - func fetchOrders(pageNumber: Int) async throws -> PagedItems -} - -struct PointOfSaleDefaultOrderFetchStrategy: PointOfSaleOrderFetchStrategy { - private let orderService: PointOfSaleOrderServiceProtocol - - init(orderService: PointOfSaleOrderServiceProtocol) { - self.orderService = orderService - } - - func fetchOrders(pageNumber: Int) async throws -> PagedItems { - try await orderService.providePointOfSaleOrders(pageNumber: pageNumber) - } -} diff --git a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift deleted file mode 100644 index 06b4aa6d1f5..00000000000 --- a/Modules/Sources/Yosemite/PointOfSale/Orders/PointOfSaleOrderFetchStrategyFactory.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import class Networking.AlamofireNetwork -import class Networking.OrdersRemote - -public protocol PointOfSaleOrderFetchStrategyFactoryProtocol { - func defaultStrategy() -> PointOfSaleOrderFetchStrategy -} - -public final class PointOfSaleOrderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol { - private let siteID: Int64 - private let ordersRemote: OrdersRemote - - public init(siteID: Int64, - credentials: Credentials?) { - self.siteID = siteID - let network = AlamofireNetwork(credentials: credentials) - self.ordersRemote = OrdersRemote(network: network) - } - - public func defaultStrategy() -> PointOfSaleOrderFetchStrategy { - PointOfSaleDefaultOrderFetchStrategy(orderService: PointOfSaleOrderService(siteID: siteID, ordersRemote: ordersRemote)) - } -} diff --git a/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift b/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift index 9d3d5c66dd1..cda5bc77cab 100644 --- a/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift +++ b/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift @@ -6,13 +6,13 @@ import enum NetworkingCore.OrderStatusEnum final class PointOfSaleOrderServiceTests: XCTestCase { private let siteID: Int64 = 13092 - private var orderProvider: PointOfSaleOrderServiceProtocol! + private var orderProvider: PointOfSaleOrderListServiceProtocol! private var mockOrdersRemote: MockPOSOrdersRemote! override func setUp() { super.setUp() mockOrdersRemote = MockPOSOrdersRemote() - orderProvider = PointOfSaleOrderService(siteID: siteID, ordersRemote: mockOrdersRemote) + orderProvider = PointOfSaleOrderListService(siteID: siteID, ordersRemote: mockOrdersRemote) } override func tearDown() { @@ -22,14 +22,14 @@ final class PointOfSaleOrderServiceTests: XCTestCase { } func test_PointOfSaleOrderServiceProtocol_when_fails_request_with_requestFailed_then_throws_error() async throws { - let expectedError = PointOfSaleOrderServiceError.requestFailed + let expectedError = PointOfSaleOrderListServiceError.requestFailed mockOrdersRemote.mockPagedOrdersResult = .failure(expectedError) do { _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) XCTFail("Expected an error, but got success.") } catch { - XCTAssertEqual(error as? PointOfSaleOrderServiceError, expectedError) + XCTAssertEqual(error as? PointOfSaleOrderListServiceError, expectedError) } } @@ -106,7 +106,7 @@ final class PointOfSaleOrderServiceTests: XCTestCase { do { _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) XCTFail("Expected error to be thrown") - } catch PointOfSaleOrderServiceError.requestFailed { + } catch PointOfSaleOrderListServiceError.requestFailed { // Expected } catch { XCTFail("Unexpected error occurred: \(error)") diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderListController.swift similarity index 84% rename from WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift rename to WooCommerce/Classes/POS/Controllers/PointOfSaleOrderListController.swift index 5bc7c04c9f4..0a110f83441 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrdersController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderListController.swift @@ -1,32 +1,32 @@ import Foundation import Observation -import enum Yosemite.PointOfSaleOrderServiceError -import protocol Yosemite.PointOfSaleOrderServiceProtocol -import protocol Yosemite.PointOfSaleOrderFetchStrategyFactoryProtocol -import protocol Yosemite.PointOfSaleOrderFetchStrategy +import enum Yosemite.PointOfSaleOrderListServiceError +import protocol Yosemite.PointOfSaleOrderListServiceProtocol +import protocol Yosemite.PointOfSaleOrderListFetchStrategyFactoryProtocol +import protocol Yosemite.PointOfSaleOrderListFetchStrategy import struct Yosemite.POSOrder import struct Yosemite.POSOrderItem import struct Yosemite.POSOrderRefund import class Yosemite.Store -protocol PointOfSaleOrdersControllerProtocol { +protocol PointOfSaleOrderListControllerProtocol { var ordersViewState: OrderListState { get } func loadOrders() async func refreshOrders() async func loadNextOrders() async } -@Observable final class PointOfSaleOrdersController: PointOfSaleOrdersControllerProtocol { +@Observable final class PointOfSaleOrderListController: PointOfSaleOrderListControllerProtocol { var ordersViewState: OrderListState private let paginationTracker: AsyncPaginationTracker - private var fetchStrategy: PointOfSaleOrderFetchStrategy + private var fetchStrategy: PointOfSaleOrderListFetchStrategy private var cachedOrders: [POSOrder] = [] - init(orderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol, + init(orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol, initialState: OrderListState = .loading([])) { self.ordersViewState = initialState self.paginationTracker = .init() - self.fetchStrategy = orderFetchStrategyFactory.defaultStrategy() + self.fetchStrategy = orderListFetchStrategyFactory.defaultStrategy() } @MainActor @@ -105,7 +105,7 @@ protocol PointOfSaleOrdersControllerProtocol { } return pagedOrders.hasMorePages - } catch PointOfSaleOrderServiceError.requestCancelled { + } catch PointOfSaleOrderListServiceError.requestCancelled { return true } } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleOrderListModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleOrderListModel.swift new file mode 100644 index 00000000000..ffc0b28d6e4 --- /dev/null +++ b/WooCommerce/Classes/POS/Models/PointOfSaleOrderListModel.swift @@ -0,0 +1,10 @@ +import Foundation +import Observation + +@Observable final class PointOfSaleOrderListModel { + let ordersController: PointOfSaleOrderListControllerProtocol + + init(ordersController: PointOfSaleOrderListControllerProtocol) { + self.ordersController = ordersController + } +} diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift deleted file mode 100644 index 95e6cdb332b..00000000000 --- a/WooCommerce/Classes/POS/Models/PointOfSaleOrdersModel.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import Observation - -@Observable final class PointOfSaleOrdersModel { - let ordersController: PointOfSaleOrdersControllerProtocol - - init(ordersController: PointOfSaleOrdersControllerProtocol) { - self.ordersController = ordersController - } -} diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift index b91c9fe3258..a1b3c2c9d31 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift @@ -7,12 +7,12 @@ struct PointOfSaleOrderDetailsView: View { let orderID: String? let onBack: () -> Void - @Environment(PointOfSaleOrdersModel.self) private var ordersModel + @Environment(PointOfSaleOrderListModel.self) private var orderListModel private var order: POSOrder? { guard let orderID = orderID, let orderIDInt = Int64(orderID) else { return nil } - return ordersModel.ordersController.ordersViewState.orders.first { $0.id == orderIDInt } + return orderListModel.ordersController.ordersViewState.orders.first { $0.id == orderIDInt } } // Show back button when in compact mode (phone) where the detail view diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderListView.swift similarity index 85% rename from WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift rename to WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderListView.swift index b9651fac5cc..9bb0d2c9d71 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersListView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderListView.swift @@ -1,15 +1,15 @@ import SwiftUI import struct Yosemite.POSOrder -struct PointOfSaleOrdersListView: View { +struct PointOfSaleOrderListView: View { @Binding var selectedOrderID: String? let onClose: () -> Void - @Environment(PointOfSaleOrdersModel.self) private var ordersModel + @Environment(PointOfSaleOrderListModel.self) private var orderListModel @StateObject private var infiniteScrollTriggerDeterminer = ThresholdInfiniteScrollTriggerDeterminer() private var ordersViewState: OrderListState { - ordersModel.ordersController.ordersViewState + orderListModel.ordersController.ordersViewState } var body: some View { @@ -29,7 +29,7 @@ struct PointOfSaleOrdersListView: View { triggerDeterminer: infiniteScrollTriggerDeterminer, loadMore: { guard case .loaded(_, let hasMoreItems) = ordersViewState, hasMoreItems else { return } - await ordersModel.ordersController.loadNextOrders() + await orderListModel.ordersController.loadNextOrders() }, content: { LazyVStack(spacing: 8) { @@ -41,7 +41,7 @@ struct PointOfSaleOrdersListView: View { case .error(let errorState): ItemListErrorCardView(errorState: errorState) { Task { @MainActor in - await ordersModel.ordersController.loadOrders() + await orderListModel.ordersController.loadOrders() } } default: @@ -65,10 +65,10 @@ struct PointOfSaleOrdersListView: View { .background(Color.posSurfaceBright) .navigationBarHidden(true) .refreshable { - await ordersModel.ordersController.refreshOrders() + await orderListModel.ordersController.refreshOrders() } .task { - await ordersModel.ordersController.loadOrders() + await orderListModel.ordersController.loadOrders() } } @@ -78,7 +78,7 @@ struct PointOfSaleOrdersListView: View { case .inlineError(_, let errorState, .refresh): ItemListErrorCardView(errorState: errorState) { Task { @MainActor in - await ordersModel.ordersController.loadOrders() + await orderListModel.ordersController.loadOrders() } } default: @@ -100,7 +100,7 @@ struct PointOfSaleOrdersListView: View { case .inlineError(_, let errorState, .pagination): ItemListErrorCardView(errorState: errorState) { Task { @MainActor in - await ordersModel.ordersController.loadNextOrders() + await orderListModel.ordersController.loadNextOrders() } } default: @@ -136,7 +136,7 @@ private struct OrderRowView: View { #if DEBUG #Preview("List") { NavigationSplitView { - PointOfSaleOrdersListView(selectedOrderID: .constant("1"), onClose: {}) + PointOfSaleOrderListView(selectedOrderID: .constant("1"), onClose: {}) .environment(POSPreviewHelpers.makePreviewOrdersModel()) } detail: { Text("Detail View") diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift index 9c5e1cbc792..1929b233b79 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift @@ -4,12 +4,12 @@ import UIKit struct PointOfSaleOrdersView: View { @Binding var isPresented: Bool @State private var selectedOrderID: String? - @Environment(PointOfSaleOrdersModel.self) private var ordersModel + @Environment(PointOfSaleOrderListModel.self) private var orderListModel @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { CustomNavigationSplitView(selection: $selectedOrderID) { _ in - PointOfSaleOrdersListView(selectedOrderID: $selectedOrderID) { + PointOfSaleOrderListView(selectedOrderID: $selectedOrderID) { isPresented = false } } detail: { selection in @@ -21,11 +21,11 @@ struct PointOfSaleOrdersView: View { ) } setDefaultValue: { if selectedOrderID == nil, - let firstOrder = ordersModel.ordersController.ordersViewState.orders.first { + let firstOrder = orderListModel.ordersController.ordersViewState.orders.first { selectedOrderID = String(firstOrder.id) } } - .onChange(of: ordersModel.ordersController.ordersViewState.orders) { oldOrders, newOrders in + .onChange(of: orderListModel.ordersController.ordersViewState.orders) { oldOrders, newOrders in guard horizontalSizeClass == .regular else { return } guard let firstOrder = newOrders.first else { diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index a53572eb47e..a4a359e8863 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -15,7 +15,7 @@ struct PointOfSaleEntryPointView: View { private let purchasableItemsSearchController: PointOfSaleSearchingItemsControllerProtocol private let couponsController: PointOfSaleCouponsControllerProtocol private let couponsSearchController: PointOfSaleSearchingItemsControllerProtocol - private let ordersController: PointOfSaleOrdersControllerProtocol + private let ordersController: PointOfSaleOrderListControllerProtocol private let cardPresentPaymentService: CardPresentPaymentFacade private let orderController: PointOfSaleOrderControllerProtocol private let collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking @@ -27,7 +27,7 @@ struct PointOfSaleEntryPointView: View { purchasableItemsSearchController: PointOfSaleSearchingItemsControllerProtocol, couponsController: PointOfSaleCouponsControllerProtocol, couponsSearchController: PointOfSaleSearchingItemsControllerProtocol, - ordersController: PointOfSaleOrdersControllerProtocol, + ordersController: PointOfSaleOrderListControllerProtocol, onPointOfSaleModeActiveStateChange: @escaping ((Bool) -> Void), cardPresentPaymentService: CardPresentPaymentFacade, orderController: PointOfSaleOrderControllerProtocol, @@ -81,7 +81,7 @@ struct PointOfSaleEntryPointView: View { .environmentObject(posModalManager) .environmentObject(posSheetManager) .environmentObject(posCoverManager) - .environment(PointOfSaleOrdersModel(ordersController: ordersController)) + .environment(PointOfSaleOrderListModel(ordersController: ordersController)) .injectKeyboardObserver() .onAppear { onPointOfSaleModeActiveStateChange(true) diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 5e10f85978d..50fe616d3b4 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -112,8 +112,8 @@ private extension POSTabCoordinator { fetchStrategyFactory: posCouponFetchStrategyFactory), couponsSearchController: PointOfSaleCouponsController(itemProvider: posCouponProvider, fetchStrategyFactory: posCouponFetchStrategyFactory), - ordersController: PointOfSaleOrdersController( - orderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactory(siteID: siteID, + ordersController: PointOfSaleOrderListController( + orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactory(siteID: siteID, credentials: credentials) ), diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index 8e9d4e5f039..a6df01878a0 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -23,8 +23,8 @@ import enum Yosemite.PointOfSaleBarcodeScanError import Combine import struct Yosemite.PaymentIntent import struct Yosemite.POSOrder -import class Yosemite.PointOfSaleOrderService -import class Yosemite.PointOfSaleOrderFetchStrategyFactory +import class Yosemite.PointOfSaleOrderListService +import class Yosemite.PointOfSaleOrderListFetchStrategyFactory // MARK: - PreviewProvider helpers // @@ -232,13 +232,13 @@ struct POSPreviewHelpers { ) } - static func makePreviewOrdersModel() -> PointOfSaleOrdersModel { - return PointOfSaleOrdersModel(ordersController: PointOfSalePreviewOrdersController()) + static func makePreviewOrdersModel() -> PointOfSaleOrderListModel { + return PointOfSaleOrderListModel(ordersController: PointOfSalePreviewOrderListController()) } } // MARK: - Preview Orders Controller -final class PointOfSalePreviewOrdersController: PointOfSaleOrdersControllerProtocol { +final class PointOfSalePreviewOrderListController: PointOfSaleOrderListControllerProtocol { var ordersViewState: OrderListState { .loaded( [ diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index f72a8de86f0..2bcd3ce289e 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -31,12 +31,12 @@ 011D7A352CEC87B70007C187 /* CardPresentModalErrorEmailSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011D7A342CEC87B60007C187 /* CardPresentModalErrorEmailSent.swift */; }; 011DF3442C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DF3432C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift */; }; 011DF3462C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DF3452C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift */; }; - 012ACB742E5C830500A49458 /* PointOfSaleOrdersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB732E5C830500A49458 /* PointOfSaleOrdersController.swift */; }; - 012ACB762E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB752E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift */; }; + 012ACB742E5C830500A49458 /* PointOfSaleOrderListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB732E5C830500A49458 /* PointOfSaleOrderListController.swift */; }; + 012ACB762E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB752E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift */; }; 012ACB782E5C84A200A49458 /* OrdersViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB772E5C84A200A49458 /* OrdersViewState.swift */; }; - 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */; }; - 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift */; }; - 012ACB822E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift */; }; + 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderListService.swift */; }; + 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift */; }; + 012ACB822E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift */; }; 01309A7F2DC4F39E00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */; }; 01309A812DC4F45300B77527 /* CardPresentModalCardInserted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */; }; 01309A832DC4F89D00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */; }; @@ -87,7 +87,7 @@ 01AB2D122DDC7AD300AA67FD /* PointOfSaleItemListAnalyticsTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AB2D112DDC7AD100AA67FD /* PointOfSaleItemListAnalyticsTrackerTests.swift */; }; 01AB2D142DDC7CD200AA67FD /* POSItemActionHandlerFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AB2D132DDC7CD000AA67FD /* POSItemActionHandlerFactoryTests.swift */; }; 01AB2D162DDC8CDA00AA67FD /* MockAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */; }; - 01ABA0282E57579300829DC0 /* PointOfSaleOrdersListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0252E57579300829DC0 /* PointOfSaleOrdersListView.swift */; }; + 01ABA0282E57579300829DC0 /* PointOfSaleOrderListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0252E57579300829DC0 /* PointOfSaleOrderListView.swift */; }; 01ABA0292E57579300829DC0 /* PointOfSaleOrderDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0242E57579300829DC0 /* PointOfSaleOrderDetailsView.swift */; }; 01ABA02A2E57579300829DC0 /* PointOfSaleOrdersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0262E57579300829DC0 /* PointOfSaleOrdersView.swift */; }; 01ADC1362C9AB4810036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ADC1352C9AB4810036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageViewModel.swift */; }; @@ -964,7 +964,7 @@ 20D5CB532AFCF8E7009A39C3 /* PaymentsToggleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D5CB522AFCF8E7009A39C3 /* PaymentsToggleRow.swift */; }; 20D920EA2CEF86520023B089 /* PointOfSaleErrorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D920E92CEF86520023B089 /* PointOfSaleErrorState.swift */; }; 20DA6DDB2B681175002AA0FB /* AdaptiveModalContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DA6DDA2B681175002AA0FB /* AdaptiveModalContainer.swift */; }; - 20DB185B2CF5D9220018D3E1 /* MockPointOfSaleOrderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DB185A2CF5D9220018D3E1 /* MockPointOfSaleOrderController.swift */; }; + 20DB185B2CF5D9220018D3E1 /* MockPointOfSaleOrderListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DB185A2CF5D9220018D3E1 /* MockPointOfSaleOrderListController.swift */; }; 20DB185D2CF5E7630018D3E1 /* PointOfSaleOrderControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DB185C2CF5E7560018D3E1 /* PointOfSaleOrderControllerTests.swift */; }; 20E188842AD059A50053E945 /* TapToPayEducationContactlessLimitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E188832AD059A50053E945 /* TapToPayEducationContactlessLimitView.swift */; }; 20EFAEA62D35337F00D35F9C /* ItemListErrorCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20EFAEA52D35337F00D35F9C /* ItemListErrorCardView.swift */; }; @@ -3231,12 +3231,12 @@ 011D7A342CEC87B60007C187 /* CardPresentModalErrorEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalErrorEmailSent.swift; sourceTree = ""; }; 011DF3432C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift; sourceTree = ""; }; 011DF3452C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift; sourceTree = ""; }; - 012ACB732E5C830500A49458 /* PointOfSaleOrdersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersController.swift; sourceTree = ""; }; - 012ACB752E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersControllerTests.swift; sourceTree = ""; }; + 012ACB732E5C830500A49458 /* PointOfSaleOrderListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListController.swift; sourceTree = ""; }; + 012ACB752E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListControllerTests.swift; sourceTree = ""; }; 012ACB772E5C84A200A49458 /* OrdersViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersViewState.swift; sourceTree = ""; }; - 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderService.swift; sourceTree = ""; }; - 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersModel.swift; sourceTree = ""; }; - 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderFetchStrategyFactory.swift; sourceTree = ""; }; + 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderListService.swift; sourceTree = ""; }; + 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListModel.swift; sourceTree = ""; }; + 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderListFetchStrategyFactory.swift; sourceTree = ""; }; 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift; sourceTree = ""; }; 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalCardInserted.swift; sourceTree = ""; }; 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageView.swift; sourceTree = ""; }; @@ -3288,7 +3288,7 @@ 01AB2D132DDC7CD000AA67FD /* POSItemActionHandlerFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSItemActionHandlerFactoryTests.swift; sourceTree = ""; }; 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAnalytics.swift; sourceTree = ""; }; 01ABA0242E57579300829DC0 /* PointOfSaleOrderDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderDetailsView.swift; sourceTree = ""; }; - 01ABA0252E57579300829DC0 /* PointOfSaleOrdersListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersListView.swift; sourceTree = ""; }; + 01ABA0252E57579300829DC0 /* PointOfSaleOrderListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListView.swift; sourceTree = ""; }; 01ABA0262E57579300829DC0 /* PointOfSaleOrdersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersView.swift; sourceTree = ""; }; 01ADC1352C9AB4810036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentIntentCreationErrorMessageViewModel.swift; sourceTree = ""; }; 01ADC1372C9AB6050036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift; sourceTree = ""; }; @@ -4171,7 +4171,7 @@ 20D5CB522AFCF8E7009A39C3 /* PaymentsToggleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsToggleRow.swift; sourceTree = ""; }; 20D920E92CEF86520023B089 /* PointOfSaleErrorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleErrorState.swift; sourceTree = ""; }; 20DA6DDA2B681175002AA0FB /* AdaptiveModalContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveModalContainer.swift; sourceTree = ""; }; - 20DB185A2CF5D9220018D3E1 /* MockPointOfSaleOrderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderController.swift; sourceTree = ""; }; + 20DB185A2CF5D9220018D3E1 /* MockPointOfSaleOrderListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderListController.swift; sourceTree = ""; }; 20DB185C2CF5E7560018D3E1 /* PointOfSaleOrderControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderControllerTests.swift; sourceTree = ""; }; 20E014E12CF63671008C823B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 20E188832AD059A50053E945 /* TapToPayEducationContactlessLimitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToPayEducationContactlessLimitView.swift; sourceTree = ""; }; @@ -6529,7 +6529,7 @@ isa = PBXGroup; children = ( 01ABA0242E57579300829DC0 /* PointOfSaleOrderDetailsView.swift */, - 01ABA0252E57579300829DC0 /* PointOfSaleOrdersListView.swift */, + 01ABA0252E57579300829DC0 /* PointOfSaleOrderListView.swift */, 01ABA0262E57579300829DC0 /* PointOfSaleOrdersView.swift */, ); path = Orders; @@ -7761,8 +7761,8 @@ 02CD3BFC2C35D01600E575C4 /* Mocks */ = { isa = PBXGroup; children = ( - 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift */, - 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderService.swift */, + 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift */, + 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderListService.swift */, 01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */, 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */, 686A71B72DC9EB6D0006E835 /* MockPOSSearchHistoryService.swift */, @@ -7772,7 +7772,7 @@ 02CD3BFD2C35D04C00E575C4 /* MockCardPresentPaymentService.swift */, 6801E4162D0FFF0100F9DF46 /* MockReceiptService.swift */, 207E71CA2C60F765008540FC /* MockPOSOrderService.swift */, - 20DB185A2CF5D9220018D3E1 /* MockPointOfSaleOrderController.swift */, + 20DB185A2CF5D9220018D3E1 /* MockPointOfSaleOrderListController.swift */, 20FCBCE02CE24CE70082DCA3 /* MockPOSItemProvider.swift */, 200BA15A2CF0A2130006DC5B /* MockPointOfSaleItemsService.swift */, 200798EF2DA804200037C505 /* MockPointOfSalePurchasableItemsSearchController.swift */, @@ -8186,7 +8186,7 @@ 68B681152D92577F0098D5CD /* PointOfSaleCouponsController.swift */, 200BA1582CF092280006DC5B /* PointOfSaleItemsController.swift */, 20CF75B92CF4E69000ACCF4A /* PointOfSaleOrderController.swift */, - 012ACB732E5C830500A49458 /* PointOfSaleOrdersController.swift */, + 012ACB732E5C830500A49458 /* PointOfSaleOrderListController.swift */, ); path = Controllers; sourceTree = ""; @@ -8198,7 +8198,7 @@ 20DB185C2CF5E7560018D3E1 /* PointOfSaleOrderControllerTests.swift */, 200BA15D2CF0A9EB0006DC5B /* PointOfSaleItemsControllerTests.swift */, 02E4F26F2E0F2C75003A31E7 /* POSEntryPointControllerTests.swift */, - 012ACB752E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift */, + 012ACB752E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift */, ); path = Controllers; sourceTree = ""; @@ -9980,7 +9980,7 @@ 68F151DF2C0DA7800082AEC8 /* Models */ = { isa = PBXGroup; children = ( - 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift */, + 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift */, 01B3A1F12DB6D48800286B7F /* ItemListType.swift */, 20FCBCDC2CE223340082DCA3 /* PointOfSaleAggregateModel.swift */, 209ECA802DB8FC280089F3D2 /* PointOfSaleViewStateCoordinator.swift */, @@ -15210,7 +15210,7 @@ 02D681AB29C3F8AC00348510 /* StoreOnboardingPaymentsSetupCoordinator.swift in Sources */, B59D49CD219B587E006BF0AD /* UILabel+OrderStatus.swift in Sources */, 265BCA0C2430E741004E53EE /* ProductCategoryTableViewCell.swift in Sources */, - 012ACB742E5C830500A49458 /* PointOfSaleOrdersController.swift in Sources */, + 012ACB742E5C830500A49458 /* PointOfSaleOrderListController.swift in Sources */, 02ACD25A2852E11700EC928E /* CloseAccountCoordinator.swift in Sources */, 0191301B2CF4E782008C0C88 /* TapToPayEducationStepViewModel.swift in Sources */, 0258D9492B68E7FE00D280D0 /* ProductsSplitViewWrapperController.swift in Sources */, @@ -15222,7 +15222,7 @@ 016910982E1D019500B731DA /* GameControllerBarcodeObserver.swift in Sources */, EE9D031B2B89E4470077CED1 /* FilterOrdersByProduct+Analytics.swift in Sources */, 20C6E7512CDE4AEA00CD124C /* ItemListState.swift in Sources */, - 01ABA0282E57579300829DC0 /* PointOfSaleOrdersListView.swift in Sources */, + 01ABA0282E57579300829DC0 /* PointOfSaleOrderListView.swift in Sources */, 01ABA0292E57579300829DC0 /* PointOfSaleOrderDetailsView.swift in Sources */, 01ABA02A2E57579300829DC0 /* PointOfSaleOrdersView.swift in Sources */, 2DB891662E27F0830001B175 /* Address+Shared.swift in Sources */, @@ -15408,7 +15408,7 @@ CCD2F51C26D697860010E679 /* ShippingLabelServicePackageListViewModel.swift in Sources */, 03076D38290C223E008EE839 /* WooNavigationSheet.swift in Sources */, 022CE91A29BB143000F210E0 /* ProductSelectorNavigationView.swift in Sources */, - 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrdersModel.swift in Sources */, + 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift in Sources */, B99686E02A13C8CC00D1AF62 /* ScanToPayView.swift in Sources */, 02B191502CCF27F300CF38C9 /* PointOfSaleCardPresentPaymentOnboardingView.swift in Sources */, CE070A3E2BBC608A00017578 /* GiftCardsReportCardViewModel.swift in Sources */, @@ -17093,7 +17093,7 @@ 3198A1E82694DC7200597213 /* MockKnownReadersProvider.swift in Sources */, DEC51B04276B30F6009F3DF4 /* SystemStatusReportViewModelTests.swift in Sources */, 26B119C224D1CD3500FED5C7 /* WooConstantsTests.swift in Sources */, - 012ACB822E5D8DCD00A49458 /* MockPointOfSaleOrderFetchStrategyFactory.swift in Sources */, + 012ACB822E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift in Sources */, DE96844D2A332CC2000FBF4E /* ShareProductCoordinatorTests.swift in Sources */, 26100B202722FCAD00473045 /* MockCardPresentPaymentsOnboardingUseCase.swift in Sources */, 263491D5299C923400594566 /* SupportFormViewModelTests.swift in Sources */, @@ -17110,7 +17110,7 @@ 20ADE9462C6B364900C91265 /* CardPresentPaymentRetryApproachTests.swift in Sources */, 265D909D2446688C00D66F0F /* ProductCategoryViewModelBuilderTests.swift in Sources */, 03FBDAFD263EE4E800ACE257 /* CouponListViewModelTests.swift in Sources */, - 20DB185B2CF5D9220018D3E1 /* MockPointOfSaleOrderController.swift in Sources */, + 20DB185B2CF5D9220018D3E1 /* MockPointOfSaleOrderListController.swift in Sources */, 036F6EA6281847D5006D84F8 /* PaymentCaptureOrchestratorTests.swift in Sources */, B555531321B57E8800449E71 /* MockUserNotificationsCenterAdapter.swift in Sources */, 682210ED2909666600814E14 /* CustomerSearchUICommandTests.swift in Sources */, @@ -17197,7 +17197,7 @@ DEF657AA2C8AC25C00ACD61E /* BlazeCampaignObjectivePickerViewModelTests.swift in Sources */, EE2EDFE12987A189004E702B /* MockABTestVariationProvider.swift in Sources */, 0273707E24C0047800167204 /* SequenceHelpersTests.swift in Sources */, - 012ACB762E5C83EC00A49458 /* PointOfSaleOrdersControllerTests.swift in Sources */, + 012ACB762E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift in Sources */, DE9A02A32A44441200193ABF /* RequirementsCheckerTests.swift in Sources */, D802547326551D0F001B2CC1 /* CardPresentModalTapCardTests.swift in Sources */, B55BC1F321A8790F0011A0C0 /* StringHTMLTests.swift in Sources */, @@ -17642,7 +17642,7 @@ 02A275C023FE58F6005C560F /* MockImageCache.swift in Sources */, EE6C6B6E2C65DC4100632BDA /* WordPressMediaLibraryPickerDataSourceTests.swift in Sources */, 20BCF6F02B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift in Sources */, - 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderService.swift in Sources */, + 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderListService.swift in Sources */, 261AA30E275506DE009530FE /* PaymentMethodsViewModelTests.swift in Sources */, 4524CDA1242D045C00B2F20A /* ProductStatusSettingListSelectorCommandTests.swift in Sources */, 26A280D72B46027A00ACEE87 /* OrderNotificationView.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderListControllerTests.swift similarity index 72% rename from WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift rename to WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderListControllerTests.swift index 7a047d8c66d..df6fbf32b69 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrdersControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderListControllerTests.swift @@ -1,31 +1,31 @@ import Testing import Foundation @testable import WooCommerce -import enum Yosemite.PointOfSaleOrderServiceError +import enum Yosemite.PointOfSaleOrderListServiceError import struct NetworkingCore.Order import Observation -final class PointOfSaleOrdersControllerTests { - private let orderProvider = MockPointOfSaleOrderService() - private lazy var fetchStrategyFactory = MockPointOfSaleOrderFetchStrategyFactory(orderService: orderProvider) - private lazy var sut = PointOfSaleOrdersController(orderFetchStrategyFactory: fetchStrategyFactory) +final class PointOfSaleOrderListControllerTests { + private let orderListService = MockPointOfSaleOrderListService() + private lazy var fetchStrategyFactory = MockPointOfSaleOrderListFetchStrategyFactory(orderService: orderListService) + private lazy var sut = PointOfSaleOrderListController(orderListFetchStrategyFactory: fetchStrategyFactory) @Test func loadOrders_requests_first_page_after_loading_two_pages() async throws { try #require(sut.ordersViewState.isLoading) - orderProvider.shouldSimulateTwoPages = true + orderListService.shouldSimulateTwoPages = true await sut.loadOrders() await sut.loadNextOrders() - try #require(orderProvider.spyLastRequestedPageNumber == 2) + try #require(orderListService.spyLastRequestedPageNumber == 2) await sut.loadOrders() - #expect(orderProvider.spyLastRequestedPageNumber == 1) + #expect(orderListService.spyLastRequestedPageNumber == 1) } @Test func loadOrders_results_in_loaded_state() async throws { - let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() - orderProvider.orderPages = [expectedOrders] + let expectedOrders = MockPointOfSaleOrderListService.makeInitialOrders() + orderListService.orderPages = [expectedOrders] try #require(sut.ordersViewState.isLoading) await sut.loadOrders() @@ -34,9 +34,9 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_with_more_pages_sets_hasMoreItems() async throws { - let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() + let expectedOrders = MockPointOfSaleOrderListService.makeInitialOrders() try #require(sut.ordersViewState.isLoading) - orderProvider.shouldSimulateTwoPages = true + orderListService.shouldSimulateTwoPages = true await sut.loadOrders() @@ -45,8 +45,8 @@ final class PointOfSaleOrdersControllerTests { @Test func loadOrders_when_called_multiple_times_then_orders_are_not_duplicated() async throws { try #require(sut.ordersViewState.isLoading) - let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() - orderProvider.orderPages = [expectedOrders] + let expectedOrders = MockPointOfSaleOrderListService.makeInitialOrders() + orderListService.orderPages = [expectedOrders] await sut.loadOrders() await sut.loadOrders() @@ -64,7 +64,7 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadNextOrders_when_initial_orders_empty_then_container_state_is_content_and_orders_state_is_empty() async throws { - orderProvider.shouldReturnZeroOrders = true + orderListService.shouldReturnZeroOrders = true try #require(sut.ordersViewState.isLoading) @@ -75,8 +75,8 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_initial_orders_has_orders_but_no_more_pages_then_state_is_loaded_with_initial_orders() async throws { - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() - orderProvider.orderPages = [initialOrders] + let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + orderListService.orderPages = [initialOrders] try #require(sut.ordersViewState.isLoading) @@ -86,7 +86,7 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadNextOrders_when_simulateFetchNextPage_then_state_is_loaded_with_expected_orders() async throws { - orderProvider.shouldSimulateTwoPages = true + orderListService.shouldSimulateTwoPages = true await sut.loadOrders() await sut.loadNextOrders() @@ -100,17 +100,17 @@ final class PointOfSaleOrdersControllerTests { @Test func loadNextOrders_requests_second_page() async throws { try #require(sut.ordersViewState.isLoading) - orderProvider.shouldSimulateTwoPages = true + orderListService.shouldSimulateTwoPages = true await sut.loadOrders() await sut.loadNextOrders() - #expect(orderProvider.spyLastRequestedPageNumber == 2) + #expect(orderListService.spyLastRequestedPageNumber == 2) } @Test func loadNextOrders_when_simulateFetchNextPage_then_state_is_loaded_with_hasMoreItems() async throws { - orderProvider.shouldSimulateTwoPages = true - orderProvider.shouldSimulateThreePages = true + orderListService.shouldSimulateTwoPages = true + orderListService.shouldSimulateThreePages = true await sut.loadOrders() await sut.loadNextOrders() @@ -124,30 +124,30 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadNextOrders_when_hasNextPage_is_false_then_does_not_fetch_next_page() async throws { - let expectedOrders = MockPointOfSaleOrderService.makeInitialOrders() - orderProvider.orderPages = [expectedOrders] + let expectedOrders = MockPointOfSaleOrderListService.makeInitialOrders() + orderListService.orderPages = [expectedOrders] await sut.loadOrders() - let spyCallCountBeforeLoadNext = orderProvider.spyCallCount + let spyCallCountBeforeLoadNext = orderListService.spyCallCount await sut.loadNextOrders() - #expect(orderProvider.spyCallCount == spyCallCountBeforeLoadNext) + #expect(orderListService.spyCallCount == spyCallCountBeforeLoadNext) } @Test func refreshOrders_requests_first_page() async throws { - orderProvider.shouldSimulateTwoPages = true + orderListService.shouldSimulateTwoPages = true await sut.loadOrders() await sut.loadNextOrders() - try #require(orderProvider.spyLastRequestedPageNumber == 2) + try #require(orderListService.spyLastRequestedPageNumber == 2) await sut.refreshOrders() - #expect(orderProvider.spyLastRequestedPageNumber == 1) + #expect(orderListService.spyLastRequestedPageNumber == 1) } @Test func loadOrders_when_error_occurs_then_shows_error_state() async throws { - orderProvider.shouldThrowError = true + orderListService.shouldThrowError = true await sut.loadOrders() @@ -159,11 +159,11 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_error_occurs_with_existing_orders_then_shows_inline_error() async throws { - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() - orderProvider.orderPages = [initialOrders] + let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + orderListService.orderPages = [initialOrders] await sut.loadOrders() - orderProvider.shouldThrowError = true + orderListService.shouldThrowError = true await sut.refreshOrders() guard case .inlineError(let orders, _, let context) = sut.ordersViewState else { @@ -175,8 +175,8 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_cached_data_available_then_shows_cached_data_with_loading_state() async throws { - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() - orderProvider.orderPages = [initialOrders] + let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + orderListService.orderPages = [initialOrders] // First load - should cache the data await sut.loadOrders() @@ -199,8 +199,8 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_when_no_cached_data_then_starts_with_empty_loading_state() async throws { - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() - orderProvider.orderPages = [initialOrders] + let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + orderListService.orderPages = [initialOrders] // Initial state should be loading with empty orders try #require(sut.ordersViewState.isLoading) @@ -221,11 +221,11 @@ final class PointOfSaleOrdersControllerTests { } @Test func loadOrders_cached_data_is_replaced_with_fresh_data() async throws { - let initialOrders = MockPointOfSaleOrderService.makeInitialOrders() - let freshOrders = MockPointOfSaleOrderService.makeSecondPageOrders() + let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let freshOrders = MockPointOfSaleOrderListService.makeSecondPageOrders() // First load - orderProvider.orderPages = [initialOrders] + orderListService.orderPages = [initialOrders] await sut.loadOrders() guard case .loaded(let firstLoadOrders, _) = sut.ordersViewState else { @@ -235,7 +235,7 @@ final class PointOfSaleOrdersControllerTests { #expect(firstLoadOrders == initialOrders) // Second load with different data - orderProvider.orderPages = [freshOrders] + orderListService.orderPages = [freshOrders] await sut.loadOrders() // Should end up showing fresh data, not cached data diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderFetchStrategyFactory.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderFetchStrategyFactory.swift deleted file mode 100644 index e2baf5f5ebf..00000000000 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderFetchStrategyFactory.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -@testable import WooCommerce -import protocol Yosemite.PointOfSaleOrderFetchStrategyFactoryProtocol -import protocol Yosemite.PointOfSaleOrderFetchStrategy -import protocol Yosemite.PointOfSaleOrderServiceProtocol -import struct NetworkingCore.PagedItems -import struct Yosemite.POSOrder - -final class MockPointOfSaleOrderFetchStrategyFactory: PointOfSaleOrderFetchStrategyFactoryProtocol { - private let orderService: PointOfSaleOrderServiceProtocol - - init(orderService: PointOfSaleOrderServiceProtocol) { - self.orderService = orderService - } - - func defaultStrategy() -> PointOfSaleOrderFetchStrategy { - MockPointOfSaleOrderFetchStrategy(orderService: orderService) - } -} - -private struct MockPointOfSaleOrderFetchStrategy: PointOfSaleOrderFetchStrategy { - let orderService: PointOfSaleOrderServiceProtocol - - func fetchOrders(pageNumber: Int) async throws -> PagedItems { - try await orderService.providePointOfSaleOrders(pageNumber: pageNumber) - } -} diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListController.swift similarity index 94% rename from WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift rename to WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListController.swift index 575935eb212..79749d2a200 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListController.swift @@ -4,7 +4,7 @@ import Combine @testable import WooCommerce import struct Yosemite.Order -final class MockPointOfSaleOrderController: PointOfSaleOrderControllerProtocol { +final class MockPointOfSaleOrderListController: PointOfSaleOrderControllerProtocol { func collectCashPayment(changeDueAmount: String?) async throws { // no-op } diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListFetchStrategyFactory.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListFetchStrategyFactory.swift new file mode 100644 index 00000000000..157a75945a0 --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListFetchStrategyFactory.swift @@ -0,0 +1,27 @@ +import Foundation +@testable import WooCommerce +import protocol Yosemite.PointOfSaleOrderListFetchStrategyFactoryProtocol +import protocol Yosemite.PointOfSaleOrderListFetchStrategy +import protocol Yosemite.PointOfSaleOrderListServiceProtocol +import struct NetworkingCore.PagedItems +import struct Yosemite.POSOrder + +final class MockPointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol { + private let orderService: PointOfSaleOrderListServiceProtocol + + init(orderService: PointOfSaleOrderListServiceProtocol) { + self.orderService = orderService + } + + func defaultStrategy() -> PointOfSaleOrderListFetchStrategy { + MockPointOfSaleOrderListFetchStrategy(orderService: orderService) + } +} + +private struct MockPointOfSaleOrderListFetchStrategy: PointOfSaleOrderListFetchStrategy { + let orderService: PointOfSaleOrderListServiceProtocol + + func fetchOrders(pageNumber: Int) async throws -> PagedItems { + try await orderService.providePointOfSaleOrders(pageNumber: pageNumber) + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListService.swift similarity index 86% rename from WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift rename to WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListService.swift index 279fa2804e6..ddb5d399fa5 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListService.swift @@ -5,7 +5,7 @@ import struct NetworkingCore.Order import enum NetworkingCore.OrderStatusEnum import WooFoundation -final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { +final class MockPointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol { var orderPages: [[POSOrder]] = [] var errorToThrow: Error? var shouldReturnZeroOrders = false @@ -21,7 +21,7 @@ final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { spyCallCount += 1 if shouldThrowError { - throw PointOfSaleOrderServiceError.requestFailed + throw PointOfSaleOrderListServiceError.requestFailed } if let errorToThrow { @@ -34,11 +34,11 @@ final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { if shouldSimulateTwoPages { if shouldSimulateThreePages && pageNumber > 1 { - return .init(items: MockPointOfSaleOrderService.makeSecondPageOrders(), hasMorePages: true, totalItems: 6) + return .init(items: MockPointOfSaleOrderListService.makeSecondPageOrders(), hasMorePages: true, totalItems: 6) } else if pageNumber > 1 { - return .init(items: MockPointOfSaleOrderService.makeSecondPageOrders(), hasMorePages: false, totalItems: 4) + return .init(items: MockPointOfSaleOrderListService.makeSecondPageOrders(), hasMorePages: false, totalItems: 4) } else { - return .init(items: MockPointOfSaleOrderService.makeInitialOrders(), hasMorePages: shouldSimulateTwoPages, totalItems: 4) + return .init(items: MockPointOfSaleOrderListService.makeInitialOrders(), hasMorePages: shouldSimulateTwoPages, totalItems: 4) } } @@ -46,7 +46,7 @@ final class MockPointOfSaleOrderService: PointOfSaleOrderServiceProtocol { } } -extension MockPointOfSaleOrderService { +extension MockPointOfSaleOrderListService { static func makeInitialOrders() -> [POSOrder] { let baseDate = Date(timeIntervalSince1970: 1672531200) // Fixed date: Jan 1, 2023 diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index feb0af114a1..8feef7aa6e8 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -201,7 +201,7 @@ struct PointOfSaleAggregateModelTests { struct OrderTests { private let cardPresentPaymentService = MockCardPresentPaymentService() - private let orderController = MockPointOfSaleOrderController() + private let orderController = MockPointOfSaleOrderListController() init() { orderController.orderStateToReturn = makeLoadedOrderState(cartTotal: "$0.00") @@ -288,7 +288,7 @@ struct PointOfSaleAggregateModelTests { @Test func sendReceipt_when_invoked_then_calls_controller() async throws { // Given - let orderController = MockPointOfSaleOrderController() + let orderController = MockPointOfSaleOrderListController() let sut = makePointOfSaleAggregateModel(orderController: orderController) // When @@ -300,7 +300,7 @@ struct PointOfSaleAggregateModelTests { @Test func sendReceipt_when_invoked_with_error_then_returns_error() async throws { // Given - let orderController = MockPointOfSaleOrderController() + let orderController = MockPointOfSaleOrderListController() orderController.shouldThrowReceiptError = true let expectedError = NSError(domain: "some error", code: -1) @@ -338,7 +338,7 @@ struct PointOfSaleAggregateModelTests { struct PaymentTests { private let cardPresentPaymentService = MockCardPresentPaymentService() - private let orderController = MockPointOfSaleOrderController() + private let orderController = MockPointOfSaleOrderListController() @Test func init_sets_card_paymentState_to_idle() async throws { // Given that we don't specify a payment state @@ -784,7 +784,7 @@ struct PointOfSaleAggregateModelTests { private let analyticsProvider = MockAnalyticsProvider() private let analytics: WooAnalytics private let cardPresentPaymentService = MockCardPresentPaymentService() - private let orderController = MockPointOfSaleOrderController() + private let orderController = MockPointOfSaleOrderListController() init() { analytics = WooAnalytics(analyticsProvider: analyticsProvider) @@ -966,7 +966,7 @@ private func makePointOfSaleAggregateModel( couponsController: PointOfSaleCouponsControllerProtocol = MockPointOfSaleCouponsController(), couponsSearchController: PointOfSaleSearchingItemsControllerProtocol = MockPointOfSaleCouponsController(), cardPresentPaymentService: CardPresentPaymentFacade = MockCardPresentPaymentService(), - orderController: PointOfSaleOrderControllerProtocol = MockPointOfSaleOrderController(), + orderController: PointOfSaleOrderControllerProtocol = MockPointOfSaleOrderListController(), analytics: Analytics = WooAnalytics(analyticsProvider: MockAnalyticsProvider()), collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking = MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: POSSearchHistoryProviding = MockPOSSearchHistoryService(), diff --git a/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift index e21a762d00a..f2d0ff1d41a 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift @@ -100,7 +100,7 @@ private func makePointOfSaleAggregateModel( couponsController: PointOfSaleCouponsControllerProtocol = MockPointOfSaleCouponsController(), couponsSearchController: PointOfSaleSearchingItemsControllerProtocol = MockPointOfSaleCouponsController(), cardPresentPaymentService: CardPresentPaymentFacade = MockCardPresentPaymentService(), - orderController: PointOfSaleOrderControllerProtocol = MockPointOfSaleOrderController(), + orderController: PointOfSaleOrderControllerProtocol = MockPointOfSaleOrderListController(), collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking = MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: POSSearchHistoryProviding = MockPOSSearchHistoryService(), popularPurchasableItemsController: PointOfSaleItemsControllerProtocol = MockPointOfSaleItemsController(),