Skip to content

Commit cdfdd7e

Browse files
authored
Booking: Implement booking cancellation (#16333)
2 parents 3ae4c12 + 0448cdb commit cdfdd7e

File tree

9 files changed

+319
-24
lines changed

9 files changed

+319
-24
lines changed

Modules/Sources/Networking/Remote/BookingsRemote.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public protocol BookingsRemoteProtocol {
1919
func updateBooking(
2020
from siteID: Int64,
2121
bookingID: Int64,
22-
attendanceStatus: BookingAttendanceStatus
22+
attendanceStatus: BookingAttendanceStatus?,
23+
bookingStatus: BookingStatus?
2324
) async throws -> Booking?
2425

2526
func fetchResource(resourceID: Int64,
@@ -150,12 +151,20 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
150151
public func updateBooking(
151152
from siteID: Int64,
152153
bookingID: Int64,
153-
attendanceStatus: BookingAttendanceStatus
154+
attendanceStatus: BookingAttendanceStatus?,
155+
bookingStatus: BookingStatus?
154156
) async throws -> Booking? {
155157
let path = "\(Path.bookings)/\(bookingID)"
156-
let parameters = [
157-
ParameterKey.attendanceStatus: attendanceStatus.rawValue
158-
]
158+
var parameters: [String: String] = [:]
159+
160+
if let attendanceStatus {
161+
parameters[ParameterKey.attendanceStatus] = attendanceStatus.rawValue
162+
}
163+
164+
if let bookingStatus {
165+
parameters[ParameterKey.status] = bookingStatus.rawValue
166+
}
167+
159168
let request = JetpackRequest(
160169
wooApiVersion: .wcBookings,
161170
method: .put,
@@ -249,5 +258,6 @@ public extension BookingsRemote {
249258
static let resource: String = "resource"
250259
static let bookingStatus: String = "booking_status"
251260
static let attendanceStatus = "attendance_status"
261+
static let status: String = "status"
252262
}
253263
}

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,14 @@ public enum BookingAction: Action {
7171
bookingID: Int64,
7272
status: BookingAttendanceStatus,
7373
onCompletion: (Error?) -> Void)
74+
75+
/// Cancels a booking by updating its status to cancelled.
76+
///
77+
/// - Parameter siteID: The site ID of the booking.
78+
/// - Parameter bookingID: The ID of the booking to be cancelled.
79+
/// - Parameter onCompletion: called when cancellation completes, returns an error in case of a failure.
80+
///
81+
case cancelBooking(siteID: Int64,
82+
bookingID: Int64,
83+
onCompletion: (Error?) -> Void)
7484
}

Modules/Sources/Yosemite/Stores/BookingStore.swift

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ public class BookingStore: Store {
7070
status: status,
7171
onCompletion: onCompletion
7272
)
73+
case .cancelBooking(let siteID, let bookingID, let onCompletion):
74+
cancelBooking(
75+
siteID: siteID,
76+
bookingID: bookingID,
77+
onCompletion: onCompletion
78+
)
7379
}
7480
}
7581
}
@@ -294,7 +300,8 @@ private extension BookingStore {
294300
if let remoteBooking = try await self.remote.updateBooking(
295301
from: siteID,
296302
bookingID: bookingID,
297-
attendanceStatus: status
303+
attendanceStatus: status,
304+
bookingStatus: nil,
298305
) {
299306
await self.upsertStoredBookingsInBackground(
300307
readOnlyBookings: [remoteBooking],
@@ -347,6 +354,36 @@ private extension BookingStore {
347354
}
348355
}, on: .main)
349356
}
357+
358+
/// Cancels a booking by updating its status to cancelled.
359+
func cancelBooking(
360+
siteID: Int64,
361+
bookingID: Int64,
362+
onCompletion: @escaping (Error?) -> Void
363+
) {
364+
Task { @MainActor in
365+
do {
366+
if let remoteBooking = try await remote.updateBooking(
367+
from: siteID,
368+
bookingID: bookingID,
369+
attendanceStatus: nil,
370+
bookingStatus: .cancelled
371+
) {
372+
await upsertStoredBookingsInBackground(
373+
readOnlyBookings: [remoteBooking],
374+
readOnlyOrders: [],
375+
siteID: siteID
376+
)
377+
378+
onCompletion(nil)
379+
} else {
380+
return onCompletion(UpdateBookingStatusError.missingRemoteBooking)
381+
}
382+
} catch {
383+
onCompletion(error)
384+
}
385+
}
386+
}
350387
}
351388

352389

Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ struct BookingsRemoteTests {
137137
from: sampleSiteID,
138138
bookingID: bookingID,
139139
attendanceStatus: .noShow,
140+
bookingStatus: nil
140141
)
141142

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

