diff --git a/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift b/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift new file mode 100644 index 00000000000..3db603653cd --- /dev/null +++ b/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift @@ -0,0 +1,31 @@ +import Foundation +import Networking +import Storage + +public protocol PointOfSaleSettingsServiceProtocol { + var siteID: Int64 { get } + func retrievePointOfSaleSettings() async throws -> [SiteSetting] +} + +public final class PointOfSaleSettingsService: PointOfSaleSettingsServiceProtocol { + public let siteID: Int64 + private let settingStoreMethods: SettingStoreMethodsProtocol + + init(siteID: Int64, + settingStoreMethods: SettingStoreMethodsProtocol) { + self.siteID = siteID + self.settingStoreMethods = settingStoreMethods + } + + public convenience init(siteID: Int64, + credentials: Credentials?, + storage: StorageManagerType) { + let network = AlamofireNetwork(credentials: credentials) + self.init(siteID: siteID, settingStoreMethods: SettingStoreMethods(storageManager: storage, + network: network)) + } + + public func retrievePointOfSaleSettings() async throws -> [SiteSetting] { + return try await settingStoreMethods.retrievePointOfSaleSettings(siteID: siteID) + } +} diff --git a/Modules/Tests/YosemiteTests/Mocks/MockSettingStoreMethods.swift b/Modules/Tests/YosemiteTests/Mocks/MockSettingStoreMethods.swift index c204645d90b..16de26a26de 100644 --- a/Modules/Tests/YosemiteTests/Mocks/MockSettingStoreMethods.swift +++ b/Modules/Tests/YosemiteTests/Mocks/MockSettingStoreMethods.swift @@ -9,9 +9,12 @@ final class MockSettingStoreMethods: SettingStoreMethodsProtocol { var retrieveAnalyticsSettingCalled = false var enableAnalyticsSettingCalled = false var retrieveTaxBasedOnSettingCalled = false + var retrievePointOfSaleSettingsCalled = false var couponsEnabled: Bool = true var featureEnabled: Result = .success(true) + var retrievePointOfSaleSettingsResult: Result<[SiteSetting], Error> = .success([]) + var retrievePointOfSaleSettingsSiteID: Int64? func synchronizeGeneralSiteSettings(siteID: Int64, onCompletion: @escaping (Error?) -> Void) { @@ -74,6 +77,13 @@ final class MockSettingStoreMethods: SettingStoreMethodsProtocol { } func retrievePointOfSaleSettings(siteID: Int64) async throws -> [SiteSetting] { - [] + retrievePointOfSaleSettingsCalled = true + retrievePointOfSaleSettingsSiteID = siteID + switch retrievePointOfSaleSettingsResult { + case .success(let settings): + return settings + case .failure(let error): + throw error + } } } diff --git a/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleSettingsServiceTests.swift b/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleSettingsServiceTests.swift new file mode 100644 index 00000000000..88acef472ad --- /dev/null +++ b/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleSettingsServiceTests.swift @@ -0,0 +1,135 @@ +import Foundation +import Testing +@testable import Yosemite + +struct PointOfSaleSettingsServiceTests { + private let sut: PointOfSaleSettingsService + private let settingStoreMethods: MockSettingStoreMethods + private let storage: MockStorageManager + private let sampleSiteID: Int64 = 123 + + init() { + self.settingStoreMethods = MockSettingStoreMethods() + self.storage = MockStorageManager() + self.sut = PointOfSaleSettingsService(siteID: sampleSiteID, + settingStoreMethods: settingStoreMethods) + } + + @Test func retrievePointOfSaleSettings_when_empty_settings_then_returns_empty_array() async throws { + // Given + settingStoreMethods.retrievePointOfSaleSettingsResult = .success([]) + + // When + let settings = try await sut.retrievePointOfSaleSettings() + + // Then + #expect(settingStoreMethods.retrievePointOfSaleSettingsCalled) + #expect(settingStoreMethods.retrievePointOfSaleSettingsSiteID == sampleSiteID) + #expect(settings.isEmpty) + } + + @Test func retrievePointOfSaleSettings_when_network_error_then_throws_error() async throws { + // Given + let expectedError = NSError(domain: "NetworkError", code: 500, userInfo: [NSLocalizedDescriptionKey: "Network request failed"]) + settingStoreMethods.retrievePointOfSaleSettingsResult = .failure(expectedError) + + // When + do { + _ = try await sut.retrievePointOfSaleSettings() + #expect(Bool(false), "Expected error to be thrown") + } catch { + // Then + #expect(settingStoreMethods.retrievePointOfSaleSettingsCalled) + #expect(settingStoreMethods.retrievePointOfSaleSettingsSiteID == sampleSiteID) + let nsError = error as NSError + #expect(nsError.domain == expectedError.domain) + #expect(nsError.code == expectedError.code) + } + } + + @Test func retrievePointOfSaleSettings_when_settingStoreMethods_throws_then_propagates_error() async throws { + // Given + let expectedError = TestError.customError + settingStoreMethods.retrievePointOfSaleSettingsResult = .failure(expectedError) + + // When + do { + _ = try await sut.retrievePointOfSaleSettings() + #expect(Bool(false), "Expected error to be thrown") + } catch { + // Then + #expect(settingStoreMethods.retrievePointOfSaleSettingsCalled) + #expect(settingStoreMethods.retrievePointOfSaleSettingsSiteID == sampleSiteID) + #expect(error as? TestError == expectedError) + } + } + + @Test func retrievePointOfSaleSettings_with_expected_pos_settings_then_returns_all_settings() async throws { + // Given + let expectedSettings = makeSiteSettings() + settingStoreMethods.retrievePointOfSaleSettingsResult = .success(expectedSettings) + + // When + let settings = try await sut.retrievePointOfSaleSettings() + + // Then + #expect(settingStoreMethods.retrievePointOfSaleSettingsCalled) + #expect(settingStoreMethods.retrievePointOfSaleSettingsSiteID == sampleSiteID) + #expect(settings.count == 5) + #expect(settings == expectedSettings) + + let storeNameSetting = settings.first { $0.settingID == "woocommerce_pos_store_name" } + #expect(storeNameSetting?.value == "WooCommerce Store") + + let addressSetting = settings.first { $0.settingID == "woocommerce_pos_store_address" } + #expect(addressSetting?.value == "123 Commerce Street\nBusiness District") + + let phoneSetting = settings.first { $0.settingID == "woocommerce_pos_store_phone" } + #expect(phoneSetting?.value == "+1 (555) 123-4567") + + let emailSetting = settings.first { $0.settingID == "woocommerce_pos_store_email" } + #expect(emailSetting?.value == "contact@store.com") + + let policySetting = settings.first { $0.settingID == "woocommerce_pos_refund_returns_policy" } + #expect(policySetting?.value == "30-day return policy with receipt") + } + + private func makeSiteSettings() -> [SiteSetting] { + return [ + SiteSetting(siteID: sampleSiteID, + settingID: "woocommerce_pos_store_name", + label: "Store Name", + settingDescription: "Name of the store for POS receipts", + value: "WooCommerce Store", + settingGroupKey: "pos"), + SiteSetting(siteID: sampleSiteID, + settingID: "woocommerce_pos_store_address", + label: "Store Address", + settingDescription: "Physical address for receipts", + value: "123 Commerce Street\nBusiness District", + settingGroupKey: "pos"), + SiteSetting(siteID: sampleSiteID, + settingID: "woocommerce_pos_store_phone", + label: "Store Phone", + settingDescription: "Contact phone number", + value: "+1 (555) 123-4567", + settingGroupKey: "pos"), + SiteSetting(siteID: sampleSiteID, + settingID: "woocommerce_pos_store_email", + label: "Store Email", + settingDescription: "Contact email address", + value: "contact@store.com", + settingGroupKey: "pos"), + SiteSetting(siteID: sampleSiteID, + settingID: "woocommerce_pos_refund_returns_policy", + label: "Refund & Returns Policy", + settingDescription: "Store policy for refunds and returns", + value: "30-day return policy with receipt", + settingGroupKey: "pos") + ] + } +} + +private enum TestError: Error, Equatable { + case customError +} diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 874b81b2af0..8764976852b 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -70,6 +70,7 @@ protocol PointOfSaleAggregateModelProtocol { let popularPurchasableItemsController: PointOfSaleItemsControllerProtocol let couponsController: PointOfSaleCouponsControllerProtocol let couponsSearchController: PointOfSaleSearchingItemsControllerProtocol + let settingsController: PointOfSaleSettingsControllerProtocol private let cardPresentPaymentService: CardPresentPaymentFacade private let orderController: PointOfSaleOrderControllerProtocol @@ -105,6 +106,7 @@ protocol PointOfSaleAggregateModelProtocol { couponsSearchController: PointOfSaleSearchingItemsControllerProtocol, cardPresentPaymentService: CardPresentPaymentFacade, orderController: PointOfSaleOrderControllerProtocol, + settingsController: PointOfSaleSettingsControllerProtocol, analytics: Analytics = ServiceLocator.analytics, collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking, searchHistoryService: POSSearchHistoryProviding, @@ -119,6 +121,7 @@ protocol PointOfSaleAggregateModelProtocol { self.couponsSearchController = couponsSearchController self.cardPresentPaymentService = cardPresentPaymentService self.orderController = orderController + self.settingsController = settingsController self.analytics = analytics self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker self.searchHistoryService = searchHistoryService diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift b/WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift new file mode 100644 index 00000000000..b765fc1b959 --- /dev/null +++ b/WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift @@ -0,0 +1,139 @@ +import Foundation +import struct Yosemite.SiteSetting +import enum Yosemite.Plugin +import class Yosemite.PluginsService +import Observation + +import protocol Yosemite.PointOfSaleSettingsServiceProtocol +import class Yosemite.PointOfSaleSettingsService +import Storage + +protocol PointOfSaleSettingsControllerProtocol { + var receiptStoreName: String? { get } + var receiptStoreAddress: String? { get } + var receiptStorePhone: String? { get } + var receiptStoreEmail: String? { get } + var receiptRefundReturnsPolicy: String? { get } + var isLoading: Bool { get } + var shouldShowReceiptInformation: Bool { get } + var storeName: String { get } + var storeAddress: String { get } + + func retrievePOSReceiptSettings() async +} + +@Observable final class PointOfSaleSettingsController: PointOfSaleSettingsControllerProtocol { + private(set) var receiptStoreName: String? + private(set) var receiptStoreAddress: String? + private(set) var receiptStorePhone: String? + private(set) var receiptStoreEmail: String? + private(set) var receiptRefundReturnsPolicy: String? + private(set) var isLoading: Bool = false + private(set) var shouldShowReceiptInformation: Bool = false + + private let defaultSiteName: String? + private let settingsService: PointOfSaleSettingsServiceProtocol + private let siteSettings: [SiteSetting] + + init(settingsService: PointOfSaleSettingsServiceProtocol, + defaultSiteName: String? = ServiceLocator.stores.sessionManager.defaultSite?.name, + siteSettings: [SiteSetting] = ServiceLocator.selectedSiteSettings.siteSettings) { + self.settingsService = settingsService + self.defaultSiteName = defaultSiteName + self.siteSettings = siteSettings + } + + var storeName: String { + if let defaultSiteName { + return defaultSiteName + } else { + return Localization.storeNameNotSet + } + } + + var storeAddress: String { + SiteAddress(siteSettings: siteSettings).address + } + + @MainActor + func retrievePOSReceiptSettings() async { + isLoading = true + + shouldShowReceiptInformation = await isPluginSupported(.wooCommerce, minimumVersion: Constants.minimumWooCommerceVersion) + + guard shouldShowReceiptInformation else { + isLoading = false + return + } + + do { + let siteSettings = try await settingsService.retrievePointOfSaleSettings() + updateReceiptSettings(from: siteSettings) + } catch { + DDLogError("Failed to load POS settings: \(error)") + } + isLoading = false + } + + @MainActor + private func isPluginSupported(_ plugin: Plugin, + storageManager: StorageManagerType = ServiceLocator.storageManager, + minimumVersion: String) async -> Bool { + let pluginsService = PluginsService(storageManager: storageManager) + guard let systemPlugin = pluginsService.loadPluginInStorage(siteID: settingsService.siteID, plugin: plugin, isActive: true), systemPlugin.active else { + return false + } + + let isSupported = VersionHelpers.isVersionSupported(version: systemPlugin.version, + minimumRequired: minimumVersion) + return isSupported + } + + private func updateReceiptSettings(from siteSettings: [SiteSetting]) { + receiptStoreName = settingValue(from: siteSettings, settingID: "woocommerce_pos_store_name") + receiptStoreAddress = settingValue(from: siteSettings, settingID: "woocommerce_pos_store_address") + receiptStorePhone = settingValue(from: siteSettings, settingID: "woocommerce_pos_store_phone") + receiptStoreEmail = settingValue(from: siteSettings, settingID: "woocommerce_pos_store_email") + receiptRefundReturnsPolicy = settingValue(from: siteSettings, settingID: "woocommerce_pos_refund_returns_policy") + } + + private func settingValue(from siteSettings: [SiteSetting], settingID: String) -> String? { + let value = siteSettings.first { $0.settingID == settingID }?.value + return value?.isEmpty == true ? nil : value + } +} + +private extension PointOfSaleSettingsController { + enum Constants { + static let minimumWooCommerceVersion: String = "10.0" + } + + enum Localization { + static let storeNameNotSet = NSLocalizedString( + "pointOfSaleSettingsService.storeNameNotSet", + value: "Not set", + comment: "Text displayed on Point of Sale settings when store has not been provided." + ) + } +} + +#if DEBUG +final class PointOfSaleSettingsPreviewController: PointOfSaleSettingsControllerProtocol { + var receiptStoreName: String? = "Sample Store" + var receiptStoreAddress: String? = "123 Main Street\nAnytown, ST 12345" + var receiptStorePhone: String? = "+1 (555) 123-4567" + var receiptStoreEmail: String? = "store@example.com" + var receiptRefundReturnsPolicy: String? = "30-day return policy" + var isLoading: Bool = false + var shouldShowReceiptInformation: Bool = true + var storeName: String = "Sample Store" + + var storeAddress: String { + "123 Main Street\nAnytown, ST 12345" + } + + func retrievePOSReceiptSettings() async { + // no-op + } +} +#endif diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 59bde614fd5..c3220bbc68e 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -132,7 +132,7 @@ struct PointOfSaleDashboardView: View { documentationView } .posFullScreenCover(isPresented: $showSettings) { - PointOfSaleSettingsView() + PointOfSaleSettingsView(settingsController: posModel.settingsController) } .onChange(of: posModel.entryPointController.eligibilityState) { oldValue, newValue in guard newValue == .eligible else { return } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 4ddf855e72b..c3ebc36978e 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -17,6 +17,7 @@ struct PointOfSaleEntryPointView: View { private let couponsSearchController: PointOfSaleSearchingItemsControllerProtocol private let cardPresentPaymentService: CardPresentPaymentFacade private let orderController: PointOfSaleOrderControllerProtocol + private let settingsController: PointOfSaleSettingsControllerProtocol private let collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking private let searchHistoryService: POSSearchHistoryProviding private let popularPurchasableItemsController: PointOfSaleItemsControllerProtocol @@ -29,6 +30,7 @@ struct PointOfSaleEntryPointView: View { onPointOfSaleModeActiveStateChange: @escaping ((Bool) -> Void), cardPresentPaymentService: CardPresentPaymentFacade, orderController: PointOfSaleOrderControllerProtocol, + settingsController: PointOfSaleSettingsControllerProtocol, collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking, searchHistoryService: POSSearchHistoryProviding, popularPurchasableItemsController: PointOfSaleItemsControllerProtocol, @@ -42,6 +44,7 @@ struct PointOfSaleEntryPointView: View { self.couponsSearchController = couponsSearchController self.cardPresentPaymentService = cardPresentPaymentService self.orderController = orderController + self.settingsController = settingsController self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker self.searchHistoryService = searchHistoryService self.popularPurchasableItemsController = popularPurchasableItemsController @@ -70,6 +73,7 @@ struct PointOfSaleEntryPointView: View { couponsSearchController: couponsSearchController, cardPresentPaymentService: cardPresentPaymentService, orderController: orderController, + settingsController: settingsController, collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker, searchHistoryService: searchHistoryService, popularPurchasableItemsController: popularPurchasableItemsController, @@ -99,6 +103,7 @@ struct PointOfSaleEntryPointView: View { onPointOfSaleModeActiveStateChange: { _ in }, cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController(), + settingsController: PointOfSaleSettingsPreviewController(), collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentPreviewAnalytics(), searchHistoryService: PointOfSalePreviewHistoryService(), popularPurchasableItemsController: PointOfSalePreviewItemsController(), diff --git a/WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsStoreDetailView.swift b/WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsStoreDetailView.swift index 2192d786ce3..919ba1dd7c6 100644 --- a/WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsStoreDetailView.swift +++ b/WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsStoreDetailView.swift @@ -1,16 +1,131 @@ import SwiftUI struct PointOfSaleSettingsStoreDetailView: View { + let settingsController: PointOfSaleSettingsControllerProtocol + var body: some View { NavigationStack { VStack(alignment: .leading) { - Text("Store Settings") - .font(.title2) - Text("Store-related configuration") - .font(.caption) - .foregroundStyle(.secondary) + Group { + Text(Localization.storeInformation) + .font(.posBodyLargeRegular()) + + Text(Localization.storeName) + Text(settingsController.storeName) + .font(.posBodyMediumRegular()) + .foregroundStyle(.secondary) + + Text(Localization.address) + Text(settingsController.storeAddress) + .font(.posBodyMediumRegular()) + .foregroundStyle(.secondary) + } + + Group { + Spacer() + Text(Localization.receiptInformation) + .font(.posBodyLargeRegular()) + Text(Localization.receiptStoreName) + settingValueView(for: settingsController.receiptStoreName) + + Text(Localization.physicalAddress) + settingValueView(for: settingsController.receiptStoreAddress) + + Text(Localization.phoneNumber) + settingValueView(for: settingsController.receiptStorePhone) + + Text(Localization.email) + settingValueView(for: settingsController.receiptStoreEmail) + + Text(Localization.refundReturnsPolicy) + settingValueView(for: settingsController.receiptRefundReturnsPolicy) + + } + .renderedIf(settingsController.shouldShowReceiptInformation) } .padding() } } + + @ViewBuilder + private func settingValueView(for value: String?) -> some View { + if settingsController.isLoading { + ProgressView() + .font(.posBodyLargeRegular()) + } else { + Text(value ?? Localization.notSet) + .font(.posBodyMediumRegular()) + .foregroundStyle(.secondary) + } + } +} + +private extension PointOfSaleSettingsStoreDetailView { + enum Localization { + static let notSet = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.notSet", + value: "Not set", + comment: "Text displayed on Point of Sale settings when any setting has not been provided." + ) + + static let storeInformation = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.storeInformation", + value: "Store Information", + comment: "Section title for store information in Point of Sale settings." + ) + + static let storeName = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.storeName", + value: "Store name", + comment: "Label for store name field in Point of Sale settings." + ) + + static let address = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.address", + value: "Address", + comment: "Label for address field in Point of Sale settings." + ) + + static let receiptInformation = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.receiptInformation", + value: "Receipt Information", + comment: "Section title for receipt information in Point of Sale settings." + ) + + static let receiptStoreName = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.receiptStoreName", + value: "Store name", + comment: "Label for receipt store name field in Point of Sale settings." + ) + + static let physicalAddress = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.physicalAddress", + value: "Physical address", + comment: "Label for physical address field in Point of Sale settings." + ) + + static let phoneNumber = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.phoneNumber", + value: "Phone number", + comment: "Label for phone number field in Point of Sale settings." + ) + + static let email = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.email", + value: "Email", + comment: "Label for email field in Point of Sale settings." + ) + + static let refundReturnsPolicy = NSLocalizedString( + "pointOfSaleSettingsStoreDetailView.refundReturnsPolicy", + value: "Refund & Returns Policy", + comment: "Label for refund and returns policy field in Point of Sale settings." + ) + } +} + +#if DEBUG +#Preview { + PointOfSaleSettingsStoreDetailView(settingsController: PointOfSaleSettingsPreviewController()) } +#endif diff --git a/WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsView.swift b/WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsView.swift index cea2e2a5b31..553eee313a6 100644 --- a/WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsView.swift +++ b/WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsView.swift @@ -4,6 +4,8 @@ struct PointOfSaleSettingsView: View { @Environment(\.dismiss) private var dismiss @State private var selection: SidebarNavigation? = .store + let settingsController: PointOfSaleSettingsControllerProtocol + var body: some View { POSPageHeaderView( title: Localization.navigationTitle, @@ -69,6 +71,9 @@ struct PointOfSaleSettingsView: View { detailView .frame(maxWidth: .infinity, maxHeight: .infinity) } + .task { + await settingsController.retrievePOSReceiptSettings() + } } } } @@ -78,7 +83,7 @@ extension PointOfSaleSettingsView { private var detailView: some View { switch selection { case .store: - PointOfSaleSettingsStoreDetailView() + PointOfSaleSettingsStoreDetailView(settingsController: settingsController) case .hardware: PointOfSaleSettingsHardwareDetailView() case .help: @@ -173,6 +178,8 @@ private extension PointOfSaleSettingsView { } } +#if DEBUG #Preview { - PointOfSaleSettingsView() + PointOfSaleSettingsView(settingsController: PointOfSaleSettingsPreviewController()) } +#endif diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 23492bdb61b..96548fcd034 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -92,6 +92,9 @@ private extension POSTabCoordinator { let cardPresentPaymentService = await CardPresentPaymentService(siteID: siteID, stores: storesManager, collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker) + let settingsService = PointOfSaleSettingsService(siteID: siteID, + credentials: credentials, + storage: storageManager) if let receiptService = POSReceiptService(siteID: siteID, credentials: credentials), let orderService = POSOrderService(siteID: siteID, @@ -118,6 +121,7 @@ private extension POSTabCoordinator { cardPresentPaymentService: cardPresentPaymentService, orderController: PointOfSaleOrderController(orderService: orderService, receiptService: receiptService), + settingsController: PointOfSaleSettingsController(settingsService: settingsService), collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker, searchHistoryService: POSSearchHistoryService(siteID: siteID), popularPurchasableItemsController: PointOfSaleItemsController( diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index 75b9da9a015..21a75d083f3 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -209,6 +209,7 @@ struct POSPreviewHelpers { couponsSearchController: PointOfSaleCouponsControllerProtocol = PointOfSalePreviewCouponsController(), cardPresentPaymentService: CardPresentPaymentFacade = CardPresentPaymentPreviewService(), orderController: PointOfSaleOrderControllerProtocol = PointOfSalePreviewOrderController(), + settingsController: PointOfSaleSettingsControllerProtocol = PointOfSaleSettingsPreviewController(), collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking = POSCollectOrderPaymentPreviewAnalytics(), searchHistoryService: POSSearchHistoryProviding = PointOfSalePreviewHistoryService(), popularItemsController: PointOfSaleItemsControllerProtocol = PointOfSalePreviewItemsController(), @@ -222,6 +223,7 @@ struct POSPreviewHelpers { couponsSearchController: couponsSearchController, cardPresentPaymentService: cardPresentPaymentService, orderController: orderController, + settingsController: settingsController, collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker, searchHistoryService: searchHistoryService, popularPurchasableItemsController: popularItemsController, diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index a88c968a987..13771c2125c 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1634,6 +1634,7 @@ 68D23B5B2E14FD1C00316BA6 /* SummaryTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D23B5A2E14FD1A00316BA6 /* SummaryTableViewCellViewModel.swift */; }; 68D3E98D2C7C371B005B6278 /* POSEdgeShadowViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D3E98C2C7C371B005B6278 /* POSEdgeShadowViewModifier.swift */; }; 68D5094E2AD39BC900B6FFD5 /* DiscountLineDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D5094D2AD39BC900B6FFD5 /* DiscountLineDetailsView.swift */; }; + 68D748102E5DB6D40048CFE9 /* PointOfSaleSettingsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D7480F2E5DB6D20048CFE9 /* PointOfSaleSettingsControllerTests.swift */; }; 68D8FBD12BFEF9C700477C42 /* TotalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D8FBD02BFEF9C700477C42 /* TotalsView.swift */; }; 68DF5A8D2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DF5A8C2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift */; }; 68DF5A8F2CB38F20000154C9 /* OrderCouponSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DF5A8E2CB38F20000154C9 /* OrderCouponSectionView.swift */; }; @@ -1651,6 +1652,7 @@ 68E952CC287536010095A23D /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E952CB287536010095A23D /* SafariView.swift */; }; 68E952D0287587BF0095A23D /* CardReaderManualRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E952CF287587BF0095A23D /* CardReaderManualRowView.swift */; }; 68E952D22875A44B0095A23D /* CardReaderType+Manual.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E952D12875A44B0095A23D /* CardReaderType+Manual.swift */; }; + 68E9F7012E5C499200D45747 /* PointOfSaleSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E9F7002E5C499000D45747 /* PointOfSaleSettingsController.swift */; }; 68ED2BD62ADD2C8C00ECA88D /* LineDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68ED2BD52ADD2C8C00ECA88D /* LineDetailView.swift */; }; 68F151E12C0DA7910082AEC8 /* Cart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68F151E02C0DA7910082AEC8 /* Cart.swift */; }; 68F68A502D6730E200BB9568 /* POSCollectOrderPaymentAnalyticsTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68F68A4F2D6730DF00BB9568 /* POSCollectOrderPaymentAnalyticsTracking.swift */; }; @@ -4813,6 +4815,7 @@ 68D23B5A2E14FD1A00316BA6 /* SummaryTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryTableViewCellViewModel.swift; sourceTree = ""; }; 68D3E98C2C7C371B005B6278 /* POSEdgeShadowViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSEdgeShadowViewModifier.swift; sourceTree = ""; }; 68D5094D2AD39BC900B6FFD5 /* DiscountLineDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscountLineDetailsView.swift; sourceTree = ""; }; + 68D7480F2E5DB6D20048CFE9 /* PointOfSaleSettingsControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleSettingsControllerTests.swift; sourceTree = ""; }; 68D8FBD02BFEF9C700477C42 /* TotalsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalsView.swift; sourceTree = ""; }; 68DF5A8C2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableOrderCouponLineViewModel.swift; sourceTree = ""; }; 68DF5A8E2CB38F20000154C9 /* OrderCouponSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCouponSectionView.swift; sourceTree = ""; }; @@ -4830,6 +4833,7 @@ 68E952CB287536010095A23D /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; 68E952CF287587BF0095A23D /* CardReaderManualRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualRowView.swift; sourceTree = ""; }; 68E952D12875A44B0095A23D /* CardReaderType+Manual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CardReaderType+Manual.swift"; sourceTree = ""; }; + 68E9F7002E5C499000D45747 /* PointOfSaleSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleSettingsController.swift; sourceTree = ""; }; 68ED2BD52ADD2C8C00ECA88D /* LineDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineDetailView.swift; sourceTree = ""; }; 68F151E02C0DA7910082AEC8 /* Cart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cart.swift; sourceTree = ""; }; 68F68A4F2D6730DF00BB9568 /* POSCollectOrderPaymentAnalyticsTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCollectOrderPaymentAnalyticsTracking.swift; sourceTree = ""; }; @@ -8187,6 +8191,7 @@ children = ( 6818E7C02D93C76200677C16 /* PointOfSaleCouponsControllerTests.swift */, 20DB185C2CF5E7560018D3E1 /* PointOfSaleOrderControllerTests.swift */, + 68D7480F2E5DB6D20048CFE9 /* PointOfSaleSettingsControllerTests.swift */, 200BA15D2CF0A9EB0006DC5B /* PointOfSaleItemsControllerTests.swift */, 02E4F26F2E0F2C75003A31E7 /* POSEntryPointControllerTests.swift */, ); @@ -9983,6 +9988,7 @@ children = ( 01B3A1F12DB6D48800286B7F /* ItemListType.swift */, 20FCBCDC2CE223340082DCA3 /* PointOfSaleAggregateModel.swift */, + 68E9F7002E5C499000D45747 /* PointOfSaleSettingsController.swift */, 209ECA802DB8FC280089F3D2 /* PointOfSaleViewStateCoordinator.swift */, 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */, 20F7B12C2D12C7B900C08193 /* ItemsContainerState.swift */, @@ -15596,6 +15602,7 @@ CE21B3D720FE669A00A259D5 /* BasicTableViewCell.swift in Sources */, 451A04EA2386D28300E368C9 /* ProductImagesHeaderViewModel.swift in Sources */, 02307924258731B2008EADEE /* PrintShippingLabelViewModel.swift in Sources */, + 68E9F7012E5C499200D45747 /* PointOfSaleSettingsController.swift in Sources */, D843D5D92248EE91001BFA55 /* ManualTrackingViewModel.swift in Sources */, 20886D3D2D96E0F900F7AE03 /* PointOfSaleCardPresentPaymentConnectingFailedLocationRequiredAlertViewModel.swift in Sources */, 204CB8102C10BB88000C9773 /* CardPresentPaymentPreviewService.swift in Sources */, @@ -17258,6 +17265,7 @@ DED039272BC7934F005D0571 /* StoreStatsChartViewModelTests.swift in Sources */, 2024966A2B0CC97100EE527D /* MockWooPaymentsDepositService.swift in Sources */, DE61978D289A5326005E4362 /* WooSetupWebViewModelTests.swift in Sources */, + 68D748102E5DB6D40048CFE9 /* PointOfSaleSettingsControllerTests.swift in Sources */, DE7B17F92C1AA26B00A6C7D8 /* MockWooSubscriptionProductsEligibilityChecker.swift in Sources */, DE02ABAF2B5545C8008E0AC4 /* BlazeTargetLocationPickerViewModelTests.swift in Sources */, D802548726552E07001B2CC1 /* CardPresentModalNonRetryableErrorTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleSettingsControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleSettingsControllerTests.swift new file mode 100644 index 00000000000..d639dcf2792 --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleSettingsControllerTests.swift @@ -0,0 +1,111 @@ + +import Testing +import Foundation +@testable import WooCommerce +@testable import Yosemite +import Storage + +// TODO: Expand controller tests in WOOMOB-1176 +struct PointOfSaleSettingsControllerTests { + private let mockSettingsService = MockPointOfSaleSettingsService() + private let mockStorageManager = MockStorageManager() + + @Test func storeName_when_defaultSiteName_provided_then_returns_defaultSiteName() async throws { + // Given + let expectedStoreName = "My Test Store" + let sut = PointOfSaleSettingsController(settingsService: mockSettingsService, + defaultSiteName: expectedStoreName, + siteSettings: []) + + // When + let actualStoreName = sut.storeName + + // Then + #expect(actualStoreName == expectedStoreName) + } + + @Test func storeName_when_defaultSiteName_nil_then_returns_notSet() async throws { + // Given + let sut = PointOfSaleSettingsController(settingsService: mockSettingsService, + defaultSiteName: nil, + siteSettings: []) + + // When + let actualStoreName = sut.storeName + + // Then + #expect(actualStoreName == "Not set") + } + + @Test func storeAddress_uses_injected_siteSettings() async throws { + // Given + let siteSettings = makeSampleSiteSettings() + let sut = PointOfSaleSettingsController(settingsService: mockSettingsService, + defaultSiteName: "Test Store", + siteSettings: siteSettings) + + // When + let storeAddress = sut.storeAddress + + // Then: address should be constructed from site settings, not empty + #expect(!storeAddress.isEmpty) + } + + private func makeSampleSiteSettings() -> [Yosemite.SiteSetting] { + return [ + SiteSetting(siteID: 123, + settingID: "woocommerce_store_address", + label: "Address", + settingDescription: "", + value: "123 Test Street", + settingGroupKey: "general"), + SiteSetting(siteID: 123, + settingID: "woocommerce_store_city", + label: "City", + settingDescription: "", + value: "Test City", + settingGroupKey: "general"), + SiteSetting(siteID: 123, + settingID: "woocommerce_default_country", + label: "Country", + settingDescription: "", + value: "US:CA", + settingGroupKey: "general") + ] + } +} + +private final class MockPointOfSaleSettingsService: PointOfSaleSettingsServiceProtocol { + var retrievePointOfSaleSettingsWasCalled = false + var retrievePointOfSaleSettingsResult: Result<[Yosemite.SiteSetting], Error> = .success([]) + let siteID: Int64 = 123 + + func retrievePointOfSaleSettings() async throws -> [Yosemite.SiteSetting] { + retrievePointOfSaleSettingsWasCalled = true + switch retrievePointOfSaleSettingsResult { + case .success(let settings): + return settings + case .failure(let error): + throw error + } + } +} + +final class MockPointOfSaleSettingsController: PointOfSaleSettingsControllerProtocol { + var receiptStoreName: String? = "Sample Store" + var receiptStoreAddress: String? = "123 Main Street\nAnytown, ST 12345" + var receiptStorePhone: String? = "+1 (555) 123-4567" + var receiptStoreEmail: String? = "store@example.com" + var receiptRefundReturnsPolicy: String? = "30-day return policy" + var isLoading: Bool = false + var shouldShowReceiptInformation: Bool = true + var storeName: String = "Sample Store" + + var storeAddress: String { + "123 Main Street\nAnytown, ST 12345" + } + + func retrievePOSReceiptSettings() async { + // no-op + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index feb0af114a1..ec9f4bbbbb7 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -967,6 +967,7 @@ private func makePointOfSaleAggregateModel( couponsSearchController: PointOfSaleSearchingItemsControllerProtocol = MockPointOfSaleCouponsController(), cardPresentPaymentService: CardPresentPaymentFacade = MockCardPresentPaymentService(), orderController: PointOfSaleOrderControllerProtocol = MockPointOfSaleOrderController(), + settingsController: PointOfSaleSettingsControllerProtocol = MockPointOfSaleSettingsController(), analytics: Analytics = WooAnalytics(analyticsProvider: MockAnalyticsProvider()), collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking = MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: POSSearchHistoryProviding = MockPOSSearchHistoryService(), @@ -983,6 +984,7 @@ private func makePointOfSaleAggregateModel( couponsSearchController: couponsSearchController, cardPresentPaymentService: cardPresentPaymentService, orderController: orderController, + settingsController: settingsController, analytics: analytics, collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker, searchHistoryService: searchHistoryService, diff --git a/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift index e21a762d00a..956b1049c05 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift @@ -101,6 +101,7 @@ private func makePointOfSaleAggregateModel( couponsSearchController: PointOfSaleSearchingItemsControllerProtocol = MockPointOfSaleCouponsController(), cardPresentPaymentService: CardPresentPaymentFacade = MockCardPresentPaymentService(), orderController: PointOfSaleOrderControllerProtocol = MockPointOfSaleOrderController(), + settingsController: PointOfSaleSettingsControllerProtocol = MockPointOfSaleSettingsController(), collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking = MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: POSSearchHistoryProviding = MockPOSSearchHistoryService(), popularPurchasableItemsController: PointOfSaleItemsControllerProtocol = MockPointOfSaleItemsController(), @@ -114,6 +115,7 @@ private func makePointOfSaleAggregateModel( couponsSearchController: couponsSearchController, cardPresentPaymentService: cardPresentPaymentService, orderController: orderController, + settingsController: settingsController, collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker, searchHistoryService: searchHistoryService, popularPurchasableItemsController: popularPurchasableItemsController,