diff --git a/Modules/Sources/PointOfSale/Models/PointOfSaleAggregateModel.swift b/Modules/Sources/PointOfSale/Models/PointOfSaleAggregateModel.swift index 822bf349858..a809b18f365 100644 --- a/Modules/Sources/PointOfSale/Models/PointOfSaleAggregateModel.swift +++ b/Modules/Sources/PointOfSale/Models/PointOfSaleAggregateModel.swift @@ -715,8 +715,17 @@ private extension PointOfSaleAggregateModel { private func performIncrementalSync() { guard let catalogSyncCoordinator else { return } + let popularPurchasableItemsController = popularPurchasableItemsController + let siteID = siteID Task { - try? await catalogSyncCoordinator.performIncrementalSync(for: siteID) + await withTaskGroup(of: Void.self) { group in + group.addTask { + try? await catalogSyncCoordinator.performIncrementalSync(for: siteID) + } + group.addTask { + await popularPurchasableItemsController.refreshItems(base: .root) + } + } } } diff --git a/Modules/Sources/PointOfSale/Presentation/ItemListView.swift b/Modules/Sources/PointOfSale/Presentation/ItemListView.swift index c909302df59..1b4dd298503 100644 --- a/Modules/Sources/PointOfSale/Presentation/ItemListView.swift +++ b/Modules/Sources/PointOfSale/Presentation/ItemListView.swift @@ -181,7 +181,14 @@ struct ItemListView: View { ) .refreshable { analyticsTracker.trackRefresh() - await itemsController(itemListType).refreshItems(base: .root) + await withTaskGroup(of: Void.self) { group in + group.addTask { + await itemsController(itemListType).refreshItems(base: .root) + } + group.addTask { + await posModel.popularPurchasableItemsController.refreshItems(base: .root) + } + } } } .task { diff --git a/Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift b/Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift index a0f86f86046..dba4f419abe 100644 --- a/Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift +++ b/Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift @@ -105,17 +105,8 @@ public extension PersistedProduct { /// Returns a request for POS-supported products (simple and variable, non-downloadable) for a given site, ordered by name /// Filters out products with trash, draft, pending, or private status to ensure only published and 3rd party custom status products are shown static func posProductsRequest(siteID: Int64) -> QueryInterfaceRequest { - let excludedStatuses = [ - "trash", - "draft", - "pending" - ] - - return PersistedProduct - .filter(Columns.siteID == siteID) - .filter([ProductType.simple.rawValue, ProductType.variable.rawValue].contains(Columns.productTypeKey)) - .filter(Columns.downloadable == false) - .filter(!excludedStatuses.contains(Columns.statusKey)) + PersistedProduct + .baseQuery(siteID: siteID) .order(Columns.name.collating(.localizedCaseInsensitiveCompare)) } @@ -124,6 +115,7 @@ public extension PersistedProduct { /// - siteID: The site ID /// - globalUniqueID: The global unique ID (barcode) to search for /// - Returns: A query request that matches products with the given global unique ID + /// Note that this may return unsupported products, so they can be shown as errors in the UI static func posProductByGlobalUniqueID(siteID: Int64, globalUniqueID: String) -> QueryInterfaceRequest { return PersistedProduct .filter(Columns.siteID == siteID) @@ -140,9 +132,7 @@ public extension PersistedProduct { let likePattern = "%\(escapedTerm)%" return PersistedProduct - .filter(Columns.siteID == siteID) - .filter([ProductType.simple.rawValue, ProductType.variable.rawValue].contains(Columns.productTypeKey)) - .filter(Columns.downloadable == false) + .baseQuery(siteID: siteID) .filter( Columns.name.like(likePattern, escape: "\\") || Columns.sku.like(likePattern, escape: "\\") || @@ -160,6 +150,20 @@ public extension PersistedProduct { .replacingOccurrences(of: "%", with: "\\%") .replacingOccurrences(of: "_", with: "\\_") } + + private static func baseQuery(siteID: Int64) -> QueryInterfaceRequest { + let excludedStatuses = [ + "trash", + "draft", + "pending" + ] + + return PersistedProduct + .filter(Columns.siteID == siteID) + .filter([ProductType.simple.rawValue, ProductType.variable.rawValue].contains(Columns.productTypeKey)) + .filter(Columns.downloadable == false) + .filter(!excludedStatuses.contains(Columns.statusKey)) + } } // periphery:ignore - TODO: remove ignore when populating database diff --git a/Modules/Tests/StorageTests/GRDB/PersistedProductSearchQueryTests.swift b/Modules/Tests/StorageTests/GRDB/PersistedProductSearchQueryTests.swift index a054f4dda3f..837f9d08c03 100644 --- a/Modules/Tests/StorageTests/GRDB/PersistedProductSearchQueryTests.swift +++ b/Modules/Tests/StorageTests/GRDB/PersistedProductSearchQueryTests.swift @@ -330,6 +330,114 @@ struct PersistedProductSearchQueryTests { #expect(!results.contains(where: { $0.productTypeKey == "grouped" })) } + @Test("posProductSearch filters out unsupported product statuses") + func test_search_only_returns_pos_supported_product_statuses() async throws { + // Given + let trashedProduct = PersistedProduct( + id: 10, + siteID: siteID, + name: "Search Test Trash", + productTypeKey: "simple", + fullDescription: nil, + shortDescription: nil, + sku: nil, + globalUniqueID: nil, + price: "10.00", + downloadable: false, + parentID: 0, + manageStock: false, + stockQuantity: nil, + stockStatusKey: "instock", + statusKey: "trash" + ) + let draftProduct = PersistedProduct( + id: 11, + siteID: siteID, + name: "Search Test Draft", + productTypeKey: "variable", + fullDescription: nil, + shortDescription: nil, + sku: nil, + globalUniqueID: nil, + price: "20.00", + downloadable: false, + parentID: 0, + manageStock: false, + stockQuantity: nil, + stockStatusKey: "instock", + statusKey: "draft" + ) + let pendingProduct = PersistedProduct( + id: 12, + siteID: siteID, + name: "Search Test Pending", + productTypeKey: "simple", + fullDescription: nil, + shortDescription: nil, + sku: nil, + globalUniqueID: nil, + price: "0.00", + downloadable: false, + parentID: 0, + manageStock: false, + stockQuantity: nil, + stockStatusKey: "instock", + statusKey: "pending" + ) + let publishedProduct = PersistedProduct( + id: 13, + siteID: siteID, + name: "Search Test Published", + productTypeKey: "simple", + fullDescription: nil, + shortDescription: nil, + sku: nil, + globalUniqueID: nil, + price: "0.00", + downloadable: false, + parentID: 0, + manageStock: false, + stockQuantity: nil, + stockStatusKey: "instock", + statusKey: "publish" + ) + let privateProduct = PersistedProduct( + id: 14, + siteID: siteID, + name: "Search Test Private", + productTypeKey: "simple", + fullDescription: nil, + shortDescription: nil, + sku: nil, + globalUniqueID: nil, + price: "0.00", + downloadable: false, + parentID: 0, + manageStock: false, + stockQuantity: nil, + stockStatusKey: "instock", + statusKey: "private" + ) + try await insertProduct(trashedProduct) + try await insertProduct(draftProduct) + try await insertProduct(pendingProduct) + try await insertProduct(publishedProduct) + try await insertProduct(privateProduct) + + // When + let results = try await grdbManager.databaseConnection.read { db in + try PersistedProduct.posProductSearch(siteID: siteID, searchTerm: "Search Test").fetchAll(db) + } + + // Then + #expect(results.count == 2) + #expect(results.contains(where: { $0.statusKey == "publish" })) + #expect(results.contains(where: { $0.statusKey == "private" })) + #expect(!results.contains(where: { $0.statusKey == "trash" })) + #expect(!results.contains(where: { $0.statusKey == "draft" })) + #expect(!results.contains(where: { $0.statusKey == "pending" })) + } + // MARK: - Site Isolation Tests @Test("posProductSearch only returns products from specified site")