Skip to content

Commit 9a24237

Browse files
[Shipping Labels] Woomob 891 make hs tariff number mandatory for eu destination (#15946)
2 parents c9c8b48 + c249322 commit 9a24237

File tree

6 files changed

+136
-14
lines changed

6 files changed

+136
-14
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
23.0
55
-----
6-
6+
- [*] Shipping Labels: Made HS tariff number field required in customs form for EU destinations [https://github.com/woocommerce/woocommerce-ios/pull/15946]
77

88
22.9
99
-----

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/ShipmentDetails/WooShippingShipmentDetailsViewModel.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ final class WooShippingShipmentDetailsViewModel: ObservableObject {
7070
order: order,
7171
shipment: shipment,
7272
originCountryCode: originCountryCodePublisher(),
73+
isHSTariffNumberRequired: isHSTariffNumberRequiredPublisher(),
7374
storageManager: storageManager
7475
) { [weak self] form in
7576
self?.customsForm = form
@@ -506,6 +507,19 @@ private extension WooShippingShipmentDetailsViewModel {
506507
.map(\.?.country)
507508
.eraseToAnyPublisher()
508509
}
510+
511+
func isHSTariffNumberRequiredPublisher() -> AnyPublisher<Bool, Never> {
512+
$destinationAddress
513+
/// HS tariff number is required for EU countries
514+
.map { address in
515+
guard let address else {
516+
return false
517+
}
518+
519+
return Country.countriesFollowingEUCustoms.contains(address.country)
520+
}
521+
.eraseToAnyPublisher()
522+
}
509523
}
510524

511525
private extension WooShippingShipmentDetailsViewModel {

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModel.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
4242
init(order: Order,
4343
shipment: Shipment,
4444
originCountryCode: AnyPublisher<String?, Never>? = nil,
45+
isHSTariffNumberRequired: AnyPublisher<Bool, Never>? = nil,
4546
storageManager: StorageManagerType = ServiceLocator.storageManager,
4647
onFormReady: @escaping (ShippingLabelCustomsForm) -> ()) {
4748
self.onFormReady = onFormReady
@@ -54,6 +55,7 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
5455
itemWeight: $0.weight,
5556
currencySymbol: currencySymbol(from: order),
5657
originCountryCode: originCountryCode,
58+
isHSTariffNumberRequired: isHSTariffNumberRequired,
5759
storageManager: storageManager)
5860
}
5961

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsItem.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,23 @@ struct WooShippingCustomsItem: View {
105105
.subheadlineStyle()
106106
.padding(.top, Layout.collapsibleViewVerticalSpacing)
107107

108-
TextField(Localization.HSTariffNumberPlaceholder, text: $viewModel.hsTariffNumber)
109-
.keyboardType(.numberPad)
110-
.padding(Layout.extraPadding)
111-
.roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderLineWidth)
108+
/// HS tariff number
109+
TextField(
110+
viewModel.isHSTariffNumberRequired ? "" : Localization.HSTariffNumberPlaceholder,
111+
text: $viewModel.hsTariffNumber
112+
)
113+
.keyboardType(.numberPad)
114+
.padding(Layout.extraPadding)
115+
.roundedBorder(
116+
cornerRadius: Layout.borderCornerRadius,
117+
lineColor: (viewModel.isHSTariffNumberRequired && viewModel.hsTariffNumber.isEmpty) ? warningRedColor : Color(.separator),
118+
lineWidth: Layout.borderLineWidth
119+
)
120+
121+
Text(Localization.valueRequiredWarningText)
122+
.foregroundColor(warningRedColor)
123+
.footnoteStyle()
124+
.renderedIf(viewModel.isHSTariffNumberRequired && viewModel.hsTariffNumber.isEmpty)
112125

113126
Text(Localization.tariffNumberRulesWarningText)
114127
.foregroundColor(warningRedColor)

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsItemViewModel.swift

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,21 @@ final class WooShippingCustomsItemViewModel: ObservableObject {
6464

6565
private var cancellables = Set<AnyCancellable>()
6666

67+
/// WOOMOB-891
68+
/// Shipments with a EU destination address must contain HS tariff number
69+
///
70+
/// Introduced to enforce tariff validation
71+
/// if `true` then `hsTariffNumber` must be valid for `requiredInformationIsEntered` to be `true`
72+
@Published private(set) var isHSTariffNumberRequired: Bool = false
73+
6774
init(itemName: String,
6875
itemProductID: Int64,
6976
itemQuantity: Decimal,
7077
itemValue: Double,
7178
itemWeight: Double,
7279
currencySymbol: String,
7380
originCountryCode: AnyPublisher<String?, Never>? = nil,
81+
isHSTariffNumberRequired: AnyPublisher<Bool, Never>? = nil,
7482
storageManager: StorageManagerType = ServiceLocator.storageManager) {
7583
self.title = itemName
7684
self.description = itemName
@@ -89,6 +97,9 @@ final class WooShippingCustomsItemViewModel: ObservableObject {
8997
originCountryCode?
9098
.assign(to: &$originCountryCode)
9199

100+
isHSTariffNumberRequired?
101+
.assign(to: &$isHSTariffNumberRequired)
102+
92103
fetchCountries()
93104

94105
combineToPreselectCountry()
@@ -128,15 +139,27 @@ private extension WooShippingCustomsItemViewModel {
128139
}
129140

130141
func combineRequiredInformationIsEntered() {
131-
Publishers.CombineLatest4($description, $valuePerUnit, $weightPerUnit, $selectedCountry)
132-
.sink { [weak self] description, valuePerUnit, weightPerUnit, selectedCountry in
133-
guard let self else { return }
134-
requiredInformationIsEntered = description.isNotEmpty &&
135-
valuePerUnit.isNotEmpty &&
136-
Self.isWeightValid(weightPerUnit) &&
137-
selectedCountry != nil
138-
}
139-
.store(in: &cancellables)
142+
Publishers.CombineLatest4(
143+
$description,
144+
$valuePerUnit,
145+
$weightPerUnit,
146+
$selectedCountry
147+
)
148+
.combineLatest($hsTariffNumber, $isHSTariffNumberRequired)
149+
.sink { [weak self] result in
150+
guard let self else { return }
151+
152+
let ((description, valuePerUnit, weightPerUnit, selectedCountry), hsTariffNumber, isHSTariffNumberRequired) = result
153+
154+
let hsTariffNumberRequirementMet = (hsTariffNumber.isEmpty && !isHSTariffNumberRequired) || (isValidTariffNumber && hsTariffNumber.isNotEmpty)
155+
156+
requiredInformationIsEntered = description.isNotEmpty &&
157+
valuePerUnit.isNotEmpty &&
158+
Self.isWeightValid(weightPerUnit) &&
159+
selectedCountry != nil &&
160+
hsTariffNumberRequirementMet
161+
}
162+
.store(in: &cancellables)
140163
}
141164

142165
func combineHSTariffNumberTotalValue() {

WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingShipmentDetailsViewModelTests.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,76 @@ final class WooShippingShipmentDetailsViewModelTests: XCTestCase {
836836
XCTAssertEqual(customsItemViewModel.selectedCountry?.code, expectedCountry)
837837
}
838838

839+
func test_customs_form_is_incomplete_when_destination_country_is_eu_and_hsTariffNumber_is_empty() throws {
840+
// Given
841+
let stores = MockStoresManager(sessionManager: .testingInstance)
842+
let storageManager = MockStorageManager()
843+
844+
let originAddressSubject = PassthroughSubject<WooShippingAddress?, Never>()
845+
let destinationAddressSubject = PassthroughSubject<WooShippingAddress?, Never>()
846+
847+
let usCountry = Country(code: "US", name: "United States", states: [])
848+
let caCountry = Country(code: "CA", name: "Canada", states: [])
849+
let euCountries = [
850+
Country(code: "FR", name: "France", states: []),
851+
Country(code: "DE", name: "Germany", states: []),
852+
Country(code: "ES", name: "Spain", states: []),
853+
Country(code: "IT", name: "Italy", states: []),
854+
Country(code: "NL", name: "Netherlands", states: [])
855+
]
856+
storageManager.insertSampleCountries(readOnlyCountries: [usCountry, caCountry] + euCountries)
857+
858+
let item = ShippingLabelPackageItem(
859+
productOrVariationID: 1,
860+
orderItemID: 1,
861+
name: "Test Item",
862+
weight: 1,
863+
quantity: 1,
864+
value: 10, // low value, shouldn't require HS Tariff # based on value
865+
dimensions: .fake(),
866+
attributes: [],
867+
imageURL: nil
868+
)
869+
let shipment = Shipment(
870+
contents: [CollapsibleShipmentItemCardViewModel(item: item, currency: "USD")],
871+
currency: "USD",
872+
currencySettings: ServiceLocator.currencySettings,
873+
shippingSettingsService: ServiceLocator.shippingSettingsService
874+
)
875+
876+
let viewModel = WooShippingShipmentDetailsViewModel(
877+
order: Order.fake(),
878+
shipment: shipment,
879+
shippingLabel: nil,
880+
originAddress: originAddressSubject.eraseToAnyPublisher(),
881+
destinationAddress: destinationAddressSubject.eraseToAnyPublisher(),
882+
stores: stores,
883+
storageManager: storageManager
884+
)
885+
886+
// When
887+
let itemViewModel = viewModel.customsFormViewModel.itemsViewModels[0]
888+
itemViewModel.hsTariffNumber = "" // Empty tariff number
889+
890+
originAddressSubject.send(sampleOriginAddress(country: usCountry.code, state: ""))
891+
destinationAddressSubject.send(sampleDestinationAddress(country: caCountry.code, state: ""))
892+
893+
// Then: HS Tariff number should not be required for non EU destination countries regardless of value
894+
XCTAssertTrue(
895+
itemViewModel.requiredInformationIsEntered,
896+
"HS Tariff number should not be required for \(usCountry.name)"
897+
)
898+
899+
// And: HS Tariff number should be required for EU countries regardless of value
900+
for euCountry in euCountries {
901+
destinationAddressSubject.send(sampleDestinationAddress(country: euCountry.code, state: ""))
902+
XCTAssertFalse(
903+
itemViewModel.requiredInformationIsEntered,
904+
"HS Tariff number should be required for \(euCountry.name)"
905+
)
906+
}
907+
}
908+
839909
func test_package_contains_complete_customs_form_when_required_data_is_prefilled() throws {
840910
// Setup
841911
let originAddressSubject = PassthroughSubject<WooShippingAddress?, Never>()

0 commit comments

Comments
 (0)