Skip to content
35 changes: 35 additions & 0 deletions Modules/Sources/Networking/Remote/BookingsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public protocol BookingsRemoteProtocol {

func fetchResource(resourceID: Int64,
siteID: Int64) async throws -> BookingResource?

func fetchResources(for siteID: Int64,
pageNumber: Int,
pageSize: Int) async throws -> [BookingResource]
}

/// Booking: Remote Endpoints
Expand Down Expand Up @@ -133,6 +137,37 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {

return try await enqueue(request, mapper: mapper)
}

/// Retrieves all of the `BookingResources` available.
///
/// - Parameters:
/// - siteID: Site for which we'll fetch remote booking resources.
/// - pageNumber: Number of page that should be retrieved.
/// - pageSize: Number of resources to be retrieved per page.
///
public func fetchResources(
for siteID: Int64,
pageNumber: Int = Default.pageNumber,
pageSize: Int = Default.pageSize
) async throws -> [BookingResource] {
let parameters = [
ParameterKey.page: String(pageNumber),
ParameterKey.perPage: String(pageSize)
]

let path = Path.resources
let request = JetpackRequest(
wooApiVersion: .wcBookings,
method: .get,
siteID: siteID,
path: path,
parameters: parameters,
availableAsRESTRequest: true
)
let mapper = ListMapper<BookingResource>(siteID: siteID)

return try await enqueue(request, mapper: mapper)
}
}

// MARK: - Constants
Expand Down
7 changes: 7 additions & 0 deletions Modules/Sources/Storage/Tools/StorageType+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,13 @@ public extension StorageType {
return objects.isEmpty ? nil : objects
}

/// Retrieves the store booking resources
func loadBookingResources(siteID: Int64, resourceIDs: [Int64]) -> [BookingResource] {
let predicate = NSPredicate(format: "siteID == %lld && resourceID in %@", siteID, resourceIDs)
let descriptor = NSSortDescriptor(keyPath: \BookingResource.resourceID, ascending: false)
return allObjects(ofType: BookingResource.self, matching: predicate, sortedBy: [descriptor])
}

