Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

23.0
-----

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

22.9
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ final class WooShippingShipmentDetailsViewModel: ObservableObject {
order: order,
shipment: shipment,
originCountryCode: originCountryCodePublisher(),
isHSTariffNumberRequired: isHSTariffNumberRequiredPublisher(),
storageManager: storageManager
) { [weak self] form in
self?.customsForm = form
Expand Down Expand Up @@ -506,6 +507,19 @@ private extension WooShippingShipmentDetailsViewModel {
.map(\.?.country)
.eraseToAnyPublisher()
}

func isHSTariffNumberRequiredPublisher() -> AnyPublisher<Bool, Never> {
$destinationAddress
/// HS tariff number is required for EU countries
.map { address in
guard let address else {
return false
}

return Country.countriesFollowingEUCustoms.contains(address.country)
}
.eraseToAnyPublisher()
}
}

private extension WooShippingShipmentDetailsViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
init(order: Order,
shipment: Shipment,
originCountryCode: AnyPublisher<String?, Never>? = nil,
isHSTariffNumberRequired: AnyPublisher<Bool, Never>? = nil,
storageManager: StorageManagerType = ServiceLocator.storageManager,
onFormReady: @escaping (ShippingLabelCustomsForm) -> ()) {
self.onFormReady = onFormReady
Expand All @@ -54,6 +55,7 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
itemWeight: $0.weight,
currencySymbol: currencySymbol(from: order),
originCountryCode: originCountryCode,
isHSTariffNumberRequired: isHSTariffNumberRequired,
storageManager: storageManager)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,23 @@ struct WooShippingCustomsItem: View {
.subheadlineStyle()
.padding(.top, Layout.collapsibleViewVerticalSpacing)

TextField(Localization.HSTariffNumberPlaceholder, text: $viewModel.hsTariffNumber)
.keyboardType(.numberPad)
.padding(Layout.extraPadding)
.roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderLineWidth)
/// HS tariff number
TextField(
viewModel.isHSTariffNumberRequired ? "" : Localization.HSTariffNumberPlaceholder,
text: $viewModel.hsTariffNumber
)
.keyboardType(.numberPad)
.padding(Layout.extraPadding)
.roundedBorder(
cornerRadius: Layout.borderCornerRadius,
lineColor: (viewModel.isHSTariffNumberRequired && viewModel.hsTariffNumber.isEmpty) ? warningRedColor : Color(.separator),
lineWidth: Layout.borderLineWidth
)

Text(Localization.valueRequiredWarningText)
.foregroundColor(warningRedColor)
.footnoteStyle()
.renderedIf(viewModel.isHSTariffNumberRequired && viewModel.hsTariffNumber.isEmpty)

