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
31 changes: 25 additions & 6 deletions Modules/Sources/Networking/Model/Bookings/Booking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable {
public let allDay: Bool
public let cost: String
public let customerID: Int64
public let dateCreated: Date
public let dateModified: Date
public let dateCreated: Date?
public let dateModified: Date?
public let endDate: Date
public let googleCalendarEventID: String?
public let orderID: Int64
Expand Down Expand Up @@ -40,8 +40,8 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable {
allDay: Bool,
cost: String,
customerID: Int64,
dateCreated: Date,
dateModified: Date,
dateCreated: Date?,
dateModified: Date?,
endDate: Date,
googleCalendarEventID: String?,
orderID: Int64,
Expand Down Expand Up @@ -95,8 +95,27 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable {
alternativeTypes: [.decimal(transform: { NSDecimalNumber(decimal: $0).stringValue })]) ?? ""

let customerID = try container.decode(Int64.self, forKey: .customerID)
let dateCreated = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .dateCreated))
let dateModified = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .dateModified))

let dateCreated: Date?
if let dateCreatedValue = try container.decodeIfPresent(
Double.self,
forKey: .dateCreated
) {
dateCreated = Date(timeIntervalSince1970: dateCreatedValue)
} else {
dateCreated = nil
}

let dateModified: Date?
if let dateModifiedValue = try container.decodeIfPresent(
Double.self,
forKey: .dateModified
) {
dateModified = Date(timeIntervalSince1970: dateModifiedValue)
} else {
dateModified = nil
}

let endDate = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .endDate))
let googleCalendarEventID = try container.decodeIfPresent(String.self, forKey: .googleCalendarEventID)
let orderID = try container.decode(Int64.self, forKey: .orderID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ extension Storage.Booking: ReadOnlyConvertible {
allDay = booking.allDay
cost = booking.cost
customerID = booking.customerID
dateCreated = booking.dateCreated
dateModified = booking.dateModified

/// Falling to back to existing values in case if new values are absent
/// Booking returned when sending a `PUT` request to `bookings/{booking_id}`
/// doesn't contain `date_created` and `date_modified` values.
dateCreated = booking.dateCreated ?? dateCreated
dateModified = booking.dateModified ?? dateModified
Comment on lines +20 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Are you sure we want to keep the old dates here? If so, please leave a comment in the code for why that's necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in b3991c7


endDate = booking.endDate
googleCalendarEventID = booking.googleCalendarEventID
orderID = booking.orderID
Expand Down
44 changes: 41 additions & 3 deletions Modules/Sources/Yosemite/Stores/BookingStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,39 @@ private extension BookingStore {
siteID: siteID,
bookingID: bookingID,
statusKey: status
) { _ in
//TODO: - booking status remote update + rollback status in case of error
onCompletion(nil)
) { [weak self] previousStatusKey in
guard let self else {
return onCompletion(UpdateBookingStatusError.undefinedState)
}

Task { @MainActor in
do {
if let remoteBooking = try await self.remote.updateBooking(
from: siteID,
bookingID: bookingID,
attendanceStatus: status
) {
await self.upsertStoredBookingsInBackground(
readOnlyBookings: [remoteBooking],
readOnlyOrders: [],
siteID: siteID
)

onCompletion(nil)
} else {
return onCompletion(UpdateBookingStatusError.missingRemoteBooking)
}
} catch {
/// Revert Optimistic Update
self.updateBookingAttendanceStatusLocally(
siteID: siteID,
bookingID: bookingID,
statusKey: previousStatusKey
) { _ in
onCompletion(error)
}
}
}
}
}

Expand Down Expand Up @@ -443,3 +473,11 @@ private extension BookingStore {
}
}
}


// MARK: - Errors
//
private enum UpdateBookingStatusError: Error {
case undefinedState
case missingRemoteBooking
}
19 changes: 19 additions & 0 deletions Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,25 @@ struct BookingsRemoteTests {
}
}

@Test func test_updateBooking_ignores_nil_dates_in_response() async throws {
// Given
let remote = BookingsRemote(network: network)
let bookingID: Int64 = 206
network.simulateResponse(requestUrlSuffix: "bookings/\(bookingID)", filename: "booking-no-create-update-dates")

// When
let booking = try await remote.updateBooking(
from: sampleSiteID,
bookingID: bookingID,
attendanceStatus: .noShow,
)

// Then
#expect(booking?.dateCreated == nil)
#expect(booking?.dateModified == nil)
#expect(booking?.id == bookingID)
}

@Test func test_fetchResources_properly_returns_parsed_resources() async throws {
// Given
let remote = BookingsRemote(network: network)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"data": {
"id": 206,
"start": 1761238800,
"end": 1761242400,
"all_day": false,
"status": "cancelled",
"attendance_status": "no-show",
"cost": "35.00",
"currency": "USD",
"customer_id": 5,
"product_id": 23,
"resource_id": 19,
"google_calendar_event_id": "0",
"order_id": 205,
"order_item_id": 21,
"parent_id": 0,
"person_counts": [],
"local_timezone": "",
"note": "edited note"
}
}
10 changes: 9 additions & 1 deletion 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 updateBookingResult: Result<Booking?, Error>?
private var fetchResourcesResult: Result<[BookingResource], Error>?

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

