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
6 changes: 2 additions & 4 deletions Modules/Sources/Networking/Model/Bookings/Booking.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// periphery:ignore:all
import Codegen
import Foundation

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

/// Computed Properties
///
// periphery: ignore - to be used later
public var bookingStatus: BookingStatus {
return BookingStatus(rawValue: statusKey) ?? .unknown
}
Expand Down Expand Up @@ -173,8 +171,8 @@ enum BookingDecodingError: Error {
// MARK: - Supporting Types
//

// periphery: ignore
/// Represents a Booking Status.
///
public enum BookingStatus: String, CaseIterable {
case complete
case paid
Expand Down
13 changes: 13 additions & 0 deletions Modules/Sources/Storage/Tools/StorageType+Deletions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,17 @@ public extension StorageType {
deleteObject($0)
}
}

// MARK: - Bookings

/// Deletes all of the stored Bookings for the provided siteID.
///
func deleteBookings(siteID: Int64) {
guard let bookings = loadBookings(siteID: siteID) else {
return
}
for booking in bookings {
deleteObject(booking)
}
}
}
26 changes: 26 additions & 0 deletions Modules/Sources/Storage/Tools/StorageType+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -940,4 +940,30 @@ public extension StorageType {
let predicate = \MetaData.product?.siteID == siteID && \MetaData.product?.productID == productID
return allObjects(ofType: MetaData.self, matching: predicate, sortedBy: nil)
}

// MARK: - Bookings

/// Retrieves the Stored Bookings given the IDs.
///
func loadBookings(siteID: Int64, bookingIDs: [Int64]) -> [Booking] {
let predicate = NSPredicate(format: "siteID == %lld && bookingID in %@", siteID, bookingIDs)
let descriptor = NSSortDescriptor(keyPath: \Booking.bookingID, ascending: false)
return allObjects(ofType: Booking.self, matching: predicate, sortedBy: [descriptor])
}

// periphery: ignore
/// Retrieves the Stored Booking.
func loadBooking(siteID: Int64, bookingID: Int64) -> Booking? {
let predicate = \Booking.bookingID == bookingID && \Booking.siteID == siteID
return firstObject(ofType: Booking.self, matching: predicate)
}

/// Retrieves all stored bookings for a site.
///
func loadBookings(siteID: Int64) -> [Booking]? {
let predicate = \Booking.siteID == siteID
let descriptor = NSSortDescriptor(keyPath: \Booking.bookingID, ascending: false)
let objects = allObjects(ofType: Booking.self, matching: predicate, sortedBy: [descriptor])
return objects.isEmpty ? nil : objects
}
}
17 changes: 17 additions & 0 deletions Modules/Sources/Yosemite/Actions/BookingAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
import Networking

// periphery: ignore
/// BookingAction: Defines all of the Actions supported by the BookingStore.
///
public enum BookingAction: Action {

/// Synchronizes the Bookings matching the specified criteria.
///
/// - Parameter onCompletion: called when sync completes, returns an error or a boolean that indicates whether there might be more bookings to sync.
///
case synchronizeBookings(siteID: Int64,
pageNumber: Int,
pageSize: Int = BookingsRemote.Default.pageSize,
onCompletion: (Result<Bool, Error>) -> Void)
}
2 changes: 1 addition & 1 deletion Modules/Sources/Yosemite/Model/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public typealias StorageBlazeCampaignListItem = Storage.BlazeCampaignListItem
public typealias StorageBlazeTargetDevice = Storage.BlazeTargetDevice
public typealias StorageBlazeTargetLanguage = Storage.BlazeTargetLanguage
public typealias StorageBlazeTargetTopic = Storage.BlazeTargetTopic
// periphery:ignore - will be used later
// periphery: ignore
public typealias StorageBooking = Storage.Booking
public typealias StorageCardReaderType = Storage.CardReaderType
public typealias StorageCoupon = Storage.Coupon
Expand Down
131 changes: 131 additions & 0 deletions Modules/Sources/Yosemite/Stores/BookingStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Foundation
import Networking
import Storage

// MARK: - BookingStore
//
public class BookingStore: Store {
private let remote: BookingsRemoteProtocol

public override convenience init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) {
let remote = BookingsRemote(network: network)
self.init(dispatcher: dispatcher, storageManager: storageManager, network: network, remote: remote)
}

