Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Revamp Shipping Labels • Customs] Validation & Required #14921

Merged
merged 9 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import WooFoundation
import Yosemite

struct WooShippingCustomsForm: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
@ObservedObject var viewModel: WooShippingCustomsFormViewModel
@State private var isShowingITNInfoWebView = false
Expand Down Expand Up @@ -59,6 +60,12 @@ struct WooShippingCustomsForm: View {
.roundedBorder(cornerRadius: Constants.borderCornerRadius, lineColor: Color(.separator), lineWidth: Constants.borderWidth)
}

private var warningRedColor: Color {
let shade: ColorStudioShade = colorScheme == .dark ? .shade40 : .shade60

return .withColorStudio(name: .red, shade: shade)
}

var body: some View {
NavigationView {
GeometryReader { geometry in
Expand Down Expand Up @@ -94,6 +101,14 @@ struct WooShippingCustomsForm: View {
.padding(Constants.borderPadding)
.roundedBorder(cornerRadius: Constants.borderCornerRadius, lineColor: Color(.separator), lineWidth: Constants.borderWidth)

Text(Localization.itnValidationWarningMessage)
.foregroundColor(warningRedColor)
.footnoteStyle()
.renderedIf(!viewModel.isValidITN())
Text(Localization.itnRequiredWarningMessage)
.foregroundColor(warningRedColor)
.footnoteStyle()
.renderedIf(viewModel.internationalTransactionNumberIsRequired && viewModel.internationalTransactionNumber.isEmpty)
Button {
isShowingITNInfoWebView = true
} label: {
Expand Down Expand Up @@ -193,6 +208,14 @@ extension WooShippingCustomsForm {
static let productDetailsTitle = NSLocalizedString("wooShipping.customs.productDetails",
value: "Product Details",
comment: "Product Details Section title")
static let itnValidationWarningMessage = NSLocalizedString("wooShipping.customs.itnValidation",
value: "Please enter a valid ITN",
comment: "Customs validation warning for the ITN field")
static let itnRequiredWarningMessage = NSLocalizedString("wooShipping.customs.itnValidation",
value: "International Transaction Number is required for shipping items " +
"valued over $2,500 per tariff number",
comment: "Customs validation warning for the ITN field")

}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import WooFoundation

final class WooShippingCustomsFormViewModel: ObservableObject {
@Published var internationalTransactionNumber: String = ""
@Published var internationalTransactionNumberIsRequired: Bool = false
@Published var returnToSenderIfNotDelivered: Bool = false

@Published var requiredInformationIsEntered: Bool = false
@Published var itemsRequiredInformationIsEntered: Bool = false

@Published var contentType: WooShippingContentType = .merchandise
@Published var restrictionType: WooShippingRestrictionType = .none
Expand All @@ -27,6 +29,8 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
}

listenToItemsRequiredInformationValues()
listenForRequiredInformation()
listenForInternationalTransactionNumberIsRequired()
}

@Published var itemsViewModels: [WooShippingCustomsItemViewModel] = []
Expand All @@ -40,7 +44,7 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
restrictionType: restrictionType.toFormRestrictionType(),
restrictionComments: "",
nonDeliveryOption: returnToSenderIfNotDelivered ? .return : .abandon,
itn: internationalTransactionNumber,
itn: isValidITN() ? internationalTransactionNumber : "",
items: itemsViewModels.map {
ShippingLabelCustomsForm.Item(description: $0.description,
quantity: $0.orderItem.quantity,
Expand All @@ -53,9 +57,67 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
)
onCompletion(form)
}

func isValidITN() -> Bool {
guard internationalTransactionNumber.isNotEmpty else {
return true
}

let pattern = "^(?:(?:AES X\\d{14})|(?:NOEEI 30\\.\\d{1,2}(?:\\([a-z]\\)(?:\\(\\d\\))?)?))$"

do {
let regex = try NSRegularExpression(pattern: pattern)
let range = NSRange(internationalTransactionNumber.startIndex..<internationalTransactionNumber.endIndex, in: internationalTransactionNumber)
return regex.firstMatch(in: internationalTransactionNumber, options: [], range: range) != nil
} catch {
return false
}
}
}

private extension WooShippingCustomsFormViewModel {
func listenForRequiredInformation() {
Publishers.CombineLatest3($itemsRequiredInformationIsEntered, $internationalTransactionNumber, $internationalTransactionNumberIsRequired)
.sink { [weak self] itemsRequiredInformationIsEntered, internationalTransactionNumber, internationalTransactionNumberIsRequired in
guard let self = self else { return }

guard itemsRequiredInformationIsEntered else {
self.requiredInformationIsEntered = false
return
}

guard internationalTransactionNumberIsRequired else {
self.requiredInformationIsEntered = true
return
}

self.requiredInformationIsEntered = internationalTransactionNumber.isNotEmpty && self.isValidITN()
}
.store(in: &cancellables)
}

func listenForInternationalTransactionNumberIsRequired() {
$itemsViewModels
.map { childViewModels in
childViewModels.map { $0.$hsTariffNumberTotalValue.eraseToAnyPublisher() }
}
.flatMap { childPublishers in
childPublishers.combineLatest()
}
.sink { [weak self] value in
// Remove nils
let compactedValues = value.compactMap { $0 }
Copy link
Contributor

Choose a reason for hiding this comment

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

tip: You can use compacted() when you want to remove nils; it does the same as compactMap { $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.

Good point thanks! Done in 46288e7


var hsTariffNumberTotalValueDictionary: [String: Decimal] = [:]
for (hsTariffNumber, totalValuePerItem) in compactedValues {
hsTariffNumberTotalValueDictionary[hsTariffNumber, default: 0] += totalValuePerItem
}

self?.internationalTransactionNumberIsRequired = hsTariffNumberTotalValueDictionary.values.contains { $0 > 2500 }
}
.store(in: &cancellables)
}

func listenToItemsRequiredInformationValues() {
// Listen to the items required information and enable the button depending on it
$itemsViewModels
Expand All @@ -69,7 +131,7 @@ private extension WooShippingCustomsFormViewModel {
childValidityArray.allSatisfy { $0 } // Check if all are valid
}
.sink { [weak self] value in
self?.requiredInformationIsEntered = value
self?.itemsRequiredInformationIsEntered = value
}
.store(in: &cancellables)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ final class WooShippingCustomsItemViewModel: ObservableObject {
@Published var valuePerUnit: String = ""
@Published var weightPerUnit: String = ""
@Published var originCountry: WooShippingCustomsCountry
// Useful to determine externally if the shipping requires an ITN
@Published var hsTariffNumberTotalValue: (String, Decimal)?

private let storageManager: StorageManagerType
private let stores: StoresManager
Expand Down Expand Up @@ -71,10 +73,11 @@ final class WooShippingCustomsItemViewModel: ObservableObject {

fetchCountries()
combineRequiredInformationIsEntered()
combineHSTariffNumberTotalValue()
}
}

extension WooShippingCustomsItemViewModel {
private extension WooShippingCustomsItemViewModel {
func fetchCountries() {
try? resultsController.performFetch()
let action = DataAction.synchronizeCountries(siteID: siteID) { [weak self] (result) in
Expand All @@ -97,4 +100,22 @@ extension WooShippingCustomsItemViewModel {
}
.store(in: &cancellables)
}

func combineHSTariffNumberTotalValue() {
Publishers.CombineLatest($valuePerUnit, $hsTariffNumber)
.sink { [weak self] valuePerUnit, hsTariffNumber in
guard let self = self else { return }

guard self.currencySymbol == "$",
let valuePerUnitDecimal = Decimal(string: valuePerUnit),
hsTariffNumber.isNotEmpty,
isValidTariffNumber else {
self.hsTariffNumberTotalValue = nil
return
}

self.hsTariffNumberTotalValue = (hsTariffNumber, valuePerUnitDecimal * orderItem.quantity)
}
.store(in: &cancellables)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ import Yosemite
class WooShippingCustomsFormViewModelTests: XCTestCase {
private var viewModel: WooShippingCustomsFormViewModel!

override func setUp() {
super.setUp()

viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), onCompletion: { _ in })
}

override func tearDown() {
super.tearDown()

viewModel = nil
}

func test_onDismiss_calls_onCompletion_with_right_values() {
// Given
let orderItems = [MockOrderItem.sampleItem(productID: 123, quantity: 2), MockOrderItem.sampleItem()]
Expand All @@ -16,7 +28,7 @@ class WooShippingCustomsFormViewModelTests: XCTestCase {

viewModel.restrictionType = .quarantine
viewModel.contentType = .gift
viewModel.internationalTransactionNumber = "1234"
viewModel.internationalTransactionNumber = "NOEEI 30.37(a)"
viewModel.returnToSenderIfNotDelivered = false

viewModel.itemsViewModels.first?.description = "Test Item"
Expand Down Expand Up @@ -62,6 +74,24 @@ class WooShippingCustomsFormViewModelTests: XCTestCase {
XCTAssertTrue(passedForm?.items.first?.hsTariffNumber.isEmpty ?? false)
}

func test_onDismiss_when_calls_onCompletion_with_invalid_itn_then_returns_empty() {
// Given
let orderItems = [MockOrderItem.sampleItem(productID: 123, quantity: 2), MockOrderItem.sampleItem()]

var passedForm: ShippingLabelCustomsForm?
viewModel = WooShippingCustomsFormViewModel(order: Order.fake().copy(items: orderItems), onCompletion: { form in
passedForm = form
})

viewModel.internationalTransactionNumber = "1234"

// When
viewModel.onDismiss()

// Then
XCTAssertTrue(passedForm?.itn.isEmpty ?? false)
}

func test_init_passes_right_currency() {
// Given
let orderItems = [MockOrderItem.sampleItem(productID: 123, quantity: 2), MockOrderItem.sampleItem()]
Expand All @@ -72,4 +102,117 @@ class WooShippingCustomsFormViewModelTests: XCTestCase {
// Then
XCTAssertEqual(viewModel.itemsViewModels.first?.currencySymbol, "$")
}

func test_isValidITN_when_internationalTransactionNumber_is_empty_then_returns_true() {
// Given
viewModel.internationalTransactionNumber = ""

// Then
XCTAssertTrue(viewModel.isValidITN())
}

func test_isValidITN_when_passing_a_valid_AES_internationalTransactionNumber_then_returns_true() {
// Given
viewModel.internationalTransactionNumber = "AES X12345678901234"

// Then
XCTAssertTrue(viewModel.isValidITN())
}

func test_isValidITN_when_passing_a_valid_NOEEI_internationalTransactionNumber_then_returns_true() {
// Given
viewModel.internationalTransactionNumber = "NOEEI 30.37(a)"

// Then
XCTAssertTrue(viewModel.isValidITN())
}

func test_isValidITN_when_passing_an_invalid_internationalTransactionNumber_then_returns_false() {
// Given
viewModel.internationalTransactionNumber = "INVALID 123456"

// Then
XCTAssertFalse(viewModel.isValidITN())
}

func test_isValidITN_when_passing_an_AES_internationalTransactionNumber_with_special_characters_then_returns_false() {
// Given
viewModel.internationalTransactionNumber = "AES X123@#4567890"

// Then
XCTAssertFalse(viewModel.isValidITN())
}

func test_isValidITN_when_passing_a_long_internationalTransactionNumber_then_returns_false() {
// Given
viewModel.internationalTransactionNumber = "AES X12345678901234567890"

// Then
XCTAssertFalse(viewModel.isValidITN())
}

func test_internationalTransactionNumberIsRequired_when_item_view_models_hsTariffNumberTotalValue_is_nil_then_returns_false() {
// Given
let orderItems = [MockOrderItem.sampleItem(productID: 123, quantity: 2), MockOrderItem.sampleItem()]

viewModel = WooShippingCustomsFormViewModel(order: Order.fake().copy(items: orderItems), onCompletion: { _ in })

// When
viewModel.itemsViewModels.first?.hsTariffNumberTotalValue = nil

// Then
XCTAssertFalse(viewModel.internationalTransactionNumberIsRequired)
}

func test_internationalTransactionNumberIsRequired_when_item_view_models_hsTariffNumberTotalValue_is_less_than_2500_then_returns_false() {
// Given
let orderItems = [MockOrderItem.sampleItem(productID: 123, quantity: 2), MockOrderItem.sampleItem()]

viewModel = WooShippingCustomsFormViewModel(order: Order.fake().copy(items: orderItems), onCompletion: { _ in })

// When
viewModel.itemsViewModels.first?.hsTariffNumberTotalValue = ("123456", 1000)

XCTAssertFalse(viewModel.requiredInformationIsEntered)
}

func test_internationalTransactionNumberIsRequired_when_item_view_models_hsTariffNumberTotalValue_is_less_than_2500_then_returns_true() {
// Given
let orderItems = [MockOrderItem.sampleItem(productID: 123, quantity: 2), MockOrderItem.sampleItem()]

viewModel = WooShippingCustomsFormViewModel(order: Order.fake().copy(items: orderItems), onCompletion: { _ in })

viewModel.itemsViewModels.first?.requiredInformationIsEntered = true
viewModel.itemsViewModels.first?.hsTariffNumberTotalValue = ("123456", 1000)
viewModel.itemsViewModels[1].hsTariffNumberTotalValue = ("123456", 2000)
viewModel.internationalTransactionNumber = ""

XCTAssertFalse(viewModel.requiredInformationIsEntered)
}

func test_requiredInformationIsEntered_when_itn_is_required_but_invalid_then_returns_false() {
// Given
let orderItems = [MockOrderItem.sampleItem(productID: 123, quantity: 2), MockOrderItem.sampleItem()]

viewModel = WooShippingCustomsFormViewModel(order: Order.fake().copy(items: orderItems), onCompletion: { _ in })

viewModel.itemsViewModels.first?.requiredInformationIsEntered = true
viewModel.internationalTransactionNumber = "1234"

XCTAssertFalse(viewModel.requiredInformationIsEntered)
}

func test_requiredInformationIsEntered_when_itn_is_required_and_valid_then_returns_true() {
// Given
let orderItems = [MockOrderItem.sampleItem()]

viewModel = WooShippingCustomsFormViewModel(order: Order.fake().copy(items: orderItems), onCompletion: { _ in })

debugPrint("viewModel.itemsViewModels", viewModel.itemsViewModels)

viewModel.internationalTransactionNumber = "NOEEI 30.37(a)"
viewModel.itemsViewModels.first?.requiredInformationIsEntered = true

XCTAssertTrue(viewModel.requiredInformationIsEntered)
}
}
Loading
Loading