From 31c03ccb28c707155c0bf27ee9a582138572a2cf Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 24 Oct 2025 15:57:07 +0300 Subject: [PATCH 01/11] Implement attendance status section insertion/removal --- .../BookingDetailsViewModel.swift | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index b373493f6da..1fbf6e9dbda 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -62,12 +62,6 @@ private extension BookingDetailsViewModel { content: .appointmentDetails(appointmentDetailsContent) ) - let attendanceSection = Section( - header: .title(Localization.attendanceSectionHeaderTitle.uppercased()), - footerText: Localization.attendanceSectionFooterText, - content: .attendance(attendanceContent) - ) - let paymentSection = Section( header: .title(Localization.paymentSectionHeaderTitle.uppercased()), content: .payment(paymentContent) @@ -81,7 +75,6 @@ private extension BookingDetailsViewModel { sections = [ headerSection, appointmentDetailsSection, - attendanceSection, paymentSection, bookingNotes ] @@ -96,10 +89,61 @@ private extension BookingDetailsViewModel { } headerContent.update(with: booking) appointmentDetailsContent.update(with: booking, resource: bookingResource) + + setupAttendanceSectionVisibility() attendanceContent.update(with: booking) + paymentContent.update(with: booking) } + func setupAttendanceSectionVisibility() { + if booking.attendanceStatus == .cancelled { + deleteAttendanceSectionIfPresent() + } else { + insertAttendanceSectionIfAbsent() + } + } + + func insertAttendanceSectionIfAbsent() { + // Avoid adding if it already exists + let attendanceSectionExists = sections.contains { + if case .attendance = $0.content { + return true + } + + return false + } + + guard !attendanceSectionExists else { + return + } + + let attendance = Section( + header: .title(Localization.attendanceSectionHeaderTitle.uppercased()), + footerText: Localization.attendanceSectionFooterText, + content: .attendance(attendanceContent) + ) + + withAnimation { + sections.insert(attendance, at: 3) + } + } + + func deleteAttendanceSectionIfPresent() { + guard let attendanceSectionIndex = sections.firstIndex(where: { + if case .attendance = $0.content { + return true + } + return false + }) else { + return + } + + withAnimation { + _ = sections.remove(at: attendanceSectionIndex) + } + } + func insertCustomerSectionIfAbsent() { // Avoid adding if it already exists let customerSectionExists = sections.contains { From ee0d8bb9baf9b062e794b1b5e28d36de504114dd Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 24 Oct 2025 16:14:41 +0300 Subject: [PATCH 02/11] Deletes customer section if billing address is absent --- .../BookingDetailsViewModel.swift | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 1fbf6e9dbda..6dbdcc23768 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -86,7 +86,10 @@ private extension BookingDetailsViewModel { if let billingAddress = booking.orderInfo?.customerInfo?.billingAddress, !billingAddress.isEmpty { customerContent.update(with: billingAddress) insertCustomerSectionIfAbsent() + } else { + deleteCustomerSectionIfPresent() } + headerContent.update(with: booking) appointmentDetailsContent.update(with: booking, resource: bookingResource) @@ -105,27 +108,41 @@ private extension BookingDetailsViewModel { } func insertAttendanceSectionIfAbsent() { - // Avoid adding if it already exists - let attendanceSectionExists = sections.contains { - if case .attendance = $0.content { + insertSectionIfAbsent( + section: Section( + header: .title(Localization.attendanceSectionHeaderTitle.uppercased()), + footerText: Localization.attendanceSectionFooterText, + content: .attendance(attendanceContent) + ), + at: 3 + ) + } + + func insertCustomerSectionIfAbsent() { + insertSectionIfAbsent( + section: Section( + header: .title(Localization.customerSectionHeaderTitle.uppercased()), + content: .customer(customerContent) + ), + at: 2 + ) + } + + func insertSectionIfAbsent(section: Section, at index: Int) { + let sectionExists = sections.contains { + if section.content.id == $0.content.id { return true } return false } - guard !attendanceSectionExists else { + guard !sectionExists else { return } - let attendance = Section( - header: .title(Localization.attendanceSectionHeaderTitle.uppercased()), - footerText: Localization.attendanceSectionFooterText, - content: .attendance(attendanceContent) - ) - withAnimation { - sections.insert(attendance, at: 3) + sections.insert(section, at: index) } } @@ -144,26 +161,18 @@ private extension BookingDetailsViewModel { } } - func insertCustomerSectionIfAbsent() { - // Avoid adding if it already exists - let customerSectionExists = sections.contains { + func deleteCustomerSectionIfPresent() { + guard let customerSectionIndex = sections.firstIndex(where: { if case .customer = $0.content { return true } - return false - } - - guard !customerSectionExists else { + }) else { return } - let customerSection = Section( - header: .title(Localization.customerSectionHeaderTitle.uppercased()), - content: .customer(customerContent) - ) withAnimation { - sections.insert(customerSection, at: 2) + _ = sections.remove(at: customerSectionIndex) } } From 28880cb347e12d2f7471a2dc169a19e2a4e6588c Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 24 Oct 2025 17:14:55 +0300 Subject: [PATCH 03/11] Perform remote attendance status update for booking --- .../Yosemite/Stores/BookingStore.swift | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/Modules/Sources/Yosemite/Stores/BookingStore.swift b/Modules/Sources/Yosemite/Stores/BookingStore.swift index 5f2927f6643..a19a7e43da7 100644 --- a/Modules/Sources/Yosemite/Stores/BookingStore.swift +++ b/Modules/Sources/Yosemite/Stores/BookingStore.swift @@ -263,9 +263,37 @@ 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 { + do { + if let remoteBooking = try await self.remote.updateBooking( + from: siteID, + bookingID: bookingID, + attendanceStatus: status + ) { + await self.upsertStoredBookingsInBackground( + readOnlyBookings: [remoteBooking], + readOnlyOrders: [], + siteID: siteID + ) + } else { + onCompletion(nil) + } + } catch { + /// Revert Optimistic Update + self.updateBookingAttendanceStatusLocally( + siteID: siteID, + bookingID: bookingID, + statusKey: previousStatusKey + ) { _ in + onCompletion(error) + } + } + } } } @@ -409,3 +437,10 @@ private extension BookingStore { } } } + + +// MARK: - Errors +// +private enum UpdateBookingStatusError: Error { + case undefinedState +} From 82f306f82e7ea1caedcab0fdef056a60ef4cc300 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 27 Oct 2025 14:08:28 +0300 Subject: [PATCH 04/11] Make creation and modification date optional for Booking --- .../Networking/Model/Bookings/Booking.swift | 31 +++++++++++++++---- .../Booking/Booking+ReadOnlyConvertible.swift | 4 +-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Modules/Sources/Networking/Model/Bookings/Booking.swift b/Modules/Sources/Networking/Model/Bookings/Booking.swift index b1588f33b9c..687a23b33ad 100644 --- a/Modules/Sources/Networking/Model/Bookings/Booking.swift +++ b/Modules/Sources/Networking/Model/Bookings/Booking.swift @@ -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 @@ -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, @@ -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) diff --git a/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift index 55a62cb3a6f..56fd2af4b50 100644 --- a/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift @@ -13,8 +13,8 @@ extension Storage.Booking: ReadOnlyConvertible { allDay = booking.allDay cost = booking.cost customerID = booking.customerID - dateCreated = booking.dateCreated - dateModified = booking.dateModified + dateCreated = booking.dateCreated ?? dateCreated + dateModified = booking.dateModified ?? dateModified endDate = booking.endDate googleCalendarEventID = booking.googleCalendarEventID orderID = booking.orderID From 9aa331fbba0953692c9f53adda66a1c60c1b30d9 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 27 Oct 2025 14:39:07 +0300 Subject: [PATCH 05/11] Add booking update tests --- .../Remote/BookingsRemoteTests.swift | 19 ++++++++++++++++ .../booking-no-create-update-dates.json | 22 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 Modules/Tests/NetworkingTests/Responses/booking-no-create-update-dates.json diff --git a/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift b/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift index 78eca50eaef..bfe1e810a9f 100644 --- a/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift +++ b/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift @@ -123,4 +123,23 @@ struct BookingsRemoteTests { _ = try await remote.fetchResource(resourceID: 22, siteID: sampleSiteID) } } + + @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) + } } diff --git a/Modules/Tests/NetworkingTests/Responses/booking-no-create-update-dates.json b/Modules/Tests/NetworkingTests/Responses/booking-no-create-update-dates.json new file mode 100644 index 00000000000..8fab8cc0e36 --- /dev/null +++ b/Modules/Tests/NetworkingTests/Responses/booking-no-create-update-dates.json @@ -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" + } +} From 463191904f8b721ff6e39853500638e88fe6ad55 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 27 Oct 2025 16:54:07 +0300 Subject: [PATCH 06/11] Fix completion closure calls in booking status update action --- Modules/Sources/Yosemite/Stores/BookingStore.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/Yosemite/Stores/BookingStore.swift b/Modules/Sources/Yosemite/Stores/BookingStore.swift index a19a7e43da7..504df57a4ad 100644 --- a/Modules/Sources/Yosemite/Stores/BookingStore.swift +++ b/Modules/Sources/Yosemite/Stores/BookingStore.swift @@ -280,8 +280,10 @@ private extension BookingStore { readOnlyOrders: [], siteID: siteID ) - } else { + onCompletion(nil) + } else { + return onCompletion(UpdateBookingStatusError.missingRemoteBooking) } } catch { /// Revert Optimistic Update @@ -443,4 +445,5 @@ private extension BookingStore { // private enum UpdateBookingStatusError: Error { case undefinedState + case missingRemoteBooking } From 54f5603289832b5420e4dc8bf032e2685eadbd32 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 27 Oct 2025 16:54:41 +0300 Subject: [PATCH 07/11] Add booking attendance status update tests in booking store --- .../Mocks/MockBookingsRemote.swift | 10 +- .../Stores/BookingStoreTests.swift | 119 ++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift b/Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift index c9d0200cc4e..063a5c4b900 100644 --- a/Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift +++ b/Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift @@ -7,6 +7,7 @@ final class MockBookingsRemote: BookingsRemoteProtocol { private var loadAllBookingsResult: Result<[Booking], Error>? private var loadBookingResult: Result? private var fetchResourceResult: Result? + private var updateBookingResult: Result? func whenLoadingAllBookings(thenReturn result: Result<[Booking], Error>) { loadAllBookingsResult = result @@ -16,6 +17,10 @@ final class MockBookingsRemote: BookingsRemoteProtocol { loadBookingResult = result } + func whenUpdatingBooking(thenReturn result: Result) { + updateBookingResult = result + } + func whenFetchingResource(thenReturn result: Result) { fetchResourceResult = result } @@ -48,6 +53,9 @@ 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() } } diff --git a/Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift index fb01fa18c5c..938331849b1 100644 --- a/Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift @@ -1,3 +1,4 @@ +import Foundation import Testing @testable import Networking @testable import Storage @@ -618,6 +619,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: - orderInfo Storage Tests @Test func synchronizeBookings_stores_complete_orderInfo_with_all_nested_properties() async throws { From 908eb5b2165f35571cda13b5c41aa3cab08283bf Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 27 Oct 2025 17:07:32 +0300 Subject: [PATCH 08/11] Add tests for hiding attendance section for cancelled booking --- .../BookingDetailsViewModel.swift | 2 +- .../BookingDetailsViewModelTests.swift | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 6dbdcc23768..e1a646b915d 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -100,7 +100,7 @@ private extension BookingDetailsViewModel { } func setupAttendanceSectionVisibility() { - if booking.attendanceStatus == .cancelled { + if booking.attendanceStatus == .cancelled || booking.bookingStatus == .cancelled { deleteAttendanceSectionIfPresent() } else { insertAttendanceSectionIfAbsent() diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift index 58970f3bb84..5766abba1cb 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift @@ -298,4 +298,25 @@ final class BookingDetailsViewModelTests: XCTestCase { XCTAssertEqual(attendanceContent.value, "No Show") } + + func test_attendance_section_is_hidden_when_booking_is_cancelled() { + // Given + let booking = Booking.fake().copy( + statusKey: "cancelled", + attendanceStatusKey: "cancelled" + ) + + // When + let viewModel = BookingDetailsViewModel(booking: booking, stores: storesManager) + + // Then + let containsAttendanceSection = viewModel.sections.contains { section in + if case .attendance = section.content { + return true + } + return false + } + + XCTAssertFalse(containsAttendanceSection) + } } From b3991c7ddaabf722d65f981bc7df9b485ec152b7 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 28 Oct 2025 14:27:00 +0300 Subject: [PATCH 09/11] Add clarifying code comment --- .../Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift index 56fd2af4b50..db92ed7c8de 100644 --- a/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift @@ -13,8 +13,13 @@ extension Storage.Booking: ReadOnlyConvertible { allDay = booking.allDay cost = booking.cost customerID = booking.customerID + + /// 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 + endDate = booking.endDate googleCalendarEventID = booking.googleCalendarEventID orderID = booking.orderID From 78de80e5ec5cf8c4c7af57e18819a4827273d2e5 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 28 Oct 2025 14:30:06 +0300 Subject: [PATCH 10/11] Run booking update task on main actor --- Modules/Sources/Yosemite/Stores/BookingStore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Sources/Yosemite/Stores/BookingStore.swift b/Modules/Sources/Yosemite/Stores/BookingStore.swift index 504df57a4ad..adbc382dcb7 100644 --- a/Modules/Sources/Yosemite/Stores/BookingStore.swift +++ b/Modules/Sources/Yosemite/Stores/BookingStore.swift @@ -268,7 +268,7 @@ private extension BookingStore { return onCompletion(UpdateBookingStatusError.undefinedState) } - Task { + Task { @MainActor in do { if let remoteBooking = try await self.remote.updateBooking( from: siteID, From 8ed229eab4f8c161ba200ef04ac38f21ee68dfd4 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 28 Oct 2025 15:00:25 +0300 Subject: [PATCH 11/11] Calculate section insertion indexes instead of hardcoding --- .../BookingDetailsViewModel.swift | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index e1a646b915d..c4a71953e57 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -83,14 +83,13 @@ private extension BookingDetailsViewModel { func updateDisplayProperties(from booking: Booking) { navigationTitle = Self.navigationTitle(for: booking) + headerContent.update(with: booking) + + setupCustomerSectionVisibility() if let billingAddress = booking.orderInfo?.customerInfo?.billingAddress, !billingAddress.isEmpty { customerContent.update(with: billingAddress) - insertCustomerSectionIfAbsent() - } else { - deleteCustomerSectionIfPresent() } - headerContent.update(with: booking) appointmentDetailsContent.update(with: booking, resource: bookingResource) setupAttendanceSectionVisibility() @@ -99,6 +98,14 @@ private extension BookingDetailsViewModel { paymentContent.update(with: booking) } + func setupCustomerSectionVisibility() { + if let billingAddress = booking.orderInfo?.customerInfo?.billingAddress, !billingAddress.isEmpty { + insertCustomerSectionIfAbsent() + } else { + deleteCustomerSectionIfPresent() + } + } + func setupAttendanceSectionVisibility() { if booking.attendanceStatus == .cancelled || booking.bookingStatus == .cancelled { deleteAttendanceSectionIfPresent() @@ -108,23 +115,46 @@ private extension BookingDetailsViewModel { } func insertAttendanceSectionIfAbsent() { + guard let insertAfterIndex = sections.firstIndex(where: { + if case .customer = $0.content { + return true + } + return false + }) ?? sections.firstIndex(where: { + if case .appointmentDetails = $0.content { + return true + } + return false + }) else { + return + } + insertSectionIfAbsent( section: Section( header: .title(Localization.attendanceSectionHeaderTitle.uppercased()), footerText: Localization.attendanceSectionFooterText, content: .attendance(attendanceContent) ), - at: 3 + at: insertAfterIndex + 1 ) } func insertCustomerSectionIfAbsent() { + guard let insertAfterIndex = sections.firstIndex(where: { + if case .appointmentDetails = $0.content { + return true + } + return false + }) else { + return + } + insertSectionIfAbsent( section: Section( header: .title(Localization.customerSectionHeaderTitle.uppercased()), content: .customer(customerContent) ), - at: 2 + at: insertAfterIndex + 1 ) }