Skip to content
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

[HACK week] Favorite products - Remote and storage code for synching favorite products using IDs #13998

Merged
merged 10 commits into from
Sep 26, 2024
Merged
12 changes: 12 additions & 0 deletions Networking/Networking/Remote/ProductsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public protocol ProductsRemoteProtocol {
productCategory: ProductCategory?,
orderBy: ProductsRemote.OrderKey,
order: ProductsRemote.Order,
productIDs: [Int64],
excludedProductIDs: [Int64],
completion: @escaping (Result<[Product], Error>) -> Void)
func searchProducts(for siteID: Int64,
Expand Down Expand Up @@ -139,6 +140,7 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
/// - productType: Optional product type filtering. Default to nil (no filtering).
/// - orderBy: the key to order the remote products. Default to product name.
/// - order: ascending or descending order. Default to ascending.
/// - productIDs: a list of product IDs to be included in the results. All products will be fetched if empty.
/// - excludedProductIDs: a list of product IDs to be excluded from the results.
/// - completion: Closure to be executed upon completion.
///
Expand All @@ -152,16 +154,26 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
productCategory: ProductCategory? = nil,
orderBy: OrderKey = .name,
order: Order = .ascending,
productIDs: [Int64] = [],
excludedProductIDs: [Int64] = [],
completion: @escaping (Result<[Product], Error>) -> Void) {
let stringOfExcludedProductIDs = excludedProductIDs.map { String($0) }
.joined(separator: ",")
let stringOfProductIDs: String = {
guard productIDs.isEmpty == false else {
return ""
}
return productIDs.map { String($0) }
.joined(separator: ",")
}()


let filterParameters = [
ParameterKey.stockStatus: stockStatus?.rawValue ?? "",
ParameterKey.productStatus: productStatus?.rawValue ?? "",
ParameterKey.productType: productType?.rawValue ?? "",
ParameterKey.category: filterProductCategoryParemeterValue(from: productCategory),
ParameterKey.include: stringOfProductIDs,
ParameterKey.exclude: stringOfExcludedProductIDs
].filter({ $0.value.isEmpty == false })

Expand Down
41 changes: 41 additions & 0 deletions Networking/NetworkingTests/Remote/ProductsRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,47 @@ final class ProductsRemoteTests: XCTestCase {
XCTAssertTrue(queryParameters.contains(expectedParam), "Expected to have param: \(expectedParam)")
}

/// Verifies that loadAllProducts with `productIDs` makes a network request with the `include` parameter.
///
func test_loadAllProducts_with_productIDs_adds_a_include_param_in_network_request() throws {
// Given
let remote = ProductsRemote(network: network)
network.simulateResponse(requestUrlSuffix: "products", filename: "products-load-all")
let productIDs: [Int64] = [13, 61]

// When
waitForExpectation { expectation in
remote.loadAllProducts(for: sampleSiteID, productIDs: productIDs) { result in
expectation.fulfill()
}
}

// Then
let queryParameters = try XCTUnwrap(network.queryParameters)
let expectedParam = "include=13,61"
XCTAssertTrue(queryParameters.contains(expectedParam), "Expected to have param: \(expectedParam)")
}

/// Verifies that loadAllProducts with empty `productIDs` makes a network request without the `include` parameter.
///
func test_loadAllProducts_with_empty_productIDs_does_not_add_include_param_in_network_request() throws {
// Given
let remote = ProductsRemote(network: network)
network.simulateResponse(requestUrlSuffix: "products", filename: "products-load-all")

// When
waitForExpectation { expectation in
remote.loadAllProducts(for: sampleSiteID, productIDs: []) { result in
expectation.fulfill()
}
}

// Then
let queryParameters = try XCTUnwrap(network.queryParameters)
let includeParam = "include"
XCTAssertFalse(queryParameters.contains(includeParam), "`include` param should not be present")
}

/// Verifies that loadAllProducts properly relays Networking Layer errors.
///
func test_loadAllProducts_properly_relays_netwoking_errors() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase {

stores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, let onCompletion):
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, let onCompletion):
// Then
if case .loading = sut.state {
// Loading state as expected
Expand Down Expand Up @@ -318,7 +318,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase {

stores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, let onCompletion):
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, let onCompletion):
// Then
XCTAssertTrue(sut.shouldRedactView)
onCompletion(.success(true))
Expand Down Expand Up @@ -421,7 +421,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase {

stores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, let onCompletion):
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, let onCompletion):
// Then
XCTAssertFalse(sut.shouldShowShowAllCampaignsButton)
onCompletion(.success(true))
Expand Down Expand Up @@ -913,7 +913,7 @@ private extension BlazeCampaignDashboardViewModelTests {
func mockSynchronizeProducts(insertProductToStorage product: Product? = nil) {
stores.whenReceivingAction(ofType: ProductAction.self) { [weak self] action in
switch action {
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, let onCompletion):
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, let onCompletion):
if let product {
self?.insertProduct(product)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ private extension DashboardViewModelTests {

stores.whenReceivingAction(ofType: ProductAction.self) { [weak self] action in
switch action {
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, let onCompletion):
case .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, let onCompletion):
for product in existingProducts {
self?.insertProduct(product)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ final class ProductSelectorViewModelTests: XCTestCase {
XCTAssertFalse(viewModel.shouldShowScrollIndicator, "Scroll indicator is not disabled at start")
stores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, onCompletion):
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, onCompletion):
XCTAssertTrue(viewModel.shouldShowScrollIndicator, "Scroll indicator is not enabled during sync")
onCompletion(.success(true))
default:
Expand All @@ -109,7 +109,7 @@ final class ProductSelectorViewModelTests: XCTestCase {
stores: stores)
stores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, onCompletion):
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, onCompletion):
XCTAssertEqual(viewModel.syncStatus, .loading)
onCompletion(.success(true))
default:
Expand Down Expand Up @@ -139,7 +139,7 @@ final class ProductSelectorViewModelTests: XCTestCase {

mockStores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, onCompletion):
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, onCompletion):
if let syncStatus = viewModel.syncStatus {
syncStatusSpy.append(syncStatus)
}
Expand Down Expand Up @@ -178,7 +178,7 @@ final class ProductSelectorViewModelTests: XCTestCase {
stores: stores)
stores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, onCompletion):
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, onCompletion):
XCTAssertEqual(viewModel.syncStatus, .results)
onCompletion(.success(true))
default:
Expand Down Expand Up @@ -446,7 +446,7 @@ final class ProductSelectorViewModelTests: XCTestCase {
case let .searchProducts(_, _, _, _, _, _, _, _, _, _, onCompletion):
self.insert(product.copy(name: "T-shirt"), withSearchTerm: "shirt")
onCompletion(.success(false))
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, onCompletion):
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, onCompletion):
onCompletion(.success(true))
expectation.fulfill()
default:
Expand Down Expand Up @@ -485,7 +485,7 @@ final class ProductSelectorViewModelTests: XCTestCase {
stores: stores)
stores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, onCompletion):
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, onCompletion):
onCompletion(.failure(NSError(domain: "Error", code: 0)))
default:
XCTFail("Received unsupported action: \(action)")
Expand Down Expand Up @@ -1173,7 +1173,7 @@ final class ProductSelectorViewModelTests: XCTestCase {

mockStores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case let .synchronizeProducts(_, _, _, stockStatus, productStatus, productType, category, _, _, _, onCompletion):
case let .synchronizeProducts(_, _, _, stockStatus, productStatus, productType, category, _, _, _, _, onCompletion):
filteredStockStatus = stockStatus
filteredProductType = productType
filteredProductStatus = productStatus
Expand Down Expand Up @@ -1306,7 +1306,7 @@ final class ProductSelectorViewModelTests: XCTestCase {
viewModel.changeSelectionStateForProduct(with: products[0].productID, selected: true)
stores.whenReceivingAction(ofType: ProductAction.self, thenCall: { action in
switch action {
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, onCompletion):
case let .synchronizeProducts(_, _, _, _, _, _, _, _, _, _, _, onCompletion):
viewModel.changeSelectionStateForProduct(with: products[1].productID, selected: true)
onCompletion(.success(true))
default:
Expand Down Expand Up @@ -1546,7 +1546,7 @@ final class ProductSelectorViewModelTests: XCTestCase {
self.insert(product, withSearchTerm: "shirt")
// No next page from the search.
onCompletion(.success(false))
case let .synchronizeProducts(_, pageNumber, _, _, _, _, _, _, _, _, onCompletion):
case let .synchronizeProducts(_, pageNumber, _, _, _, _, _, _, _, _, _, onCompletion):
synchronizeProductsPages.append(pageNumber)
let hasNextPage = pageNumber < 2
onCompletion(.success(hasNextPage))
Expand Down
1 change: 1 addition & 0 deletions Yosemite/Yosemite/Actions/ProductAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public enum ProductAction: Action {
productType: ProductType?,
productCategory: ProductCategory?,
sortOrder: ProductsSortOrder,
productIDs: [Int64] = [],
excludedProductIDs: [Int64] = [],
shouldDeleteStoredProductsOnFirstPage: Bool = true,
onCompletion: (Result<Bool, Error>) -> Void)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct MockProductActionHandler: MockActionHandler {
requestMissingProducts(for: order, onCompletion: onCompletion)
case .retrieveProducts(let siteID, let productIDs, _, _, let onCompletion):
retrieveProducts(siteId: siteID, productIds: productIDs, onCompletion: onCompletion)
case .synchronizeProducts(let siteID, _, _, _, _, _, _, _, let excludedProductIDs, _, let onCompletion):
case .synchronizeProducts(let siteID, _, _, _, _, _, _, _, _, let excludedProductIDs, _, let onCompletion):
synchronizeProducts(siteID: siteID, excludedProductIDs: excludedProductIDs, onCompletion: onCompletion)
default: unimplementedAction(action: action)
}
Expand Down
4 changes: 4 additions & 0 deletions Yosemite/Yosemite/Stores/ProductStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class ProductStore: Store {
let productType,
let productCategory,
let sortOrder,
let productIDs,
let excludedProductIDs,
let shouldDeleteStoredProductsOnFirstPage,
let onCompletion):
Expand All @@ -101,6 +102,7 @@ public class ProductStore: Store {
productType: productType,
productCategory: productCategory,
sortOrder: sortOrder,
productIDs: productIDs,
excludedProductIDs: excludedProductIDs,
shouldDeleteStoredProductsOnFirstPage: shouldDeleteStoredProductsOnFirstPage,
onCompletion: onCompletion)
Expand Down Expand Up @@ -287,6 +289,7 @@ private extension ProductStore {
productType: ProductType?,
productCategory: ProductCategory?,
sortOrder: ProductsSortOrder,
productIDs: [Int64],
excludedProductIDs: [Int64],
shouldDeleteStoredProductsOnFirstPage: Bool,
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
Expand All @@ -300,6 +303,7 @@ private extension ProductStore {
productCategory: productCategory,
orderBy: sortOrder.remoteOrderKey,
order: sortOrder.remoteOrder,
productIDs: productIDs,
excludedProductIDs: excludedProductIDs) { [weak self] result in
switch result {
case .failure(let error):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ extension NSPredicate {
public static func createProductPredicate(siteID: Int64,
stockStatus: ProductStockStatus? = nil,
productStatus: ProductStatus? = nil,
productType: ProductType? = nil) -> NSPredicate {
productType: ProductType? = nil,
productIDs: [Int64]? = nil) -> NSPredicate {
let siteIDPredicate = NSPredicate(format: "siteID == %lld", siteID)

let stockStatusPredicate = stockStatus.flatMap { stockStatus -> NSPredicate? in
Expand All @@ -23,14 +24,31 @@ extension NSPredicate {
return NSPredicate(format: "\(key) == %@", productType.rawValue)
}

let subpredicates = [siteIDPredicate, stockStatusPredicate, productStatusPredicate, productTypePredicate].compactMap({ $0 })
let productIDsPredicate = productIDs.flatMap { productIDs -> NSPredicate? in
NSPredicate(format: "productID in %@", productIDs)
}

let subpredicates = [siteIDPredicate,
stockStatusPredicate,
productStatusPredicate,
productTypePredicate,
productIDsPredicate].compactMap({ $0 })


return NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)
}
}

extension ResultsController where T: StorageProduct {
public func updatePredicate(siteID: Int64, stockStatus: ProductStockStatus? = nil, productStatus: ProductStatus? = nil, productType: ProductType? = nil) {
self.predicate = NSPredicate.createProductPredicate(siteID: siteID, stockStatus: stockStatus, productStatus: productStatus, productType: productType)
public func updatePredicate(siteID: Int64,
stockStatus: ProductStockStatus? = nil,
productStatus: ProductStatus? = nil,
productType: ProductType? = nil,
productIDs: [Int64]? = nil) {
self.predicate = NSPredicate.createProductPredicate(siteID: siteID,
stockStatus: stockStatus,
productStatus: productStatus,
productType: productType,
productIDs: productIDs)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ final class MockProductsRemote {
private(set) var searchProductWithProductType: ProductType?
private(set) var searchProductWithProductCategory: ProductCategory?

private(set) var synchronizeProductsTriggered: Bool = false
private(set) var synchronizeProductsWithStockStatus: ProductStockStatus?
private(set) var synchronizeProductsWithProductStatus: ProductStatus?
private(set) var synchronizeProductsWithProductType: ProductType?
private(set) var synchronizeProductsWithProductCategory: ProductCategory?
private(set) var synchronizeProductsSortOrderBy: ProductsRemote.OrderKey?
private(set) var synchronizeProductsOrder: ProductsRemote.Order?
private(set) var synchronizeProductsProductIDs: [Int64]?
private(set) var synchronizeProductsExcludedProductIDs: [Int64]?

private struct ResultKey: Hashable {
let siteID: Int64
let productIDs: [Int64]
Expand Down Expand Up @@ -195,8 +205,18 @@ extension MockProductsRemote: ProductsRemoteProtocol {
productCategory: ProductCategory?,
orderBy: ProductsRemote.OrderKey,
order: ProductsRemote.Order,
productIDs: [Int64],
excludedProductIDs: [Int64],
completion: @escaping (Result<[Product], Error>) -> Void) {
synchronizeProductsTriggered = true
synchronizeProductsWithStockStatus = stockStatus
synchronizeProductsWithProductStatus = productStatus
synchronizeProductsWithProductType = productType
synchronizeProductsWithProductCategory = productCategory
synchronizeProductsSortOrderBy = orderBy
synchronizeProductsOrder = order
synchronizeProductsProductIDs = productIDs
synchronizeProductsExcludedProductIDs = excludedProductIDs
if let result = loadAllProductsResultsBySiteID[siteID] {
completion(result)
} else {
Expand Down
Loading