Skip to content

Commit

Permalink
[Woo POS] Extract items code from aggregate model (#14498)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshheald authored Nov 26, 2024
2 parents 01ccadc + a9a1da0 commit 89b3adc
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 368 deletions.
76 changes: 13 additions & 63 deletions WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ import Foundation
import Combine

import protocol Yosemite.POSItem
import protocol Yosemite.POSItemProvider
import protocol WooFoundation.Analytics
import struct Yosemite.Order
import struct Yosemite.OrderItem
import protocol Yosemite.POSOrderServiceProtocol
import struct Yosemite.POSCartItem
import class WooFoundation.CurrencyFormatter
import enum Yosemite.POSProductProviderError

protocol PointOfSaleAggregateModelProtocol {
var orderStage: PointOfSaleOrderStage { get }
Expand Down Expand Up @@ -58,32 +56,31 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt

private var order: Order? = nil

private let itemProvider: POSItemProvider
private let itemsService: PointOfSaleItemsServiceProtocol

private let cardPresentPaymentService: CardPresentPaymentFacade
private let orderService: POSOrderServiceProtocol
private let currencyFormatter: CurrencyFormatter
private let analytics: Analytics

private var allItems: [POSItem] = []
private var currentPage: Int = Constants.initialPage
private var mightHaveMorePages: Bool = true
private var startPaymentOnCardReaderConnection: AnyCancellable?
private var cardReaderDisconnection: AnyCancellable?

private var cancellables: Set<AnyCancellable> = []

init(itemProvider: POSItemProvider,
init(itemsService: PointOfSaleItemsServiceProtocol,
cardPresentPaymentService: CardPresentPaymentFacade,
orderService: POSOrderServiceProtocol,
currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings),
analytics: Analytics = ServiceLocator.analytics,
paymentState: PointOfSalePaymentState = .idle) {
self.itemProvider = itemProvider
self.itemsService = itemsService
self.cardPresentPaymentService = cardPresentPaymentService
self.orderService = orderService
self.currencyFormatter = currencyFormatter
self.analytics = analytics
self.paymentState = paymentState
publishItemListState()
publishCardReaderConnectionStatus()
publishPaymentMessages()
setupReaderReconnectionObservation()
Expand All @@ -92,70 +89,23 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt

// MARK: - ItemList
extension PointOfSaleAggregateModel {
private func publishItemListState() {
itemsService.itemListStatePublisher.assign(to: &$itemListState)
}

@MainActor
func loadInitialItems() async {
mightHaveMorePages = true
itemListState = .initialLoading
try? await load(pageNumber: Constants.initialPage)
await itemsService.loadInitialItems()
}

@MainActor
func loadNextItems() async {
do {
guard mightHaveMorePages else {
return
}
itemListState = .loading(allItems)

let nextPage = currentPage + 1
try await load(pageNumber: nextPage)
currentPage = nextPage
} catch {
// No need to do anything; this avoids us incorrectly incrementing currentPage.
}
await itemsService.loadNextItems()
}

@MainActor
func reload() async {
allItems.removeAll()
currentPage = Constants.initialPage
mightHaveMorePages = true
itemListState = .loading(allItems)
try? await load(pageNumber: currentPage)
}

@MainActor
private func load(pageNumber: Int) async throws {
do {
try await fetchItems(pageNumber: pageNumber)

mightHaveMorePages = true
updateItemListStateAfterLoadAttempt()
} catch POSProductProviderError.pageOutOfRange {
mightHaveMorePages = false
updateItemListStateAfterLoadAttempt()
throw POSProductProviderError.pageOutOfRange
} catch {
itemListState = .error(PointOfSaleErrorState.errorOnLoadingProducts())
throw error
}
}

@MainActor
private func fetchItems(pageNumber: Int) async throws {
let newItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber)
let uniqueNewItems = newItems.filter { newItem in
!allItems.contains(where: { $0.productID == newItem.productID })
}
allItems.append(contentsOf: uniqueNewItems)
}

private func updateItemListStateAfterLoadAttempt() {
if allItems.count == 0 {
itemListState = .empty
} else {
itemListState = .loaded(allItems)
}
await itemsService.reload()
}
}

Expand Down Expand Up @@ -411,7 +361,7 @@ extension PointOfSaleAggregateModel {
return
}
// calculate totals and sync order if there was a change in the cart
await syncOrder(for: cart, allItems: allItems)
await syncOrder(for: cart, allItems: itemsService.allItems)
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct PointOfSaleEntryPointView: View {
orderService: POSOrderServiceProtocol) {
self.onPointOfSaleModeActiveStateChange = onPointOfSaleModeActiveStateChange

let posModel = PointOfSaleAggregateModel(itemProvider: itemProvider,
let posModel = PointOfSaleAggregateModel(itemsService: PointOfSaleItemsService(itemProvider: itemProvider),
cardPresentPaymentService: cardPresentPaymentService,
orderService: orderService)

Expand Down
2 changes: 1 addition & 1 deletion WooCommerce/Classes/POS/Presentation/TotalsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ private extension View {
#if DEBUG
#Preview {
let posModel = PointOfSaleAggregateModel(
itemProvider: POSItemProviderPreview(),
itemsService: PointOfSaleItemsPreviewService(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderService: POSOrderPreviewService())
TotalsView()
Expand Down
97 changes: 97 additions & 0 deletions WooCommerce/Classes/POS/Services/PointOfSaleItemsService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Foundation
import Combine
import protocol Yosemite.POSItem
import protocol Yosemite.POSItemProvider
import enum Yosemite.POSProductProviderError

protocol PointOfSaleItemsServiceProtocol {
var itemListStatePublisher: any Publisher<ItemListState, Never> { get }
@available(*, deprecated, message: "allItems will be removed in a future release. Use itemListState's associated value instead")
var allItems: [POSItem] { get }
func loadInitialItems() async
func loadNextItems() async
func reload() async
}

class PointOfSaleItemsService: PointOfSaleItemsServiceProtocol {
private(set) var itemListStatePublisher: any Publisher<ItemListState, Never>
private var itemListStateSubject: PassthroughSubject<ItemListState, Never> = .init()
private(set) var allItems: [POSItem] = []
private var currentPage: Int = Constants.initialPage
private var mightHaveMorePages: Bool = true
private let itemProvider: POSItemProvider

init(itemProvider: POSItemProvider) {
self.itemProvider = itemProvider
itemListStatePublisher = itemListStateSubject.eraseToAnyPublisher()
}

@MainActor
func loadInitialItems() async {
mightHaveMorePages = true
itemListStateSubject.send(.initialLoading)
try? await load(pageNumber: Constants.initialPage)
}

@MainActor
func loadNextItems() async {
do {
guard mightHaveMorePages else {
return
}
itemListStateSubject.send(.loading(allItems))

let nextPage = currentPage + 1
try await load(pageNumber: nextPage)
currentPage = nextPage
} catch {
// Handle errors without incrementing currentPage.
}
}

@MainActor
func reload() async {
allItems.removeAll()
currentPage = Constants.initialPage
mightHaveMorePages = true
itemListStateSubject.send(.loading(allItems))
try? await load(pageNumber: currentPage)
}

@MainActor
private func load(pageNumber: Int) async throws {
do {
try await fetchItems(pageNumber: pageNumber)
mightHaveMorePages = true
updateItemListStateAfterLoadAttempt()
} catch POSProductProviderError.pageOutOfRange {
mightHaveMorePages = false
updateItemListStateAfterLoadAttempt()
throw POSProductProviderError.pageOutOfRange
} catch {
itemListStateSubject.send(.error(PointOfSaleErrorState.errorOnLoadingProducts()))
throw error
}
}

@MainActor
private func fetchItems(pageNumber: Int) async throws {
let newItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber)
let uniqueNewItems = newItems.filter { newItem in
!allItems.contains(where: { $0.productID == newItem.productID })
}
allItems.append(contentsOf: uniqueNewItems)
}

private func updateItemListStateAfterLoadAttempt() {
if allItems.isEmpty {
itemListStateSubject.send(.empty)
} else {
itemListStateSubject.send(.loaded(allItems))
}
}

private enum Constants {
static let initialPage: Int = 1
}
}
34 changes: 28 additions & 6 deletions WooCommerce/Classes/POS/Utils/PreviewHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@ final class POSItemProviderPreview: POSItemProvider {
}

func providePointOfSaleItems() -> [POSItem] {
return [
POSProductPreview(itemID: UUID(), productID: 1, name: "Product 1", price: "1.00", formattedPrice: "$1.00", itemCategories: [], productType: .simple),
POSProductPreview(itemID: UUID(), productID: 2, name: "Product 2", price: "2.00", formattedPrice: "$2.00", itemCategories: [], productType: .simple),
POSProductPreview(itemID: UUID(), productID: 3, name: "Product 3", price: "3.00", formattedPrice: "$3.00", itemCategories: [], productType: .simple),
POSProductPreview(itemID: UUID(), productID: 4, name: "Product 4", price: "4.00", formattedPrice: "$4.00", itemCategories: [], productType: .simple)
]
return mockItems
}

func providePointOfSaleItem() -> POSItem {
Expand All @@ -44,6 +39,33 @@ final class POSItemProviderPreview: POSItemProvider {
}
}

final class PointOfSaleItemsPreviewService: PointOfSaleItemsServiceProtocol {
@Published var itemListState: ItemListState = .initialLoading
var itemListStatePublisher: any Publisher<ItemListState, Never> { $itemListState }

var allItems: [any Yosemite.POSItem] = []

func loadInitialItems() async {
itemListState = .loaded(mockItems)
}

func loadNextItems() async {
itemListState = .loading(mockItems)
}

func reload() async {
itemListState = .loaded([])
}
}

private var mockItems: [POSItem] {
return [
POSProductPreview(itemID: UUID(), productID: 1, name: "Product 1", price: "1.00", formattedPrice: "$1.00", itemCategories: [], productType: .simple),
POSProductPreview(itemID: UUID(), productID: 2, name: "Product 2", price: "2.00", formattedPrice: "$2.00", itemCategories: [], productType: .simple),
POSProductPreview(itemID: UUID(), productID: 3, name: "Product 3", price: "3.00", formattedPrice: "$3.00", itemCategories: [], productType: .simple),
POSProductPreview(itemID: UUID(), productID: 4, name: "Product 4", price: "4.00", formattedPrice: "$4.00", itemCategories: [], productType: .simple)
]
}

final class POSConnectivityObserverPreview: ConnectivityObserver {
@Published private(set) var currentStatus: ConnectivityStatus = .unknown
Expand Down
28 changes: 28 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
2004E2E92C0DFE2B00D62521 /* PointOfSaleCardPresentPaymentAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2004E2E82C0DFE2B00D62521 /* PointOfSaleCardPresentPaymentAlert.swift */; };
2004E2EB2C0E219D00D62521 /* CardPresentPaymentPreflightAdaptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2004E2EA2C0E219D00D62521 /* CardPresentPaymentPreflightAdaptor.swift */; };
2004E2ED2C0F5DD800D62521 /* CardPresentPaymentCollectOrderPaymentUseCaseAdaptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2004E2EC2C0F5DD800D62521 /* CardPresentPaymentCollectOrderPaymentUseCaseAdaptor.swift */; };
200BA1592CF092280006DC5B /* PointOfSaleItemsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200BA1582CF092280006DC5B /* PointOfSaleItemsService.swift */; };
200BA15B2CF0A2130006DC5B /* MockPointOfSaleItemsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200BA15A2CF0A2130006DC5B /* MockPointOfSaleItemsService.swift */; };
200BA15E2CF0A9EB0006DC5B /* PointOfSaleItemsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200BA15D2CF0A9EB0006DC5B /* PointOfSaleItemsServiceTests.swift */; };
201F5AC52AD4061800EF6C55 /* AboutTapToPayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201F5AC42AD4061800EF6C55 /* AboutTapToPayViewModel.swift */; };
20203AB22B31EEF1009D0C11 /* ExpandableBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20203AB12B31EEF1009D0C11 /* ExpandableBottomSheet.swift */; };
2023E2AE2C21D8EA00FC365A /* PointOfSaleCardPresentPaymentInLineMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2023E2AD2C21D8EA00FC365A /* PointOfSaleCardPresentPaymentInLineMessage.swift */; };
Expand Down Expand Up @@ -3880,6 +3883,9 @@
2004E2EA2C0E219D00D62521 /* CardPresentPaymentPreflightAdaptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentPreflightAdaptor.swift; sourceTree = "<group>"; };
2004E2EC2C0F5DD800D62521 /* CardPresentPaymentCollectOrderPaymentUseCaseAdaptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentCollectOrderPaymentUseCaseAdaptor.swift; sourceTree = "<group>"; };
200B84AD2BEB99AC00EAAB23 /* WooCommercePOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WooCommercePOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
200BA1582CF092280006DC5B /* PointOfSaleItemsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleItemsService.swift; sourceTree = "<group>"; };
200BA15A2CF0A2130006DC5B /* MockPointOfSaleItemsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleItemsService.swift; sourceTree = "<group>"; };
200BA15D2CF0A9EB0006DC5B /* PointOfSaleItemsServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleItemsServiceTests.swift; sourceTree = "<group>"; };
201F5AC42AD4061800EF6C55 /* AboutTapToPayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutTapToPayViewModel.swift; sourceTree = "<group>"; };
20203AB12B31EEF1009D0C11 /* ExpandableBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableBottomSheet.swift; sourceTree = "<group>"; };
2023E2AD2C21D8EA00FC365A /* PointOfSaleCardPresentPaymentInLineMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentInLineMessage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7159,6 +7165,7 @@
029327662BF59D2D00D703E7 /* POS */ = {
isa = PBXGroup;
children = (
200BA1572CF092150006DC5B /* Services */,
02D1D2D82CD3CD710069A93F /* Analytics */,
2004E2C02C076CCA00D62521 /* Card Present Payments */,
68F151DF2C0DA7800082AEC8 /* Models */,
Expand Down Expand Up @@ -7370,6 +7377,7 @@
02CD3BFD2C35D04C00E575C4 /* MockCardPresentPaymentService.swift */,
207E71CA2C60F765008540FC /* MockPOSOrderService.swift */,
20FCBCE02CE24CE70082DCA3 /* MockPOSItemProvider.swift */,
200BA15A2CF0A2130006DC5B /* MockPointOfSaleItemsService.swift */,
20FCBCE22CE24F5D0082DCA3 /* MockPointOfSaleAggregateModel.swift */,
);
path = Mocks;
Expand Down Expand Up @@ -7782,6 +7790,22 @@
path = "Card Present Payments";
sourceTree = "<group>";
};
200BA1572CF092150006DC5B /* Services */ = {
isa = PBXGroup;
children = (
200BA1582CF092280006DC5B /* PointOfSaleItemsService.swift */,
);
path = Services;
sourceTree = "<group>";
};
200BA15C2CF0A9D90006DC5B /* Services */ = {
isa = PBXGroup;
children = (
200BA15D2CF0A9EB0006DC5B /* PointOfSaleItemsServiceTests.swift */,
);
path = Services;
sourceTree = "<group>";
};
2023E2AC2C21D8A400FC365A /* Connection Alerts */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -12539,6 +12563,7 @@
DABF35242C11B40C006AF826 /* POS */ = {
isa = PBXGroup;
children = (
200BA15C2CF0A9D90006DC5B /* Services */,
20ADE9442C6B361500C91265 /* Card Present Payments */,
DAD988C72C4A9D49009DE9E3 /* Models */,
02CD3BFC2C35D01600E575C4 /* Mocks */,
Expand Down Expand Up @@ -16212,6 +16237,7 @@
DEC6C51E27479280006832D3 /* JetpackInstallSteps.swift in Sources */,
021AEF9E2407F55C00029D28 /* PHAssetImageLoader.swift in Sources */,
DECE13FB27993F6500816ECD /* TitleAndSubtitleAndStatusTableViewCell.swift in Sources */,
200BA1592CF092280006DC5B /* PointOfSaleItemsService.swift in Sources */,
DEFC9BE22B2FF62C00138B05 /* WooAnalyticsEvent+Themes.swift in Sources */,
EE35AFA32B0491960074E7AC /* SubscriptionTrialViewModel.swift in Sources */,
26BCA0402C35E9A9000BE96C /* BackgroundTaskRefreshDispatcher.swift in Sources */,
Expand Down Expand Up @@ -16827,6 +16853,7 @@
45EF798624509B4C00B22BA2 /* ArrayIndexPathTests.swift in Sources */,
D8610BDD256F5ABF00A5DF27 /* JetpackErrorViewModelTests.swift in Sources */,
746791632108D7C0007CF1DC /* WooAnalyticsTests.swift in Sources */,
200BA15E2CF0A9EB0006DC5B /* PointOfSaleItemsServiceTests.swift in Sources */,
2667BFDD252F61C5008099D4 /* RefundShippingDetailsViewModelTests.swift in Sources */,
DE7B479727A3C4980018742E /* CouponDetailsViewModelTests.swift in Sources */,
0271125D2887D4E900FCD13C /* LoggedOutAppSettingsTests.swift in Sources */,
Expand Down Expand Up @@ -16872,6 +16899,7 @@
CC77488E2719A07D0043CDD7 /* ShippingLabelAddressTopBannerFactoryTests.swift in Sources */,
B935D3612A9F50F50067B927 /* MockWPAdminTaxSettingsURLProvider.swift in Sources */,
B517EA1A218B2D2600730EC4 /* StringFormatterTests.swift in Sources */,
200BA15B2CF0A2130006DC5B /* MockPointOfSaleItemsService.swift in Sources */,
26F65C9E25DEDE67008FAE29 /* GenerateVariationUseCaseTests.swift in Sources */,
03D798602A960FDF00809B0E /* MockPaymentCaptureOrchestrator.swift in Sources */,
77307809251EA07100178696 /* ProductDownloadSettingsViewModelTests.swift in Sources */,
Expand Down
Loading

0 comments on commit 89b3adc

Please sign in to comment.