-
Notifications
You must be signed in to change notification settings - Fork 121
[POS Settings] Store section and PointOfSaleSettingsService
#16034
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
Changes from all commits
059bcd9
433a5d8
4713200
af3b76e
4201416
7b66d8f
46035e4
e49dad7
4bb9542
03834df
8a02151
fe32ce4
e19018b
c3c2ca0
8f91b68
a626d12
b102193
f3de3be
9bedfa1
50c3efe
e2e84b2
72bed3b
ae533c5
3b82a84
61728b2
0398758
2e5c028
d036798
b7b3bee
cf78712
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 == "[email protected]") | ||
|
|
||
| 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: "[email protected]", | ||
| 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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 } | ||
iamgabrielma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe the site ID can be DI'ed to the controller as well? then
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think so 🤔 I've logged this one separately for now: WOOMOB-1188 |
||
| 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) | ||
iamgabrielma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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? = "[email protected]" | ||
| 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: all the content of the protocol feels like specific to the store settings section
PointOfSaleSettingsStoreDetailView, instead of the whole settings. For example,isLoadingis just for the store section, but being in the settings controller makes it like it's the loading state for some global state in the settings.Maybe we can keep the POS settings controller, as there could be other global states in the settings later, and a separate
@Observableview model like class to provide the following interface forPointOfSaleSettingsStoreDetailView?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I've added a note on WOOMOB-1188 so signature and dependencies can be adjusted in one go 👍