Skip to content

Conversation

@iamgabrielma
Copy link
Contributor

@iamgabrielma iamgabrielma commented Oct 20, 2022

Part of: #7741
Closes: #7941

Description

This PR deals with:

  • Shows an empty view when we start a new keyword search, for parity with Android.
  • Fill the customer details into the Order details fields once a Customer is selected.
  • Adds a new view when no Customer search results are found.
  • Ignores searches for non-registered customers.

Changes

  • The OrderCustomerListView receives a completion handler with a Customer object when a merchant taps in the search button. Then we use this data to fill the EditOrderAddressForm viewModel.
  • Removes customerSearchTapped(), as we're not using it anymore.
  • When mapping SearchResults to Customer objects, we skip mapping those with id = 0 (unregistered), avoiding requests to /wc/v3/customers/0 that will always fail.

Testing instructions

  1. Go to Orders > + > Create new Order > + Add Customer details > Tap on the magnifying glass icon.
  2. Search for a Customer name, and tap on it.
  1. Confirm that Customer fields are filled (will fill both Shipping/Billing if these are different as well).
  2. When searching for non-matching keywords, confirm that the empty search results show up.

  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

@iamgabrielma iamgabrielma changed the title fill Customer fields on Order screen Customer search: Fill customer fields on Order screen Oct 20, 2022
@iamgabrielma iamgabrielma mentioned this pull request Oct 20, 2022
15 tasks
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Oct 20, 2022

You can test the changes from this Pull Request by:
  • Clicking here or scanning the QR code below to access App Center
  • Then installing the build number pr7885-2b772b7 on your iPhone

If you need access to App Center, please ask a maintainer to add you.

@iamgabrielma iamgabrielma added type: enhancement A request for an enhancement. category: parity Match what's supported by the other platform. feature: order creation All tasks related to creating an order labels Oct 20, 2022
@iamgabrielma iamgabrielma modified the milestones: 10.9, 11.0 Oct 20, 2022
@iamgabrielma iamgabrielma added the status: draft This is a draft, still need more work but can be reviewed and commented if asked. label Oct 24, 2022
Base automatically changed from issue/741-implement-customer-search-ui to trunk October 26, 2022 07:45
@iamgabrielma iamgabrielma removed the status: draft This is a draft, still need more work but can be reviewed and commented if asked. label Oct 27, 2022
@iamgabrielma iamgabrielma marked this pull request as ready for review October 27, 2022 02:40

// When
let predicate = command.searchResultsPredicate(keyword: "some")
let expectedQuery = "siteID == 123 AND ANY searchResults.keyword == \"some\""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double check if this actually filters down multiple sites.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed 👍 , for 2 customers with the same name and customer ID, in two different sites, only one is shown.

@iamgabrielma iamgabrielma modified the milestones: 11.0, 11.1 Oct 28, 2022
@iamgabrielma
Copy link
Contributor Author

iamgabrielma commented Oct 28, 2022

Thanks for your time yesterday to go through the review/pairing @joshheald, I made the following changes:

  • Extracted the fillCustomerFields() method to the viewModel: 9cf8388
  • Added Unit Tests to check if fields are correctly being filled: ed04317
  • Make the shipping details form display automatically if addresses differ: 5dffa97

@joshheald joshheald self-assigned this Oct 28, 2022
Copy link
Contributor

@joshheald joshheald left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much there on this, good work.

I noticed another difference from Android (the fields used for name and email) and a problem with one of your tests. A couple of other small comments too.

Comment on lines 414 to 423
fields = populate(fields: fields, with: customer, address: customer.billing)
secondaryFields = populate(fields: secondaryFields, with: customer, address: customer.shipping)
}

