Skip to content
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

[Woo POS] Extract items code from aggregate model #14498

Open
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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