Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
059bcd9
make PointOfSaleSettingsService
iamgabrielma Aug 25, 2025
433a5d8
update init
iamgabrielma Aug 25, 2025
4713200
move version check to service
iamgabrielma Aug 25, 2025
af3b76e
make settingValueView
iamgabrielma Aug 25, 2025
4201416
early return if version not supported
iamgabrielma Aug 25, 2025
7b66d8f
cleanup
iamgabrielma Aug 25, 2025
46035e4
update NSLocalizedStrings
iamgabrielma Aug 25, 2025
e49dad7
lint and POS fonts
iamgabrielma Aug 25, 2025
4bb9542
move setting retrieval to the parent view
iamgabrielma Aug 25, 2025
03834df
update imports and localization
iamgabrielma Aug 25, 2025
8a02151
make setting view create its own service
iamgabrielma Aug 25, 2025
fe32ce4
Revert "make setting view create its own service"
iamgabrielma Aug 25, 2025
e19018b
split settings service into controller-service
iamgabrielma Aug 26, 2025
c3c2ca0
extract controller init to posmodel
iamgabrielma Aug 26, 2025
8f91b68
make protocol. DI settings controller into POS aggregate
iamgabrielma Aug 26, 2025
a626d12
make test target compile
iamgabrielma Aug 26, 2025
b102193
DI storage into settings service
iamgabrielma Aug 26, 2025
f3de3be
DI default values for store name and address
iamgabrielma Aug 26, 2025
9bedfa1
add PointOfSaleSettingsServiceTests
iamgabrielma Aug 26, 2025
50c3efe
make PointOfSaleSettingsServiceProtocol and add tests
iamgabrielma Aug 26, 2025
e2e84b2
clean up unused storage
iamgabrielma Aug 26, 2025
72bed3b
restore commented out preview
iamgabrielma Aug 26, 2025
ae533c5
Merge branch 'trunk' into task/WOOMOB-1040-POSSettings-store-section
iamgabrielma Aug 27, 2025
3b82a84
remove unnecessary test
iamgabrielma Aug 27, 2025
61728b2
adjust access control
iamgabrielma Aug 27, 2025
0398758
update var name
iamgabrielma Aug 27, 2025
2e5c028
merge tests
iamgabrielma Aug 27, 2025
d036798
make mock and wrap preview in debug flag
iamgabrielma Aug 27, 2025
b7b3bee
update preview
iamgabrielma Aug 27, 2025
cf78712
remove unnecessary imports
iamgabrielma Aug 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
}
}
12 changes: 11 additions & 1 deletion Modules/Tests/YosemiteTests/Mocks/MockSettingStoreMethods.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bool, Error> = .success(true)
var retrievePointOfSaleSettingsResult: Result<[SiteSetting], Error> = .success([])
var retrievePointOfSaleSettingsSiteID: Int64?

func synchronizeGeneralSiteSettings(siteID: Int64,
onCompletion: @escaping (Error?) -> Void) {
Expand Down Expand Up @@ -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
}
}
}
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
Expand Up @@ -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
Expand Down Expand Up @@ -105,6 +106,7 @@ protocol PointOfSaleAggregateModelProtocol {
couponsSearchController: PointOfSaleSearchingItemsControllerProtocol,
cardPresentPaymentService: CardPresentPaymentFacade,
orderController: PointOfSaleOrderControllerProtocol,
settingsController: PointOfSaleSettingsControllerProtocol,
analytics: Analytics = ServiceLocator.analytics,
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking,
searchHistoryService: POSSearchHistoryProviding,
Expand All @@ -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
Expand Down
139 changes: 139 additions & 0 deletions WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift
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 {
Copy link
Contributor

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, isLoading is 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 @Observable view model like class to provide the following interface for PointOfSaleSettingsStoreDetailView?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I've added a note on WOOMOB-1188 so signature and dependencies can be adjusted in one go 👍

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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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 siteID isn't needed in PointOfSaleSettingsServiceProtocol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading