Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,29 @@ extension OrdersRemote: POSOrdersRemoteProtocol {
let hasMorePages = orders.count == pageSize
return PagedItems(items: orders, hasMorePages: hasMorePages, totalItems: nil)
}

public func searchPOSOrders(siteID: Int64, searchTerm: String, pageNumber: Int, pageSize: Int) async throws -> PagedItems<Order> {
let parameters: [String: Any] = [
ParameterKeys.keyword: searchTerm,
ParameterKeys.page: String(pageNumber),
ParameterKeys.perPage: String(pageSize),
ParameterKeys.statusKey: Defaults.statusAny,
ParameterKeys.usesGMTDates: true,
ParameterKeys.fields: ParameterValues.fieldValues,
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have/want access to totalItems here? I have not tested this much, but consider using the the PagedItems method that parses the responseHeaders as well, ie:

let (orders, responseHeaders) = try await enqueueWithResponseHeaders(request, mapper: mapper)
return createPagedItems(items: orders, responseHeaders: responseHeaders, currentPageNumber: pageNumber) 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll check it out!

}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ public protocol POSOrdersRemoteProtocol {
func loadPOSOrders(siteID: Int64,
pageNumber: Int,
pageSize: Int) async throws -> PagedItems<Order>

func searchPOSOrders(siteID: Int64,
searchTerm: String,
pageNumber: Int,
pageSize: Int) async throws -> PagedItems<Order>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import struct NetworkingCore.PagedItems

public protocol PointOfSaleOrderListFetchStrategy {
func fetchOrders(pageNumber: Int) async throws -> PagedItems<POSOrder>
var supportsCaching: Bool { get }
var showsLoadingWithItems: Bool { get }
}

struct PointOfSaleDefaultOrderListFetchStrategy: PointOfSaleOrderListFetchStrategy {
private let orderListService: PointOfSaleOrderListServiceProtocol

var supportsCaching: Bool { true }
var showsLoadingWithItems: Bool { true }

init(orderListService: PointOfSaleOrderListServiceProtocol) {
self.orderListService = orderListService
}
Expand All @@ -16,3 +21,20 @@ struct PointOfSaleDefaultOrderListFetchStrategy: PointOfSaleOrderListFetchStrate
try await orderListService.providePointOfSaleOrders(pageNumber: pageNumber)
}
}

struct PointOfSaleSearchOrderListFetchStrategy: PointOfSaleOrderListFetchStrategy {
private let orderListService: PointOfSaleOrderListServiceProtocol
private let searchTerm: String

var supportsCaching: Bool { false }
var showsLoadingWithItems: Bool { false }

init(orderListService: PointOfSaleOrderListServiceProtocol, searchTerm: String) {
self.orderListService = orderListService
self.searchTerm = searchTerm
}

func fetchOrders(pageNumber: Int) async throws -> PagedItems<POSOrder> {
try await orderListService.searchPointOfSaleOrders(searchTerm: searchTerm, pageNumber: pageNumber)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import class WooFoundationCore.CurrencyFormatter

public protocol PointOfSaleOrderListFetchStrategyFactoryProtocol {
func defaultStrategy() -> PointOfSaleOrderListFetchStrategy
func searchStrategy(searchTerm: String) -> PointOfSaleOrderListFetchStrategy
}

public final class PointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol {
Expand All @@ -30,4 +31,15 @@ public final class PointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderLis
)
)
}

public func searchStrategy(searchTerm: String) -> PointOfSaleOrderListFetchStrategy {
PointOfSaleSearchOrderListFetchStrategy(
orderListService: PointOfSaleOrderListService(
siteID: siteID,
ordersRemote: ordersRemote,
currencyFormatter: currencyFormatter
),
searchTerm: searchTerm
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,30 @@ public final class PointOfSaleOrderListService: PointOfSaleOrderListServiceProto
throw PointOfSaleOrderListServiceError.requestFailed
}
}

public func searchPointOfSaleOrders(searchTerm: String, pageNumber: Int = 1) async throws -> PagedItems<POSOrder> {
do {
let pagedOrders = try await ordersRemote.searchPOSOrders(
siteID: siteID,
searchTerm: searchTerm,
pageNumber: pageNumber,
pageSize: 25
)

if pageNumber != 1 && pagedOrders.items.count == 0 {
return .init(items: [], hasMorePages: false, totalItems: 0)
}

// Convert Order objects to POSOrder objects
let posOrders = pagedOrders.items.map { mapper.map(order: $0) }

return .init(items: posOrders,
hasMorePages: pagedOrders.hasMorePages,
totalItems: pagedOrders.totalItems)
} catch AFError.explicitlyCancelled {
throw PointOfSaleOrderListServiceError.requestCancelled
} catch {
throw PointOfSaleOrderListServiceError.requestFailed
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public enum PointOfSaleOrderListServiceError: Error, Equatable {

public protocol PointOfSaleOrderListServiceProtocol {
func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems<POSOrder>
func searchPointOfSaleOrders(searchTerm: String, pageNumber: Int) async throws -> PagedItems<POSOrder>
}
50 changes: 50 additions & 0 deletions Modules/Tests/NetworkingTests/Remote/OrdersRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,56 @@ final class OrdersRemoteTests: XCTestCase {
"value": cashPaymentChangeDueAmount]]
assertEqual(received, expected)
}

func test_searchPOSOrders_properly_returns_parsed_orders() async throws {
// Given
let remote = OrdersRemote(network: network)
network.simulateResponse(requestUrlSuffix: "orders", filename: "orders-load-all")

// When
let result = try await remote.searchPOSOrders(siteID: sampleSiteID, searchTerm: "test", pageNumber: 1, pageSize: 25)

// Then
XCTAssert(result.items.count == 4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering, should we use order-load-all for this test? As is always going to return the 4 orders no matter the searchTerm we pass through. Maybe is there some other file that fits better the test? Or a new response? Not a big deal in any case since we're checking that the function parses the response.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mm... There's actually not much to test here since our request expects the same type of Order that we already tested in other tests. Search query doesn't produce any special results. Only the second test is meaningful to verify that we're making the request with the expected parameters.

XCTAssertEqual(result.hasMorePages, false)
}

func test_searchPOSOrders_sends_correct_parameters() async throws {
// Given
let remote = OrdersRemote(network: network)
let searchTerm = "test search"
let pageNumber = 2
let pageSize = 10

// When
_ = try? await remote.searchPOSOrders(siteID: sampleSiteID, searchTerm: searchTerm, pageNumber: pageNumber, pageSize: pageSize)

// Then
let request = try XCTUnwrap(network.requestsForResponseData.last as? JetpackRequest)
let parameters = request.parameters

XCTAssertEqual(parameters["search"] as? String, searchTerm)
XCTAssertEqual(parameters["page"] as? String, String(pageNumber))
XCTAssertEqual(parameters["per_page"] as? String, String(pageSize))
XCTAssertEqual(parameters["status"] as? String, "any")
XCTAssertEqual(parameters["created_via"] as? String, "pos-rest-api")
XCTAssertEqual(parameters["dates_are_gmt"] as? Bool, true)
XCTAssertNotNil(parameters["_fields"] as? String)
}

func test_searchPOSOrders_properly_relays_networking_error() async throws {
// Given
let remote = OrdersRemote(network: network)

do {
// When
_ = try await remote.searchPOSOrders(siteID: sampleSiteID, searchTerm: "test", pageNumber: 1, pageSize: 25)
XCTFail("Expected error to be thrown")
} catch {
// Then
XCTAssertEqual(error as? NetworkError, .notFound(response: nil))
}
}
}

private extension OrdersRemoteTests {
Expand Down
19 changes: 19 additions & 0 deletions Modules/Tests/YosemiteTests/Mocks/MockPOSOrdersRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,23 @@ final class MockPOSOrdersRemote: POSOrdersRemoteProtocol {
throw error
}
}

var mockSearchPagedOrdersResult: Result<PagedItems<Order>, Error> = .success(PagedItems(items: [], hasMorePages: false, totalItems: 0))
var searchPOSOrdersCalled = false
var spySearchTerm: String?

func searchPOSOrders(siteID: Int64, searchTerm: String, pageNumber: Int, pageSize: Int) async throws -> PagedItems<Order> {
searchPOSOrdersCalled = true
spySiteID = siteID
spySearchTerm = searchTerm
spyPageNumber = pageNumber
spyPageSize = pageSize

switch mockSearchPagedOrdersResult {
case .success(let pagedOrders):
return pagedOrders
case .failure(let error):
throw error
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,24 @@ protocol PointOfSaleOrderListControllerProtocol {
func selectOrder(_ order: POSOrder?)
}

@Observable final class PointOfSaleOrderListController: PointOfSaleOrderListControllerProtocol {
protocol PointOfSaleSearchingOrderListControllerProtocol: PointOfSaleOrderListControllerProtocol {
func searchOrders(searchTerm: String) async
func clearSearchOrders()
}

@Observable final class PointOfSaleOrderListController: PointOfSaleSearchingOrderListControllerProtocol {
var ordersViewState: POSOrderListState
private let paginationTracker: AsyncPaginationTracker
private var fetchStrategy: PointOfSaleOrderListFetchStrategy
private var cachedOrders: [POSOrder] = []
private(set) var selectedOrder: POSOrder?
private let orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol

init(orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol,
initialState: POSOrderListState = .loading([])) {
self.ordersViewState = initialState
self.paginationTracker = .init()
self.orderListFetchStrategyFactory = orderListFetchStrategyFactory
self.fetchStrategy = orderListFetchStrategyFactory.defaultStrategy()
}

Expand All @@ -50,7 +57,7 @@ protocol PointOfSaleOrderListControllerProtocol {
return
}
let currentOrders = ordersViewState.orders
ordersViewState = .loading(currentOrders)
ordersViewState = fetchStrategy.showsLoadingWithItems ? .loading(currentOrders) : .loading([])
do {
_ = try await paginationTracker.ensureNextPageIsSynced { [weak self] pageNumber in
guard let self else { return true }
Expand Down Expand Up @@ -83,6 +90,11 @@ protocol PointOfSaleOrderListControllerProtocol {
}

private func setLoadingState() {
if !fetchStrategy.showsLoadingWithItems {
ordersViewState = .loading([])
return
}

let orders = ordersViewState.orders
let isInitialState = ordersViewState.isLoading && orders.isEmpty
if !isInitialState {
Expand All @@ -103,7 +115,7 @@ protocol PointOfSaleOrderListControllerProtocol {

ordersViewState = allOrders.isEmpty ? .empty : .loaded(allOrders, hasMoreItems: pagedOrders.hasMorePages)

if pageNumber == 1 && !appendToExistingOrders {
if fetchStrategy.supportsCaching {
cachedOrders = allOrders
}

Expand All @@ -115,6 +127,10 @@ protocol PointOfSaleOrderListControllerProtocol {

@MainActor
private func setCachedData() {
guard fetchStrategy.supportsCaching else {
return
}

guard !ordersViewState.orders.isEmpty || !cachedOrders.isEmpty else {
return
}
Expand All @@ -126,4 +142,24 @@ protocol PointOfSaleOrderListControllerProtocol {
func selectOrder(_ order: POSOrder?) {
selectedOrder = order
}

@MainActor
func searchOrders(searchTerm: String) async {
fetchStrategy = orderListFetchStrategyFactory.searchStrategy(searchTerm: searchTerm)
ordersViewState = .loading([])
await loadFirstPage()
}

@MainActor
func clearSearchOrders() {
fetchStrategy = orderListFetchStrategyFactory.defaultStrategy()
if cachedOrders.isNotEmpty {
ordersViewState = .loaded(cachedOrders, hasMoreItems: true)
} else {
ordersViewState = .loading([])
Task {
await loadFirstPage()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import Foundation
import Observation

@Observable final class PointOfSaleOrderListModel {
let ordersController: PointOfSaleOrderListControllerProtocol
let ordersController: PointOfSaleSearchingOrderListControllerProtocol

init(ordersController: PointOfSaleOrderListControllerProtocol) {
init(ordersController: PointOfSaleSearchingOrderListControllerProtocol) {
self.ordersController = ordersController
}
}
Loading