diff --git a/Modules/Sources/Networking/Model/Bookings/BookingCustomerInfo.swift b/Modules/Sources/Networking/Model/Bookings/BookingCustomerInfo.swift index 613793724ce..7acf87b4e3a 100644 --- a/Modules/Sources/Networking/Model/Bookings/BookingCustomerInfo.swift +++ b/Modules/Sources/Networking/Model/Bookings/BookingCustomerInfo.swift @@ -2,8 +2,10 @@ import Foundation public struct BookingCustomerInfo: Hashable { public let billingAddress: Address + public let note: String? - public init(billingAddress: Address) { + public init(billingAddress: Address, note: String? = nil) { self.billingAddress = billingAddress + self.note = note } } diff --git a/Modules/Sources/Networking/Model/Bookings/BookingOrderInfo.swift b/Modules/Sources/Networking/Model/Bookings/BookingOrderInfo.swift index 3a5c7bf5779..c09f46ff725 100644 --- a/Modules/Sources/Networking/Model/Bookings/BookingOrderInfo.swift +++ b/Modules/Sources/Networking/Model/Bookings/BookingOrderInfo.swift @@ -22,7 +22,10 @@ public struct BookingOrderInfo: Hashable { guard let billingAddress = order.billingAddress else { return nil } - return BookingCustomerInfo(billingAddress: billingAddress) + return BookingCustomerInfo( + billingAddress: billingAddress, + note: order.customerNote + ) }() self.productInfo = BookingProductInfo(name: order.items.first(where: { $0.productID == booking.productID })?.name ?? "") self.paymentInfo = BookingPaymentInfo( diff --git a/Modules/Sources/Storage/Model/Booking/BookingCustomerInfo+CoreDataProperties.swift b/Modules/Sources/Storage/Model/Booking/BookingCustomerInfo+CoreDataProperties.swift index eb87bcd0de5..ef9f924d657 100644 --- a/Modules/Sources/Storage/Model/Booking/BookingCustomerInfo+CoreDataProperties.swift +++ b/Modules/Sources/Storage/Model/Booking/BookingCustomerInfo+CoreDataProperties.swift @@ -13,6 +13,7 @@ extension BookingCustomerInfo { @NSManaged public var billingPhone: String? @NSManaged public var billingPostcode: String? @NSManaged public var billingState: String? + @NSManaged public var note: String? @NSManaged public var orderInfo: BookingOrderInfo? } diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion index f6128c44773..2a2d7263f91 100644 --- a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model 130.xcdatamodel + Model 131.xcdatamodel diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 131.xcdatamodel/contents b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 131.xcdatamodel/contents new file mode 100644 index 00000000000..1e4ce0ec789 --- /dev/null +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 131.xcdatamodel/contents @@ -0,0 +1,1178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Sources/Yosemite/Model/Booking/BookingCustomerInfo+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Booking/BookingCustomerInfo+ReadOnlyConvertible.swift index 868360a76f7..8961959d4dc 100644 --- a/Modules/Sources/Yosemite/Model/Booking/BookingCustomerInfo+ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Model/Booking/BookingCustomerInfo+ReadOnlyConvertible.swift @@ -16,6 +16,7 @@ extension Storage.BookingCustomerInfo: ReadOnlyConvertible { billingPhone = customerInfo.billingAddress.phone billingPostcode = customerInfo.billingAddress.postcode billingState = customerInfo.billingAddress.state + note = customerInfo.note } public func toReadOnly() -> Yosemite.BookingCustomerInfo { @@ -30,6 +31,6 @@ extension Storage.BookingCustomerInfo: ReadOnlyConvertible { country: billingCountry ?? "", phone: billingPhone, email: billingEmail) - return .init(billingAddress: address) + return .init(billingAddress: address, note: note) } } diff --git a/Modules/Tests/StorageTests/CoreData/MigrationTests.swift b/Modules/Tests/StorageTests/CoreData/MigrationTests.swift index 2ab77b12df3..bf558fda070 100644 --- a/Modules/Tests/StorageTests/CoreData/MigrationTests.swift +++ b/Modules/Tests/StorageTests/CoreData/MigrationTests.swift @@ -2333,6 +2333,36 @@ final class MigrationTests: XCTestCase { // `note` should be present in `migratedBooking` XCTAssertNotNil(migratedBooking.entity.attributesByName["note"]) } + + func test_migrating_from_130_to_131_adds_note_attribute_to_bookingCustomerInfo() throws { + // Given + let sourceContainer = try startPersistentContainer("Model 130") + let sourceContext = sourceContainer.viewContext + + let customerInfo = insertBookingCustomerInfo(to: sourceContext) + try sourceContext.save() + + XCTAssertNil(customerInfo.entity.attributesByName["note"], "Precondition. Attribute does not exist.") + + // When + let targetContainer = try migrate(sourceContainer, to: "Model 131") + + // Then + let targetContext = targetContainer.viewContext + let migratedCustomerInfo = try XCTUnwrap(targetContext.first(entityName: "BookingCustomerInfo")) + + // `note` should be present in `migratedCustomerInfo` + XCTAssertNotNil(migratedCustomerInfo.entity.attributesByName["note"]) + + let noteValue = migratedCustomerInfo.value(forKey: "note") as? String + XCTAssertNil(noteValue) + + let updatedNote = "Customer note" + migratedCustomerInfo.setValue(updatedNote, forKey: "note") + try targetContext.save() + + XCTAssertEqual(migratedCustomerInfo.value(forKey: "note") as? String, updatedNote) + } } // MARK: - Persistent Store Setup and Migrations @@ -3275,7 +3305,7 @@ private extension MigrationTests { @discardableResult func insertBookingCustomerInfo(to context: NSManagedObjectContext) -> NSManagedObject { - context.insert(entityName: "BookingCustomerInfo", properties: [ + var properties: [String: Any] = [ "billingFirstName": "John", "billingLastName": "Doe", "billingEmail": "john.doe@example.com", @@ -3284,7 +3314,12 @@ private extension MigrationTests { "billingState": "CA", "billingPostcode": "94102", "billingCountry": "US" - ]) + ] + if let entity = NSEntityDescription.entity(forEntityName: "BookingCustomerInfo", in: context), + entity.attributesByName.keys.contains("note") { + properties["note"] = "Sample note" + } + return context.insert(entityName: "BookingCustomerInfo", properties: properties) } @discardableResult diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 248389bd3fb..0591b8a32ed 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -92,8 +92,10 @@ private extension BookingDetailsViewModel { headerContent.update(with: booking) setupCustomerSectionVisibility() - if let billingAddress = booking.orderInfo?.customerInfo?.billingAddress, !billingAddress.isEmpty { - customerContent.update(with: billingAddress) + if let orderInfo = booking.orderInfo, + let customerInfo = orderInfo.customerInfo, + customerInfo.billingAddress.isEmpty == false { + customerContent.update(with: customerInfo) } appointmentDetailsContent.update(with: booking, resource: bookingResource) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift index ffd92738bd8..7daba539e73 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift @@ -7,12 +7,15 @@ extension BookingDetailsViewModel { @Published var emailText: String? @Published var phoneText: String? @Published var billingAddressText: String? + @Published var noteText: String? - func update(with billingAddress: Address) { + func update(with customerInfo: BookingCustomerInfo) { + let billingAddress = customerInfo.billingAddress nameText = billingAddress.fullName emailText = billingAddress.email ?? "" phoneText = billingAddress.phone ?? "" billingAddressText = formatAddress(billingAddress) + noteText = formattedNote(customerInfo.note) } private func formatAddress(_ address: Address) -> String { @@ -28,5 +31,13 @@ extension BookingDetailsViewModel { .filter { !$0.isEmpty } .joined(separator: "\n") } + + private func formattedNote(_ note: String?) -> String? { + guard let trimmedNote = note?.trimmingCharacters(in: .whitespacesAndNewlines), + trimmedNote.isEmpty == false else { + return nil + } + return trimmedNote + } } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/CustomerDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/CustomerDetailsView.swift index e79d792a298..10129d0ebe6 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/CustomerDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/CustomerDetailsView.swift @@ -13,6 +13,7 @@ extension BookingDetailsView { case email(String) case phone(String) case billingAddress(String) + case note(String) } private var rows: [Row] { @@ -29,6 +30,9 @@ extension BookingDetailsView { if let address = content.billingAddressText, !address.isEmpty { result.append(.billingAddress(address)) } + if let note = content.noteText, !note.isEmpty { + result.append(.note(note)) + } return result } @@ -56,6 +60,8 @@ extension BookingDetailsView { phoneView(with: phoneText) case .billingAddress(let billingAddressText): billingAddressView(with: billingAddressText) + case .note(let noteText): + noteView(with: noteText) } } @@ -136,6 +142,21 @@ extension BookingDetailsView { } .padding(.vertical, Layout.rowTextVerticalPadding) } + + private func noteView(with noteText: String) -> some View { + HStack { + VStack(alignment: .leading) { + Text(Localization.noteRowTitle) + .rowTextStyle() + Text(noteText) + .font(TextFont.bodyMedium) + .foregroundStyle(.secondary) + .multilineTextAlignment(.leading) + } + Spacer() + } + .padding(.vertical, Layout.rowTextVerticalPadding) + } } } @@ -177,5 +198,11 @@ private extension BookingDetailsView.CustomerDetailsView { value: "Billing address", comment: "Billing address row title in customer section in booking details view." ) + + static let noteRowTitle = NSLocalizedString( + "BookingDetailsView.customer.note.title", + value: "Note", + comment: "Customer note row title in customer section in booking details view." + ) } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift index f8e2069c646..fa4439ccd74 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift @@ -211,6 +211,47 @@ final class BookingDetailsViewModelTests: XCTestCase { XCTAssertEqual(customerContent.billingAddressText, expectedAddress) } + func test_customer_content_includes_customer_note() { + // Given + let billingAddress = Address.fake().copy( + firstName: "Alice", + lastName: "Johnson", + address1: "456 Main St", + city: "Springfield", + state: "IL", + postcode: "62701", + country: "US" + ) + let note = "Please ring the bell twice" + let customerInfo = BookingCustomerInfo(billingAddress: billingAddress, note: note) + let orderInfo = BookingOrderInfo( + statusKey: "confirmed", + paymentInfo: nil, + customerInfo: customerInfo, + productInfo: nil + ) + let booking = Booking.fake().copy(orderInfo: orderInfo) + + // When + let viewModel = BookingDetailsViewModel(booking: booking, stores: storesManager) + + // Then + let customerSection = viewModel.sections.first { section in + if case .customer = section.content { + return true + } + return false + } + + guard let customerSection = customerSection, + case let .customer(customerContent) = customerSection.content else { + XCTFail("Customer section not found") + return + } + + XCTAssertEqual(customerContent.noteText, note) + } + func test_navigation_title_includes_booking_id() { // Given let booking = Booking.fake().copy(bookingID: 12345)