private func populate(fields: AddressFormFields, with customer: Customer, address: Address?) -> AddressFormFields {
var fields = fields

fields.firstName = customer.firstName ?? ""
fields.lastName = customer.lastName ?? ""
fields.email = customer.email
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fields = populate(fields: fields, with: customer, address: customer.billing)
secondaryFields = populate(fields: secondaryFields, with: customer, address: customer.shipping)
}
private func populate(fields: AddressFormFields, with customer: Customer, address: Address?) -> AddressFormFields {
var fields = fields
fields.firstName = customer.firstName ?? ""
fields.lastName = customer.lastName ?? ""
fields.email = customer.email
fields = populate(fields: fields, with: customer.billing)
secondaryFields = populate(fields: secondaryFields, with: customer.shipping)
}
private func populate(fields: AddressFormFields, with address: Address?) -> AddressFormFields {
var fields = fields
fields.firstName = address.firstName ?? ""
fields.lastName = address.lastName ?? ""
fields.email = address.email

Instead of the name fields on customer, I think we should use address.firstName and address.lastName. Testing Android, with my always-strange test data with different names all over the place, shows that they use these fields to update the data.

This makes sense, because the shipping address may be to a different person, e.g. if it's a gift.

There's address.email, which could differ from customer.email. Android seems to use billingAddress.email there. The email for the secondary fields isn't shown on either platform... but I'm not sure whether it's used in any requests.

It's worth checking that: what happens if we save a different email address in each place? Does it get added to the order twice? Is that what Android does? Is it a bug, one way or the other?

It's also just a nicer function... "Populate address fields with address"

Copy link
Contributor Author

@iamgabrielma iamgabrielma Nov 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I made the changes here: 121ba8a

Something that peaks my interest with this change is that the email property is now an optional, but it can't actually be optional because is a mandatory field for shippable orders. This seems to happen because when we declare both fields and secondaryFields in the protocol, both inherit from AddressFormFields, however these have slightly different parameters in the API (for example shipping details does not have an email field)

what happens if we save a different email address in each place? Does it get added to the order twice? Is that what Android does? Is it a bug, one way or the other?

This seems a bug from our end when declaring the model, as with the email example above, we're reusing the same AddressFormFields model for both shipping and billing, but these are not exactly the same in the API or WC core (it would seem that the API doc is outdated as well, as phone does not appear as property for shipping but in WC core it does). I've opened an issue here: #7993 as most likely this can be improved.

print("3 - Customer Callback. Fill Order data with Customer details")
print("4 - Customer ID: \(customer.customerID) - Name: \(customer.firstName ?? ""))")
viewModel.customerSelectedFromSearch(customer: customer)
showingCustomerSearch = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can move to the view model too if you like? Can wait for a future PR

viewModel.customerSelectedFromSearch(customer: customer)

// Then
XCTAssertEqual(viewModel.fields.email, customer.email)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget about assertEqual: the nicer diffs when stuff is wrong are worth it, even though it's annoying having to swap around the actual and expected when you swap from XCTAssertEqual

Comment on lines +776 to +788
func test_OrderAddressForm_shows_different_address_form_fields_when_addresses_differ_and_customerSelectedFromSearch() {
// Given
let viewModel = EditOrderAddressFormViewModel(order: Order.fake(), type: .billing)
let billing = sampleAddressWithEmptyNullableFields()
let shipping = Address.fake().copy(address1: "123 different fake street")
let customer = Customer.fake().copy(billing: billing, shipping: shipping)

// When
viewModel.customerSelectedFromSearch(customer: customer)

// Then
XCTAssertTrue(viewModel.showDifferentAddressForm)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏👏👏 So good to be able to test this kind of thing with confidence

Comment on lines 8 to 14
override func setUp() {
super.setUp()
}

override func tearDown() {
super.tearDown()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can delete these

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! 7b58531

Comment on lines 188 to 205
func test_searchCustomers_returns_no_customers_when_customer_is_not_registered() throws {
// Given
let customerID = 0
network.simulateResponse(requestUrlSuffix: "customers", filename: "wc-analytics-customers")
network.simulateResponse(requestUrlSuffix: "customers/\(customerID)", filename: "customer")

// When
let response: Result<[Networking.Customer], Error> = waitFor { promise in
let action = CustomerAction.searchCustomers(siteID: self.dummySiteID, keyword: self.dummyKeyword) { result in
promise(result)
}
self.dispatcher.dispatch(action)
}

// Then
let customers = try response.get()
XCTAssertEqual(customers.count, 0)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test! This is business logic which is well worth checking.

However... it doesn't really test what it looks like. I had a go at making it fail (by removing the if result.userID == 0 check in the store) and it still passed.... When you're testing a negative, it's especially worthwhile making sure that the test fails when you expect. Think Red-Green-Refactor, even when you're not actually doing TDD.

The reason is that we don't have anyone in wc-analytics-customer.json with user_id: 0, you'll need to add another entry to that file to get the test to work.

We can improve it further though... here's another way to do it, which more directly checks that the request is not sent, without needing to see what the customer response is:

(N.B. This still needs an analytics customer with user_id: 0 in the json file)

Suggested change
func test_searchCustomers_returns_no_customers_when_customer_is_not_registered() throws {
// Given
let customerID = 0
network.simulateResponse(requestUrlSuffix: "customers", filename: "wc-analytics-customers")
network.simulateResponse(requestUrlSuffix: "customers/\(customerID)", filename: "customer")
// When
let response: Result<[Networking.Customer], Error> = waitFor { promise in
let action = CustomerAction.searchCustomers(siteID: self.dummySiteID, keyword: self.dummyKeyword) { result in
promise(result)
}
self.dispatcher.dispatch(action)
}
// Then
let customers = try response.get()
XCTAssertEqual(customers.count, 0)
}
func test_searchCustomers_returns_no_customers_when_customer_is_not_registered() throws {
// Given
network.simulateResponse(requestUrlSuffix: "customers", filename: "wc-analytics-customers")
// When
waitFor { promise in
let action = CustomerAction.searchCustomers(siteID: self.dummySiteID, keyword: self.dummyKeyword) { result in
promise(())
}
self.dispatcher.dispatch(action)
}
// Then
let requests = network.requestsForResponseData.compactMap { $0 as? JetpackRequest }
XCTAssertFalse(requests.contains(where: { request in
request.path == "customers/0"
}))
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's nice! Made the changes on 7b58531

We’ll be populating the fields using the Address data rather than Customer data for these 3 fields, for consistency with Android. There’s some inconsistency with the API though, reported here: #7993
@iamgabrielma iamgabrielma merged commit daffe2d into trunk Nov 1, 2022
@iamgabrielma iamgabrielma deleted the issue/7741-fill-order-data-from-customer-search branch November 1, 2022 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category: parity Match what's supported by the other platform. feature: order creation All tasks related to creating an order type: enhancement A request for an enhancement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Customer Search: Empty state for non-results searches

4 participants