Skip to content

Commit 76449d5

Browse files
committed
Add new action for syncing resources
1 parent c7f9f92 commit 76449d5

File tree

4 files changed

+207
-5
lines changed

4 files changed

+207
-5
lines changed

Modules/Sources/Storage/Tools/StorageType+Extensions.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,13 @@ public extension StorageType {
967967
return objects.isEmpty ? nil : objects
968968
}
969969

970+
/// Retrieves the store booking resources
971+
func loadBookingResources(siteID: Int64, resourceIDs: [Int64]) -> [BookingResource] {
972+
let predicate = NSPredicate(format: "siteID == %lld && resourceID in %@", siteID, resourceIDs)
973+
let descriptor = NSSortDescriptor(keyPath: \BookingResource.resourceID, ascending: false)
974+
return allObjects(ofType: BookingResource.self, matching: predicate, sortedBy: [descriptor])
975+
}
976+
970977
/// Retrieves the store booking resource
971978
func loadBookingResource(siteID: Int64, resourceID: Int64) -> BookingResource? {
972979
let predicate = \BookingResource.resourceID == resourceID && \BookingResource.siteID == siteID

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ public enum BookingAction: Action {
5353
resourceID: Int64,
5454
onCompletion: (Result<BookingResource, Error>) -> Void)
5555

56+
/// Synchronizes booking resources matching the specified criteria.
57+
///
58+
/// - Parameter onCompletion: called when sync completes, returns an error in case of a failure or empty in case of success.
59+
///
60+
case synchronizeResources(siteID: Int64,
61+
pageNumber: Int,
62+
pageSize: Int = BookingsRemote.Default.pageSize,
63+
onCompletion: (Result<Void, Error>) -> Void)
64+
5665
/// Updates a booking attendance status.
5766
///
5867
/// - Parameter siteID: The site ID of the booking.

Modules/Sources/Yosemite/Stores/BookingStore.swift

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ public class BookingStore: Store {
6363
onCompletion: onCompletion)
6464
case let .fetchResource(siteID, resourceID, onCompletion):
6565
fetchResource(siteID: siteID, resourceID: resourceID, onCompletion: onCompletion)
66+
case let .synchronizeResources(siteID, pageNumber, pageSize, onCompletion):
67+
synchronizeResources(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
6668
case .updateBookingAttendanceStatus(let siteID, let bookingID, let status, let onCompletion):
6769
performUpdateBookingAttendanceStatus(
6870
siteID: siteID,
@@ -244,7 +246,8 @@ private extension BookingStore {
244246
return
245247
}
246248

247-
await upsertBookingResourceInBackground(readOnlyBookingResource: resource)
249+
await upsertBookingResourcesInBackground(siteID: resource.siteID,
250+
readOnlyBookingResources: [resource])
248251

249252
onCompletion(.success(resource))
250253
} catch {
@@ -253,6 +256,30 @@ private extension BookingStore {
253256
}
254257
}
255258

259+
/// Synchronizes booking resources for the specified site.
260+
///
261+
func synchronizeResources(siteID: Int64,
262+
pageNumber: Int,
263+
pageSize: Int,
264+
onCompletion: @escaping (Result<Void, Error>) -> Void) {
265+
Task { @MainActor in
266+
do {
267+
let resources = try await remote.fetchResources(
268+
for: siteID,
269+
pageNumber: pageNumber,
270+
pageSize: pageSize
271+
)
272+
273+
await upsertBookingResourcesInBackground(siteID: siteID,
274+
readOnlyBookingResources: resources)
275+
276+
onCompletion(.success(()))
277+
} catch {
278+
onCompletion(.failure(error))
279+
}
280+
}
281+
}
282+
256283
func performUpdateBookingAttendanceStatus(
257284
siteID: Int64,
258285
bookingID: Int64,
@@ -393,16 +420,22 @@ private extension BookingStore {
393420
}
394421

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

402429
storageManager.performAndSave({ storage in
403-
let storedItem = storage.loadBookingResource(siteID: readOnlyBookingResource.siteID, resourceID: readOnlyBookingResource.resourceID)
404-
let storageResource = storedItem ?? storage.insertNewObject(ofType: Storage.BookingResource.self)
405-
storageResource.update(with: readOnlyBookingResource)
430+
let storedItems = storage.loadBookingResources(
431+
siteID: siteID,
432+
resourceIDs: readOnlyBookingResources.map { $0.resourceID }
433+
)
434+
for item in readOnlyBookingResources {
435+
let storageResource = storedItems.first(where: { $0.resourceID == item.resourceID }) ??
436+
storage.insertNewObject(ofType: Storage.BookingResource.self)
437+
storageResource.update(with: item)
438+
}
406439
}, completion: {
407440
continuation.resume()
408441
}, on: .main)

Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ struct BookingStoreTests {
3636
return viewStorage.countObjects(ofType: StorageBooking.self)
3737
}
3838

39+
/// Convenience: returns the number of stored booking resources
40+
///
41+
private var storedBookingResourceCount: Int {
42+
return viewStorage.countObjects(ofType: Storage.BookingResource.self)
43+
}
44+
3945
/// SiteID
4046
///
4147
private let sampleSiteID: Int64 = 120934
@@ -618,6 +624,153 @@ struct BookingStoreTests {
618624
#expect(orderInfo.statusKey == "processing")
619625
}
620626

627+
// MARK: - synchronizeResources
628+
629+
@Test func synchronizeResources_stores_resources_upon_success() async throws {
630+
// Given
631+
let resource = BookingResource.fake().copy(siteID: sampleSiteID, resourceID: 123)
632+
remote.whenFetchingResources(thenReturn: .success([resource]))
633+
let store = BookingStore(dispatcher: Dispatcher(),
634+
storageManager: storageManager,
635+
network: network,
636+
remote: remote,
637+
ordersRemote: ordersRemote)
638+
#expect(storedBookingResourceCount == 0)
639+
640+
// When
641+
let result = await withCheckedContinuation { continuation in
642+
store.onAction(BookingAction.synchronizeResources(siteID: sampleSiteID,
643+
pageNumber: defaultPageNumber,
644+
pageSize: defaultPageSize,
645+
onCompletion: { result in
646+
continuation.resume(returning: result)
647+
}))
648+
}
649+
650+
// Then
651+
#expect(result.isSuccess)
652+
#expect(storedBookingResourceCount == 1)
653+
}
654+
655+
@Test func synchronizeResources_updates_existing_resource_when_resource_already_exists() async throws {
656+
// Given
657+
let originalResource = BookingResource.fake().copy(siteID: sampleSiteID, resourceID: 123, name: "Original Name")
658+
storeBookingResource(originalResource)
659+
#expect(storedBookingResourceCount == 1)
660+
661+
let updatedResource = originalResource.copy(name: "Updated Name")
662+
remote.whenFetchingResources(thenReturn: .success([updatedResource]))
663+
let store = BookingStore(dispatcher: Dispatcher(),
664+
storageManager: storageManager,
665+
network: network,
666+
remote: remote,
667+
ordersRemote: ordersRemote)
668+
669+
// When
670+
let result = await withCheckedContinuation { continuation in
671+
store.onAction(BookingAction.synchronizeResources(siteID: sampleSiteID,
672+
pageNumber: defaultPageNumber,
673+
pageSize: defaultPageSize,
674+
onCompletion: { result in
675+
continuation.resume(returning: result)
676+
}))
677+
}
678+
679+
// Then
680+
#expect(result.isSuccess)
681+
#expect(storedBookingResourceCount == 1)
682+
let storedResource = try #require(viewStorage.loadBookingResource(siteID: sampleSiteID, resourceID: 123))
683+
#expect(storedResource.name == "Updated Name")
684+
}
685+
686+
@Test func synchronizeResources_stores_multiple_resources_upon_success() async throws {
687+
// Given
688+
let resource1 = BookingResource.fake().copy(siteID: sampleSiteID, resourceID: 123)
689+
let resource2 = BookingResource.fake().copy(siteID: sampleSiteID, resourceID: 456)
690+
remote.whenFetchingResources(thenReturn: .success([resource1, resource2]))
691+
let store = BookingStore(dispatcher: Dispatcher(),
692+
storageManager: storageManager,
693+
network: network,
694+
remote: remote,
695+
ordersRemote: ordersRemote)
696+
#expect(storedBookingResourceCount == 0)
697+
698+
// When
699+
let result = await withCheckedContinuation { continuation in
700+
store.onAction(BookingAction.synchronizeResources(siteID: sampleSiteID,
701+
pageNumber: defaultPageNumber,
702+
pageSize: defaultPageSize,
703+
onCompletion: { result in
704+
continuation.resume(returning: result)
705+
}))
706+
}
707+
708+
// Then
709+
#expect(result.isSuccess)
710+
#expect(storedBookingResourceCount == 2)
711+
}
712+
713+
@Test func synchronizeResources_returns_error_on_failure() async throws {
714+
// Given
715+
remote.whenFetchingResources(thenReturn: .failure(NetworkError.timeout()))
716+
let store = BookingStore(dispatcher: Dispatcher(),
717+
storageManager: storageManager,
718+
network: network,
719+
remote: remote,
720+
ordersRemote: ordersRemote)
721+
722+
// When
723+
let result = await withCheckedContinuation { continuation in
724+
store.onAction(BookingAction.synchronizeResources(siteID: sampleSiteID,
725+
pageNumber: defaultPageNumber,
726+
pageSize: defaultPageSize,
727+
onCompletion: { result in
728+
continuation.resume(returning: result)
729+
}))
730+
}
731+
732+
// Then
733+
#expect(result.isFailure)
734+
let error = result.failure as? NetworkError
735+
#expect(error == .timeout())
736+
}
737+
738+
@Test func synchronizeResources_preserves_existing_resources_from_previous_pages() async throws {
739+
// Given
740+
let existingResource = BookingResource.fake().copy(siteID: sampleSiteID, resourceID: 999)
741+
storeBookingResource(existingResource)
742+
#expect(storedBookingResourceCount == 1)
743+
744+
let newResource = BookingResource.fake().copy(siteID: sampleSiteID, resourceID: 123)
745+
remote.whenFetchingResources(thenReturn: .success([newResource]))
746+
let store = BookingStore(dispatcher: Dispatcher(),
747+
storageManager: storageManager,
748+
network: network,
749+
remote: remote,
750+
ordersRemote: ordersRemote)
751+
752+
// When
753+
let result = await withCheckedContinuation { continuation in
754+
store.onAction(BookingAction.synchronizeResources(siteID: sampleSiteID,
755+
pageNumber: defaultPageNumber,
756+
pageSize: defaultPageSize,
757+
onCompletion: { result in
758+
continuation.resume(returning: result)
759+
}))
760+
}
761+
762+
// Then
763+
#expect(result.isSuccess)
764+
#expect(storedBookingResourceCount == 2)
765+
766+
// Verify both resources exist
767+
let newStoredResource = try #require(viewStorage.loadBookingResource(siteID: sampleSiteID, resourceID: 123))
768+
#expect(newStoredResource.resourceID == 123)
769+
770+
let existingStoredResource = try #require(viewStorage.loadBookingResource(siteID: sampleSiteID, resourceID: 999))
771+
#expect(existingStoredResource.resourceID == 999)
772+
}
773+
621774
// MARK: - orderInfo Storage Tests
622775

623776
@Test func synchronizeBookings_stores_complete_orderInfo_with_all_nested_properties() async throws {

0 commit comments

Comments
 (0)