149+
@Test func test_updateBooking_sends_correct_parameters_for_attendance_status() async throws {
150+
// Given
151+
let remote = BookingsRemote(network: network)
152+
let bookingID: Int64 = 206
153+
network.simulateResponse(requestUrlSuffix: "bookings/\(bookingID)", filename: "booking-no-create-update-dates")
154+
155+
// When
156+
_ = try await remote.updateBooking(
157+
from: sampleSiteID,
158+
bookingID: bookingID,
159+
attendanceStatus: .noShow,
160+
bookingStatus: nil
161+
)
162+
163+
// Then
164+
let request = try #require(network.requestsForResponseData.first as? JetpackRequest)
165+
let parameters = request.parameters
166+
167+
#expect((parameters["attendance_status"] as? String) == "no-show")
168+
#expect(parameters["status"] == nil)
169+
}
170+
171+
@Test func test_updateBooking_sends_correct_parameters_for_booking_status() async throws {
172+
// Given
173+
let remote = BookingsRemote(network: network)
174+
let bookingID: Int64 = 206
175+
network.simulateResponse(requestUrlSuffix: "bookings/\(bookingID)", filename: "booking-no-create-update-dates")
176+
177+
// When
178+
_ = try await remote.updateBooking(
179+
from: sampleSiteID,
180+
bookingID: bookingID,
181+
attendanceStatus: nil,
182+
bookingStatus: .confirmed
183+
)
184+
185+
// Then
186+
let request = try #require(network.requestsForResponseData.first as? JetpackRequest)
187+
let parameters = request.parameters
188+
189+
#expect(parameters["attendance_status"] == nil)
190+
#expect((parameters["status"] as? String) == "confirmed")
191+
}
192+
193+
@Test func test_updateBooking_sends_correct_parameters_for_both_statuses() async throws {
194+
// Given
195+
let remote = BookingsRemote(network: network)
196+
let bookingID: Int64 = 206
197+
network.simulateResponse(requestUrlSuffix: "bookings/\(bookingID)", filename: "booking-no-create-update-dates")
198+
199+
// When
200+
_ = try await remote.updateBooking(
201+
from: sampleSiteID,
202+
bookingID: bookingID,
203+
attendanceStatus: .booked,
204+
bookingStatus: .paid
205+
)
206+
207+
// Then
208+
let request = try #require(network.requestsForResponseData.first as? JetpackRequest)
209+
let parameters = request.parameters
210+
211+
#expect((parameters["attendance_status"] as? String) == "booked")
212+
#expect((parameters["status"] as? String) == "paid")
213+
}
214+
148215
@Test func test_fetchResources_properly_returns_parsed_resources() async throws {
149216
// Given
150217
let remote = BookingsRemote(network: network)

Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,28 +42,31 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
4242
return try result.get()
4343
}
4444

