Skip to content

Commit 5a7ceb2

Browse files
authored
Bookings: Update Yosemite layer to support syncing bookings (#16170)
2 parents 90c88a7 + 8fb28ad commit 5a7ceb2

File tree

9 files changed

+429
-6
lines changed

9 files changed

+429
-6
lines changed

Modules/Sources/Networking/Model/Bookings/Booking.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// periphery:ignore:all
21
import Codegen
32
import Foundation
43

@@ -23,8 +22,7 @@ public struct Booking: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
2322
public let statusKey: String
2423
public let localTimezone: String
2524

26-
/// Computed Properties
27-
///
25+
// periphery: ignore - to be used later
2826
public var bookingStatus: BookingStatus {
2927
return BookingStatus(rawValue: statusKey) ?? .unknown
3028
}
@@ -173,8 +171,8 @@ enum BookingDecodingError: Error {
173171
// MARK: - Supporting Types
174172
//
175173

174+
// periphery: ignore
176175
/// Represents a Booking Status.
177-
///
178176
public enum BookingStatus: String, CaseIterable {
179177
case complete
180178
case paid

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,17 @@ public extension StorageType {
243243
deleteObject($0)
244244
}
245245
}
246+
247+
// MARK: - Bookings
248+
249+
/// Deletes all of the stored Bookings for the provided siteID.
250+
///
251+
func deleteBookings(siteID: Int64) {
252+
guard let bookings = loadBookings(siteID: siteID) else {
253+
return
254+
}
255+
for booking in bookings {
256+
deleteObject(booking)
257+
}
258+
}
246259
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,4 +940,30 @@ public extension StorageType {
940940
let predicate = \MetaData.product?.siteID == siteID && \MetaData.product?.productID == productID
941941
return allObjects(ofType: MetaData.self, matching: predicate, sortedBy: nil)
942942
}
943+
944+
// MARK: - Bookings
945+
946+
/// Retrieves the Stored Bookings given the IDs.
947+
///
948+
func loadBookings(siteID: Int64, bookingIDs: [Int64]) -> [Booking] {
949+
let predicate = NSPredicate(format: "siteID == %lld && bookingID in %@", siteID, bookingIDs)
950+
let descriptor = NSSortDescriptor(keyPath: \Booking.bookingID, ascending: false)
951+
return allObjects(ofType: Booking.self, matching: predicate, sortedBy: [descriptor])
952+
}
953+
954+
// periphery: ignore
955+
/// Retrieves the Stored Booking.
956+
func loadBooking(siteID: Int64, bookingID: Int64) -> Booking? {
957+
let predicate = \Booking.bookingID == bookingID && \Booking.siteID == siteID
958+
return firstObject(ofType: Booking.self, matching: predicate)
959+
}
960+
961+
/// Retrieves all stored bookings for a site.
962+
///
963+
func loadBookings(siteID: Int64) -> [Booking]? {
964+
let predicate = \Booking.siteID == siteID
965+
let descriptor = NSSortDescriptor(keyPath: \Booking.bookingID, ascending: false)
966+
let objects = allObjects(ofType: Booking.self, matching: predicate, sortedBy: [descriptor])
967+
return objects.isEmpty ? nil : objects
968+
}
943969
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
import Networking
3+
4+
// periphery: ignore
5+
/// BookingAction: Defines all of the Actions supported by the BookingStore.
6+
///
7+
public enum BookingAction: Action {
8+
9+
/// Synchronizes the Bookings matching the specified criteria.
10+
///
11+
/// - Parameter onCompletion: called when sync completes, returns an error or a boolean that indicates whether there might be more bookings to sync.
12+
///
13+
case synchronizeBookings(siteID: Int64,
14+
pageNumber: Int,
15+
pageSize: Int = BookingsRemote.Default.pageSize,
16+
onCompletion: (Result<Bool, Error>) -> Void)
17+
}

Modules/Sources/Yosemite/Model/Model.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ public typealias StorageBlazeCampaignListItem = Storage.BlazeCampaignListItem
266266
public typealias StorageBlazeTargetDevice = Storage.BlazeTargetDevice
267267
public typealias StorageBlazeTargetLanguage = Storage.BlazeTargetLanguage
268268
public typealias StorageBlazeTargetTopic = Storage.BlazeTargetTopic
269-
// periphery:ignore - will be used later
269+
// periphery: ignore
270270
public typealias StorageBooking = Storage.Booking
271271
public typealias StorageCardReaderType = Storage.CardReaderType
272272
public typealias StorageCoupon = Storage.Coupon
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import Foundation
2+
import Networking
3+
import Storage
4+
5+
// MARK: - BookingStore
6+
//
7+
public class BookingStore: Store {
8+
private let remote: BookingsRemoteProtocol
9+
10+
public override convenience init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) {
11+
let remote = BookingsRemote(network: network)
12+
self.init(dispatcher: dispatcher, storageManager: storageManager, network: network, remote: remote)
13+
}
14+
15+
public init(dispatcher: Dispatcher,
16+
storageManager: StorageManagerType,
17+
network: Network,
18+
remote: BookingsRemoteProtocol) {
19+
self.remote = remote
20+
super.init(dispatcher: dispatcher, storageManager: storageManager, network: network)
21+
}
22+
23+
/// Registers for supported Actions.
24+
///
25+
override public func registerSupportedActions(in dispatcher: Dispatcher) {
26+
dispatcher.register(processor: self, for: BookingAction.self)
27+
}
28+
29+
/// Receives and executes Actions.
30+
///
31+
override public func onAction(_ action: Action) {
32+
guard let action = action as? BookingAction else {
33+
assertionFailure("BookingStore received an unsupported action")
34+
return
35+
}
36+
37+
switch action {
38+
case let .synchronizeBookings(siteID, pageNumber, pageSize, onCompletion):
39+
synchronizeBookings(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
40+
}
41+
}
42+
}
43+
44+
45+
// MARK: - Services
46+
//
47+
private extension BookingStore {
48+
49+
/// Synchronizes the bookings for the specified site.
50+
///
51+
func synchronizeBookings(siteID: Int64,
52+
pageNumber: Int,
53+
pageSize: Int,
54+
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
55+
Task { @MainActor in
56+
do {
57+
let bookings = try await remote.loadAllBookings(for: siteID,
58+
pageNumber: pageNumber,
59+
pageSize: pageSize)
60+
await upsertStoredBookingsInBackground(readOnlyBookings: bookings, siteID: siteID)
61+
let hasNextPage = bookings.count == pageSize
62+
onCompletion(.success(hasNextPage))
63+
} catch {
64+
onCompletion(.failure(error))
65+
}
66+
}
67+
}
68+
}
69+
70+
71+
// MARK: - Storage: Booking
72+
//
73+
extension BookingStore {
74+
75+
/// Updates (OR Inserts) the specified ReadOnly Booking Entities *in a background thread* async.
76+
/// Also deletes existing bookings if requested.
77+
func upsertStoredBookingsInBackground(readOnlyBookings: [Yosemite.Booking],
78+
siteID: Int64,
79+
shouldDeleteExistingBookings: Bool = false) async {
80+
await withCheckedContinuation { [weak self] continuation in
81+
guard let self else {
82+
return continuation.resume()
83+
}
84+
upsertStoredBookingsInBackground(readOnlyBookings: readOnlyBookings,
85+
siteID: siteID,
86+
shouldDeleteExistingBookings: shouldDeleteExistingBookings) {
87+
continuation.resume()
88+
}
89+
}
90+
}
91+
92+
/// Updates (OR Inserts) the specified ReadOnly Booking Entities *in a background thread*.
93+
/// Also deletes existing bookings if requested.
94+
/// `onCompletion` will be called on the main thread!
95+
///
96+
func upsertStoredBookingsInBackground(readOnlyBookings: [Yosemite.Booking],
97+
siteID: Int64,
98+
shouldDeleteExistingBookings: Bool = false,
99+
onCompletion: @escaping () -> Void) {
100+
storageManager.performAndSave({ [weak self] storage in
101+
guard let self else {
102+
return onCompletion()
103+
}
104+
if shouldDeleteExistingBookings {
105+
storage.deleteBookings(siteID: siteID)
106+
}
107+
upsertStoredBookings(readOnlyBookings: readOnlyBookings, in: storage)
108+
}, completion: onCompletion, on: .main)
109+
}
110+
111+
/// Updates (OR Inserts) the specified ReadOnly Booking Entities into the Storage Layer.
112+
///
113+
/// - Parameters:
114+
/// - readOnlyBookings: Remote Bookings to be persisted.
115+
/// - storage: Where we should save all the things!
116+
///
117+
func upsertStoredBookings(readOnlyBookings: [Networking.Booking], in storage: StorageType) {
118+
// Fetch all existing bookings for the site at once
119+
let bookingIDs = readOnlyBookings.map { $0.bookingID }
120+
let siteID = readOnlyBookings.first?.siteID ?? 0
121+
let storedBookings = storage.loadBookings(siteID: siteID, bookingIDs: bookingIDs)
122+
123+
for readOnlyBooking in readOnlyBookings {
124+
// Filter to find existing booking by booking ID
125+
let storageBooking = storedBookings.first { $0.bookingID == readOnlyBooking.bookingID } ??
126+
storage.insertNewObject(ofType: StorageBooking.self)
127+
128+
storageBooking.update(with: readOnlyBooking)
129+
}
130+
}
131+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
@testable import Networking
3+
4+
/// Mock for BookingsRemoteProtocol
5+
///
6+
final class MockBookingsRemote: BookingsRemoteProtocol {
7+
private var loadAllBookingsResult: Result<[Booking], Error>?
8+
9+
func whenLoadingAllBookings(thenReturn result: Result<[Booking], Error>) {
10+
loadAllBookingsResult = result
11+
}
12+
13+
func loadAllBookings(for siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> [Booking] {
14+
guard let result = loadAllBookingsResult else {
15+
throw NetworkError.timeout()
16+
}
17+
return try result.get()
18+
}
19+
}

0 commit comments

Comments
 (0)