Skip to content

Commit dfab840

Browse files
authored
Bookings: Update tab eligibility to include booking check (#16172)
2 parents bf2777e + 632fa60 commit dfab840

File tree

5 files changed

+251
-4
lines changed

5 files changed

+251
-4
lines changed

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ public enum BookingAction: Action {
1414
pageNumber: Int,
1515
pageSize: Int = BookingsRemote.Default.pageSize,
1616
onCompletion: (Result<Bool, Error>) -> Void)
17+
18+
/// Checks if the store already has any bookings.
19+
/// Returns `false` if the store has no bookings.
20+
///
21+
case checkIfStoreHasBookings(siteID: Int64,
22+
onCompletion: (Result<Bool, Error>) -> Void)
1723
}

Modules/Sources/Yosemite/Stores/BookingStore.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class BookingStore: Store {
3737
switch action {
3838
case let .synchronizeBookings(siteID, pageNumber, pageSize, onCompletion):
3939
synchronizeBookings(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
40+
case let .checkIfStoreHasBookings(siteID, onCompletion):
41+
checkIfStoreHasBookings(siteID: siteID, onCompletion: onCompletion)
4042
}
4143
}
4244
}
@@ -65,6 +67,31 @@ private extension BookingStore {
6567
}
6668
}
6769
}
70+
71+
/// Checks if the store already has any bookings.
72+
/// Returns `false` if the store has no bookings.
73+
///
74+
func checkIfStoreHasBookings(siteID: Int64, onCompletion: @escaping (Result<Bool, Error>) -> Void) {
75+
let derivedStorage = storageManager.viewStorage
76+
let hasLocalBookings = derivedStorage.countObjects(ofType: StorageBooking.self, matching: NSPredicate(format: "siteID == %lld", siteID)) > 0
77+
78+
if hasLocalBookings {
79+
onCompletion(.success(true))
80+
return
81+
}
82+
83+
Task { @MainActor in
84+
do {
85+
let bookings = try await remote.loadAllBookings(for: siteID,
86+
pageNumber: 1,
87+
pageSize: 1)
88+
let hasRemoteBookings = !bookings.isEmpty
89+
onCompletion(.success(hasRemoteBookings))
90+
} catch {
91+
onCompletion(.failure(error))
92+
}
93+
}
94+
}
6895
}
6996

7097

Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,103 @@ struct BookingStoreTests {
206206
#expect(result.isSuccess)
207207
#expect(storedBookingCount == 2)
208208
}
209+
210+
// MARK: - checkIfStoreHasBookings
211+
212+
@Test func checkIfStoreHasBookings_returns_true_when_bookings_exist_locally() async throws {
213+
// Given
214+
let booking = Booking.fake().copy(siteID: sampleSiteID, bookingID: 123)
215+
storeBooking(booking)
216+
#expect(storedBookingCount == 1)
217+
218+
let store = BookingStore(dispatcher: Dispatcher(),
219+
storageManager: storageManager,
220+
network: network,
221+
remote: remote)
222+
223+
// When
224+
let result = await withCheckedContinuation { continuation in
225+
store.onAction(BookingAction.checkIfStoreHasBookings(siteID: sampleSiteID,
226+
onCompletion: { result in
227+
continuation.resume(returning: result)
228+
}))
229+
}
230+
231+
// Then
232+
let hasBookings = try result.get()
233+
#expect(hasBookings == true)
234+
}
235+
236+
@Test func checkIfStoreHasBookings_returns_true_when_no_local_bookings_but_remote_has_bookings() async throws {
237+
// Given
238+
#expect(storedBookingCount == 0)
239+
let remoteBooking = Booking.fake()
240+
remote.whenLoadingAllBookings(thenReturn: .success([remoteBooking]))
241+
242+
let store = BookingStore(dispatcher: Dispatcher(),
243+
storageManager: storageManager,
244+
network: network,
245+
remote: remote)
246+
247+
// When
248+
let result = await withCheckedContinuation { continuation in
249+
store.onAction(BookingAction.checkIfStoreHasBookings(siteID: sampleSiteID,
250+
onCompletion: { result in
251+
continuation.resume(returning: result)
252+
}))
253+
}
254+
255+
// Then
256+
let hasBookings = try result.get()
257+
#expect(hasBookings == true)
258+
}
259+
260+
@Test func checkIfStoreHasBookings_returns_false_when_no_bookings_exist_locally_or_remotely() async throws {
261+
// Given
262+
#expect(storedBookingCount == 0)
263+
remote.whenLoadingAllBookings(thenReturn: .success([]))
264+
265+
let store = BookingStore(dispatcher: Dispatcher(),
266+
storageManager: storageManager,
267+
network: network,
268+
remote: remote)
269+
270+
// When
271+
let result = await withCheckedContinuation { continuation in
272+
store.onAction(BookingAction.checkIfStoreHasBookings(siteID: sampleSiteID,
273+
onCompletion: { result in
274+
continuation.resume(returning: result)
275+
}))
276+
}
277+
278+
// Then
279+
let hasBookings = try result.get()
280+
#expect(hasBookings == false)
281+
}
282+
283+
@Test func checkIfStoreHasBookings_returns_error_on_remote_failure() async throws {
284+
// Given
285+
#expect(storedBookingCount == 0)
286+
remote.whenLoadingAllBookings(thenReturn: .failure(NetworkError.timeout()))
287+
288+
let store = BookingStore(dispatcher: Dispatcher(),
289+
storageManager: storageManager,
290+
network: network,
291+
remote: remote)
292+
293+
// When
294+
let result = await withCheckedContinuation { continuation in
295+
store.onAction(BookingAction.checkIfStoreHasBookings(siteID: sampleSiteID,
296+
onCompletion: { result in
297+
continuation.resume(returning: result)
298+
}))
299+
}
300+
301+
// Then
302+
#expect(result.isFailure)
303+
let error = result.failure as? NetworkError
304+
#expect(error == .timeout())
305+
}
209306
}
210307

