diff --git a/Modules/Sources/Networking/Remote/BookingsRemote.swift b/Modules/Sources/Networking/Remote/BookingsRemote.swift index b33aa2b5b71..84b3afa86d5 100644 --- a/Modules/Sources/Networking/Remote/BookingsRemote.swift +++ b/Modules/Sources/Networking/Remote/BookingsRemote.swift @@ -9,8 +9,7 @@ public protocol BookingsRemoteProtocol { func loadAllBookings(for siteID: Int64, pageNumber: Int, pageSize: Int, - startDateBefore: String?, - startDateAfter: String?, + filters: BookingFilters?, searchQuery: String?, order: BookingsRemote.Order) async throws -> [Booking] @@ -31,6 +30,35 @@ public protocol BookingsRemoteProtocol { pageSize: Int) async throws -> [BookingResource] } +/// Filters for booking queries +public struct BookingFilters { + public let productIDs: [Int64] + public let customerIDs: [Int64] + public let resourceIDs: [Int64] + public let startDateBefore: String? + public let startDateAfter: String? + public let bookingStatuses: [String] + public let attendanceStatuses: [String] + + public init( + productIDs: [Int64] = [], + customerIDs: [Int64] = [], + resourceIDs: [Int64] = [], + startDateBefore: String? = nil, + startDateAfter: String? = nil, + bookingStatuses: [String] = [], + attendanceStatuses: [String] = [] + ) { + self.productIDs = productIDs + self.customerIDs = customerIDs + self.resourceIDs = resourceIDs + self.startDateBefore = startDateBefore + self.startDateAfter = startDateAfter + self.bookingStatuses = bookingStatuses + self.attendanceStatuses = attendanceStatuses + } +} + /// Booking: Remote Endpoints /// public final class BookingsRemote: Remote, BookingsRemoteProtocol { @@ -43,30 +71,51 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol { /// - siteID: Site for which we'll fetch remote bookings. /// - pageNumber: Number of page that should be retrieved. /// - pageSize: Number of bookings to be retrieved per page. - /// - startDateBefore: Filter bookings with start date before this timestamp. - /// - startDateAfter: Filter bookings with start date after this timestamp. + /// - filters: Optional filters for bookings (products, customers, resources, dates, statuses). /// - searchQuery: Search query to filter bookings. /// - order: Sort order for bookings (ascending or descending). /// public func loadAllBookings(for siteID: Int64, pageNumber: Int = Default.pageNumber, pageSize: Int = Default.pageSize, - startDateBefore: String? = nil, - startDateAfter: String? = nil, + filters: BookingFilters? = nil, searchQuery: String? = nil, order: Order) async throws -> [Booking] { - var parameters = [ + var parameters: [String: Any] = [ ParameterKey.page: String(pageNumber), ParameterKey.perPage: String(pageSize), ParameterKey.order: order.rawValue ] - if let startDateBefore = startDateBefore { - parameters[ParameterKey.startDateBefore] = startDateBefore - } + // Apply filters if provided + if let filters { + if filters.productIDs.isNotEmpty { + parameters[ParameterKey.product] = filters.productIDs.map(String.init) + } + + if filters.customerIDs.isNotEmpty { + parameters[ParameterKey.customer] = filters.customerIDs.map(String.init) + } + + if filters.resourceIDs.isNotEmpty { + parameters[ParameterKey.resource] = filters.resourceIDs.map(String.init) + } + + if let startDateBefore = filters.startDateBefore { + parameters[ParameterKey.startDateBefore] = startDateBefore + } + + if let startDateAfter = filters.startDateAfter { + parameters[ParameterKey.startDateAfter] = startDateAfter + } + + if filters.bookingStatuses.isNotEmpty { + parameters[ParameterKey.bookingStatus] = filters.bookingStatuses + } - if let startDateAfter = startDateAfter { - parameters[ParameterKey.startDateAfter] = startDateAfter + if filters.attendanceStatuses.isNotEmpty { + parameters[ParameterKey.attendanceStatus] = filters.attendanceStatuses + } } if let searchQuery = searchQuery, !searchQuery.isEmpty { @@ -195,6 +244,10 @@ public extension BookingsRemote { static let startDateAfter: String = "start_date_after" static let search: String = "search" static let order: String = "order" + static let product: String = "product" + static let customer: String = "customer" + static let resource: String = "resource" + static let bookingStatus: String = "booking_status" static let attendanceStatus = "attendance_status" } } diff --git a/Modules/Sources/Yosemite/Actions/BookingAction.swift b/Modules/Sources/Yosemite/Actions/BookingAction.swift index 095089d2ef4..b6b71a33899 100644 --- a/Modules/Sources/Yosemite/Actions/BookingAction.swift +++ b/Modules/Sources/Yosemite/Actions/BookingAction.swift @@ -13,8 +13,7 @@ public enum BookingAction: Action { case synchronizeBookings(siteID: Int64, pageNumber: Int, pageSize: Int = BookingsRemote.Default.pageSize, - startDateBefore: String? = nil, - startDateAfter: String? = nil, + filters: BookingFilters? = nil, order: BookingsRemote.Order = .descending, shouldClearCache: Bool = false, onCompletion: (Result) -> Void) @@ -40,8 +39,7 @@ public enum BookingAction: Action { searchQuery: String, pageNumber: Int, pageSize: Int = BookingsRemote.Default.pageSize, - startDateBefore: String? = nil, - startDateAfter: String? = nil, + filters: BookingFilters? = nil, order: BookingsRemote.Order = .descending, onCompletion: (Result<[Booking], Error>) -> Void) diff --git a/Modules/Sources/Yosemite/Model/Model.swift b/Modules/Sources/Yosemite/Model/Model.swift index 5b683a0ad65..a877cdcaa3b 100644 --- a/Modules/Sources/Yosemite/Model/Model.swift +++ b/Modules/Sources/Yosemite/Model/Model.swift @@ -26,6 +26,7 @@ public typealias BlazeTargetOptions = Networking.BlazeTargetOptions public typealias BlazeTargetLocation = Networking.BlazeTargetLocation public typealias BlazeTargetTopic = Networking.BlazeTargetTopic public typealias Booking = Networking.Booking +public typealias BookingFilters = Networking.BookingFilters public typealias BookingStatus = Networking.BookingStatus public typealias BookingOrderInfo = Networking.BookingOrderInfo public typealias BookingCustomerInfo = Networking.BookingCustomerInfo diff --git a/Modules/Sources/Yosemite/Stores/BookingStore.swift b/Modules/Sources/Yosemite/Stores/BookingStore.swift index 916087b0a96..eb6f6b01e6f 100644 --- a/Modules/Sources/Yosemite/Stores/BookingStore.swift +++ b/Modules/Sources/Yosemite/Stores/BookingStore.swift @@ -39,12 +39,11 @@ public class BookingStore: Store { } switch action { - case let .synchronizeBookings(siteID, pageNumber, pageSize, startDateBefore, startDateAfter, order, shouldClearCache, onCompletion): + case let .synchronizeBookings(siteID, pageNumber, pageSize, filters, order, shouldClearCache, onCompletion): synchronizeBookings(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, - startDateBefore: startDateBefore, - startDateAfter: startDateAfter, + filters: filters, order: order, shouldClearCache: shouldClearCache, onCompletion: onCompletion) @@ -52,13 +51,12 @@ public class BookingStore: Store { synchronizeBooking(siteID: siteID, bookingID: bookingID, onCompletion: onCompletion) case let .checkIfStoreHasBookings(siteID, onCompletion): checkIfStoreHasBookings(siteID: siteID, onCompletion: onCompletion) - case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, startDateBefore, startDateAfter, order, onCompletion): + case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, filters, order, onCompletion): searchBookings(siteID: siteID, searchQuery: searchQuery, pageNumber: pageNumber, pageSize: pageSize, - startDateBefore: startDateBefore, - startDateAfter: startDateAfter, + filters: filters, order: order, onCompletion: onCompletion) case let .fetchResource(siteID, resourceID, onCompletion): @@ -85,8 +83,7 @@ private extension BookingStore { func synchronizeBookings(siteID: Int64, pageNumber: Int, pageSize: Int, - startDateBefore: String?, - startDateAfter: String?, + filters: BookingFilters?, order: BookingsRemote.Order, shouldClearCache: Bool, onCompletion: @escaping (Result) -> Void) { @@ -95,8 +92,7 @@ private extension BookingStore { let bookings = try await remote.loadAllBookings(for: siteID, pageNumber: pageNumber, pageSize: pageSize, - startDateBefore: startDateBefore, - startDateAfter: startDateAfter, + filters: filters, searchQuery: nil, order: order) @@ -175,8 +171,7 @@ private extension BookingStore { let bookings = try await remote.loadAllBookings(for: siteID, pageNumber: 1, pageSize: 1, - startDateBefore: nil, - startDateAfter: nil, + filters: nil, searchQuery: nil, order: .descending) let hasRemoteBookings = !bookings.isEmpty @@ -194,8 +189,7 @@ private extension BookingStore { searchQuery: String, pageNumber: Int, pageSize: Int, - startDateBefore: String?, - startDateAfter: String?, + filters: BookingFilters?, order: BookingsRemote.Order, onCompletion: @escaping (Result<[Booking], Error>) -> Void) { Task { @MainActor in @@ -203,8 +197,7 @@ private extension BookingStore { let bookings = try await remote.loadAllBookings(for: siteID, pageNumber: pageNumber, pageSize: pageSize, - startDateBefore: startDateBefore, - startDateAfter: startDateAfter, + filters: filters, searchQuery: searchQuery, order: order) let orders = try await ordersRemote.loadOrders( diff --git a/Modules/Sources/Yosemite/Tools/Bookings/ResultsController+FilterBookings.swift b/Modules/Sources/Yosemite/Tools/Bookings/ResultsController+FilterBookings.swift new file mode 100644 index 00000000000..5c7f546c068 --- /dev/null +++ b/Modules/Sources/Yosemite/Tools/Bookings/ResultsController+FilterBookings.swift @@ -0,0 +1,47 @@ +import CoreData + +extension NSPredicate { + public static func createBookingPredicate(siteID: Int64, filters: BookingFilters) -> NSPredicate { + let siteIDPredicate = NSPredicate(format: "siteID == %lld", siteID) + + let productIDsPredicate = filters.productIDs.isNotEmpty ? NSPredicate(format: "productID IN %@", filters.productIDs) : nil + + let customerIDsPredicate = filters.customerIDs.isNotEmpty ? NSPredicate(format: "customerID IN %@", filters.customerIDs) : nil + + let resourceIDsPredicate = filters.resourceIDs.isNotEmpty ? NSPredicate(format: "resourceID IN %@", filters.resourceIDs) : nil + + let startDateBeforePredicate = filters.startDateBefore.flatMap { dateString -> NSPredicate? in + guard let date = ISO8601DateFormatter().date(from: dateString) else { return nil } + return NSPredicate(format: "startDate < %@", date as NSDate) + } + + let startDateAfterPredicate = filters.startDateAfter.flatMap { dateString -> NSPredicate? in + guard let date = ISO8601DateFormatter().date(from: dateString) else { return nil } + return NSPredicate(format: "startDate > %@", date as NSDate) + } + + let bookingStatusesPredicate = filters.bookingStatuses.isNotEmpty ? NSPredicate(format: "statusKey IN %@", filters.bookingStatuses) : nil + + let attendanceStatusesPredicate = filters.attendanceStatuses.isNotEmpty ? + NSPredicate(format: "attendanceStatusKey IN %@", filters.attendanceStatuses) : nil + + let subpredicates = [ + siteIDPredicate, + productIDsPredicate, + customerIDsPredicate, + resourceIDsPredicate, + startDateBeforePredicate, + startDateAfterPredicate, + bookingStatusesPredicate, + attendanceStatusesPredicate + ].compactMap({ $0 }) + + return NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates) + } +} + +extension ResultsController where T: StorageBooking { + public func updatePredicate(siteID: Int64, filters: BookingFilters) { + self.predicate = NSPredicate.createBookingPredicate(siteID: siteID, filters: filters) + } +} diff --git a/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift b/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift index ca2e7946ab2..8fc87e9b5c9 100644 --- a/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift +++ b/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift @@ -44,14 +44,17 @@ struct BookingsRemoteTests { let startDateBefore = "2024-12-31T23:59:59" let startDateAfter = "2024-01-01T00:00:00" let searchQuery = "test search" + let filters = BookingFilters( + startDateBefore: startDateBefore, + startDateAfter: startDateAfter + ) network.simulateResponse(requestUrlSuffix: "bookings", filename: "booking-list") // When _ = try await remote.loadAllBookings(for: sampleSiteID, pageNumber: 2, pageSize: 50, - startDateBefore: startDateBefore, - startDateAfter: startDateAfter, + filters: filters, searchQuery: searchQuery, order: .ascending) @@ -74,8 +77,7 @@ struct BookingsRemoteTests { // When _ = try await remote.loadAllBookings(for: sampleSiteID, - startDateBefore: nil, - startDateAfter: nil, + filters: nil, searchQuery: nil, order: .descending) diff --git a/Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift b/Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift index 76ee035d4e7..af0202caa12 100644 --- a/Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift +++ b/Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift @@ -28,8 +28,7 @@ final class MockBookingsRemote: BookingsRemoteProtocol { func loadAllBookings(for siteID: Int64, pageNumber: Int, pageSize: Int, - startDateBefore: String?, - startDateAfter: String?, + filters: BookingFilters?, searchQuery: String?, order: BookingsRemote.Order) async throws -> [Booking] { guard let result = loadAllBookingsResult else { diff --git a/WooCommerce/Classes/Bookings/BookingFilters/BookingDateTimeFilterView.swift b/WooCommerce/Classes/Bookings/BookingFilters/BookingDateTimeFilterView.swift index d86bc5767c0..65d9a62b4f5 100644 --- a/WooCommerce/Classes/Bookings/BookingFilters/BookingDateTimeFilterView.swift +++ b/WooCommerce/Classes/Bookings/BookingFilters/BookingDateTimeFilterView.swift @@ -63,10 +63,22 @@ struct BookingDateTimeFilterView: View { selectedToDate = newValue } .onChange(of: selectedFromDate) { _, newValue in - onSelection(newValue, selectedToDate) + guard let newValue else { + return onSelection(nil, selectedToDate) + } + /// Bookings backend treats dates as local time with no time zone. + /// Convert the date to keep the selected components but with UTC as time zone. + let convertedDate = convertToUTCDate(newValue) + onSelection(convertedDate, selectedToDate) } .onChange(of: selectedToDate) { _, newValue in - onSelection(selectedFromDate, newValue) + guard let newValue else { + return onSelection(selectedFromDate, nil) + } + /// Bookings backend treats dates as local time with no time zone. + /// Convert the date to keep the selected components but with UTC as time zone. + let convertedDate = convertToUTCDate(newValue) + onSelection(selectedFromDate, convertedDate) } } } @@ -156,6 +168,19 @@ private extension BookingDateTimeFilterView { return fromDate...Date.distantFuture } } + + /// Converts a date by extracting its components in the local timezone + /// and reconstructing a new date with those same components in UTC. + /// This effectively treats the selected date/time as if it were in UTC. + func convertToUTCDate(_ date: Date) -> Date { + let localCalendar = Calendar.current + let components = localCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) + + var utcCalendar = Calendar(identifier: .gregorian) + utcCalendar.timeZone = TimeZone(identifier: "UTC")! + + return utcCalendar.date(from: components) ?? date + } } private extension BookingDateTimeFilterView { diff --git a/WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift b/WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift index fb4cc328ef4..ee7f549d659 100644 --- a/WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift +++ b/WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift @@ -135,6 +135,18 @@ final class BookingFiltersViewModel: FilterListViewModel { return readable.joined(separator: ", ") } + + var bookingFilters: BookingFilters { + BookingFilters( + productIDs: products.map { $0.productID }, + customerIDs: customers.map { $0.customerID }, + resourceIDs: teamMembers.map { $0.resourceID }, + startDateBefore: dateRange?.endDate?.ISO8601Format(), + startDateAfter: dateRange?.startDate?.ISO8601Format(), + bookingStatuses: paymentStatuses.map { $0.rawValue }, + attendanceStatuses: attendanceStatuses.map { $0.rawValue } + ) + } } } diff --git a/WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift b/WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift index 38feac6f001..09d5681cb27 100644 --- a/WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift +++ b/WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift @@ -108,7 +108,8 @@ final class BookingListContainerViewModel: ObservableObject { func updateFilters(_ filters: BookingFiltersViewModel.Filters) { self.filters = filters self.numberOfActiveFilters = filters.numberOfActiveFilters - // TODO: Apply filters to All tab + allListViewModel.updateFilters(filters) + allSearchViewModel.updateFilters(filters) } } diff --git a/WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift b/WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift index f1a255ef8a0..86f10ca8f23 100644 --- a/WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift +++ b/WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift @@ -12,10 +12,7 @@ final class BookingListViewModel: ObservableObject { @Published var errorFetching = false - var hasFilters: Bool { - // TODO: Update when adding filters - return false - } + @Published private(set) var hasFilters = false var emptyStateTitle: String { type.emptyStateTitle(hasFilters: hasFilters) @@ -29,9 +26,10 @@ final class BookingListViewModel: ObservableObject { private let type: BookingListTab private let stores: StoresManager private let storage: StorageManagerType - private let currentDate: Date private var currentOrder: SortBy = .newestToOldest + private var filters: BookingFilters + private static let refreshCacheReason = "refresh-cache" private static let reorderReason = "reorder" @@ -50,14 +48,7 @@ final class BookingListViewModel: ObservableObject { /// Booking ResultsController. private lazy var resultsController: ResultsController = { - var predicates = [NSPredicate(format: "siteID == %lld", siteID)] - if let before = type.startDateBefore(currentDate: currentDate) { - predicates.append(NSPredicate(format: "startDate < %@", before as NSDate)) - } - if let after = type.startDateAfter(currentDate: currentDate) { - predicates.append(NSPredicate(format: "startDate > %@", after as NSDate)) - } - let combinedPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) + let combinedPredicate = NSPredicate.createBookingPredicate(siteID: siteID, filters: filters) let sortDescriptorByDate = NSSortDescriptor(key: "startDate", ascending: false) let resultsController = ResultsController(storageManager: storage, matching: combinedPredicate, @@ -74,9 +65,20 @@ final class BookingListViewModel: ObservableObject { self.type = type self.stores = stores self.storage = storage - self.currentDate = currentDate self.paginationTracker = PaginationTracker(pageFirstIndex: pageFirstIndex) + self.filters = { + switch type { + case .all: + BookingFilters() // TODO: check local storage for persisted filters + case .today, .upcoming: + BookingFilters( + startDateBefore: type.startDateBefore(currentDate: currentDate)?.ISO8601Format(), + startDateAfter: type.startDateAfter(currentDate: currentDate)?.ISO8601Format() + ) + } + }() + configureResultsController() configurePaginationTracker() } @@ -112,6 +114,15 @@ final class BookingListViewModel: ObservableObject { paginationTracker.resync(reason: Self.reorderReason) {} } + func updateFilters(_ filters: BookingFiltersViewModel.Filters) { + /// Only support filters for All tab + guard type == .all else { return } + hasFilters = filters.numberOfActiveFilters > 0 + self.filters = filters.bookingFilters + resultsController.updatePredicate(siteID: siteID, filters: self.filters) + paginationTracker.resync(reason: Self.refreshCacheReason) {} + } + /// Converts SortBy to BookingsRemote.Order private func remoteOrder(from sortBy: SortBy) -> BookingsRemote.Order { sortBy == .oldestToNewest ? .ascending : .descending @@ -159,8 +170,7 @@ extension BookingListViewModel: PaginationTrackerDelegate { siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, - startDateBefore: type.startDateBefore(currentDate: currentDate)?.ISO8601Format(), - startDateAfter: type.startDateAfter(currentDate: currentDate)?.ISO8601Format(), + filters: filters, order: remoteOrder(from: currentOrder), shouldClearCache: shouldClearCache ) { [weak self] result in diff --git a/WooCommerce/Classes/Bookings/BookingList/BookingSearchViewModel.swift b/WooCommerce/Classes/Bookings/BookingList/BookingSearchViewModel.swift index 013791a2081..66f902aedd4 100644 --- a/WooCommerce/Classes/Bookings/BookingList/BookingSearchViewModel.swift +++ b/WooCommerce/Classes/Bookings/BookingList/BookingSearchViewModel.swift @@ -16,10 +16,11 @@ final class BookingSearchViewModel: ObservableObject { private let siteID: Int64 private let type: BookingListTab private let stores: StoresManager - private let currentDate: Date private var searchQuerySubscription: AnyCancellable? private var currentOrder: BookingListViewModel.SortBy = .newestToOldest + private var filters: BookingFilters + /// Tracks if the infinite scroll indicator should be displayed. @Published private(set) var shouldShowBottomActivityIndicator = false @@ -38,9 +39,20 @@ final class BookingSearchViewModel: ObservableObject { self.siteID = siteID self.type = type self.stores = stores - self.currentDate = currentDate self.searchPaginationTracker = PaginationTracker(pageFirstIndex: pageFirstIndex) + self.filters = { + switch type { + case .all: + BookingFilters() // TODO: check local storage for persisted filters + case .today, .upcoming: + BookingFilters( + startDateBefore: type.startDateBefore(currentDate: currentDate)?.ISO8601Format(), + startDateAfter: type.startDateAfter(currentDate: currentDate)?.ISO8601Format() + ) + } + }() + configureSearchPaginationTracker() configureSearchQuerySubscription(searchQueryPublisher: searchQueryPublisher) } @@ -70,6 +82,13 @@ final class BookingSearchViewModel: ObservableObject { } } + func updateFilters(_ filters: BookingFiltersViewModel.Filters) { + /// Only support filters for All tab + guard type == .all else { return } + self.filters = filters.bookingFilters + searchPaginationTracker.resync(reason: nil) {} + } + /// Converts SortBy to BookingsRemote.Order private func remoteOrder(from sortBy: BookingListViewModel.SortBy) -> BookingsRemote.Order { sortBy == .oldestToNewest ? .ascending : .descending @@ -123,8 +142,7 @@ extension BookingSearchViewModel: PaginationTrackerDelegate { searchQuery: currentSearchQuery, pageNumber: pageNumber, pageSize: pageSize, - startDateBefore: type.startDateBefore(currentDate: currentDate)?.ISO8601Format(), - startDateAfter: type.startDateAfter(currentDate: currentDate)?.ISO8601Format(), + filters: filters, order: remoteOrder(from: currentOrder) ) { [weak self] result in guard let self else { return } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingListViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingListViewModelTests.swift index c8553af4731..19fc9c1bb0a 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingListViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingListViewModelTests.swift @@ -93,7 +93,7 @@ struct BookingListViewModelTests { let stores = MockStoresManager(sessionManager: .testingInstance) let booking = createBooking(id: 1, startDate: Date()) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, _, onCompletion) = action else { return } self.insertBookings([booking]) @@ -131,7 +131,7 @@ struct BookingListViewModelTests { // Given let stores = MockStoresManager(sessionManager: .testingInstance) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, _, onCompletion) = action else { return } onCompletion(.success(false)) @@ -171,7 +171,7 @@ struct BookingListViewModelTests { let firstPageItems = (1...2).map { createBooking(id: Int64($0), startDate: Date()) } let secondPageItems = [createBooking(id: 3, startDate: Date())] stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, pageNumber, _, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, pageNumber, _, _, _, _, onCompletion) = action else { return } invocationCountOfLoadBookings += 1 @@ -219,7 +219,7 @@ struct BookingListViewModelTests { let booking1 = createBooking(id: 9, startDate: Date()) let booking2 = createBooking(id: 10, startDate: Date()) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, _, onCompletion) = action else { return } self.insertBookings([booking1, booking2]) @@ -244,7 +244,7 @@ struct BookingListViewModelTests { // Given let stores = MockStoresManager(sessionManager: .testingInstance) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, _, onCompletion) = action else { return } onCompletion(.success(false)) @@ -267,7 +267,7 @@ struct BookingListViewModelTests { let olderBooking = Booking.fake().copy(siteID: sampleSiteID, bookingID: 1, dateCreated: Date(timeIntervalSince1970: 1000), startDate: Date()) let newerBooking = Booking.fake().copy(siteID: sampleSiteID, bookingID: 3, dateCreated: Date(timeIntervalSince1970: 2000), startDate: Date()) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, _, onCompletion) = action else { return } let items = [olderBooking, newerBooking] @@ -296,7 +296,7 @@ struct BookingListViewModelTests { var invocationCountOfLoadBookings = 0 var skip: Int? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, pageNumber, pageSize, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, pageNumber, pageSize, _, _, _, onCompletion) = action else { return } invocationCountOfLoadBookings += 1 @@ -320,15 +320,13 @@ struct BookingListViewModelTests { // Given let testDate = Date(timeIntervalSince1970: 1609459200) // 2021-01-01 00:00:00 UTC let stores = MockStoresManager(sessionManager: .testingInstance) - var capturedStartDateBefore: String? - var capturedStartDateAfter: String? + var capturedFilters: BookingFilters? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, startDateBefore, startDateAfter, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, filters, _, _, onCompletion) = action else { return } - capturedStartDateBefore = startDateBefore - capturedStartDateAfter = startDateAfter + capturedFilters = filters onCompletion(.success(false)) } @@ -338,23 +336,21 @@ struct BookingListViewModelTests { viewModel.loadBookings() // Then - #expect(capturedStartDateAfter == "2020-12-31T23:59:59Z", "Today tab should filter after start of day") - #expect(capturedStartDateBefore == "2021-01-02T00:00:00Z", "Today tab should filter before end of day") + #expect(capturedFilters?.startDateAfter == "2020-12-31T23:59:59Z", "Today tab should filter after start of day") + #expect(capturedFilters?.startDateBefore == "2021-01-02T00:00:00Z", "Today tab should filter before end of day") } @Test func upcoming_tab_passes_correct_date_filters_to_booking_action() { // Given let testDate = Date(timeIntervalSince1970: 1609459200) // 2021-01-01 00:00:00 UTC let stores = MockStoresManager(sessionManager: .testingInstance) - var capturedStartDateBefore: String? - var capturedStartDateAfter: String? + var capturedFilters: BookingFilters? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, startDateBefore, startDateAfter, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, filters, _, _, onCompletion) = action else { return } - capturedStartDateBefore = startDateBefore - capturedStartDateAfter = startDateAfter + capturedFilters = filters onCompletion(.success(false)) } @@ -367,23 +363,21 @@ struct BookingListViewModelTests { viewModel.loadBookings() // Then - #expect(capturedStartDateBefore == nil, "Upcoming tab should not have startDateBefore filter") - #expect(capturedStartDateAfter == "2021-01-01T23:59:59Z", "Upcoming tab should filter after end of day") + #expect(capturedFilters?.startDateBefore == nil, "Upcoming tab should not have startDateBefore filter") + #expect(capturedFilters?.startDateAfter == "2021-01-01T23:59:59Z", "Upcoming tab should filter after end of day") } @Test func all_tab_passes_no_date_filters_to_booking_action() { // Given let testDate = Date(timeIntervalSince1970: 1609459200) // 2021-01-01 00:00:00 UTC let stores = MockStoresManager(sessionManager: .testingInstance) - var capturedStartDateBefore: String? - var capturedStartDateAfter: String? + var capturedFilters: BookingFilters? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, startDateBefore, startDateAfter, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, filters, _, _, onCompletion) = action else { return } - capturedStartDateBefore = startDateBefore - capturedStartDateAfter = startDateAfter + capturedFilters = filters onCompletion(.success(false)) } @@ -393,8 +387,8 @@ struct BookingListViewModelTests { viewModel.loadBookings() // Then - #expect(capturedStartDateBefore == nil, "All tab should not have startDateBefore filter") - #expect(capturedStartDateAfter == nil, "All tab should not have startDateAfter filter") + #expect(capturedFilters?.startDateBefore == nil, "All tab should not have startDateBefore filter") + #expect(capturedFilters?.startDateAfter == nil, "All tab should not have startDateAfter filter") } // MARK: - Cache clearing logic @@ -405,7 +399,7 @@ struct BookingListViewModelTests { var capturedShouldClearCache: Bool? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, shouldClearCache, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, shouldClearCache, onCompletion) = action else { return } capturedShouldClearCache = shouldClearCache @@ -428,7 +422,7 @@ struct BookingListViewModelTests { var actionCallCount = 0 stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, shouldClearCache, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, shouldClearCache, onCompletion) = action else { return } actionCallCount += 1 @@ -453,7 +447,7 @@ struct BookingListViewModelTests { var capturedShouldClearCache: Bool? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, shouldClearCache, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, shouldClearCache, onCompletion) = action else { return } capturedShouldClearCache = shouldClearCache @@ -585,7 +579,7 @@ struct BookingListViewModelTests { // Given let stores = MockStoresManager(sessionManager: .testingInstance) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, _, onCompletion) = action else { return } onCompletion(.success(false)) @@ -626,7 +620,7 @@ struct BookingListViewModelTests { // Given let stores = MockStoresManager(sessionManager: .testingInstance) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, _, _, onCompletion) = action else { return } onCompletion(.success(false)) @@ -669,7 +663,7 @@ struct BookingListViewModelTests { var capturedOrder: BookingsRemote.Order? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, order, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, order, _, onCompletion) = action else { return } capturedOrder = order @@ -691,7 +685,7 @@ struct BookingListViewModelTests { var capturedOrder: BookingsRemote.Order? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, order, _, onCompletion) = action else { + guard case let .synchronizeBookings(_, _, _, _, order, _, onCompletion) = action else { return } capturedOrder = order diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingSearchViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingSearchViewModelTests.swift index d283d46461e..baeef7eae6a 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingSearchViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingSearchViewModelTests.swift @@ -39,7 +39,7 @@ struct BookingSearchViewModelTests { let stores = MockStoresManager(sessionManager: .testingInstance) var invocationCount = 0 stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, _, _, onCompletion) = action else { return } invocationCount += 1 @@ -67,7 +67,7 @@ struct BookingSearchViewModelTests { let stores = MockStoresManager(sessionManager: .testingInstance) var capturedSearchQuery: String? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, searchQuery, _, _, _, _, _, onCompletion) = action else { + guard case let .searchBookings(_, searchQuery, _, _, _, _, onCompletion) = action else { return } capturedSearchQuery = searchQuery @@ -96,7 +96,7 @@ struct BookingSearchViewModelTests { let booking1 = Booking.fake().copy(siteID: sampleSiteID, bookingID: 1, startDate: Date()) let booking2 = Booking.fake().copy(siteID: sampleSiteID, bookingID: 2, startDate: Date()) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, _, _, onCompletion) = action else { return } onCompletion(.success([booking1, booking2])) @@ -124,7 +124,7 @@ struct BookingSearchViewModelTests { let searchQuerySubject = PassthroughSubject() let stores = MockStoresManager(sessionManager: .testingInstance) stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, _, _, onCompletion) = action else { return } onCompletion(.failure(NSError(domain: "test", code: 1))) @@ -158,7 +158,7 @@ struct BookingSearchViewModelTests { let secondPageBookings = [Booking.fake().copy(siteID: sampleSiteID, bookingID: 26, startDate: Date())] stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, pageNumber, _, _, _, _, onCompletion) = action else { + guard case let .searchBookings(_, _, pageNumber, _, _, _, onCompletion) = action else { return } capturedPageNumbers.append(pageNumber) @@ -196,7 +196,7 @@ struct BookingSearchViewModelTests { var searchCount = 0 stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, _, _, onCompletion) = action else { return } searchCount += 1 @@ -234,15 +234,13 @@ struct BookingSearchViewModelTests { let testDate = Date(timeIntervalSince1970: 1609459200) // 2021-01-01 00:00:00 UTC let searchQuerySubject = PassthroughSubject() let stores = MockStoresManager(sessionManager: .testingInstance) - var capturedStartDateBefore: String? - var capturedStartDateAfter: String? + var capturedFilters: BookingFilters? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, startDateBefore, startDateAfter, _, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, filters, _, onCompletion) = action else { return } - capturedStartDateBefore = startDateBefore - capturedStartDateAfter = startDateAfter + capturedFilters = filters onCompletion(.success([])) } @@ -259,8 +257,8 @@ struct BookingSearchViewModelTests { try await Task.sleep(nanoseconds: 400_000_000) // Then - #expect(capturedStartDateAfter == "2020-12-31T23:59:59Z", "Today tab should filter after start of day") - #expect(capturedStartDateBefore == "2021-01-02T00:00:00Z", "Today tab should filter before end of day") + #expect(capturedFilters?.startDateAfter == "2020-12-31T23:59:59Z", "Today tab should filter after start of day") + #expect(capturedFilters?.startDateBefore == "2021-01-02T00:00:00Z", "Today tab should filter before end of day") } @Test func upcoming_tab_passes_correct_date_filters_to_search_action() async throws { @@ -268,15 +266,13 @@ struct BookingSearchViewModelTests { let testDate = Date(timeIntervalSince1970: 1609459200) // 2021-01-01 00:00:00 UTC let searchQuerySubject = PassthroughSubject() let stores = MockStoresManager(sessionManager: .testingInstance) - var capturedStartDateBefore: String? - var capturedStartDateAfter: String? + var capturedFilters: BookingFilters? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, startDateBefore, startDateAfter, _, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, filters, _, onCompletion) = action else { return } - capturedStartDateBefore = startDateBefore - capturedStartDateAfter = startDateAfter + capturedFilters = filters onCompletion(.success([])) } @@ -293,8 +289,8 @@ struct BookingSearchViewModelTests { try await Task.sleep(nanoseconds: 400_000_000) // Then - #expect(capturedStartDateBefore == nil, "Upcoming tab should not have startDateBefore filter") - #expect(capturedStartDateAfter == "2021-01-01T23:59:59Z", "Upcoming tab should filter after end of day") + #expect(capturedFilters?.startDateBefore == nil, "Upcoming tab should not have startDateBefore filter") + #expect(capturedFilters?.startDateAfter == "2021-01-01T23:59:59Z", "Upcoming tab should filter after end of day") } @Test func all_tab_passes_no_date_filters_to_search_action() async throws { @@ -302,15 +298,13 @@ struct BookingSearchViewModelTests { let testDate = Date(timeIntervalSince1970: 1609459200) // 2021-01-01 00:00:00 UTC let searchQuerySubject = PassthroughSubject() let stores = MockStoresManager(sessionManager: .testingInstance) - var capturedStartDateBefore: String? - var capturedStartDateAfter: String? + var capturedFilters: BookingFilters? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, startDateBefore, startDateAfter, _, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, filters, _, onCompletion) = action else { return } - capturedStartDateBefore = startDateBefore - capturedStartDateAfter = startDateAfter + capturedFilters = filters onCompletion(.success([])) } @@ -327,8 +321,8 @@ struct BookingSearchViewModelTests { try await Task.sleep(nanoseconds: 400_000_000) // Then - #expect(capturedStartDateBefore == nil, "All tab should not have startDateBefore filter") - #expect(capturedStartDateAfter == nil, "All tab should not have startDateAfter filter") + #expect(capturedFilters?.startDateBefore == nil, "All tab should not have startDateBefore filter") + #expect(capturedFilters?.startDateAfter == nil, "All tab should not have startDateAfter filter") } // MARK: - Refresh action @@ -340,7 +334,7 @@ struct BookingSearchViewModelTests { var searchCount = 0 stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, _, _, _, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, _, _, onCompletion) = action else { return } searchCount += 1 @@ -376,7 +370,7 @@ struct BookingSearchViewModelTests { var capturedOrder: BookingsRemote.Order? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, _, _, order, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, _, order, onCompletion) = action else { return } capturedOrder = order @@ -409,7 +403,7 @@ struct BookingSearchViewModelTests { var capturedOrder: BookingsRemote.Order? stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, _, _, order, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, _, order, onCompletion) = action else { return } capturedOrder = order @@ -470,7 +464,7 @@ struct BookingSearchViewModelTests { var capturedOrders: [BookingsRemote.Order] = [] stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .searchBookings(_, _, _, _, _, _, order, onCompletion) = action else { + guard case let .searchBookings(_, _, _, _, _, order, onCompletion) = action else { return } capturedOrders.append(order)