-
Notifications
You must be signed in to change notification settings - Fork 121
[Woo POS] Coupons: Sync from remote #15423
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
Changes from all commits
99109bc
d47f648
0f236e7
3f240a2
1a23559
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,7 +1,6 @@ | ||||||
| import protocol Networking.Network | ||||||
| import protocol Networking.ProductVariationsRemoteProtocol | ||||||
| import class Networking.ProductsRemote | ||||||
| import class Networking.ProductVariationsRemote | ||||||
| import class Networking.CouponsRemote | ||||||
| import class Networking.AlamofireNetwork | ||||||
| import class WooFoundation.CurrencyFormatter | ||||||
| import class WooFoundation.CurrencySettings | ||||||
|
|
@@ -10,52 +9,56 @@ import Storage | |||||
| public final class PointOfSaleCouponService: PointOfSaleItemServiceProtocol { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Theoretically, Having a separate protocol allows us to define an interface that would support our needs. E.g., notifying a controller when remote updates.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a very good point, I initially stuck with conforming to the As long as we keep the same protocol for the controller, we can switch them in the dashboard to switch between products and coupons ( |
||||||
| private var siteID: Int64 | ||||||
| private let currencyFormatter: CurrencyFormatter | ||||||
| private let productsRemote: ProductsRemote | ||||||
| private let variationRemote: ProductVariationsRemoteProtocol | ||||||
| private let couponsRemote: CouponsRemote | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use a Or is it preferred to mock
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question, I'm not sure what we'd want to test better right now: Passing a predefined response from mocking the network, or mocking the remote logic, since a lot of it should be covered by pre-existing tests. Perhaps both! I'll take a look into it asap separately with some unit tests. |
||||||
| private let stores: StoresManager? | ||||||
| private let storage: StorageManagerType? | ||||||
|
|
||||||
| public init(siteID: Int64, | ||||||
| currencySettings: CurrencySettings, | ||||||
| network: Network, | ||||||
| stores: StoresManager? = nil, | ||||||
| storage: StorageManagerType? = nil) { | ||||||
| self.siteID = siteID | ||||||
| self.currencyFormatter = CurrencyFormatter(currencySettings: currencySettings) | ||||||
| self.productsRemote = ProductsRemote(network: network) | ||||||
| self.variationRemote = ProductVariationsRemote(network: network) | ||||||
| self.couponsRemote = CouponsRemote(network: network) | ||||||
| self.stores = stores | ||||||
| self.storage = storage | ||||||
| } | ||||||
|
|
||||||
| public convenience init(siteID: Int64, | ||||||
| currencySettings: CurrencySettings, | ||||||
| credentials: Credentials?, | ||||||
| stores: StoresManager, | ||||||
| storage: StorageManagerType) { | ||||||
| self.init(siteID: siteID, | ||||||
| currencySettings: currencySettings, | ||||||
| network: AlamofireNetwork(credentials: credentials), | ||||||
| stores: stores, | ||||||
| storage: storage) | ||||||
| } | ||||||
|
|
||||||
| // TODO: | ||||||
| // gh-15326 - Return PagedItems<POSItem> instead. | ||||||
| @MainActor | ||||||
| public func providePointOfSaleCoupons() -> [POSItem] { | ||||||
| public func providePointOfSaleCoupons() async -> [POSItem] { | ||||||
| guard let storage = storage else { | ||||||
| return [] | ||||||
| } | ||||||
|
|
||||||
| let predicate = NSPredicate(format: "siteID == %lld", siteID) | ||||||
| let descriptor = NSSortDescriptor(keyPath: \StorageCoupon.dateCreated, | ||||||
| ascending: false) | ||||||
|
|
||||||
| let resultsController = ResultsController<StorageCoupon>(storageManager: storage, | ||||||
| matching: predicate, | ||||||
| sortedBy: [descriptor]) | ||||||
| matching: predicate, | ||||||
| sortedBy: [descriptor]) | ||||||
|
|
||||||
| do { | ||||||
| try resultsController.performFetch() | ||||||
| let storeCoupons = resultsController.fetchedObjects | ||||||
| return mapCouponsToPOSItems(coupons: storeCoupons) | ||||||
| let storageCoupons = resultsController.fetchedObjects | ||||||
| return mapCouponsToPOSItems(coupons: storageCoupons) | ||||||
| } catch { | ||||||
| debugPrint(error) | ||||||
| debugPrint("Failed to load coupons from storage:", error) | ||||||
| return [] | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -73,7 +76,39 @@ public final class PointOfSaleCouponService: PointOfSaleItemServiceProtocol { | |||||
|
|
||||||
| @MainActor | ||||||
| public func providePointOfSaleItems(pageNumber: Int) async throws -> PagedItems<POSItem> { | ||||||
| let coupons = providePointOfSaleCoupons() | ||||||
| return .init(items: coupons, hasMorePages: false) | ||||||
| let coupons = await providePointOfSaleCoupons() | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idea: If coupons is empty, we could wait for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds good, updated here: 1a23559 . I was a bit wary of using a detached Task attached to a dispatcher action, but from testing seems to work fine, let me know if raises any red flag:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think it's a right usage of a detached Task. We want the storage to be updated with the newest coupons independent from the |
||||||
|
|
||||||
| if !coupons.isEmpty { | ||||||
| // Fire-and-forget sync | ||||||
| Task.detached { | ||||||
| await self.syncCouponsFromRemote(pageNumber: pageNumber) | ||||||
| } | ||||||
| return .init(items: coupons, hasMorePages: false) | ||||||
| } else { | ||||||
| // Wait for the sync to complete | ||||||
| await syncCouponsFromRemote(pageNumber: pageNumber) | ||||||
| let refreshedCoupons = await providePointOfSaleCoupons() | ||||||
| return .init(items: refreshedCoupons, hasMorePages: false) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private func syncCouponsFromRemote(pageNumber: Int) async { | ||||||
| guard let stores = stores else { | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| await withCheckedContinuation { continuation in | ||||||
| let action = CouponAction.synchronizeCoupons( | ||||||
| siteID: siteID, | ||||||
| pageNumber: pageNumber, | ||||||
| pageSize: 25, | ||||||
| onCompletion: { _ in | ||||||
| continuation.resume() | ||||||
| } | ||||||
| ) | ||||||
| Task { @MainActor in | ||||||
| stores.dispatch(action) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super nit; not loading all coupons here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure to understand:
loadAllCouponsloads all whileloadCouponsloads a subset which we know the couponIDs of, right? "makes a single request for a list of coupons" but we don't have that list so we have to fetch them all.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just meant from a readability point of view.
loadAllCouponssounds like we're loading ALL coupons, rather than specific page. But I see now thatCouponsRemoteProtocolhasloadAllCouponsandloadCouponswhich is different. 👍 So we can keep it as it is. 👍There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah gotcha, I agree the names are a bit confusing thought, I'll see if I can sneak a task separately to rename these better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine, not necessary 👍