Skip to content

Commit 0323407

Browse files
committed
Merge branch 'trunk' into woomob-1335-woo-poslocal-catalog-perform-full-sync-on-cellular-data
2 parents 19a75ae + 550dd42 commit 0323407

File tree

59 files changed

+1217
-303
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1217
-303
lines changed

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
102102
return buildConfig == .localDeveloper || buildConfig == .alpha
103103
case .pointOfSaleSettingsCardReaderFlow:
104104
return buildConfig == .localDeveloper || buildConfig == .alpha
105+
case .pointOfSaleCatalogAPI:
106+
return false
105107
default:
106108
return true
107109
}

Modules/Sources/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,8 @@ public enum FeatureFlag: Int {
211211
/// Enables card reader connection flow within POS settings
212212
///
213213
case pointOfSaleSettingsCardReaderFlow
214+
215+
/// Enables using the catalog API endpoint for Point of Sale catalog full sync
216+
///
217+
case pointOfSaleCatalogAPI
214218
}

Modules/Sources/Networking/Remote/BookingsRemote.swift

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ public protocol BookingsRemoteProtocol {
99
func loadAllBookings(for siteID: Int64,
1010
pageNumber: Int,
1111
pageSize: Int,
12-
startDateBefore: String?,
13-
startDateAfter: String?,
12+
filters: BookingFilters?,
1413
searchQuery: String?,
1514
order: BookingsRemote.Order) async throws -> [Booking]
1615

@@ -31,6 +30,35 @@ public protocol BookingsRemoteProtocol {
3130
pageSize: Int) async throws -> [BookingResource]
3231
}
3332

