diff --git a/WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift b/WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift index 808d6469c78..102ca3cc023 100644 --- a/WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift +++ b/WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift @@ -3,15 +3,6 @@ import Foundation import Yosemite -protocol CIABEligibilityCheckerProtocol { - var isCurrentSiteCIAB: Bool { get } - - func isSiteCIAB(_ site: Site) -> Bool - - func isFeatureSupportedForCurrentSite(_ feature: CIABAffectedFeature) -> Bool - func isFeatureSupported(_ feature: CIABAffectedFeature, for site: Site) -> Bool -} - final class CIABEligibilityChecker { private let stores: StoresManager diff --git a/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift b/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift new file mode 100644 index 00000000000..6c7cd0aac92 --- /dev/null +++ b/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift @@ -0,0 +1,12 @@ +import Foundation +import Yosemite + +/// periphery: ignore - Will be used in upcoming changes for app feature gating +protocol CIABEligibilityCheckerProtocol { + var isCurrentSiteCIAB: Bool { get } + + func isSiteCIAB(_ site: Site) -> Bool + + func isFeatureSupportedForCurrentSite(_ feature: CIABAffectedFeature) -> Bool + func isFeatureSupported(_ feature: CIABAffectedFeature, for site: Site) -> Bool +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift index 2a73f947904..26abd650dbb 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift @@ -182,7 +182,7 @@ private extension WooShippingCreateLabelsView { .padding(.horizontal) } .accessibilityHint(Localization.Accessibility.editButtonHint) - .renderedIf(viewModel.hasUnfulfilledShipments) + .renderedIf(viewModel.editSplitShipmentsOptionVisible) } .disabled(viewModel.isPurchasingLabel) } @@ -191,7 +191,7 @@ private extension WooShippingCreateLabelsView { var mainView: some View { ScrollView { VStack(spacing: Layout.verticalSpacing) { - if viewModel.splitShipmentsAvailable { + if viewModel.splitShipmentsRowVisible { WooShippingSplitShipmentsRow(onShowingSplitShipments: { showingSplitShipments = true }) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift index 2c4f19b240c..b4ddef0f290 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift @@ -80,12 +80,6 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { /// View model for split shipments. private(set) var splitShipmentsViewModel: WooShippingSplitShipmentsViewModel - var splitShipmentsAvailable: Bool { - itemsDataSource.items.map(\.quantity).reduce(0, +) > 1 && - shipments.count == 1 && - canViewLabel == false - } - private(set) var shipmentDetailViewModels: [WooShippingShipmentDetailsViewModel] = [] var currentShipmentDetailsViewModel: WooShippingShipmentDetailsViewModel { @@ -241,6 +235,10 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { ) }() + /// Provides checks for CIAB + /// Used to determine "Split Shipments" feature availability + private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol + /// Initialize the view model with or without an existing shipping label. init(order: Order, preselection: WooShippingCreateLabelSelection? = nil, @@ -249,10 +247,15 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { stores: StoresManager = ServiceLocator.stores, storageManager: StorageManagerType = ServiceLocator.storageManager, analytics: Analytics = ServiceLocator.analytics, + siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker(), initialNoticeDelay: RunLoop.SchedulerTimeType.Stride = .seconds(2), onLabelPurchase: ((Bool) -> Void)? = nil) { self.order = order - self.itemsDataSource = DefaultWooShippingItemsDataSource(order: order) + self.itemsDataSource = DefaultWooShippingItemsDataSource( + order: order, + storageManager: storageManager, + stores: stores + ) self.orderItems = WooShippingItemsViewModel(dataSource: itemsDataSource) self.onLabelPurchase = onLabelPurchase self.currencySettings = currencySettings @@ -261,6 +264,7 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { self.stores = stores self.storageManager = storageManager self.analytics = analytics + self.siteCIABEligibilityChecker = siteCIABEligibilityChecker self.shippingSettingsService = shippingSettingsService self.weightUnit = shippingSettingsService.weightUnit ?? "" self.dimensionsUnit = shippingSettingsService.dimensionUnit ?? "" @@ -504,6 +508,33 @@ private extension WooShippingCreateLabelsViewModel { } } +// MARK: Split Shipments +// Accessors describing Split Shipments elements availability +extension WooShippingCreateLabelsViewModel { + private var hasMultipleProducts: Bool { + return itemsDataSource.items.map(\.quantity).reduce(0, +) > 1 + } + + private var splitShipmentsFeatureAvailable: Bool { + return siteCIABEligibilityChecker.isFeatureSupportedForCurrentSite(.splitShipments) + } + + /// Determines if the "Edit split shipments" (pencil icon) is visible in top shipments bar. + var editSplitShipmentsOptionVisible: Bool { + splitShipmentsFeatureAvailable && + hasMultipleProducts && + hasUnfulfilledShipments + } + + /// Determines if the "Split Shipments" row is visible above the "Products" section + var splitShipmentsRowVisible: Bool { + splitShipmentsFeatureAvailable && + hasMultipleProducts && + shipments.count == 1 && + canViewLabel == false + } +} + // MARK: Utils private extension WooShippingCreateLabelsViewModel { diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 3649e8afdc3..d2924ca63cb 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1245,6 +1245,8 @@ 2DB8916E2E27F7840001B175 /* OrderListCellViewModel+Localizations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB8916A2E27F6CE0001B175 /* OrderListCellViewModel+Localizations.swift */; }; 2DCB54FA2E6AE8E100621F90 /* CIABEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB54F92E6AE8D800621F90 /* CIABEligibilityChecker.swift */; }; 2DCB54FC2E6AFE6A00621F90 /* CIABAffectedFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB54FB2E6AFE6900621F90 /* CIABAffectedFeature.swift */; }; + 2DE9DDFB2E6EF4A500155408 /* MockCIABEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE9DDFA2E6EF4A300155408 /* MockCIABEligibilityChecker.swift */; }; + 2DE9DDFD2E6EF53C00155408 /* CIABEligibilityCheckerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE9DDFC2E6EF52E00155408 /* CIABEligibilityCheckerProtocol.swift */; }; 2DF0D1BC2E2907C100F8995C /* MarkOrderAsReadUseCase+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB88DA32E27DD790001B175 /* MarkOrderAsReadUseCase+Woo.swift */; }; 310D1B482734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310D1B472734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift */; }; 311237EE2714DA240033C44E /* CardPresentModalDisplayMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311237ED2714DA240033C44E /* CardPresentModalDisplayMessage.swift */; }; @@ -4434,6 +4436,8 @@ 2DB8916A2E27F6CE0001B175 /* OrderListCellViewModel+Localizations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderListCellViewModel+Localizations.swift"; sourceTree = ""; }; 2DCB54F92E6AE8D800621F90 /* CIABEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityChecker.swift; sourceTree = ""; }; 2DCB54FB2E6AFE6900621F90 /* CIABAffectedFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABAffectedFeature.swift; sourceTree = ""; }; + 2DE9DDFA2E6EF4A300155408 /* MockCIABEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCIABEligibilityChecker.swift; sourceTree = ""; }; + 2DE9DDFC2E6EF52E00155408 /* CIABEligibilityCheckerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityCheckerProtocol.swift; sourceTree = ""; }; 310D1B472734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsLiveSiteInTestModeView.swift; sourceTree = ""; }; 311237ED2714DA240033C44E /* CardPresentModalDisplayMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalDisplayMessage.swift; sourceTree = ""; }; 311D21E7264AEDB900102316 /* CardPresentModalScanningForReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalScanningForReader.swift; sourceTree = ""; }; @@ -9058,6 +9062,7 @@ 2DCB54F82E6AE8C900621F90 /* CIAB */ = { isa = PBXGroup; children = ( + 2DE9DDFC2E6EF52E00155408 /* CIABEligibilityCheckerProtocol.swift */, 2DCB54FB2E6AFE6900621F90 /* CIABAffectedFeature.swift */, 2DCB54F92E6AE8D800621F90 /* CIABEligibilityChecker.swift */, ); @@ -10093,6 +10098,7 @@ 746791642108D853007CF1DC /* Mocks */ = { isa = PBXGroup; children = ( + 2DE9DDFA2E6EF4A300155408 /* MockCIABEligibilityChecker.swift */, 022900892E3019020028F6D7 /* MockPluginsService.swift */, 02F36C3F2E0130E900DD8CB6 /* MockPOSEligibilityService.swift */, 02B8E41A2DFBC33C001D01FD /* MockPOSEligibilityChecker.swift */, @@ -16257,6 +16263,7 @@ B626C71B287659D60083820C /* CustomFieldsListView.swift in Sources */, 02ECD1E624FFB4E900735BE5 /* ProductFactory.swift in Sources */, 260520F42B87BA23005D5D59 /* WooAnalyticsEvent+ConnectivityTool.swift in Sources */, + 2DE9DDFD2E6EF53C00155408 /* CIABEligibilityCheckerProtocol.swift in Sources */, 579CDEFF274D7E7900E8903D /* StoreStatsUsageTracksEventEmitter.swift in Sources */, 203163AD2C1C5C54001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedNonRetryableAlertViewModel.swift in Sources */, CEDBDA472B6BEF2E002047D4 /* AnalyticsWebReport.swift in Sources */, @@ -17150,6 +17157,7 @@ EE8B421B2C04D18B0077C4E7 /* LastOrdersDashboardCardViewModelTests.swift in Sources */, 095A077E27CF486C007A61D2 /* ValueOneTableViewCellTests.swift in Sources */, E17E3BF9266917C10009D977 /* CardPresentModalScanningFailedTests.swift in Sources */, + 2DE9DDFB2E6EF4A500155408 /* MockCIABEligibilityChecker.swift in Sources */, DE66C56F2978F24200DAA978 /* ApplicationPasswordDisabledViewModelTests.swift in Sources */, EE1905942B62AE9100617C53 /* BlazeCreateCampaignIntroViewModelTests.swift in Sources */, 269098B627D2C09D001FEB07 /* ShippingInputTransformerTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift b/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift new file mode 100644 index 00000000000..059e4c0d37c --- /dev/null +++ b/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift @@ -0,0 +1,31 @@ +import Foundation +import Yosemite +@testable import WooCommerce + +final class MockCIABEligibilityChecker: CIABEligibilityCheckerProtocol { + private let mockedIsCurrentSiteCIAB: Bool + private let mockedCIABSites: [Site] + private let mockedCIABDisabledFeatures: [CIABAffectedFeature] + + init(mockedIsCurrentSiteCIAB: Bool, mockedCIABSites: [Site] = [], mockedCIABDisabledFeatures: [CIABAffectedFeature] = CIABAffectedFeature.allCases) { + self.mockedIsCurrentSiteCIAB = mockedIsCurrentSiteCIAB + self.mockedCIABSites = mockedCIABSites + self.mockedCIABDisabledFeatures = mockedCIABDisabledFeatures + } + + var isCurrentSiteCIAB: Bool { + return mockedIsCurrentSiteCIAB + } + + func isSiteCIAB(_ site: Site) -> Bool { + return mockedCIABSites.contains(site) + } + + func isFeatureSupportedForCurrentSite(_ feature: CIABAffectedFeature) -> Bool { + return !mockedCIABDisabledFeatures.contains(feature) + } + + func isFeatureSupported(_ feature: CIABAffectedFeature, for site: Site) -> Bool { + return !mockedCIABDisabledFeatures.contains(feature) && mockedCIABSites.contains(site) + } +} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift index 2549abd298c..d8d71411b3d 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift @@ -1038,6 +1038,490 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { XCTAssertTrue(originAddressesLoaded, "Origin addresses should be loaded from remote when missing") XCTAssertFalse(viewModel.originAddress.isEmpty, "Origin address should be set from loaded data") } + + // MARK: - split shipments feature visibility tests + + private func initialConfigurationForSplitShipmentsTest(stores: MockStoresManager) { + stores.whenReceivingAction(ofType: WooShippingAction.self) { action in + switch action { + case .verifyDestinationAddress(_, _, let completion): + completion( + .success( + WooShippingVerifyDestinationAddressSuccess( + normalizedAddress: WooShippingNormalizedAddress.fake(), + isTrivialNormalization: nil, + isVerified: true + ) + ) + ) + case .loadAccountSettings(_, let completion): + completion(.success(self.settings)) + case .loadOriginAddresses(_, let completion): + let originAddress = WooShippingOriginAddress.fake().copy(address1: "123 Main Street", defaultAddress: true) + completion(.success([originAddress])) + default: + XCTFail("Unexpected action: \(action)") + } + } + } + + func test_splitShipmentsRowVisible_is_false_when_split_shipments_feature_is_unavailable() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Is CIAB site with the `.splitShipments` disabled + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: true, + mockedCIABDisabledFeatures: [.splitShipments] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID), + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.splitShipmentsRowVisible) + } + + func test_splitShipmentsRowVisible_is_false_when_only_single_product_item_exists() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Single product + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + storageManager.insertSampleProduct(readOnlyProduct: product1) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.splitShipmentsRowVisible) + } + + func test_splitShipmentsRowVisible_is_false_when_order_has_multiple_shipments() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Multiple shipments + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID) + ]), + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_1", + items: [ + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.splitShipmentsRowVisible) + } + + func test_splitShipmentsRowVisible_is_true_when_requirements_met() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site (feature available) + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID), + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertTrue(viewModel.splitShipmentsRowVisible) + } + + func test_editSplitShipmentsOptionVisible_is_false_when_split_shipments_feature_is_unavailable() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Is CIAB site with the `.splitShipments` disabled + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: true, + mockedCIABDisabledFeatures: [.splitShipments] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID), + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.editSplitShipmentsOptionVisible) + } + + func test_editSplitShipmentsOptionVisible_is_false_when_only_single_product_item_exists() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site (split shipments available) + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Single product + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + storageManager.insertSampleProduct(readOnlyProduct: product1) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.editSplitShipmentsOptionVisible) + } + + func test_editSplitShipmentsOptionVisible_is_false_when_all_shipments_fulfilled() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site (split shipments available) + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Each shipment is fulfilled + let shippingLabel1 = ShippingLabel.fake().copy(siteID: siteID, orderID: orderID, shippingLabelID: 134) + let shippingLabel2 = ShippingLabel.fake().copy(siteID: siteID, orderID: orderID, shippingLabelID: 245) + + /// 2 fulfilled shipments + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID) + ], + shippingLabel: shippingLabel1 + ), + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_1", + items: [ + .fake().copy(id: product2.productID) + ], + shippingLabel: shippingLabel2 + ) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.editSplitShipmentsOptionVisible) + } + + func test_editSplitShipmentsOptionVisible_is_true_when_all_requirements_met() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site (split shipments available) + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID), + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertTrue(viewModel.editSplitShipmentsOptionVisible) + } } private extension WooShippingCreateLabelsViewModelTests { @@ -1057,6 +1541,25 @@ private extension WooShippingCreateLabelsViewModelTests { return mapGeneralSettings(from: "settings-general") } + func setupInitialDataLoadingMocks( + for stores: MockStoresManager, + originAddressesResult: Result<[WooShippingOriginAddress], Error> = .success([.fake().copy(id: "default", defaultAddress: true)]), + accountSettingsResult: Result = .success(.fake()) + ) { + stores.whenReceivingAction(ofType: WooShippingAction.self) { action in + switch action { + case .loadOriginAddresses(_, let completion): + completion(originAddressesResult) + case .loadAccountSettings(_, let completion): + completion(accountSettingsResult) + case .loadPackages, .verifyDestinationAddress: + break // Ignored for these tests + default: + XCTFail("Unexpected action: \(action)") + } + } + } + func insert(shipments: [WooShippingShipment], order: Order) { let storageOrder = storage.insertNewObject(ofType: StorageOrder.self) storageOrder.update(with: order) @@ -1091,6 +1594,7 @@ private extension WooShippingCreateLabelsViewModelTests { func insert(originAddress: WooShippingOriginAddress) { let storageAddress = storage.insertNewObject(ofType: StorageWooShippingOriginAddress.self) storageAddress.update(with: originAddress) + } func insert(accountSettings: ShippingLabelAccountSettings) { @@ -1098,3 +1602,10 @@ private extension WooShippingCreateLabelsViewModelTests { storageSettings.update(with: accountSettings) } } + +private extension WooShippingAccountSettings { + static func fake() -> WooShippingAccountSettings { + .init(storeOptions: .init(currencySymbol: "$", dimensionUnit: "in", weightUnit: "lbs", originCountry: "US"), + accountSettings: .fake()) + } +}