Skip to content

Commit 722d258

Browse files
authored
[POS Settings] Store section and PointOfSaleSettingsService (#16034)
2 parents 7e36659 + cf78712 commit 722d258

File tree

15 files changed

+583
-9
lines changed

15 files changed

+583
-9
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Foundation
2+
import Networking
3+
import Storage
4+
5+
public protocol PointOfSaleSettingsServiceProtocol {
6+
var siteID: Int64 { get }
7+
func retrievePointOfSaleSettings() async throws -> [SiteSetting]
8+
}
9+
10+
public final class PointOfSaleSettingsService: PointOfSaleSettingsServiceProtocol {
11+
public let siteID: Int64
12+
private let settingStoreMethods: SettingStoreMethodsProtocol
13+
14+
init(siteID: Int64,
15+
settingStoreMethods: SettingStoreMethodsProtocol) {
16+
self.siteID = siteID
17+
self.settingStoreMethods = settingStoreMethods
18+
}
19+
20+
public convenience init(siteID: Int64,
21+
credentials: Credentials?,
22+
storage: StorageManagerType) {
23+
let network = AlamofireNetwork(credentials: credentials)
24+
self.init(siteID: siteID, settingStoreMethods: SettingStoreMethods(storageManager: storage,
25+
network: network))
26+
}
27+
28+
public func retrievePointOfSaleSettings() async throws -> [SiteSetting] {
29+
return try await settingStoreMethods.retrievePointOfSaleSettings(siteID: siteID)
30+
}
31+
}

Modules/Tests/YosemiteTests/Mocks/MockSettingStoreMethods.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ final class MockSettingStoreMethods: SettingStoreMethodsProtocol {
99
var retrieveAnalyticsSettingCalled = false
1010
var enableAnalyticsSettingCalled = false
1111
var retrieveTaxBasedOnSettingCalled = false
12+
var retrievePointOfSaleSettingsCalled = false
1213

1314
var couponsEnabled: Bool = true
1415
var featureEnabled: Result<Bool, Error> = .success(true)
16+
var retrievePointOfSaleSettingsResult: Result<[SiteSetting], Error> = .success([])
17+
var retrievePointOfSaleSettingsSiteID: Int64?
1518

1619
func synchronizeGeneralSiteSettings(siteID: Int64,
1720
onCompletion: @escaping (Error?) -> Void) {
@@ -74,6 +77,13 @@ final class MockSettingStoreMethods: SettingStoreMethodsProtocol {
7477
}
7578

7679
func retrievePointOfSaleSettings(siteID: Int64) async throws -> [SiteSetting] {
77-
[]
80+
retrievePointOfSaleSettingsCalled = true
81+
retrievePointOfSaleSettingsSiteID = siteID
82+
switch retrievePointOfSaleSettingsResult {
83+
case .success(let settings):
84+
return settings
85+
case .failure(let error):
86+
throw error
87+
}
7888
}
7989
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import Foundation
2+
import Testing
3+
@testable import Yosemite
4+
5+
struct PointOfSaleSettingsServiceTests {
6+
private let sut: PointOfSaleSettingsService
7+
private let settingStoreMethods: MockSettingStoreMethods
8+
private let storage: MockStorageManager
9+
private let sampleSiteID: Int64 = 123
10+
11+
init() {
12+
self.settingStoreMethods = MockSettingStoreMethods()
13+
self.storage = MockStorageManager()
14+
self.sut = PointOfSaleSettingsService(siteID: sampleSiteID,
15+
settingStoreMethods: settingStoreMethods)
16+
}
17+
18+
@Test func retrievePointOfSaleSettings_when_empty_settings_then_returns_empty_array() async throws {
19+
// Given
20+
settingStoreMethods.retrievePointOfSaleSettingsResult = .success([])
21+
22+
// When
23+
let settings = try await sut.retrievePointOfSaleSettings()
24+
25+
// Then
26+
#expect(settingStoreMethods.retrievePointOfSaleSettingsCalled)
27+
#expect(settingStoreMethods.retrievePointOfSaleSettingsSiteID == sampleSiteID)
28+
#expect(settings.isEmpty)
29+
}
30+
31+
@Test func retrievePointOfSaleSettings_when_network_error_then_throws_error() async throws {
32+
// Given
33+
let expectedError = NSError(domain: "NetworkError", code: 500, userInfo: [NSLocalizedDescriptionKey: "Network request failed"])
34+
settingStoreMethods.retrievePointOfSaleSettingsResult = .failure(expectedError)
35+
36+
// When
37+
do {
38+
_ = try await sut.retrievePointOfSaleSettings()
39+
#expect(Bool(false), "Expected error to be thrown")
40+
} catch {
41+
// Then
42+
#expect(settingStoreMethods.retrievePointOfSaleSettingsCalled)
43+
#expect(settingStoreMethods.retrievePointOfSaleSettingsSiteID == sampleSiteID)
44+
let nsError = error as NSError
45+
#expect(nsError.domain == expectedError.domain)
46+
#expect(nsError.code == expectedError.code)
47+
}
48+
}
49+
50+
@Test func retrievePointOfSaleSettings_when_settingStoreMethods_throws_then_propagates_error() async throws {
51+
// Given
52+
let expectedError = TestError.customError
53+
settingStoreMethods.retrievePointOfSaleSettingsResult = .failure(expectedError)
54+
55+
// When
56+
do {
57+
_ = try await sut.retrievePointOfSaleSettings()
58+
#expect(Bool(false), "Expected error to be thrown")
59+
} catch {
60+
// Then
61+
#expect(settingStoreMethods.retrievePointOfSaleSettingsCalled)
62+
#expect(settingStoreMethods.retrievePointOfSaleSettingsSiteID == sampleSiteID)
63+
#expect(error as? TestError == expectedError)
64+
}
65+
}
66+
67+
@Test func retrievePointOfSaleSettings_with_expected_pos_settings_then_returns_all_settings() async throws {
68+
// Given
69+
let expectedSettings = makeSiteSettings()
70+
settingStoreMethods.retrievePointOfSaleSettingsResult = .success(expectedSettings)
71+
72+
// When
73+
let settings = try await sut.retrievePointOfSaleSettings()
74+
75+
// Then
76+
#expect(settingStoreMethods.retrievePointOfSaleSettingsCalled)
77+
#expect(settingStoreMethods.retrievePointOfSaleSettingsSiteID == sampleSiteID)
78+
#expect(settings.count == 5)
79+
#expect(settings == expectedSettings)
80+
81+
let storeNameSetting = settings.first { $0.settingID == "woocommerce_pos_store_name" }
82+
#expect(storeNameSetting?.value == "WooCommerce Store")
83+
84+
let addressSetting = settings.first { $0.settingID == "woocommerce_pos_store_address" }
85+
#expect(addressSetting?.value == "123 Commerce Street\nBusiness District")
86+
87+
let phoneSetting = settings.first { $0.settingID == "woocommerce_pos_store_phone" }
88+
#expect(phoneSetting?.value == "+1 (555) 123-4567")
89+
90+
let emailSetting = settings.first { $0.settingID == "woocommerce_pos_store_email" }
91+
#expect(emailSetting?.value == "[email protected]")
92+
93+
let policySetting = settings.first { $0.settingID == "woocommerce_pos_refund_returns_policy" }
94+
#expect(policySetting?.value == "30-day return policy with receipt")
95+
}
96+
97+
private func makeSiteSettings() -> [SiteSetting] {
98+
return [
99+
SiteSetting(siteID: sampleSiteID,
100+
settingID: "woocommerce_pos_store_name",
101+
label: "Store Name",
102+
settingDescription: "Name of the store for POS receipts",
103+
value: "WooCommerce Store",
104+
settingGroupKey: "pos"),
105+
SiteSetting(siteID: sampleSiteID,
106+
settingID: "woocommerce_pos_store_address",
107+
label: "Store Address",
108+
settingDescription: "Physical address for receipts",
109+
value: "123 Commerce Street\nBusiness District",
110+
settingGroupKey: "pos"),
111+
SiteSetting(siteID: sampleSiteID,
112+
settingID: "woocommerce_pos_store_phone",
113+
label: "Store Phone",
114+
settingDescription: "Contact phone number",
115+
value: "+1 (555) 123-4567",
116+
settingGroupKey: "pos"),
117+
SiteSetting(siteID: sampleSiteID,
118+
settingID: "woocommerce_pos_store_email",
119+
label: "Store Email",
120+
settingDescription: "Contact email address",
121+
122+
settingGroupKey: "pos"),
123+
SiteSetting(siteID: sampleSiteID,
124+
settingID: "woocommerce_pos_refund_returns_policy",
125+
label: "Refund & Returns Policy",
126+
settingDescription: "Store policy for refunds and returns",
127+
value: "30-day return policy with receipt",
128+
settingGroupKey: "pos")
129+
]
130+
}
131+
}
132+
133+
private enum TestError: Error, Equatable {
134+
case customError
135+
}

WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ protocol PointOfSaleAggregateModelProtocol {
7070
let popularPurchasableItemsController: PointOfSaleItemsControllerProtocol
7171
let couponsController: PointOfSaleCouponsControllerProtocol
7272
let couponsSearchController: PointOfSaleSearchingItemsControllerProtocol
73+
let settingsController: PointOfSaleSettingsControllerProtocol
7374

7475
private let cardPresentPaymentService: CardPresentPaymentFacade
7576
private let orderController: PointOfSaleOrderControllerProtocol
@@ -105,6 +106,7 @@ protocol PointOfSaleAggregateModelProtocol {
105106
couponsSearchController: PointOfSaleSearchingItemsControllerProtocol,
106107
cardPresentPaymentService: CardPresentPaymentFacade,
107108
orderController: PointOfSaleOrderControllerProtocol,
109+
settingsController: PointOfSaleSettingsControllerProtocol,
108110
analytics: Analytics = ServiceLocator.analytics,
109111
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking,
110112
searchHistoryService: POSSearchHistoryProviding,
@@ -119,6 +121,7 @@ protocol PointOfSaleAggregateModelProtocol {
119121
self.couponsSearchController = couponsSearchController
120122
self.cardPresentPaymentService = cardPresentPaymentService
121123
self.orderController = orderController
124+
self.settingsController = settingsController
122125
self.analytics = analytics
123126
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker
124127
self.searchHistoryService = searchHistoryService
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import Foundation
2+
import struct Yosemite.SiteSetting
3+
import enum Yosemite.Plugin
4+
import class Yosemite.PluginsService
5+
import Observation
6+
7+
import protocol Yosemite.PointOfSaleSettingsServiceProtocol
8+
import class Yosemite.PointOfSaleSettingsService
9+
import Storage
10+
11+
protocol PointOfSaleSettingsControllerProtocol {
12+
var receiptStoreName: String? { get }
13+
var receiptStoreAddress: String? { get }
14+
var receiptStorePhone: String? { get }
15+
var receiptStoreEmail: String? { get }
16+
var receiptRefundReturnsPolicy: String? { get }
17+
var isLoading: Bool { get }
18+
var shouldShowReceiptInformation: Bool { get }
19+
var storeName: String { get }
20+
var storeAddress: String { get }
21+
22+
func retrievePOSReceiptSettings() async
23+
}
24+
25+
@Observable final class PointOfSaleSettingsController: PointOfSaleSettingsControllerProtocol {
26+
private(set) var receiptStoreName: String?
27+
private(set) var receiptStoreAddress: String?
28+
private(set) var receiptStorePhone: String?
29+
private(set) var receiptStoreEmail: String?
30+
private(set) var receiptRefundReturnsPolicy: String?
31+
private(set) var isLoading: Bool = false
32+
private(set) var shouldShowReceiptInformation: Bool = false
33+
34+
private let defaultSiteName: String?
35+
private let settingsService: PointOfSaleSettingsServiceProtocol
36+
private let siteSettings: [SiteSetting]
37+
38+
init(settingsService: PointOfSaleSettingsServiceProtocol,
39+
defaultSiteName: String? = ServiceLocator.stores.sessionManager.defaultSite?.name,
40+
siteSettings: [SiteSetting] = ServiceLocator.selectedSiteSettings.siteSettings) {
41+
self.settingsService = settingsService
42+
self.defaultSiteName = defaultSiteName
43+
self.siteSettings = siteSettings
44+
}
45+
46+
var storeName: String {
47+
if let defaultSiteName {
48+
return defaultSiteName
49+
} else {
50+
return Localization.storeNameNotSet
51+
}
52+
}
53+
54+
var storeAddress: String {
55+
SiteAddress(siteSettings: siteSettings).address
56+
}
57+
58+
@MainActor
59+
func retrievePOSReceiptSettings() async {
60+
isLoading = true
61+
62+
shouldShowReceiptInformation = await isPluginSupported(.wooCommerce, minimumVersion: Constants.minimumWooCommerceVersion)
63+
64+
guard shouldShowReceiptInformation else {
65+
isLoading = false
66+
return
67+
}
68+
69+
do {
70+
let siteSettings = try await settingsService.retrievePointOfSaleSettings()
71+
updateReceiptSettings(from: siteSettings)
72+
} catch {
73+
DDLogError("Failed to load POS settings: \(error)")
74+
}
75+
isLoading = false
76+
}
77+
78+
@MainActor
79+
private func isPluginSupported(_ plugin: Plugin,
80+
storageManager: StorageManagerType = ServiceLocator.storageManager,
81+
minimumVersion: String) async -> Bool {
82+
let pluginsService = PluginsService(storageManager: storageManager)
83+
guard let systemPlugin = pluginsService.loadPluginInStorage(siteID: settingsService.siteID, plugin: plugin, isActive: true), systemPlugin.active else {
84+
return false
85+
}
86+
87+
let isSupported = VersionHelpers.isVersionSupported(version: systemPlugin.version,
88+
minimumRequired: minimumVersion)
89+
return isSupported
90+
}
91+
92+
private func updateReceiptSettings(from siteSettings: [SiteSetting]) {
93+
receiptStoreName = settingValue(from: siteSettings, settingID: "woocommerce_pos_store_name")
94+
receiptStoreAddress = settingValue(from: siteSettings, settingID: "woocommerce_pos_store_address")
95+
receiptStorePhone = settingValue(from: siteSettings, settingID: "woocommerce_pos_store_phone")
96+
receiptStoreEmail = settingValue(from: siteSettings, settingID: "woocommerce_pos_store_email")
97+
receiptRefundReturnsPolicy = settingValue(from: siteSettings, settingID: "woocommerce_pos_refund_returns_policy")
98+
}
99+
100+
private func settingValue(from siteSettings: [SiteSetting], settingID: String) -> String? {
101+
let value = siteSettings.first { $0.settingID == settingID }?.value
102+
return value?.isEmpty == true ? nil : value
103+
}
104+
}
105+
106+
private extension PointOfSaleSettingsController {
107+
enum Constants {
108+
static let minimumWooCommerceVersion: String = "10.0"
109+
}
110+
111+
enum Localization {
112+
static let storeNameNotSet = NSLocalizedString(
113+
"pointOfSaleSettingsService.storeNameNotSet",
114+
value: "Not set",
115+
comment: "Text displayed on Point of Sale settings when store has not been provided."
116+
)
117+
}
118+
}
119+
120+
#if DEBUG
121+
final class PointOfSaleSettingsPreviewController: PointOfSaleSettingsControllerProtocol {
122+
var receiptStoreName: String? = "Sample Store"
123+
var receiptStoreAddress: String? = "123 Main Street\nAnytown, ST 12345"
124+
var receiptStorePhone: String? = "+1 (555) 123-4567"
125+
var receiptStoreEmail: String? = "[email protected]"
126+
var receiptRefundReturnsPolicy: String? = "30-day return policy"
127+
var isLoading: Bool = false
128+
var shouldShowReceiptInformation: Bool = true
129+
var storeName: String = "Sample Store"
130+
131+
var storeAddress: String {
132+
"123 Main Street\nAnytown, ST 12345"
133+
}
134+
135+
func retrievePOSReceiptSettings() async {
136+
// no-op
137+
}
138+
}
139+
#endif

WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ struct PointOfSaleDashboardView: View {
132132
documentationView
133133
}
134134
.posFullScreenCover(isPresented: $showSettings) {
135-
PointOfSaleSettingsView()
135+
PointOfSaleSettingsView(settingsController: posModel.settingsController)
136136
}
137137
.onChange(of: posModel.entryPointController.eligibilityState) { oldValue, newValue in
138138
guard newValue == .eligible else { return }

0 commit comments

Comments
 (0)