211308
private extension BookingStoreTests {

WooCommerce/Classes/Bookings/BookingsTabEligibilityChecker.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,18 @@ final class BookingsTabEligibilityChecker: BookingsTabEligibilityCheckerProtocol
4545
return false
4646
}
4747

48+
// Check if store has bookable products or bookings
49+
let isVisible: Bool = await {
50+
if await checkIfStoreHasBookableProducts() {
51+
return true
52+
} else if await checkIfStoreHasBookings() {
53+
return true
54+
} else {
55+
return false
56+
}
57+
}()
58+
4859
// Cache the result
49-
let isVisible = await checkIfStoreHasBookableProducts()
5060
userDefaults.cacheBookingsTabVisibility(siteID: site.siteID, isVisible: isVisible)
5161

5262
return isVisible
@@ -65,6 +75,16 @@ private extension BookingsTabEligibilityChecker {
6575
})
6676
}
6777
}
78+
79+
@MainActor
80+
func checkIfStoreHasBookings() async -> Bool {
81+
await withCheckedContinuation { continuation in
82+
stores.dispatch(BookingAction.checkIfStoreHasBookings(siteID: site.siteID) { result in
83+
let hasBookings = (try? result.get()) ?? false
84+
continuation.resume(returning: hasBookings)
85+
})
86+
}
87+
}
6888
}
6989