45-
func loadBooking(bookingID: Int64, siteID: Int64) async throws -> Networking.Booking? {
45+
func loadBooking(bookingID: Int64, siteID: Int64) async throws -> Booking? {
4646
guard let result = loadBookingResult else {
4747
throw NetworkError.timeout()
4848
}
4949
return try result.get()
5050
}
5151

52-
func fetchResource(resourceID: Int64, siteID: Int64) async throws -> Networking.BookingResource? {
52+
func fetchResource(resourceID: Int64, siteID: Int64) async throws -> BookingResource? {
5353
guard let result = fetchResourceResult else {
5454
throw NetworkError.timeout()
5555
}
5656
return try result.get()
5757
}
5858

59-
func updateBooking(from siteID: Int64, bookingID: Int64, attendanceStatus: Networking.BookingAttendanceStatus) async throws -> Networking.Booking? {
59+
func updateBooking(from siteID: Int64,
60+
bookingID: Int64,
61+
attendanceStatus: BookingAttendanceStatus?,
62+
bookingStatus: BookingStatus?) async throws -> Booking? {
6063
guard let result = updateBookingResult else {
6164
throw NetworkError.timeout()
6265
}
6366
return try result.get()
6467
}
6568

66-
func fetchResources(for siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> [Networking.BookingResource] {
69+
func fetchResources(for siteID: Int64, pageNumber: Int, pageSize: Int) async throws -> [BookingResource] {
6770
guard let result = fetchResourcesResult else {
6871
throw NetworkError.timeout()
6972
}

Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,112 @@ struct BookingStoreTests {
743743
#expect(storedBooking.attendanceStatusKey == BookingAttendanceStatus.booked.rawValue)
744744
}
745745

746+
// MARK: - cancelBooking
747+
748+
@Test func cancelBooking_updates_local_booking_to_cancelled_status() async throws {
749+
// Given
750+
let booking = Booking.fake().copy(
751+
siteID: sampleSiteID,
752+
bookingID: 1,
753+
statusKey: "confirmed"
754+
)
755+
storeBooking(booking)
756+
757+
let cancelledBooking = booking.copy(statusKey: "cancelled")
758+
remote.whenUpdatingBooking(thenReturn: .success(cancelledBooking))
759+
let store = BookingStore(dispatcher: Dispatcher(),
760+
storageManager: storageManager,
761+
network: network,
762+
remote: remote,
763+
ordersRemote: ordersRemote)
764+
765+
// When
766+
let error = await withCheckedContinuation { continuation in
767+
store.onAction(
768+
BookingAction.cancelBooking(
769+
siteID: sampleSiteID,
770+
bookingID: 1,
771+
onCompletion: { error in
772+
continuation.resume(returning: error)
773+
}
774+
)
775+
)
776+
}
777+
778+
// Then
779+
#expect(error == nil)
780+
let storedBooking = try #require(viewStorage.loadBooking(siteID: sampleSiteID, bookingID: 1))
781+
#expect(storedBooking.statusKey == "cancelled")
782+
}
783+
784+
@Test func cancelBooking_returns_error_on_remote_failure() async throws {
785+
// Given
786+
let booking = Booking.fake().copy(
787+
siteID: sampleSiteID,
788+
bookingID: 1,
789+
statusKey: "confirmed"
790+
)
791+
storeBooking(booking)
792+
793+
remote.whenUpdatingBooking(thenReturn: .failure(NetworkError.timeout()))
794+
let store = BookingStore(dispatcher: Dispatcher(),
795+
storageManager: storageManager,
796+
network: network,
797+
remote: remote,
798+
ordersRemote: ordersRemote)
799+
800+
// When
801+
let error = await withCheckedContinuation { continuation in
802+
store.onAction(
803+
BookingAction.cancelBooking(
804+
siteID: sampleSiteID,
805+
bookingID: 1,
806+
onCompletion: { error in
807+
continuation.resume(returning: error)
808+
}
809+
)
810+
)
811+
}
812+
813+
// Then
814+
#expect(error != nil)
815+
let networkError = error as? NetworkError
816+
#expect(networkError == .timeout())
817+
}
818+
819+
@Test func cancelBooking_returns_error_when_remote_booking_is_missing() async throws {
820+
// Given
821+
let booking = Booking.fake().copy(
822+
siteID: sampleSiteID,
823+
bookingID: 1,
824+
statusKey: "confirmed"
825+
)
826+
storeBooking(booking)
827+
828+
remote.whenUpdatingBooking(thenReturn: .success(nil))
829+
let store = BookingStore(dispatcher: Dispatcher(),
830+
storageManager: storageManager,
831+
network: network,
832+
remote: remote,
833+
ordersRemote: ordersRemote)
834+
835+
// When
836+
let error = await withCheckedContinuation { continuation in
837+
store.onAction(
838+
BookingAction.cancelBooking(
839+
siteID: sampleSiteID,
840+
bookingID: 1,
841+
onCompletion: { error in
842+
continuation.resume(returning: error)
843+
}
844+
)
845+
)
846+
}
847+
848+
// Then
849+
#expect(error != nil)
850+
}
851+
746852
// MARK: - synchronizeResources
747853

748854
@Test func synchronizeResources_returns_false_for_hasNextPage_when_number_of_retrieved_results_is_zero() async throws {

WooCommerce/Classes/Bookings/BookingList/BookingListView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ private extension BookingListView {
9090

9191
var loadingView: some View {
9292
VStack {
93+
header
9394
Spacer()
9495
ProgressView().progressViewStyle(.circular)
9596
Spacer()
@@ -158,7 +159,8 @@ private extension BookingListView {
158159
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
159160
Section {
160161
emptyStateContent(isSearching: isSearching)
161-
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
162+
.frame(minWidth: proxy.size.width,
163+
minHeight: proxy.size.height - BookingListViewLayout.defaultHeaderHeight * scale)
162164
} header: {
163165
header
164166
}
@@ -233,6 +235,7 @@ fileprivate enum BookingListViewLayout {
233235
static let emptyStatePadding: CGFloat = 24
234236
static let emptyStateImageWidth: CGFloat = 67
235237
static let cornerRadius: CGFloat = 8
238+
static let defaultHeaderHeight: CGFloat = 98
236239
}
237240

238241
fileprivate enum BookingListViewLocalization {

0 commit comments

Comments
 (0)