Skip to content

Commit 681abc0

Browse files
authored
Merge pull request #7587 from woocommerce/issue/7546-order-details-address-validation
Introduce order details address validation analytics
2 parents 71ac5d6 + 593e0c0 commit 681abc0

File tree

6 files changed

+285
-0
lines changed

6 files changed

+285
-0
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ extension WooAnalyticsEvent {
370370
static let orderID = "id"
371371
static let hasMultipleShippingLines = "has_multiple_shipping_lines"
372372
static let hasMultipleFeeLines = "has_multiple_fee_lines"
373+
static let errorMessage = "error_message"
374+
static let validationScenario = "validation_scenario"
373375
}
374376

375377
static func orderOpen(order: Order) -> WooAnalyticsEvent {
@@ -488,6 +490,25 @@ extension WooAnalyticsEvent {
488490
static func pluginsNotSyncedYet() -> WooAnalyticsEvent {
489491
WooAnalyticsEvent(statName: .pluginsNotSyncedYet, properties: [:])
490492
}
493+
494+
/// Tracked when the Order details view detects a malformed shipping address data.
495+
///
496+
static func addressValidationFailed(error: AddressValidator.AddressValidationError, orderID: Int64) -> WooAnalyticsEvent {
497+
switch error {
498+
case .local(let errorMessage):
499+
return WooAnalyticsEvent(statName: .orderAddressValidationError, properties: [
500+
Keys.errorMessage: errorMessage,
501+
Keys.validationScenario: "local",
502+
Keys.orderID: orderID
503+
])
504+
case .remote(let error):
505+
return WooAnalyticsEvent(statName: .orderAddressValidationError, properties: [
506+
Keys.errorMessage: "\(String(describing: error?.addressError ?? "Unknown"))",
507+
Keys.validationScenario: "remote",
508+
Keys.orderID: orderID
509+
])
510+
}
511+
}
491512
}
492513
}
493514

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ public enum WooAnalyticsStat: String {
269269
case collectPaymentTapped = "payments_flow_order_collect_payment_tapped"
270270
case orderViewCustomFieldsTapped = "order_view_custom_fields_tapped"
271271
case orderDetailWaitingTimeLoaded = "order_detail_waiting_time_loaded"
272+
case orderAddressValidationError = "order_address_validation_error"
272273

273274
// MARK: Order List Sorting/Filtering
274275
//

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ final class OrderDetailsViewModel {
1515
private let stores: StoresManager
1616
private let storageManager: StorageManagerType
1717
private let currencyFormatter: CurrencyFormatter
18+
private let addressValidator: AddressValidator
1819

1920
private(set) var order: Order
2021

@@ -34,6 +35,7 @@ final class OrderDetailsViewModel {
3435
self.stores = stores
3536
self.storageManager = storageManager
3637
self.currencyFormatter = currencyFormatter
38+
addressValidator = AddressValidator(siteID: order.siteID, stores: stores)
3739
}
3840

3941
func update(order newOrder: Order) {
@@ -544,6 +546,11 @@ extension OrderDetailsViewModel {
544546
func syncShippingLabels(onCompletion: ((Error?) -> ())? = nil) {
545547
// If the plugin is not active, there is no point on continuing with a request that will fail.
546548
isPluginActive(SitePlugin.SupportedPlugin.WCShip) { [weak self] isActive in
549+
// We're looking to measure how often an order contains an invalid address
550+
// and validate it through the Shipping label plugin, so we need to trigger
551+
// this call when it's possible to verify the plugin presence
552+
self?.startAddressValidation(shippingLabelPluginIsActive: isActive)
553+
547554
guard let self = self, isActive else {
548555
onCompletion?(nil)
549556
return
@@ -569,6 +576,20 @@ extension OrderDetailsViewModel {
569576
}
570577
}
571578

579+
func startAddressValidation(shippingLabelPluginIsActive: Bool) {
580+
guard let orderShippingAddress = order.shippingAddress, orderShippingAddress != Address.empty else {
581+
return
582+
}
583+
584+
let orderID = order.orderID
585+
586+
addressValidator.validate(address: orderShippingAddress, onlyLocally: !shippingLabelPluginIsActive) { result in
587+
if let error = result.failure {
588+
ServiceLocator.analytics.track(event: WooAnalyticsEvent.Orders.addressValidationFailed(error: error, orderID: orderID))
589+
}
590+
}
591+
}
592+
572593
func syncSavedReceipts(onCompletion: ((Error?) -> ())? = nil) {
573594
let action = ReceiptAction.loadReceipt(order: order) { [weak self] result in
574595
switch result {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import Yosemite
2+
3+
/// Reusable implementation of the Address validation introduced by the
4+
/// `ShippingLabelAddressFormViewModel`, but striped of the specifics to serve the form UI
5+
///
6+
class AddressValidator {
7+
let siteID: Int64
8+
private let stores: StoresManager
9+
10+
init(siteID: Int64, stores: StoresManager) {
11+
self.siteID = siteID
12+
self.stores = stores
13+
}
14+
15+
func validate(address: Address, onlyLocally: Bool, onCompletion: @escaping (Result<Void, AddressValidationError>) -> Void) {
16+
let convertedAddress = ShippingLabelAddress(company: address.company ?? "",
17+
name: address.fullName,
18+
phone: address.phone ?? "",
19+
country: address.country,
20+
state: address.state,
21+
address1: address.address1 ,
22+
address2: address.address2 ?? "",
23+
city: address.city,
24+
postcode: address.postcode)
25+
26+
let localErrors = validateAddressLocally(addressToBeValidated: convertedAddress)
27+
if localErrors.isNotEmpty {
28+
let localErrorMessage = localErrors.map { $0.rawValue }.joined(separator: ", ")
29+
onCompletion(.failure(.local(localErrorMessage)))
30+
return
31+
}
32+
33+
if onlyLocally {
34+
onCompletion(.success(()))
35+
return
36+
}
37+
38+
let addressToBeVerified = ShippingLabelAddressVerification(address: convertedAddress, type: .destination)
39+
let action = ShippingLabelAction.validateAddress(siteID: siteID, address: addressToBeVerified) { result in
40+
switch result {
41+
case .success:
42+
onCompletion(.success(()))
43+
case .failure(let error):
44+
onCompletion(.failure(.remote(error as? ShippingLabelAddressValidationError)))
45+
}
46+
}
47+
stores.dispatch(action)
48+
}
49+
50+
private func validateAddressLocally(addressToBeValidated: ShippingLabelAddress) -> [LocalValidationError] {
51+
var errors: [LocalValidationError] = []
52+
53+
if addressToBeValidated.name.isEmpty && addressToBeValidated.company.isEmpty {
54+
errors.append(.name)
55+
}
56+
if addressToBeValidated.address1.isEmpty {
57+
errors.append(.address)
58+
}
59+
if addressToBeValidated.city.isEmpty {
60+
errors.append(.city)
61+
}
62+
if addressToBeValidated.postcode.isEmpty {
63+
errors.append(.postcode)
64+
}
65+
if addressToBeValidated.state.isEmpty {
66+
errors.append(.state)
67+
}
68+
if addressToBeValidated.country.isEmpty {
69+
errors.append(.country)
70+
}
71+
72+
return errors
73+
}
74+
75+
enum LocalValidationError: String {
76+
case name = "Customer name and company are empty"
77+
case address = "Address is empty"
78+
case city = "City is empty"
79+
case postcode = "Postcode is empty"
80+
case state = "State is empty"
81+
case country = "Country is empty"
82+
}
83+
84+
enum AddressValidationError: Error {
85+
case local(String)
86+
case remote(ShippingLabelAddressValidationError?)
87+
}
88+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,7 @@
905905
45FDDD66267784AD00ADACE8 /* ShippingLabelSummaryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45FDDD64267784AD00ADACE8 /* ShippingLabelSummaryTableViewCell.xib */; };
906906
532842FC64B572D4545BD98E /* OrderFormCustomerNoteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53284C9FD06F2BDABC554BEE /* OrderFormCustomerNoteViewModel.swift */; };
907907
532846FAFFFCA93169B5E0BC /* WaitingTimeTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53284FB62FF7F94F18F0D3FF /* WaitingTimeTracker.swift */; };
908+
53284C6D36BD5E88B99FECFA /* AddressValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53284768ECB0EABA5D3A10DA /* AddressValidator.swift */; };
908909
570AAB052472FACB00516C0C /* OrderDetailsDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570AAB042472FACB00516C0C /* OrderDetailsDataSourceTests.swift */; };
909910
57150E0F24F462C200E81611 /* TestKit in Frameworks */ = {isa = PBXBuildFile; productRef = 57150E0E24F462C200E81611 /* TestKit */; };
910911
5718852C2465D9EC00E2486F /* ReviewsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5718852B2465D9EC00E2486F /* ReviewsCoordinator.swift */; };
@@ -1259,6 +1260,7 @@
12591260
B626C71B287659D60083820C /* OrderCustomFieldsDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626C71A287659D60083820C /* OrderCustomFieldsDetails.swift */; };
12601261
B63AAF4B254AD2C6000B28A2 /* URL+SurveyViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63AAF4A254AD2C6000B28A2 /* URL+SurveyViewControllerTests.swift */; };
12611262
B651474527D644FF00C9C4E6 /* CustomerNoteSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B651474427D644FF00C9C4E6 /* CustomerNoteSection.swift */; };
1263+
B65EB44A28BD670000DF595D /* AddressValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65EB44928BD670000DF595D /* AddressValidatorTests.swift */; };
12621264
B687940C27699D420092BCA0 /* RefundFeesCalculationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B687940B27699D410092BCA0 /* RefundFeesCalculationUseCase.swift */; };
12631265
B6C838DE28793B3A003AB786 /* OrderCustomFieldsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C838DD28793B3A003AB786 /* OrderCustomFieldsViewModel.swift */; };
12641266
B6E851F3276320C70041D1BA /* RefundFeesDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E851F2276320C70041D1BA /* RefundFeesDetailsViewModel.swift */; };
@@ -2721,6 +2723,7 @@
27212723
45FBDF3B238D4EA800127F77 /* ExtendedAddProductImageCollectionViewCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedAddProductImageCollectionViewCellTests.swift; sourceTree = "<group>"; };
27222724
45FDDD63267784AD00ADACE8 /* ShippingLabelSummaryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelSummaryTableViewCell.swift; sourceTree = "<group>"; };
27232725
45FDDD64267784AD00ADACE8 /* ShippingLabelSummaryTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShippingLabelSummaryTableViewCell.xib; sourceTree = "<group>"; };
2726+
53284768ECB0EABA5D3A10DA /* AddressValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressValidator.swift; sourceTree = "<group>"; };
27242727
53284C9FD06F2BDABC554BEE /* OrderFormCustomerNoteViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderFormCustomerNoteViewModel.swift; sourceTree = "<group>"; };
27252728
53284FB62FF7F94F18F0D3FF /* WaitingTimeTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitingTimeTracker.swift; sourceTree = "<group>"; };
27262729
570AAB042472FACB00516C0C /* OrderDetailsDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OrderDetailsDataSourceTests.swift; path = "Order Details/OrderDetailsDataSourceTests.swift"; sourceTree = "<group>"; };
@@ -3105,6 +3108,7 @@
31053108
B626C71A287659D60083820C /* OrderCustomFieldsDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCustomFieldsDetails.swift; sourceTree = "<group>"; };
31063109
B63AAF4A254AD2C6000B28A2 /* URL+SurveyViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+SurveyViewControllerTests.swift"; sourceTree = "<group>"; };
31073110
B651474427D644FF00C9C4E6 /* CustomerNoteSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerNoteSection.swift; sourceTree = "<group>"; };
3111+
B65EB44928BD670000DF595D /* AddressValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressValidatorTests.swift; sourceTree = "<group>"; };
31083112
B687940B27699D410092BCA0 /* RefundFeesCalculationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundFeesCalculationUseCase.swift; sourceTree = "<group>"; };
31093113
B6C838DD28793B3A003AB786 /* OrderCustomFieldsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCustomFieldsViewModel.swift; sourceTree = "<group>"; };
31103114
B6E851F2276320C70041D1BA /* RefundFeesDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundFeesDetailsViewModel.swift; sourceTree = "<group>"; };
@@ -4280,6 +4284,7 @@
42804284
0277AE9A256CA8A200F45C4A /* AggregatedShippingLabelOrderItemsTests.swift */,
42814285
025678C625773399009D7E6C /* Collection+ShippingLabelTests.swift */,
42824286
0298431125936DFC00979CAE /* ShippingLabelsTopBannerFactoryTests.swift */,
4287+
B65EB44928BD670000DF595D /* AddressValidatorTests.swift */,
42834288
);
42844289
path = "Shipping Labels";
42854290
sourceTree = "<group>";
@@ -5265,6 +5270,7 @@
52655270
45C91CFD25E55A1200FD8812 /* ShippingLabelAddressTopBannerFactory.swift */,
52665271
45BBE5C6268CBB090017D8F8 /* ShippingLabelStateOfACountryListSelectorCommand.swift */,
52675272
DE46133826B2BEB8001DE59C /* ShippingLabelCountryListSelectorCommand.swift */,
5273+
53284768ECB0EABA5D3A10DA /* AddressValidator.swift */,
52685274
);
52695275
path = "Shipping Address Validation";
52705276
sourceTree = "<group>";
@@ -10122,6 +10128,7 @@
1012210128
ABC3521A374A2355001E3CD6 /* CardReaderSettingsSearchingViewController.swift in Sources */,
1012310129
532842FC64B572D4545BD98E /* OrderFormCustomerNoteViewModel.swift in Sources */,
1012410130
532846FAFFFCA93169B5E0BC /* WaitingTimeTracker.swift in Sources */,
10131+
53284C6D36BD5E88B99FECFA /* AddressValidator.swift in Sources */,
1012510132
);
1012610133
runOnlyForDeploymentPostprocessing = 0;
1012710134
};
@@ -10280,6 +10287,7 @@
1028010287
E17E3BFB266917E20009D977 /* CardPresentModalBluetoothRequiredTests.swift in Sources */,
1028110288
CCD2E68925DD52C100BD975D /* ProductVariationsViewModelTests.swift in Sources */,
1028210289
26FE09E424DCFE5200B9BDF5 /* InAppFeedbackCardViewControllerTests.swift in Sources */,
10290+
B65EB44A28BD670000DF595D /* AddressValidatorTests.swift in Sources */,
1028310291
0215320D2423309B003F2BBD /* UIStackView+SubviewsTests.swift in Sources */,
1028410292
027B8BBD23FE0DE10040944E /* ProductImageActionHandlerTests.swift in Sources */,
1028510293
CC53FB3E2758E2D500C4CA4F /* ProductRowViewModelTests.swift in Sources */,

0 commit comments

Comments
 (0)