7090
extension UserDefaults {

WooCommerce/WooCommerceTests/Bookings/BookingsTabEligibilityCheckerTests.swift

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ struct BookingsTabEligibilityCheckerTests {
9898
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
9999
let ciabEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: false)
100100
setupStoreHasBookableProducts(hasProducts: true)
101+
setupStoreHasBookings(hasBookings: true)
101102
let checker = BookingsTabEligibilityChecker(site: site,
102103
stores: stores,
103104
featureFlagService: featureFlagService,
@@ -111,11 +112,12 @@ struct BookingsTabEligibilityCheckerTests {
111112
#expect(result == false)
112113
}
113114

114-
@Test func checkVisibility_returns_false_when_store_has_no_bookable_products() async throws {
115+
@Test func checkVisibility_returns_false_when_store_has_no_bookable_products_and_no_bookings() async throws {
115116
// Given
116117
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
117118
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
118119
setupStoreHasBookableProducts(hasProducts: false)
120+
setupStoreHasBookings(hasBookings: false)
119121
let checker = BookingsTabEligibilityChecker(site: site,
120122
stores: stores,
121123
featureFlagService: featureFlagService,
@@ -129,11 +131,12 @@ struct BookingsTabEligibilityCheckerTests {
129131
#expect(result == false)
130132
}
131133

132-
@Test func checkVisibility_returns_true_when_all_conditions_are_satisfied() async throws {
134+
@Test func checkVisibility_returns_true_when_store_has_bookable_products() async throws {
133135
// Given
134136
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
135137
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
136138
setupStoreHasBookableProducts(hasProducts: true)
139+
setupStoreHasBookings(hasBookings: false)
137140
let checker = BookingsTabEligibilityChecker(site: site,
138141
stores: stores,
139142
featureFlagService: featureFlagService,
@@ -152,6 +155,7 @@ struct BookingsTabEligibilityCheckerTests {
152155
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
153156
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
154157
setupStoreHasBookableProducts(hasProducts: true)
158+
setupStoreHasBookings(hasBookings: false)
155159
let checker = BookingsTabEligibilityChecker(site: site,
156160
stores: stores,
157161
featureFlagService: featureFlagService,
@@ -171,6 +175,7 @@ struct BookingsTabEligibilityCheckerTests {
171175
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
172176
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
173177
setupStoreHasBookableProducts(hasProducts: false)
178+
setupStoreHasBookings(hasBookings: false)
174179
let checker = BookingsTabEligibilityChecker(site: site,
175180
stores: stores,
176181
featureFlagService: featureFlagService,
@@ -185,11 +190,88 @@ struct BookingsTabEligibilityCheckerTests {
185190
#expect(userDefaults.loadCachedBookingsTabVisibility(siteID: siteID) == false)
186191
}
187192

188-
@Test func checkVisibility_handles_store_check_failure_gracefully() async throws {
193+
@Test func checkVisibility_handles_bookable_products_check_failure_gracefully() async throws {
189194
// Given
190195
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
191196
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
192197
setupStoreHasBookableProducts(hasProducts: false, shouldFail: true)
198+
setupStoreHasBookings(hasBookings: false)
199+
let checker = BookingsTabEligibilityChecker(site: site,
200+
stores: stores,
201+
featureFlagService: featureFlagService,
202+
ciabEligibilityChecker: ciabEligibilityChecker,
203+
userDefaults: userDefaults)
204+
205+
// When
206+
let result = await checker.checkVisibility()
207+
208+
// Then
209+
#expect(result == false)
210+
}
211+
212+
@Test func checkVisibility_returns_true_when_store_has_bookings_but_no_bookable_products() async throws {
213+
// Given
214+
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
215+
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
216+
setupStoreHasBookableProducts(hasProducts: false)
217+
setupStoreHasBookings(hasBookings: true)
218+
let checker = BookingsTabEligibilityChecker(site: site,
219+
stores: stores,
220+
featureFlagService: featureFlagService,
221+
ciabEligibilityChecker: ciabEligibilityChecker,
222+
userDefaults: userDefaults)
223+
224+
// When
225+
let result = await checker.checkVisibility()
226+
227+
// Then
228+
#expect(result == true)
229+
}
230+
231+
@Test func checkVisibility_returns_true_when_store_has_both_bookings_and_bookable_products() async throws {
232+
// Given
233+
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
234+
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
235+
setupStoreHasBookableProducts(hasProducts: true)
236+
setupStoreHasBookings(hasBookings: true)
237+
let checker = BookingsTabEligibilityChecker(site: site,
238+
stores: stores,
239+
featureFlagService: featureFlagService,
240+
ciabEligibilityChecker: ciabEligibilityChecker,
241+
userDefaults: userDefaults)
242+
243+
// When
244+
let result = await checker.checkVisibility()
245+
246+
// Then
247+
#expect(result == true)
248+
}
249+
250+
@Test func checkVisibility_handles_bookings_check_failure_gracefully() async throws {
251+
// Given
252+
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
253+
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
254+
setupStoreHasBookableProducts(hasProducts: false)
255+
setupStoreHasBookings(hasBookings: false, shouldFail: true)
256+
let checker = BookingsTabEligibilityChecker(site: site,
257+
stores: stores,
258+
featureFlagService: featureFlagService,
259+
ciabEligibilityChecker: ciabEligibilityChecker,
260+
userDefaults: userDefaults)
261+
262+
// When
263+
let result = await checker.checkVisibility()
264+
265+
// Then
266+
#expect(result == false)
267+
}
268+
269+
@Test func checkVisibility_handles_both_checks_failure_gracefully() async throws {
270+
// Given
271+
let userDefaults = UserDefaults(suiteName: UUID().uuidString)!
272+
let featureFlagService = MockFeatureFlagService(isCIABBookingsEnabled: true)
273+
setupStoreHasBookableProducts(hasProducts: false, shouldFail: true)
274+
setupStoreHasBookings(hasBookings: false, shouldFail: true)
193275
let checker = BookingsTabEligibilityChecker(site: site,
194276
stores: stores,
195277
featureFlagService: featureFlagService,
@@ -276,4 +358,19 @@ private extension BookingsTabEligibilityCheckerTests {
276358
}
277359
}
278360
}
361+
362+
func setupStoreHasBookings(hasBookings: Bool, shouldFail: Bool = false) {
363+
stores.whenReceivingAction(ofType: BookingAction.self) { action in
364+
switch action {
365+
case .checkIfStoreHasBookings(_, let completion):
366+
if shouldFail {
367+
completion(.failure(NSError(domain: "test", code: 500)))
368+
} else {
369+
completion(.success(hasBookings))
370+
}
371+
default:
372+
break
373+
}
374+
}
375+
}
279376
}

0 commit comments

Comments
 (0)