Text(Localization.tariffNumberRulesWarningText)
.foregroundColor(warningRedColor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,21 @@ final class WooShippingCustomsItemViewModel: ObservableObject {

private var cancellables = Set<AnyCancellable>()

/// WOOMOB-891
/// Shipments with a EU destination address must contain HS tariff number
///
/// Introduced to enforce tariff validation
/// if `true` then `hsTariffNumber` must be valid for `requiredInformationIsEntered` to be `true`
@Published private(set) var isHSTariffNumberRequired: Bool = false

init(itemName: String,
itemProductID: Int64,
itemQuantity: Decimal,
itemValue: Double,
itemWeight: Double,
currencySymbol: String,
originCountryCode: AnyPublisher<String?, Never>? = nil,
isHSTariffNumberRequired: AnyPublisher<Bool, Never>? = nil,
storageManager: StorageManagerType = ServiceLocator.storageManager) {
self.title = itemName
self.description = itemName
Expand All @@ -89,6 +97,9 @@ final class WooShippingCustomsItemViewModel: ObservableObject {
originCountryCode?
.assign(to: &$originCountryCode)

isHSTariffNumberRequired?
.assign(to: &$isHSTariffNumberRequired)

fetchCountries()

combineToPreselectCountry()
Expand Down Expand Up @@ -128,15 +139,27 @@ private extension WooShippingCustomsItemViewModel {
}

func combineRequiredInformationIsEntered() {
Publishers.CombineLatest4($description, $valuePerUnit, $weightPerUnit, $selectedCountry)
.sink { [weak self] description, valuePerUnit, weightPerUnit, selectedCountry in
guard let self else { return }
requiredInformationIsEntered = description.isNotEmpty &&
valuePerUnit.isNotEmpty &&
Self.isWeightValid(weightPerUnit) &&
selectedCountry != nil
}
.store(in: &cancellables)
Publishers.CombineLatest4(
$description,
$valuePerUnit,
$weightPerUnit,
$selectedCountry
)
.combineLatest($hsTariffNumber, $isHSTariffNumberRequired)
.sink { [weak self] result in
guard let self else { return }

let ((description, valuePerUnit, weightPerUnit, selectedCountry), hsTariffNumber, isHSTariffNumberRequired) = result

let hsTariffNumberRequirementMet = (hsTariffNumber.isEmpty && !isHSTariffNumberRequired) || (isValidTariffNumber && hsTariffNumber.isNotEmpty)

requiredInformationIsEntered = description.isNotEmpty &&
valuePerUnit.isNotEmpty &&
Self.isWeightValid(weightPerUnit) &&
selectedCountry != nil &&
hsTariffNumberRequirementMet
}
.store(in: &cancellables)
}

func combineHSTariffNumberTotalValue() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,76 @@ final class WooShippingShipmentDetailsViewModelTests: XCTestCase {
XCTAssertEqual(customsItemViewModel.selectedCountry?.code, expectedCountry)
}

func test_customs_form_is_incomplete_when_destination_country_is_eu_and_hsTariffNumber_is_empty() throws {
// Given
let stores = MockStoresManager(sessionManager: .testingInstance)
let storageManager = MockStorageManager()

let originAddressSubject = PassthroughSubject<WooShippingAddress?, Never>()
let destinationAddressSubject = PassthroughSubject<WooShippingAddress?, Never>()

let usCountry = Country(code: "US", name: "United States", states: [])
let caCountry = Country(code: "CA", name: "Canada", states: [])
let euCountries = [
Country(code: "FR", name: "France", states: []),
Country(code: "DE", name: "Germany", states: []),
Country(code: "ES", name: "Spain", states: []),
Country(code: "IT", name: "Italy", states: []),
Country(code: "NL", name: "Netherlands", states: [])
]
storageManager.insertSampleCountries(readOnlyCountries: [usCountry, caCountry] + euCountries)

let item = ShippingLabelPackageItem(
productOrVariationID: 1,
orderItemID: 1,
name: "Test Item",
weight: 1,
quantity: 1,
value: 10, // low value, shouldn't require HS Tariff # based on value
dimensions: .fake(),
attributes: [],
imageURL: nil
)
let shipment = Shipment(
contents: [CollapsibleShipmentItemCardViewModel(item: item, currency: "USD")],
currency: "USD",
currencySettings: ServiceLocator.currencySettings,
shippingSettingsService: ServiceLocator.shippingSettingsService
)

let viewModel = WooShippingShipmentDetailsViewModel(
order: Order.fake(),
shipment: shipment,
shippingLabel: nil,
originAddress: originAddressSubject.eraseToAnyPublisher(),
destinationAddress: destinationAddressSubject.eraseToAnyPublisher(),
stores: stores,
storageManager: storageManager
)

// When
let itemViewModel = viewModel.customsFormViewModel.itemsViewModels[0]
itemViewModel.hsTariffNumber = "" // Empty tariff number

originAddressSubject.send(sampleOriginAddress(country: usCountry.code, state: ""))
destinationAddressSubject.send(sampleDestinationAddress(country: caCountry.code, state: ""))

// Then: HS Tariff number should not be required for non EU destination countries regardless of value
XCTAssertTrue(
itemViewModel.requiredInformationIsEntered,
"HS Tariff number should not be required for \(usCountry.name)"
)

// And: HS Tariff number should be required for EU countries regardless of value
for euCountry in euCountries {
destinationAddressSubject.send(sampleDestinationAddress(country: euCountry.code, state: ""))
XCTAssertFalse(
itemViewModel.requiredInformationIsEntered,
"HS Tariff number should be required for \(euCountry.name)"
)
}
}

func test_package_contains_complete_customs_form_when_required_data_is_prefilled() throws {
// Setup
let originAddressSubject = PassthroughSubject<WooShippingAddress?, Never>()
Expand Down