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)