public init(dispatcher: Dispatcher,
storageManager: StorageManagerType,
network: Network,
remote: BookingsRemoteProtocol) {
self.remote = remote
super.init(dispatcher: dispatcher, storageManager: storageManager, network: network)
}

/// Registers for supported Actions.
///
override public func registerSupportedActions(in dispatcher: Dispatcher) {
dispatcher.register(processor: self, for: BookingAction.self)
}

/// Receives and executes Actions.
///
override public func onAction(_ action: Action) {
guard let action = action as? BookingAction else {
assertionFailure("BookingStore received an unsupported action")
return
}

switch action {
case let .synchronizeBookings(siteID, pageNumber, pageSize, onCompletion):
synchronizeBookings(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
}
}
}


// MARK: - Services
//
private extension BookingStore {

/// Synchronizes the bookings for the specified site.
///
func synchronizeBookings(siteID: Int64,
pageNumber: Int,
pageSize: Int,
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
Task { @MainActor in
do {
let bookings = try await remote.loadAllBookings(for: siteID,
pageNumber: pageNumber,
pageSize: pageSize)
await upsertStoredBookingsInBackground(readOnlyBookings: bookings, siteID: siteID)
let hasNextPage = bookings.count == pageSize
onCompletion(.success(hasNextPage))
} catch {
onCompletion(.failure(error))
}
}
}
}


// MARK: - Storage: Booking
//
extension BookingStore {

/// Updates (OR Inserts) the specified ReadOnly Booking Entities *in a background thread* async.
/// Also deletes existing bookings if requested.
func upsertStoredBookingsInBackground(readOnlyBookings: [Yosemite.Booking],
siteID: Int64,
shouldDeleteExistingBookings: Bool = false) async {
await withCheckedContinuation { [weak self] continuation in
guard let self else {
return continuation.resume()
}
upsertStoredBookingsInBackground(readOnlyBookings: readOnlyBookings,
siteID: siteID,
shouldDeleteExistingBookings: shouldDeleteExistingBookings) {
continuation.resume()
}
}
}

/// Updates (OR Inserts) the specified ReadOnly Booking Entities *in a background thread*.
/// Also deletes existing bookings if requested.
/// `onCompletion` will be called on the main thread!
///
func upsertStoredBookingsInBackground(readOnlyBookings: [Yosemite.Booking],
siteID: Int64,
shouldDeleteExistingBookings: Bool = false,
onCompletion: @escaping () -> Void) {
storageManager.performAndSave({ [weak self] storage in
guard let self else {
return onCompletion()
}
if shouldDeleteExistingBookings {
storage.deleteBookings(siteID: siteID)
}
upsertStoredBookings(readOnlyBookings: readOnlyBookings, in: storage)
}, completion: onCompletion, on: .main)
}

/// Updates (OR Inserts) the specified ReadOnly Booking Entities into the Storage Layer.
///
/// - Parameters:
/// - readOnlyBookings: Remote Bookings to be persisted.
/// - storage: Where we should save all the things!
///
func upsertStoredBookings(readOnlyBookings: [Networking.Booking], in storage: StorageType) {
// Fetch all existing bookings for the site at once
let bookingIDs = readOnlyBookings.map { $0.bookingID }
let siteID = readOnlyBookings.first?.siteID ?? 0
let storedBookings = storage.loadBookings(siteID: siteID, bookingIDs: bookingIDs)

for readOnlyBooking in readOnlyBookings {
// Filter to find existing booking by booking ID
let storageBooking = storedBookings.first { $0.bookingID == readOnlyBooking.bookingID } ??
storage.insertNewObject(ofType: StorageBooking.self)

storageBooking.update(with: readOnlyBooking)
}
}
}
19 changes: 19 additions & 0 deletions Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation
@testable import Networking

/// Mock for BookingsRemoteProtocol
///
final class MockBookingsRemote: BookingsRemoteProtocol {
private var loadAllBookingsResult: Result<[Booking], Error>?

func whenLoadingAllBookings(thenReturn result: Result<[Booking], Error>) {
loadAllBookingsResult = result
}

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