/// Retrieves the store booking resource
func loadBookingResource(siteID: Int64, resourceID: Int64) -> BookingResource? {
let predicate = \BookingResource.resourceID == resourceID && \BookingResource.siteID == siteID
Expand Down
9 changes: 9 additions & 0 deletions Modules/Sources/Yosemite/Actions/BookingAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ public enum BookingAction: Action {
resourceID: Int64,
onCompletion: (Result<BookingResource, Error>) -> Void)

/// Synchronizes booking resources matching the specified criteria.
///
/// - Parameter onCompletion: called when sync completes, returns an error or a boolean that indicates whether there might be more resources to sync.
///
case synchronizeResources(siteID: Int64,
pageNumber: Int,
pageSize: Int = BookingsRemote.Default.pageSize,
onCompletion: (Result<Bool, Error>) -> Void)

/// Updates a booking attendance status.
///
/// - Parameter siteID: The site ID of the booking.
Expand Down
1 change: 1 addition & 0 deletions Modules/Sources/Yosemite/Model/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ public typealias StorageBlazeTargetLanguage = Storage.BlazeTargetLanguage
public typealias StorageBlazeTargetTopic = Storage.BlazeTargetTopic
// periphery: ignore
public typealias StorageBooking = Storage.Booking
public typealias StorageBookingResource = Storage.BookingResource
public typealias StorageCardReaderType = Storage.CardReaderType
public typealias StorageCoupon = Storage.Coupon
public typealias StorageCustomer = Storage.Customer
Expand Down
44 changes: 39 additions & 5 deletions Modules/Sources/Yosemite/Stores/BookingStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public class BookingStore: Store {
onCompletion: onCompletion)
case let .fetchResource(siteID, resourceID, onCompletion):
fetchResource(siteID: siteID, resourceID: resourceID, onCompletion: onCompletion)
case let .synchronizeResources(siteID, pageNumber, pageSize, onCompletion):
synchronizeResources(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
case .updateBookingAttendanceStatus(let siteID, let bookingID, let status, let onCompletion):
performUpdateBookingAttendanceStatus(
siteID: siteID,
Expand Down Expand Up @@ -244,7 +246,8 @@ private extension BookingStore {
return
}

await upsertBookingResourceInBackground(readOnlyBookingResource: resource)
await upsertBookingResourcesInBackground(siteID: resource.siteID,
readOnlyBookingResources: [resource])

onCompletion(.success(resource))
} catch {
Expand All @@ -253,6 +256,31 @@ private extension BookingStore {
}
}

/// Synchronizes booking resources for the specified site.
///
func synchronizeResources(siteID: Int64,
pageNumber: Int,
pageSize: Int,
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
Task { @MainActor in
do {
let resources = try await remote.fetchResources(
for: siteID,
pageNumber: pageNumber,
pageSize: pageSize
)

await upsertBookingResourcesInBackground(siteID: siteID,
readOnlyBookingResources: resources)

let hasNextPage = resources.count == pageSize
onCompletion(.success(hasNextPage))
} catch {
onCompletion(.failure(error))
}
}
}

func performUpdateBookingAttendanceStatus(
siteID: Int64,
bookingID: Int64,
Expand Down Expand Up @@ -393,16 +421,22 @@ private extension BookingStore {
}

/// Updates (OR Inserts) the specified ReadOnly BookingResource Entities *in a background thread* async.
func upsertBookingResourceInBackground(readOnlyBookingResource: BookingResource) async {
func upsertBookingResourcesInBackground(siteID: Int64, readOnlyBookingResources: [BookingResource]) async {
await withCheckedContinuation { [weak self] continuation in
guard let self else {
return continuation.resume()
}

storageManager.performAndSave({ storage in
let storedItem = storage.loadBookingResource(siteID: readOnlyBookingResource.siteID, resourceID: readOnlyBookingResource.resourceID)
let storageResource = storedItem ?? storage.insertNewObject(ofType: Storage.BookingResource.self)
storageResource.update(with: readOnlyBookingResource)
let storedItems = storage.loadBookingResources(
siteID: siteID,
resourceIDs: readOnlyBookingResources.map { $0.resourceID }
)
for item in readOnlyBookingResources {
let storageResource = storedItems.first(where: { $0.resourceID == item.resourceID }) ??
storage.insertNewObject(ofType: Storage.BookingResource.self)
storageResource.update(with: item)
}
}, completion: {
continuation.resume()
}, on: .main)
Expand Down
43 changes: 43 additions & 0 deletions Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,47 @@ struct BookingsRemoteTests {
_ = try await remote.fetchResource(resourceID: 22, siteID: sampleSiteID)
}
}

@Test func test_fetchResources_properly_returns_parsed_resources() async throws {
// Given
let remote = BookingsRemote(network: network)
network.simulateResponse(requestUrlSuffix: "resources/team-members", filename: "booking-resource-list")

// When
let resources = try await remote.fetchResources(for: sampleSiteID)

// Then
#expect(resources.count == 2)
let firstResource = try #require(resources.first)
#expect(firstResource.resourceID == 22)
#expect(firstResource.name == "Joel (Sample resource)")
#expect(firstResource.quantity == 1)
#expect(firstResource.siteID == sampleSiteID)
}

@Test func test_fetchResources_properly_relays_networking_errors() async {
// Given
let remote = BookingsRemote(network: network)

// Then
await #expect(throws: NetworkError.notFound()) {
_ = try await remote.fetchResources(for: sampleSiteID)
}
}

@Test func test_fetchResources_sends_correct_parameters() async throws {
// Given
let remote = BookingsRemote(network: network)
network.simulateResponse(requestUrlSuffix: "resources/team-members", filename: "booking-resource-list")

// When
_ = try await remote.fetchResources(for: sampleSiteID, pageNumber: 3, pageSize: 100)

// Then
let request = try #require(network.requestsForResponseData.first as? JetpackRequest)
let parameters = request.parameters

#expect((parameters["page"] as? String) == "3")
#expect((parameters["per_page"] as? String) == "100")
}
}
26 changes: 26 additions & 0 deletions Modules/Tests/NetworkingTests/Responses/booking-resource-list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"id": 22,
"name": "Joel (Sample resource)",
"qty": 1,
"role": "",
"email": "",
"phone_number": "",
"image_id": 0,
"image_url": "",
"description": "",
"note": ""
},
{
"id": 23,
"name": "Sarah (Sample resource)",
"qty": 2,
"role": "Manager",
"email": "[email protected]",
"phone_number": "+1234567890",
"image_id": 45,
"image_url": "https://example.com/image.jpg",
"description": "Sample resource description",
"note": "Sample note"
}
]
12 changes: 12 additions & 0 deletions Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
private var loadAllBookingsResult: Result<[Booking], Error>?
private var loadBookingResult: Result<Booking?, Error>?
private var fetchResourceResult: Result<BookingResource?, Error>?
private var fetchResourcesResult: Result<[BookingResource], Error>?

func whenLoadingAllBookings(thenReturn result: Result<[Booking], Error>) {
loadAllBookingsResult = result
Expand All @@ -20,6 +21,10 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
fetchResourceResult = result
}

func whenFetchingResources(thenReturn result: Result<[BookingResource], Error>) {
fetchResourcesResult = result
}

func loadAllBookings(for siteID: Int64,
pageNumber: Int,
pageSize: Int,
Expand Down Expand Up @@ -50,4 +55,11 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
func updateBooking(from siteID: Int64, bookingID: Int64, attendanceStatus: Networking.BookingAttendanceStatus) async throws -> Networking.Booking? {
return nil
}

func fetchResources(for siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> [Networking.BookingResource] {
guard let result = fetchResourcesResult else {
throw NetworkError.timeout()
}
return try result.get()
}
}
Loading