33+
/// Filters for booking queries
34+
public struct BookingFilters {
35+
public let productIDs: [Int64]
36+
public let customerIDs: [Int64]
37+
public let resourceIDs: [Int64]
38+
public let startDateBefore: String?
39+
public let startDateAfter: String?
40+
public let bookingStatuses: [String]
41+
public let attendanceStatuses: [String]
42+
43+
public init(
44+
productIDs: [Int64] = [],
45+
customerIDs: [Int64] = [],
46+
resourceIDs: [Int64] = [],
47+
startDateBefore: String? = nil,
48+
startDateAfter: String? = nil,
49+
bookingStatuses: [String] = [],
50+
attendanceStatuses: [String] = []
51+
) {
52+
self.productIDs = productIDs
53+
self.customerIDs = customerIDs
54+
self.resourceIDs = resourceIDs
55+
self.startDateBefore = startDateBefore
56+
self.startDateAfter = startDateAfter
57+
self.bookingStatuses = bookingStatuses
58+
self.attendanceStatuses = attendanceStatuses
59+
}
60+
}
61+
3462
/// Booking: Remote Endpoints
3563
///
3664
public final class BookingsRemote: Remote, BookingsRemoteProtocol {
@@ -43,30 +71,51 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
4371
/// - siteID: Site for which we'll fetch remote bookings.
4472
/// - pageNumber: Number of page that should be retrieved.
4573
/// - pageSize: Number of bookings to be retrieved per page.
46-
/// - startDateBefore: Filter bookings with start date before this timestamp.
47-
/// - startDateAfter: Filter bookings with start date after this timestamp.
74+
/// - filters: Optional filters for bookings (products, customers, resources, dates, statuses).
4875
/// - searchQuery: Search query to filter bookings.
4976
/// - order: Sort order for bookings (ascending or descending).
5077
///
5178
public func loadAllBookings(for siteID: Int64,
5279
pageNumber: Int = Default.pageNumber,
5380
pageSize: Int = Default.pageSize,
54-
startDateBefore: String? = nil,
55-
startDateAfter: String? = nil,
81+
filters: BookingFilters? = nil,
5682
searchQuery: String? = nil,
5783
order: Order) async throws -> [Booking] {
58-
var parameters = [
84+
var parameters: [String: Any] = [
5985
ParameterKey.page: String(pageNumber),
6086
ParameterKey.perPage: String(pageSize),
6187
ParameterKey.order: order.rawValue
6288
]
6389

64-
if let startDateBefore = startDateBefore {
65-
parameters[ParameterKey.startDateBefore] = startDateBefore
66-
}
90+
// Apply filters if provided
91+
if let filters {
92+
if filters.productIDs.isNotEmpty {
93+
parameters[ParameterKey.product] = filters.productIDs.map(String.init)
94+
}
95+
96+
if filters.customerIDs.isNotEmpty {
97+
parameters[ParameterKey.customer] = filters.customerIDs.map(String.init)
98+
}
99+
100+
if filters.resourceIDs.isNotEmpty {
101+
parameters[ParameterKey.resource] = filters.resourceIDs.map(String.init)
102+
}
103+
104+
if let startDateBefore = filters.startDateBefore {
105+
parameters[ParameterKey.startDateBefore] = startDateBefore
106+
}
107+
108+
if let startDateAfter = filters.startDateAfter {
109+
parameters[ParameterKey.startDateAfter] = startDateAfter
110+
}
111+
112+
if filters.bookingStatuses.isNotEmpty {
113+
parameters[ParameterKey.bookingStatus] = filters.bookingStatuses
114+
}
67115

68-
if let startDateAfter = startDateAfter {
69-
parameters[ParameterKey.startDateAfter] = startDateAfter
116+
if filters.attendanceStatuses.isNotEmpty {
117+
parameters[ParameterKey.attendanceStatus] = filters.attendanceStatuses
118+
}
70119
}
71120

72121
if let searchQuery = searchQuery, !searchQuery.isEmpty {
@@ -195,6 +244,10 @@ public extension BookingsRemote {
195244
static let startDateAfter: String = "start_date_after"
196245
static let search: String = "search"
197246
static let order: String = "order"
247+
static let product: String = "product"
248+
static let customer: String = "customer"
249+
static let resource: String = "resource"
250+
static let bookingStatus: String = "booking_status"
198251
static let attendanceStatus = "attendance_status"
199252
}
200253
}

Modules/Sources/Networking/Remote/POSCatalogSyncRemote.swift

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,25 @@ public protocol POSCatalogSyncRemoteProtocol {
2424
// periphery:ignore
2525
func loadProductVariations(modifiedAfter: Date, siteID: Int64, pageNumber: Int) async throws -> PagedItems<POSProductVariation>
2626

27+
/// Starts generation of a POS catalog.
28+
/// The catalog is generated asynchronously and a download URL may be returned when the file is ready.
29+
///
30+
/// - Parameters:
31+
/// - siteID: Site ID to generate catalog for.
32+
/// - forceGeneration: Whether to always generate a catalog.
33+
/// - Returns: Catalog job response with job ID.
34+
///
35+
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
36+
func requestCatalogGeneration(for siteID: Int64, forceGeneration: Bool) async throws -> POSCatalogRequestResponse
37+
38+
/// Downloads the generated catalog at the specified download URL.
39+
/// - Parameters:
40+
/// - siteID: Site ID to download catalog for.
41+
/// - downloadURL: Download URL of the catalog file.
42+
/// - Returns: List of products and variations in the POS catalog.
43+
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
44+
func downloadCatalog(for siteID: Int64, downloadURL: String) async throws -> POSCatalogResponse
45+
2746
/// Loads POS products for full sync.
2847
///
2948
/// - Parameters:
@@ -127,6 +146,53 @@ public class POSCatalogSyncRemote: Remote, POSCatalogSyncRemoteProtocol {
127146

128147
// MARK: - Full Sync Endpoints
129148

149+
/// Starts generation of a POS catalog.
150+
/// The catalog is generated asynchronously and a download URL may be returned immediately or via the status response endpoint associated with a job ID.
151+
///
152+
/// - Parameters:
153+
/// - siteID: Site ID to generate catalog for.
154+
/// - Returns: Catalog job response with job ID.
155+
///
156+
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
157+
public func requestCatalogGeneration(for siteID: Int64, forceGeneration: Bool) async throws -> POSCatalogRequestResponse {
158+
let path = "products/catalog"
159+
let parameters: [String: Any] = [
160+
ParameterKey.fullSyncFields: POSProduct.requestFields,
161+
ParameterKey.forceGenerate: forceGeneration
162+
]
163+
let request = JetpackRequest(
164+
wooApiVersion: .mark3,
165+
method: .post,
166+
siteID: siteID,
167+
path: path,
168+
parameters: parameters,
169+
availableAsRESTRequest: true
170+
)
171+
let mapper = SingleItemMapper<POSCatalogRequestResponse>(siteID: siteID)
172+
return try await enqueue(request, mapper: mapper)
173+
}
174+
175+
/// Downloads the generated catalog at the specified download URL.
176+
/// - Parameters:
177+
/// - siteID: Site ID to download catalog for.
178+
/// - downloadURL: Download URL of the catalog file.
179+
/// - Returns: List of products and variations in the POS catalog.
180+
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
181+
public func downloadCatalog(for siteID: Int64, downloadURL: String) async throws -> POSCatalogResponse {
182+
// TODO: WOOMOB-1173 - move download task to the background using `URLSessionConfiguration.background`
183+
guard let url = URL(string: downloadURL) else {
184+
throw NetworkError.invalidURL
185+
}
186+
let request = URLRequest(url: url)
187+
let mapper = ListMapper<POSProduct>(siteID: siteID)
188+
let items = try await enqueue(request, mapper: mapper)
189+
let variationProductTypeKey = "variation"
190+
let products = items.filter { $0.productTypeKey != variationProductTypeKey }
191+
let variations = items.filter { $0.productTypeKey == variationProductTypeKey }
192+
.map { $0.toVariation }
193+
return POSCatalogResponse(products: products, variations: variations)
194+
}
195+
130196
/// Loads POS products for full sync.
131197
///
132198
/// - Parameters:
@@ -252,10 +318,69 @@ private extension POSCatalogSyncRemote {
252318
static let page = "page"
253319
static let perPage = "per_page"
254320
static let fields = "_fields"
321+
static let fullSyncFields = "fields"
322+
static let forceGenerate = "force_generate"
255323
}
256324

257325
enum Path {
258326
static let products = "products"
259327
static let variations = "variations"
260328
}
261329
}
330+
331+
// MARK: - Response Models
332+
333+
/// Response from catalog generation request.
334+
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
335+
public struct POSCatalogRequestResponse: Decodable {
336+
/// Current status of the catalog generation job.
337+
public let status: POSCatalogStatus
338+
/// Download URL when it is already available.
339+
public let downloadURL: String?
340+
341+
private enum CodingKeys: String, CodingKey {
342+
case status
343+
case downloadURL = "download_url"
344+
}
345+
}
346+
347+
/// Catalog generation status.
348+
public enum POSCatalogStatus: String, Decodable {
349+
case pending
350+
case processing
351+
case complete
352+
case failed
353+
}
354+
355+
/// POS catalog from download.
356+
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
357+
public struct POSCatalogResponse {
358+
public let products: [POSProduct]
359+
public let variations: [POSProductVariation]
360+
}
361+
362+
private extension POSProduct {
363+
var toVariation: POSProductVariation {
364+
let variationAttributes = attributes.compactMap { attribute in
365+
try? attribute.toProductVariationAttribute()
366+
}
367+
368+
let firstImage = images.first
369+
370+
return .init(
371+
siteID: siteID,
372+
productID: parentID,
373+
productVariationID: productID,
374+
attributes: variationAttributes,
375+
image: firstImage,
376+
fullDescription: fullDescription,
377+
sku: sku,
378+
globalUniqueID: globalUniqueID,
379+
price: price,
380+
downloadable: downloadable,
381+
manageStock: manageStock,
382+
stockQuantity: stockQuantity,
383+
stockStatusKey: stockStatusKey
384+
)
385+
}
386+
}

Modules/Sources/Networking/Remote/ProductsRemote.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public protocol ProductsRemoteProtocol {
2323
excludedProductIDs: [Int64]) async throws -> [Product]
2424
func searchProducts(for siteID: Int64,
2525
keyword: String,
26+
searchFields: [ProductSearchField],
2627
pageNumber: Int,
2728
pageSize: Int,
2829
stockStatus: ProductStockStatus?,
@@ -291,7 +292,7 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
291292
}
292293

293294
/// Remote search of products for the Point of Sale. Simple and variable products are loaded for WC version 9.6+, otherwise only simple products are loaded.
294-
/// `search` is used, which searches in `name`, `description`, `short_description` fields.
295+
/// `search` is used, which searches in `name`, `sku`, `globalUniqueID` fields.
295296
/// We also send `search_name_or_sku`, which will be used in preference to `search` when implemented on a site (in future.)
296297
///
297298
/// - Parameter siteID: Site for which we'll fetch remote products.
@@ -313,7 +314,11 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
313314
parameters.updateValue(query, forKey: ParameterKey.searchNameOrSKU)
314315

315316
// Takes precedence over `search_name_or_sku` from WC 10.1+ and is combined with `search` value
316-
parameters.updateValue([SearchField.name, SearchField.sku, SearchField.globalUniqueID], forKey: ParameterKey.searchFields)
317+
parameters.updateValue([
318+
ProductSearchField.name.rawValue,
319+
ProductSearchField.sku.rawValue,
320+
ProductSearchField.globalUniqueID.rawValue
321+
], forKey: ParameterKey.searchFields)
317322

318323
return try await makePagedPointOfSaleProductsRequest(
319324
for: siteID,
@@ -429,6 +434,7 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
429434
///
430435
public func searchProducts(for siteID: Int64,
431436
keyword: String,
437+
searchFields: [ProductSearchField],
432438
pageNumber: Int,
433439
pageSize: Int,
434440
stockStatus: ProductStockStatus? = nil,
@@ -447,10 +453,11 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
447453
ParameterKey.exclude: stringOfExcludedProductIDs
448454
].filter({ $0.value.isEmpty == false })
449455

450-
let parameters = [
456+
let parameters: [String: Any] = [
451457
ParameterKey.page: String(pageNumber),
452458
ParameterKey.perPage: String(pageSize),
453459
ParameterKey.search: keyword,
460+
ParameterKey.searchFields: searchFields.map { $0.rawValue },
454461
ParameterKey.exclude: stringOfExcludedProductIDs,
455462
ParameterKey.contextKey: Default.context
456463
].merging(filterParameters, uniquingKeysWith: { (first, _) in first })
@@ -774,12 +781,12 @@ public extension ProductsRemote {
774781
static let productSegment = "product"
775782
static let itemsSold = "items_sold"
776783
}
784+
}
777785

778-
private enum SearchField {
779-
static let name = "name"
780-
static let sku = "sku"
781-
static let globalUniqueID = "global_unique_id"
782-
}
786+
public enum ProductSearchField: String {
787+
case name
788+
case sku
789+
case globalUniqueID = "global_unique_id"
783790
}
784791

785792
private extension ProductsRemote {

Modules/Sources/PointOfSale/Presentation/Settings/POSSettingsLocalCatalogViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ final class POSSettingsLocalCatalogViewModel {
5353
defer { isRefreshingCatalog = false }
5454

5555
do {
56-
try await catalogSyncCoordinator.performFullSync(for: siteID)
56+
try await catalogSyncCoordinator.performFullSync(for: siteID, regenerateCatalog: true)
5757
await loadCatalogData()
5858
} catch {
5959
DDLogError("⛔️ POSSettingsLocalCatalog: Failed to refresh catalog: \(error)")

Modules/Sources/PointOfSale/Utils/PreviewHelpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ final class POSPreviewCatalogSettingsService: POSCatalogSettingsServiceProtocol
617617
}
618618

619619
final class POSPreviewCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
620-
func performFullSyncIfApplicable(for siteID: Int64, maxAge: TimeInterval) async throws {
620+
func performFullSyncIfApplicable(for siteID: Int64, maxAge: TimeInterval, regenerateCatalog: Bool) async throws {
621621
// Simulates a full sync operation with a 1 second delay.
622622
try await Task.sleep(nanoseconds: 1_000_000_000)
623623
}

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ public enum BookingAction: Action {
1313
case synchronizeBookings(siteID: Int64,
1414
pageNumber: Int,
1515
pageSize: Int = BookingsRemote.Default.pageSize,
16-
startDateBefore: String? = nil,
17-
startDateAfter: String? = nil,
16+
filters: BookingFilters? = nil,
1817
order: BookingsRemote.Order = .descending,
1918
shouldClearCache: Bool = false,
2019
onCompletion: (Result<Bool, Error>) -> Void)
@@ -40,8 +39,7 @@ public enum BookingAction: Action {
4039
searchQuery: String,
4140
pageNumber: Int,
4241
pageSize: Int = BookingsRemote.Default.pageSize,
43-
startDateBefore: String? = nil,
44-
startDateAfter: String? = nil,
42+
filters: BookingFilters? = nil,
4543
order: BookingsRemote.Order = .descending,
4644
onCompletion: (Result<[Booking], Error>) -> Void)
4745

0 commit comments

Comments
 (0)