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
20 changes: 15 additions & 5 deletions Modules/Sources/Networking/Remote/BookingsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public protocol BookingsRemoteProtocol {
func updateBooking(
from siteID: Int64,
bookingID: Int64,
attendanceStatus: BookingAttendanceStatus
attendanceStatus: BookingAttendanceStatus?,
bookingStatus: BookingStatus?
) async throws -> Booking?

func fetchResource(resourceID: Int64,
Expand Down Expand Up @@ -150,12 +151,20 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
public func updateBooking(
from siteID: Int64,
bookingID: Int64,
attendanceStatus: BookingAttendanceStatus
attendanceStatus: BookingAttendanceStatus?,
bookingStatus: BookingStatus?
) async throws -> Booking? {
let path = "\(Path.bookings)/\(bookingID)"
let parameters = [
ParameterKey.attendanceStatus: attendanceStatus.rawValue
]
var parameters: [String: String] = [:]

if let attendanceStatus {
parameters[ParameterKey.attendanceStatus] = attendanceStatus.rawValue
}

if let bookingStatus {
parameters[ParameterKey.status] = bookingStatus.rawValue
}

let request = JetpackRequest(
wooApiVersion: .wcBookings,
method: .put,
Expand Down Expand Up @@ -249,5 +258,6 @@ public extension BookingsRemote {
static let resource: String = "resource"
static let bookingStatus: String = "booking_status"
static let attendanceStatus = "attendance_status"
static let status: String = "status"
}
}
10 changes: 10 additions & 0 deletions Modules/Sources/Yosemite/Actions/BookingAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,14 @@ public enum BookingAction: Action {
bookingID: Int64,
status: BookingAttendanceStatus,
onCompletion: (Error?) -> Void)

/// Cancels a booking by updating its status to cancelled.
///
/// - Parameter siteID: The site ID of the booking.
/// - Parameter bookingID: The ID of the booking to be cancelled.
/// - Parameter onCompletion: called when cancellation completes, returns an error in case of a failure.
///
case cancelBooking(siteID: Int64,
bookingID: Int64,
onCompletion: (Error?) -> Void)
}
39 changes: 38 additions & 1 deletion Modules/Sources/Yosemite/Stores/BookingStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ public class BookingStore: Store {
status: status,
onCompletion: onCompletion
)
case .cancelBooking(let siteID, let bookingID, let onCompletion):
cancelBooking(
siteID: siteID,
bookingID: bookingID,
onCompletion: onCompletion
)
}
}
}
Expand Down Expand Up @@ -294,7 +300,8 @@ private extension BookingStore {
if let remoteBooking = try await self.remote.updateBooking(
from: siteID,
bookingID: bookingID,
attendanceStatus: status
attendanceStatus: status,
bookingStatus: nil,
) {
await self.upsertStoredBookingsInBackground(
readOnlyBookings: [remoteBooking],
Expand Down Expand Up @@ -347,6 +354,36 @@ private extension BookingStore {
}
}, on: .main)
}

/// Cancels a booking by updating its status to cancelled.
func cancelBooking(
siteID: Int64,
bookingID: Int64,
onCompletion: @escaping (Error?) -> Void
) {
Task { @MainActor in
do {
if let remoteBooking = try await remote.updateBooking(
from: siteID,
bookingID: bookingID,
attendanceStatus: nil,
bookingStatus: .cancelled
) {
await upsertStoredBookingsInBackground(
readOnlyBookings: [remoteBooking],
readOnlyOrders: [],
siteID: siteID
)

onCompletion(nil)
} else {
return onCompletion(UpdateBookingStatusError.missingRemoteBooking)
}
} catch {
onCompletion(error)
}
}
}
}


Expand Down
67 changes: 67 additions & 0 deletions Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ struct BookingsRemoteTests {
from: sampleSiteID,
bookingID: bookingID,
attendanceStatus: .noShow,
bookingStatus: nil
)

// Then
Expand All @@ -145,6 +146,72 @@ struct BookingsRemoteTests {
#expect(booking?.id == bookingID)
}

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

// When
_ = try await remote.updateBooking(
from: sampleSiteID,
bookingID: bookingID,
attendanceStatus: .noShow,
bookingStatus: nil
)

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

#expect((parameters["attendance_status"] as? String) == "no-show")
#expect(parameters["status"] == nil)
}

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

// When
_ = try await remote.updateBooking(
from: sampleSiteID,
bookingID: bookingID,
attendanceStatus: nil,
bookingStatus: .confirmed
)

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

#expect(parameters["attendance_status"] == nil)
#expect((parameters["status"] as? String) == "confirmed")
}

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

