Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 25 additions & 4 deletions Modules/Sources/NetworkingCore/Remote/OrdersRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ extension OrdersRemote: POSOrdersRemoteProtocol {
ParameterKeys.statusKey: Defaults.statusAny,
ParameterKeys.usesGMTDates: true,
ParameterKeys.fields: ParameterValues.fieldValues,
ParameterKeys.createdVia: "pos-rest-api"
ParameterKeys.createdVia: ParameterValues.posFilter
]

let path = Constants.ordersPath
Expand All @@ -469,10 +469,30 @@ extension OrdersRemote: POSOrdersRemoteProtocol {
parameters: parameters,
availableAsRESTRequest: true)
let mapper = OrderListMapper(siteID: siteID)
let (orders, responseHeaders) = try await enqueueWithResponseHeaders(request, mapper: mapper)
return createPagedItems(items: orders, responseHeaders: responseHeaders, currentPageNumber: pageNumber)
}

let orders: [Order] = try await enqueue(request, mapper: mapper)
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: ParameterValues.posFilter
]
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, responseHeaders) = try await enqueueWithResponseHeaders(request, mapper: mapper)
return createPagedItems(items: orders, responseHeaders: responseHeaders, currentPageNumber: pageNumber)
}
}

Expand Down Expand Up @@ -522,6 +542,7 @@ public extension OrdersRemote {
"is_editable", "needs_payment", "needs_processing", "gift_cards", "created_via"
]
static let dateModifiedField = "date_modified_gmt"
static let posFilter = "pos-rest-api"
}

enum NestedFieldKeys {
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,10 +3,21 @@ import struct NetworkingCore.PagedItems

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

extension PointOfSaleOrderListFetchStrategy {
var id: String {
String(describing: type(of: self))
}
}

struct PointOfSaleDefaultOrderListFetchStrategy: PointOfSaleOrderListFetchStrategy {
private let orderListService: PointOfSaleOrderListServiceProtocol
let supportsCaching: Bool = true
var showsLoadingWithItems: Bool = true

init(orderListService: PointOfSaleOrderListServiceProtocol) {
self.orderListService = orderListService
Expand All @@ -16,3 +27,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 = 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>
}
37 changes: 37 additions & 0 deletions Modules/Tests/NetworkingTests/Remote/OrdersRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,43 @@ final class OrdersRemoteTests: XCTestCase {
"value": cashPaymentChangeDueAmount]]
assertEqual(received, expected)
}

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,31 @@ 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 strategyPaginationTracker: [String: AsyncPaginationTracker] = [:]
private var fetchStrategy: PointOfSaleOrderListFetchStrategy
private var cachedOrders: [POSOrder] = []
private(set) var selectedOrder: POSOrder?
private let orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol
private var paginationTracker: AsyncPaginationTracker {
if let existing = strategyPaginationTracker[fetchStrategy.id] {
return existing
}
let tracker = AsyncPaginationTracker()
strategyPaginationTracker[fetchStrategy.id] = tracker
return tracker
}

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

Expand All @@ -50,7 +64,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 +97,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 +122,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 +134,10 @@ protocol PointOfSaleOrderListControllerProtocol {

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

guard !ordersViewState.orders.isEmpty || !cachedOrders.isEmpty else {
return
}
Expand All @@ -126,4 +149,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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import protocol Yosemite.POSSearchHistoryProviding
import enum Yosemite.POSItem

final class POSProductSearchable: POSSearchable {
let itemListType: ItemListType
private let itemListType: ItemListType
private let itemsController: PointOfSaleSearchingItemsControllerProtocol
private let searchHistoryProvider: POSSearchHistoryProviding

Expand All @@ -20,6 +20,10 @@ final class POSProductSearchable: POSSearchable {
searchHistoryProvider.searchHistory(for: itemListType.itemType)
}

var searchFieldPlaceholder: String {
itemListType.itemType.searchFieldLabel
}

func performSearch(term: String) async {
await itemsController.searchItems(searchTerm: term, baseItem: .root)
}
Expand Down
Loading