func whenUpdatingBooking(thenReturn result: Result<Booking?, Error>) {
updateBookingResult = result
}

func whenFetchingResource(thenReturn result: Result<BookingResource?, Error>) {
fetchResourceResult = result
}
Expand Down Expand Up @@ -53,7 +58,10 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
}

func updateBooking(from siteID: Int64, bookingID: Int64, attendanceStatus: Networking.BookingAttendanceStatus) async throws -> Networking.Booking? {
return nil
guard let result = updateBookingResult else {
throw NetworkError.timeout()
}
return try result.get()
}

func fetchResources(for siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> [Networking.BookingResource] {
Expand Down
119 changes: 119 additions & 0 deletions Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Foundation
import Testing
@testable import Networking
@testable import Storage
Expand Down Expand Up @@ -624,6 +625,124 @@ struct BookingStoreTests {
#expect(orderInfo.statusKey == "processing")
}

// MARK: - performUpdateBookingAttendanceStatus

@Test func performUpdateBookingAttendanceStatus_updates_localBooking() async throws {
// Given
let booking = Booking.fake().copy(
siteID: sampleSiteID,
bookingID: 1,
attendanceStatusKey: BookingAttendanceStatus.booked.rawValue
)
storeBooking(booking)

let remoteBooking = booking.copy(attendanceStatusKey: BookingAttendanceStatus.checkedIn.rawValue)
remote.whenUpdatingBooking(thenReturn: .success(remoteBooking))
let store = BookingStore(dispatcher: Dispatcher(),
storageManager: storageManager,
network: network,
remote: remote,
ordersRemote: ordersRemote)

// When
let error = await withCheckedContinuation { continuation in
store.onAction(
BookingAction.updateBookingAttendanceStatus(
siteID: sampleSiteID,
bookingID: 1,
status: .checkedIn,
onCompletion: { error in
continuation.resume(returning: error)
}
)
)
}

// Then
#expect(error == nil)
let storedBooking = try #require(viewStorage.loadBooking(siteID: sampleSiteID, bookingID: 1))
#expect(storedBooking.attendanceStatusKey == BookingAttendanceStatus.checkedIn.rawValue)
}

@Test func performUpdateBookingAttendanceStatus_keeps_existing_create_and_update_dates() async throws {
// Given
let date = Date(timeIntervalSince1970: 0)
let booking = Booking.fake().copy(
siteID: sampleSiteID,
bookingID: 1,
dateCreated: date,
dateModified: date
)
storeBooking(booking)

let remoteBooking = booking.copy(
dateCreated: nil,
dateModified: nil
)
remote.whenUpdatingBooking(thenReturn: .success(remoteBooking))
let store = BookingStore(dispatcher: Dispatcher(),
storageManager: storageManager,
network: network,
remote: remote,
ordersRemote: ordersRemote)

// When
let error = await withCheckedContinuation { continuation in
store.onAction(
BookingAction.updateBookingAttendanceStatus(
siteID: sampleSiteID,
bookingID: 1,
status: .checkedIn,
onCompletion: { error in
continuation.resume(returning: error)
}
)
)
}

// Then
#expect(error == nil)
let storedBooking = try #require(viewStorage.loadBooking(siteID: sampleSiteID, bookingID: 1))
#expect(storedBooking.dateCreated == date)
#expect(storedBooking.dateModified == date)
}

@Test func performUpdateBookingAttendanceStatus_reverts_old_status_on_error() async throws {
// Given
let booking = Booking.fake().copy(
siteID: sampleSiteID,
bookingID: 1,
attendanceStatusKey: BookingAttendanceStatus.booked.rawValue
)
storeBooking(booking)

remote.whenUpdatingBooking(thenReturn: .failure(NetworkError.timeout()))
let store = BookingStore(dispatcher: Dispatcher(),
storageManager: storageManager,
network: network,
remote: remote,
ordersRemote: ordersRemote)

// When
let error = await withCheckedContinuation { continuation in
store.onAction(
BookingAction.updateBookingAttendanceStatus(
siteID: sampleSiteID,
bookingID: 1,
status: .checkedIn,
onCompletion: { error in
continuation.resume(returning: error)
}
)
)
}

// Then
#expect(error != nil)
let storedBooking = try #require(viewStorage.loadBooking(siteID: sampleSiteID, bookingID: 1))
#expect(storedBooking.attendanceStatusKey == BookingAttendanceStatus.booked.rawValue)
}

// MARK: - synchronizeResources

@Test func synchronizeResources_returns_false_for_hasNextPage_when_number_of_retrieved_results_is_zero() async throws {
Expand Down
Loading