// When
_ = try await remote.updateBooking(
from: sampleSiteID,
bookingID: bookingID,
attendanceStatus: .booked,
bookingStatus: .paid
)

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

#expect((parameters["attendance_status"] as? String) == "booked")
#expect((parameters["status"] as? String) == "paid")
}

@Test func test_fetchResources_properly_returns_parsed_resources() async throws {
// Given
let remote = BookingsRemote(network: network)
Expand Down
11 changes: 7 additions & 4 deletions Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,31 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
return try result.get()
}

func loadBooking(bookingID: Int64, siteID: Int64) async throws -> Networking.Booking? {
func loadBooking(bookingID: Int64, siteID: Int64) async throws -> Booking? {
guard let result = loadBookingResult else {
throw NetworkError.timeout()
}
return try result.get()
}

func fetchResource(resourceID: Int64, siteID: Int64) async throws -> Networking.BookingResource? {
func fetchResource(resourceID: Int64, siteID: Int64) async throws -> BookingResource? {
guard let result = fetchResourceResult else {
throw NetworkError.timeout()
}
return try result.get()
}

func updateBooking(from siteID: Int64, bookingID: Int64, attendanceStatus: Networking.BookingAttendanceStatus) async throws -> Networking.Booking? {
func updateBooking(from siteID: Int64,
bookingID: Int64,
attendanceStatus: BookingAttendanceStatus?,
bookingStatus: BookingStatus?) async throws -> Booking? {
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] {
func fetchResources(for siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> [BookingResource] {
guard let result = fetchResourcesResult else {
throw NetworkError.timeout()
}
Expand Down
106 changes: 106 additions & 0 deletions Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,112 @@ struct BookingStoreTests {
#expect(storedBooking.attendanceStatusKey == BookingAttendanceStatus.booked.rawValue)
}

// MARK: - cancelBooking

@Test func cancelBooking_updates_local_booking_to_cancelled_status() async throws {
// Given
let booking = Booking.fake().copy(
siteID: sampleSiteID,
bookingID: 1,
statusKey: "confirmed"
)
storeBooking(booking)

let cancelledBooking = booking.copy(statusKey: "cancelled")
remote.whenUpdatingBooking(thenReturn: .success(cancelledBooking))
let store = BookingStore(dispatcher: Dispatcher(),
storageManager: storageManager,
network: network,
remote: remote,
ordersRemote: ordersRemote)

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

// Then
#expect(error == nil)
let storedBooking = try #require(viewStorage.loadBooking(siteID: sampleSiteID, bookingID: 1))
#expect(storedBooking.statusKey == "cancelled")
}

@Test func cancelBooking_returns_error_on_remote_failure() async throws {
// Given
let booking = Booking.fake().copy(
siteID: sampleSiteID,
bookingID: 1,
statusKey: "confirmed"
)
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.cancelBooking(
siteID: sampleSiteID,
bookingID: 1,
onCompletion: { error in
continuation.resume(returning: error)
}
)
)
}

// Then
#expect(error != nil)
let networkError = error as? NetworkError
#expect(networkError == .timeout())
}

@Test func cancelBooking_returns_error_when_remote_booking_is_missing() async throws {
// Given
let booking = Booking.fake().copy(
siteID: sampleSiteID,
bookingID: 1,
statusKey: "confirmed"
)
storeBooking(booking)

remote.whenUpdatingBooking(thenReturn: .success(nil))
let store = BookingStore(dispatcher: Dispatcher(),
storageManager: storageManager,
network: network,
remote: remote,
ordersRemote: ordersRemote)

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

// Then
#expect(error != nil)
}

// MARK: - synchronizeResources

@Test func synchronizeResources_returns_false_for_hasNextPage_when_number_of_retrieved_results_is_zero() async throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ private extension BookingListView {

var loadingView: some View {
VStack {
header
Spacer()
ProgressView().progressViewStyle(.circular)
Spacer()
Expand Down Expand Up @@ -158,7 +159,8 @@ private extension BookingListView {
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
Section {
emptyStateContent(isSearching: isSearching)
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
.frame(minWidth: proxy.size.width,
minHeight: proxy.size.height - BookingListViewLayout.defaultHeaderHeight * scale)
} header: {
header
}
Expand Down Expand Up @@ -233,6 +235,7 @@ fileprivate enum BookingListViewLayout {
static let emptyStatePadding: CGFloat = 24
static let emptyStateImageWidth: CGFloat = 67
static let cornerRadius: CGFloat = 8
static let defaultHeaderHeight: CGFloat = 98
}

fileprivate enum BookingListViewLocalization {
Expand Down
Loading