From 594b83cae94ef9b50fc00262dfe123b2e18ab0f0 Mon Sep 17 00:00:00 2001 From: hani Date: Wed, 27 Dec 2023 19:01:39 +0530 Subject: [PATCH] Add missing file after PR merge --- AnonymousUserEventTracking.md | 19 +- .../project.pbxproj | 16 + swift-sdk/Constants.swift | 7 +- .../AnonymousUserManager+Functions.swift | 327 ++++++++++++++++++ .../{ => Internal}/AnonymousUserManager.swift | 217 +++--------- .../AnonymousUserManagerProtocol.swift | 18 + .../DependencyContainerProtocol.swift | 5 +- swift-sdk/Internal/InternalIterableAPI.swift | 60 ++-- swift-sdk/Internal/IterableUserDefaults.swift | 14 +- swift-sdk/Internal/LocalStorage.swift | 2 +- swift-sdk/Internal/LocalStorageProtocol.swift | 2 +- .../Resources/anoncriteria_response.json | 130 +++++++ tests/common/MockLocalStorage.swift | 6 + .../AnonymousUserCriteriaMatchTests.swift | 211 +++++++++++ tests/unit-tests/BlankApiClient.swift | 22 +- 15 files changed, 842 insertions(+), 214 deletions(-) create mode 100644 swift-sdk/Internal/AnonymousUserManager+Functions.swift rename swift-sdk/{ => Internal}/AnonymousUserManager.swift (52%) create mode 100644 swift-sdk/Internal/AnonymousUserManagerProtocol.swift create mode 100644 swift-sdk/Resources/anoncriteria_response.json create mode 100644 tests/unit-tests/AnonymousUserCriteriaMatchTests.swift diff --git a/AnonymousUserEventTracking.md b/AnonymousUserEventTracking.md index bd20945f4..a9c51e503 100644 --- a/AnonymousUserEventTracking.md +++ b/AnonymousUserEventTracking.md @@ -2,8 +2,9 @@ ## Class Introduction -The `AnonymousUserManager` class is responsible for managing anonymous user sessions and tracking events. -It includes methods for updating sessions, tracking events (i.e regular, update cart and purchase) and create a user if criterias are met. +The `AnonymousUserManager` class is responsible for managing anonymous user sessions and tracking events. +The `AnonymousUserManager+Functions` class is contains util functions and `CriteriaCompletionChecker` struct which contains criteria checking logic. +It includes methods for updating sessions, tracking events (i.e custom event, update cart, update user and purchase) and create a user if criterias are met. We call track methods of this class internally to make sure we have tracked the events even when user is NOT logged in and after certain criterias are met we create a user and logs them automatically and sync events through Iterable API. ## Class Structure @@ -14,6 +15,7 @@ The `AnonymousUserManager` class includes the following key components: - `updateAnonSession()`: Updates the anonymous user session. - `trackAnonEvent(name: String, dataFields: [AnyHashable: Any]?)`: Tracks an anonymous event and store it locally. - `trackAnonPurchaseEvent(total: NSNumber, items: [CommerceItem], dataFields: [AnyHashable: Any]?)`: Tracks an anonymous purchase event and store it locally. + - `trackAnonUpdateUser(_ dataFields: [AnyHashable: Any])`: Tracks an anonymous update user event and store it locally. - `trackAnonUpdateCart(items: [CommerceItem])`: Tracks an anonymous cart event and store it locally. - `trackAnonTokenRegistration(token: String)`: Tracks an anonymous token registration event and store it locally. - `getAnonCriteria()`: Gets the anonymous criteria. @@ -26,10 +28,7 @@ The `AnonymousUserManager` class includes the following key components: - `syncNonSyncedEvents()`: Syncs unsynced data which might have failed to sync when calling syncEvents for the first time after criterias met. - `convertCommerceItems(from dictionaries: [[AnyHashable: Any]]) -> [CommerceItem]`: Convert to commerce items from dictionaries. - `convertCommerceItemsToDictionary(_ items: [CommerceItem]) -> [[AnyHashable:Any]]`: Convert commerce items to dictionaries. - - `filterEvents(byType type: String) -> [[AnyHashable: Any]]?`: Filter events by type. - - `filterEvents(byType type: String, andName name: String?) -> [[AnyHashable: Any]]?`: Filter events by type and name. - `getUTCDateTime()`: Converts UTC Datetime from current time. - - `filterEvents(excludingTimestamps excludedTimestamps: [Int]) -> [[AnyHashable: Any]]?`: Filter non-synced data. ## Methods Description @@ -59,6 +58,14 @@ This method tracks an anonymous purchase event. It does the following: * Stores the purchase event data in local storage. * Checks criteria completion and creates a known user if criteria are met. +### `trackAnonUpdateUser(dataFields: [AnyHashable: Any]?)` + +This method tracks an anonymous update user event. It does the following: + +* Creates a dictionary object with event details, including the event name, timestamp, data fields, and tracking type. +* Stores the event data in local storage, and if data of this event already exists it replaces the data. +* Checks criteria completion and creates a known user if criteria are met. + ### `trackAnonUpdateCart(items: [CommerceItem])` This method tracks an anonymous cart update. It does the following: @@ -78,7 +85,7 @@ This method is responsible for fetching criteria data. It simulates calling an A ### `checkCriteriaCompletion()` -This private method checks if criteria for creating a known user are met. It compares stored event data with predefined criteria and returns `true` if criteria are completed. +This private method checks if criteria for creating a known user are met. It compares stored event data with predefined criteria and returns `criteriaId` if any of the criteria is matched. ### `createKnownUser()` diff --git a/sample-apps/swift-sample-app/swift-sample-app.xcodeproj/project.pbxproj b/sample-apps/swift-sample-app/swift-sample-app.xcodeproj/project.pbxproj index 17659f0ab..5e3d1d519 100644 --- a/sample-apps/swift-sample-app/swift-sample-app.xcodeproj/project.pbxproj +++ b/sample-apps/swift-sample-app/swift-sample-app.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 37088F332B3C38250000B218 /* IterableAppExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 37088F322B3C38250000B218 /* IterableAppExtensions */; }; + 37088F352B3C38250000B218 /* IterableSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 37088F342B3C38250000B218 /* IterableSDK */; }; 551A5FF1251AB1950004C9A0 /* IterableSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 551A5FF0251AB1950004C9A0 /* IterableSDK */; }; 551A5FF3251AB19B0004C9A0 /* IterableAppExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 551A5FF2251AB19B0004C9A0 /* IterableAppExtensions */; }; AC1BDF5820E304BC000010CA /* CoffeeListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5ECD9E20E304000081E1DA /* CoffeeListTableViewController.swift */; }; @@ -73,6 +75,7 @@ buildActionMask = 2147483647; files = ( 551A5FF1251AB1950004C9A0 /* IterableSDK in Frameworks */, + 37088F352B3C38250000B218 /* IterableSDK in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,6 +83,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 37088F332B3C38250000B218 /* IterableAppExtensions in Frameworks */, 551A5FF3251AB19B0004C9A0 /* IterableAppExtensions in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -202,6 +206,7 @@ name = "swift-sample-app"; packageProductDependencies = ( 551A5FF0251AB1950004C9A0 /* IterableSDK */, + 37088F342B3C38250000B218 /* IterableSDK */, ); productName = "swift-sample-app"; productReference = ACA3A13520E2F6AF00FEF74F /* swift-sample-app.app */; @@ -222,6 +227,7 @@ name = "swift-sample-app-notification-extension"; packageProductDependencies = ( 551A5FF2251AB19B0004C9A0 /* IterableAppExtensions */, + 37088F322B3C38250000B218 /* IterableAppExtensions */, ); productName = "swift-sample-app-notification-extension"; productReference = ACA3A14E20E2F83D00FEF74F /* swift-sample-app-notification-extension.appex */; @@ -264,6 +270,8 @@ Base, ); mainGroup = ACA3A12C20E2F6AF00FEF74F; + packageReferences = ( + ); productRefGroup = ACA3A13620E2F6AF00FEF74F /* Products */; projectDirPath = ""; projectRoot = ""; @@ -572,6 +580,14 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 37088F322B3C38250000B218 /* IterableAppExtensions */ = { + isa = XCSwiftPackageProductDependency; + productName = IterableAppExtensions; + }; + 37088F342B3C38250000B218 /* IterableSDK */ = { + isa = XCSwiftPackageProductDependency; + productName = IterableSDK; + }; 551A5FF0251AB1950004C9A0 /* IterableSDK */ = { isa = XCSwiftPackageProductDependency; productName = IterableSDK; diff --git a/swift-sdk/Constants.swift b/swift-sdk/Constants.swift index ce6fb5d53..ca868df46 100644 --- a/swift-sdk/Constants.swift +++ b/swift-sdk/Constants.swift @@ -11,8 +11,9 @@ enum Endpoint { } enum EventType { - static let track = "track" - static let trackPurchase = "trackPurchase" + static let customEvent = "customEvent" + static let purchase = "purchase" + static let updateUser = "updateUser" static let cartUpdate = "cartUpdate" static let anonSession = "anonSession" static let tokenRegistration = "tokenRegistration" @@ -187,7 +188,7 @@ enum JsonKey { static let contentType = "Content-Type" static let createNewFields = "createNewFields" - static let eventType = "eventType" + static let eventType = "dataType" static let eventTimeStamp = "eventTimeStamp" enum ActionButton { diff --git a/swift-sdk/Internal/AnonymousUserManager+Functions.swift b/swift-sdk/Internal/AnonymousUserManager+Functions.swift new file mode 100644 index 000000000..7fdf4d4c7 --- /dev/null +++ b/swift-sdk/Internal/AnonymousUserManager+Functions.swift @@ -0,0 +1,327 @@ +// +// File.swift +// +// +// Created by HARDIK MASHRU on 13/11/23. +// + +import Foundation + +// Convert commerce items to dictionaries +func convertCommerceItemsToDictionary(_ items: [CommerceItem]) -> [[AnyHashable:Any]] { + let dictionaries = items.map { item in + return item.toDictionary() + } + return dictionaries +} + +// Convert to commerce items from dictionaries +func convertCommerceItems(from dictionaries: [[AnyHashable: Any]]) -> [CommerceItem] { + return dictionaries.compactMap { dictionary in + let item = CommerceItem(id: dictionary[JsonKey.CommerceItem.id] as? String ?? "", name: dictionary[JsonKey.CommerceItem.name] as? String ?? "", price: dictionary[JsonKey.CommerceItem.price] as? NSNumber ?? 0, quantity: dictionary[JsonKey.CommerceItem.quantity] as? UInt ?? 0) + item.sku = dictionary[JsonKey.CommerceItem.sku] as? String + item.itemDescription = dictionary[JsonKey.CommerceItem.description] as? String + item.url = dictionary[JsonKey.CommerceItem.url] as? String + item.imageUrl = dictionary[JsonKey.CommerceItem.imageUrl] as? String + item.categories = dictionary[JsonKey.CommerceItem.categories] as? [String] + item.dataFields = dictionary[JsonKey.CommerceItem.dataFields] as? [AnyHashable: Any] + + return item + } +} + +func convertToDictionary(data: Codable) -> [AnyHashable: Any] { + do { + let encoder = JSONEncoder() + let data = try encoder.encode(data) + if let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [AnyHashable: Any] { + return dictionary + } + } catch { + print("Error converting to dictionary: \(error)") + } + return [:] +} + +// Converts UTC Datetime from current time +func getUTCDateTime() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + dateFormatter.timeZone = TimeZone(identifier: "UTC") + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + + let utcDate = Date() + return dateFormatter.string(from: utcDate) +} + +struct CriteriaCompletionChecker { + init(anonymousCriteria: Data, anonymousEvents: [[AnyHashable: Any]]) { + self.anonymousEvents = anonymousEvents + self.anonymousCriteria = anonymousCriteria + } + + func getMatchedCriteria() -> Int? { + var criteriaId: Int? = nil + if let json = try? JSONSerialization.jsonObject(with: anonymousCriteria, options: []) as? [String: Any] { + // Access the criteriaList + if let criteriaList = json["criteriaList"] as? [[String: Any]] { + // Iterate over the criteria + for criteria in criteriaList { + // Perform operations on each criteria + if let searchQuery = criteria["searchQuery"] as? [String: Any], let currentCriteriaId = criteria["criteriaId"] as? Int { + // we will split purhase/updatecart event items as seperate events because we need to compare it against the single item in criteria json + var eventsToProcess = getEventsWithCartItems() + eventsToProcess.append(contentsOf: getNonCartEvents()) + let result = evaluateTree(node: searchQuery, localEventData: eventsToProcess) + if (result) { + criteriaId = currentCriteriaId + break + } + } + } + } + } + return criteriaId + } + + func getMappedKeys(event: [AnyHashable: Any]) -> [String] { + var itemKeys: [String] = [] + for (_ , value) in event { + if let arrayValue = value as? [[AnyHashable: Any]], arrayValue.count > 0 { // this is a special case of items array in purchase event + // If the value is an array, handle it + itemKeys.append(contentsOf: extractKeys(dict: arrayValue[0])) + } else { + itemKeys.append(contentsOf: extractKeys(dict: event)) + } + } + return itemKeys + } + + func getNonCartEvents() -> [[AnyHashable: Any]] { + let nonPurchaseEvents = anonymousEvents.filter { dictionary in + if let dataType = dictionary[JsonKey.eventType] as? String { + return dataType != EventType.purchase && dataType != EventType.cartUpdate + } + return false + } + var processedEvents: [[AnyHashable: Any]] = [[:]] + for eventItem in nonPurchaseEvents { + var updatedItem = eventItem + // handle dataFields if any + if let dataFields = eventItem["dataFields"] as? [AnyHashable: Any] { + for (key, value) in dataFields { + if key is String { + updatedItem[key] = value + } + } + updatedItem.removeValue(forKey: "dataFields") + } + processedEvents.append(updatedItem) + } + return processedEvents + } + + func getEventsWithCartItems() -> [[AnyHashable: Any]] { + let purchaseEvents = anonymousEvents.filter { dictionary in + if let dataType = dictionary[JsonKey.eventType] as? String { + return dataType == EventType.purchase || dataType == EventType.cartUpdate + } + return false + } + + var processedEvents: [[AnyHashable: Any]] = [[:]] + for eventItem in purchaseEvents { + if let items = eventItem["items"] as? [[AnyHashable: Any]] { + let itemsWithOtherProps = items.map { item -> [AnyHashable: Any] in + var updatedItem = [AnyHashable: Any]() + + for (key, value) in item { + if let stringKey = key as? String { + updatedItem["shoppingCartItems." + stringKey] = value + } + } + + // handle dataFields if any + if let dataFields = eventItem["dataFields"] as? [AnyHashable: Any] { + for (key, value) in dataFields { + if key is String { + updatedItem[key] = value + } + } + } + + for (key, value) in eventItem { + if (key as! String != "items" && key as! String != "dataFields") { + updatedItem[key] = value + } + } + return updatedItem + } + processedEvents.append(contentsOf: itemsWithOtherProps) + } + } + return processedEvents + } + + func extractKeys(jsonObject: [String: Any]) -> [String] { + return Array(jsonObject.keys) + } + + func extractKeys(dict: [AnyHashable: Any]) -> [String] { + var keys: [String] = [] + for key in dict.keys { + if let stringKey = key as? String { + // If needed, use stringKey which is now guaranteed to be a String + keys.append(stringKey) + } + } + return keys + } + + func evaluateTree(node: [String: Any], localEventData: [[AnyHashable: Any]]) -> Bool { + if let searchQueries = node["searchQueries"] as? [[String: Any]], let combinator = node["combinator"] as? String { + if combinator == "And" { + for query in searchQueries { + if !evaluateTree(node: query, localEventData: localEventData) { + return false // If any subquery fails, return false + } + } + return true // If all subqueries pass, return true + } else if combinator == "Or" { + for query in searchQueries { + if evaluateTree(node: query, localEventData: localEventData) { + return true // If any subquery passes, return true + } + } + return false // If all subqueries fail, return false + } + } else if let searchCombo = node["searchCombo"] as? [String: Any] { + return evaluateTree(node: searchCombo, localEventData: localEventData) + } else if node["field"] != nil { + return evaluateField(node: node, localEventData: localEventData) + } + + return false + } + + func evaluateField(node: [String: Any], localEventData: [[AnyHashable: Any]]) -> Bool { + do { + return try evaluateFieldLogic(node: node, localEventData: localEventData) + } catch { + print("evaluateField JSON ERROR: \(error)") + } + return false + } + + func evaluateFieldLogic(node: [String: Any], localEventData: [[AnyHashable: Any]]) throws -> Bool { + var isEvaluateSuccess = false + for eventData in localEventData { + let localDataKeys = eventData.keys + if node["dataType"] as? String == eventData["dataType"] as? String { + if let field = node["field"] as? String, + let comparatorType = node["comparatorType"] as? String, + let fieldType = node["fieldType"] as? String { + for key in localDataKeys { + if field == key as! String, let matchObj = eventData[key] { + if evaluateComparison(comparatorType: comparatorType, fieldType: fieldType, matchObj: matchObj, valueToCompare: node["value"] as? String) { + isEvaluateSuccess = true + break + } + } + } + } + } + } + return isEvaluateSuccess + } + + func evaluateComparison(comparatorType: String, fieldType: String, matchObj: Any, valueToCompare: String?) -> Bool { + guard let stringValue = valueToCompare else { + return false + } + + switch comparatorType { + case "Equals": + return compareValueEquality(matchObj, stringValue) + case "DoesNotEquals": + return !compareValueEquality(matchObj, stringValue) + case "GreaterThan": + print("GreatherThan:: \(compareNumericValues(matchObj, stringValue, compareOperator: >))") + return compareNumericValues(matchObj, stringValue, compareOperator: >) + case "LessThan": + return compareNumericValues(matchObj, stringValue, compareOperator: <) + case "GreaterThanOrEqualTo": + print("GreaterThanOrEqualTo:: \(compareNumericValues(matchObj, stringValue, compareOperator: >=))") + return compareNumericValues(matchObj, stringValue, compareOperator: >=) + case "LessThanOrEqualTo": + return compareNumericValues(matchObj, stringValue, compareOperator: <=) + case "Contains": + return compareStringContains(matchObj, stringValue) + case "StartsWith": + return compareStringStartsWith(matchObj, stringValue) + case "MatchesRegex": + return compareWithRegex(matchObj as? String ?? "", pattern: stringValue) + default: + return false + } + } + + func compareValueEquality(_ sourceTo: Any, _ stringValue: String) -> Bool { + switch (sourceTo, stringValue) { + case (let doubleNumber as Double, let value): return doubleNumber == Double(value) + case (let intNumber as Int, let value): return intNumber == Int(value) + case (let longNumber as Int64, let value): return longNumber == Int64(value) + case (let booleanValue as Bool, let value): return booleanValue == Bool(value) + case (let stringTypeValue as String, let value): return stringTypeValue == value + default: return false + } + } + + func compareNumericValues(_ sourceTo: Any, _ stringValue: String, compareOperator: (Double, Double) -> Bool) -> Bool { + if let sourceNumber = Double(stringValue) { + switch sourceTo { + case let doubleNumber as Double: + return compareOperator(doubleNumber, sourceNumber) + case let intNumber as Int: + return compareOperator(Double(intNumber), sourceNumber) + case let longNumber as Int64: + return compareOperator(Double(longNumber), sourceNumber) + case let stringNumber as String: + if let doubleFromString = Double(stringNumber) { + return compareOperator(doubleFromString, sourceNumber) + } else { + return false // Handle the case where string cannot be converted to a Double + } + default: + return false + } + } else { + return false // Handle the case where stringValue cannot be converted to a Double + } + } + + + func compareStringContains(_ sourceTo: Any, _ stringValue: String) -> Bool { + guard let stringTypeValue = sourceTo as? String else { return false } + return stringTypeValue.contains(stringValue) + } + + func compareStringStartsWith(_ sourceTo: Any, _ stringValue: String) -> Bool { + guard let stringTypeValue = sourceTo as? String else { return false } + return stringTypeValue.hasPrefix(stringValue) + } + + func compareWithRegex(_ sourceTo: String, pattern: String) -> Bool { + do { + let regex = try NSRegularExpression(pattern: pattern) + let range = NSRange(sourceTo.startIndex.. [[AnyHashable:Any]] { - let dictionaries = items.map { item in - return item.toDictionary() - } - return dictionaries + storeEventData(type: EventType.customEvent, data: body) } - // Convert to commerce items from dictionaries - private func convertCommerceItems(from dictionaries: [[AnyHashable: Any]]) -> [CommerceItem] { - return dictionaries.compactMap { dictionary in - let item = CommerceItem(id: dictionary[JsonKey.CommerceItem.id] as? String ?? "", name: dictionary[JsonKey.CommerceItem.name] as? String ?? "", price: dictionary[JsonKey.CommerceItem.price] as? NSNumber ?? 0, quantity: dictionary[JsonKey.CommerceItem.quantity] as? UInt ?? 0) - item.sku = dictionary[JsonKey.CommerceItem.sku] as? String - item.itemDescription = dictionary[JsonKey.CommerceItem.description] as? String - item.url = dictionary[JsonKey.CommerceItem.url] as? String - item.imageUrl = dictionary[JsonKey.CommerceItem.imageUrl] as? String - item.categories = dictionary[JsonKey.CommerceItem.categories] as? [String] - item.dataFields = dictionary[JsonKey.CommerceItem.dataFields] as? [AnyHashable: Any] - - return item - } + public func trackAnonUpdateUser(_ dataFields: [AnyHashable: Any]) { + var body = [AnyHashable: Any]() + body[JsonKey.dataFields] = dataFields + storeEventData(type: EventType.updateUser, data: body, shouldOverWrite: true) } // Tracks an anonymous purchase event and store it locally public func trackAnonPurchaseEvent(total: NSNumber, items: [CommerceItem], dataFields: [AnyHashable: Any]?) { var body = [AnyHashable: Any]() - body.setValue(for: JsonKey.Body.createdAt, value: Int(dateProvider.currentDate.timeIntervalSince1970)) + body.setValue(for: JsonKey.Body.createdAt, value: Int(dateProvider.currentDate.timeIntervalSince1970) * 1000) body.setValue(for: JsonKey.Commerce.total, value: total.stringValue) body.setValue(for: JsonKey.Commerce.items, value: convertCommerceItemsToDictionary(items)) if let dataFields = dataFields { body[JsonKey.dataFields] = dataFields } - storeEventData(type: EventType.trackPurchase, data: body) + storeEventData(type: EventType.purchase, data: body) } // Tracks an anonymous cart event and store it locally public func trackAnonUpdateCart(items: [CommerceItem]) { var body = [AnyHashable: Any]() + body.setValue(for: JsonKey.Body.createdAt, value: Int(dateProvider.currentDate.timeIntervalSince1970) * 1000) body.setValue(for: JsonKey.Commerce.items, value: convertCommerceItemsToDictionary(items)) storeEventData(type: EventType.cartUpdate, data: body) } @@ -111,27 +86,19 @@ public class AnonymousUserManager: AnonymousUserManagerProtocol { } } - func convertToDictionary(data: Codable) -> [AnyHashable: Any] { - do { - let encoder = JSONEncoder() - let data = try encoder.encode(data) - if let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [AnyHashable: Any] { - return dictionary + // Creates a user after criterias met and login the user and then sync the data through track APIs + private func createKnownUserIfCriteriaMatched(criteriaId: Int?) { + if (criteriaId != nil) { + let userId = IterableUtil.generateUUID() + IterableAPI.setUserId(userId) + var anonSessions = convertToDictionary(data: localStorage.anonymousSessions?.itbl_anon_sessions) + anonSessions["anon_criteria_id"] = criteriaId + notificationStateProvider.isNotificationsEnabled { isEnabled in + anonSessions["pushOptIn"] = isEnabled + IterableAPI.track(event: "itbl_anon_sessions", dataFields: anonSessions) + self.syncEvents() } - } catch { - print("Error converting to dictionary: \(error)") } - return [:] - } - - // Creates a user after criterias met and login the user and then sync the data through track APIs - public func createKnownUser() { - let userId = IterableUtil.generateUUID() - print("userID: \(userId)") - IterableAPI.setUserId(userId) - IterableAPI.updateUser(convertToDictionary(data: localStorage.anonymousSessions), mergeNestedObjects: false, onSuccess: { result in - self.syncEvents() - }) } // Syncs unsynced data which might have failed to sync when calling syncEvents for the first time after criterias met @@ -155,12 +122,12 @@ public class AnonymousUserManager: AnonymousUserManagerProtocol { if let eventType = eventData[JsonKey.eventType] as? String { eventData.removeValue(forKey: JsonKey.eventType) switch eventType { - case EventType.track: + case EventType.customEvent: IterableAPI.implementation?.track(eventData[JsonKey.eventName] as? String ?? "", withBody: eventData, onSuccess: {result in successfulSyncedData.append(eventData[JsonKey.eventTimeStamp] as? Int ?? 0) }) break - case EventType.trackPurchase: + case EventType.purchase: var userDict = [AnyHashable: Any]() userDict[JsonKey.userId] = localStorage.userId userDict[JsonKey.preferUserId] = true @@ -184,6 +151,9 @@ public class AnonymousUserManager: AnonymousUserManagerProtocol { successfulSyncedData.append(eventData[JsonKey.eventTimeStamp] as? Int ?? 0) }) break + case EventType.updateUser: + IterableAPI.implementation?.updateUser(eventData[JsonKey.dataFields] as? [AnyHashable : Any] ?? [:], mergeNestedObjects: false) + break default: break } @@ -203,112 +173,33 @@ public class AnonymousUserManager: AnonymousUserManagerProtocol { } } - // Checks if criterias are being met. - private func checkCriteriaCompletion() -> Bool { - var isCriteriaMet = false - let criteriaData = localStorage.criteriaData - if let _criteriaData = criteriaData { - for criteria in _criteriaData { - for criteriaItem in criteria.criteriaList { - // right now we are considering track events only which has eventname. // we will later on consider other eventtypes and add related logic here - if let events = filterEvents(byType: criteriaItem.criteriaType, andName: criteriaItem.name) { - if events.count >= criteriaItem.aggregateCount ?? 1 { - isCriteriaMet = true - break - } - } - } - } - } - return isCriteriaMet - } - - // Filter non-synced data - private func filterEvents(excludingTimestamps excludedTimestamps: [Int]) -> [[AnyHashable: Any]]? { - guard let events = localStorage.anonymousUserEvents else { - return nil - } - - let filteredEvents = events.filter { eventData in - if let eventTimestamp = eventData[JsonKey.eventTimeStamp] as? Int, - !excludedTimestamps.contains(eventTimestamp) { - return true - } - return false - } - - return filteredEvents.isEmpty ? nil : filteredEvents - } - - // Filter events by type - private func filterEvents(byType type: String) -> [[AnyHashable: Any]]? { - guard let events = localStorage.anonymousUserEvents else { + // Checks if criterias are being met and returns criteriaId if it matches the criteria. + private func evaluateCriteriaAndReturnID() -> Int? { + guard let events = localStorage.anonymousUserEvents, let criteriaData = localStorage.criteriaData else { return nil } - - let filteredEvents = events.filter { eventData in - if let eventType = eventData[JsonKey.eventType] as? String, eventType == type { - return true - } - return false - } - - return filteredEvents.isEmpty ? nil : filteredEvents + let matchedCriteriaId = CriteriaCompletionChecker(anonymousCriteria: criteriaData, anonymousEvents: events).getMatchedCriteria() + return matchedCriteriaId } - - // Filter events by type and name - private func filterEvents(byType type: String, andName name: String?) -> [[AnyHashable: Any]]? { - guard let events = localStorage.anonymousUserEvents else { - return nil - } - - let filteredEvents = events.filter { eventData in - if let eventType = eventData[JsonKey.eventType] as? String, eventType == type { - if let eventName = eventData[JsonKey.eventName] as? String { - if let filterName = name { - return eventName == filterName - } else { - return true - } - } else { - return true - } - } - return false - } - - return filteredEvents.isEmpty ? nil : filteredEvents - } - - // Converts UTC Datetime from current time - private func getUTCDateTime() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" - dateFormatter.timeZone = TimeZone(identifier: "UTC") - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - - let utcDate = Date() - return dateFormatter.string(from: utcDate) - } - // Gets the anonymous criteria public func getAnonCriteria() { - // call API when it is available and save data in userdefaults, until then just save the data in userdefaults using static data - let data: [Criteria] = [ - Criteria(criteriaId: "12", criteriaList: [ - CriteriaItem(criteriaType: "track", comparator: "equal", name: "viewedMocha", aggregateCount: 5, total: nil), - CriteriaItem(criteriaType: "track", comparator: "equal", name: "viewedCappuccino", aggregateCount: 3, total: nil) - ]), - Criteria(criteriaId: "13", criteriaList: [ - CriteriaItem(criteriaType: "trackPurchase", comparator: nil, name: nil, aggregateCount: nil, total: 3), - CriteriaItem(criteriaType: "cartUpdate", comparator: nil, name: nil, aggregateCount: nil, total: nil), - ]) - ] - localStorage.criteriaData = data + // call API when it is available and save data in userdefaults, until then just save the data in userdefaults using static data from anoncriteria_response.json + if let path = Bundle.module.path(forResource: "anoncriteria_response", ofType: "json") { + let fileURL = URL(fileURLWithPath: path) + do { + let data = try Data(contentsOf: fileURL) + // Process your data here + localStorage.criteriaData = data + } catch { + print("Error reading file: \(error)") + } + } else { + print("File not found in the package") + } } // Stores event data locally - private func storeEventData(type: String, data: [AnyHashable: Any]) { + private func storeEventData(type: String, data: [AnyHashable: Any], shouldOverWrite: Bool? = false) { let storedData = localStorage.anonymousUserEvents var eventsDataObjects: [[AnyHashable: Any]] = [[:]] @@ -319,10 +210,12 @@ public class AnonymousUserManager: AnonymousUserManagerProtocol { appendData.setValue(for: JsonKey.eventType, value: type) appendData.setValue(for: JsonKey.eventTimeStamp, value: Int(dateProvider.currentDate.timeIntervalSince1970)) // this we use as unique idenfier too - eventsDataObjects.append(appendData) - localStorage.anonymousUserEvents = eventsDataObjects - if (checkCriteriaCompletion()) { - createKnownUser() + if shouldOverWrite == true { + eventsDataObjects = eventsDataObjects.map { var subDictionary = $0; subDictionary[type] = data; return subDictionary } + } else { + eventsDataObjects.append(appendData) } + localStorage.anonymousUserEvents = eventsDataObjects + createKnownUserIfCriteriaMatched(criteriaId: evaluateCriteriaAndReturnID()) } } diff --git a/swift-sdk/Internal/AnonymousUserManagerProtocol.swift b/swift-sdk/Internal/AnonymousUserManagerProtocol.swift new file mode 100644 index 000000000..d03f33700 --- /dev/null +++ b/swift-sdk/Internal/AnonymousUserManagerProtocol.swift @@ -0,0 +1,18 @@ +// +// AnonymousUserManagerProtocol.swift +// +// +// Created by HARDIK MASHRU on 09/11/23. +// +import Foundation +@objc public protocol AnonymousUserManagerProtocol { + func trackAnonEvent(name: String, dataFields: [AnyHashable: Any]?) + func trackAnonPurchaseEvent(total: NSNumber, items: [CommerceItem], dataFields: [AnyHashable: Any]?) + func trackAnonUpdateCart(items: [CommerceItem]) + func trackAnonTokenRegistration(token: String) + func trackAnonUpdateUser(_ dataFields: [AnyHashable: Any]) + func updateAnonSession() + func getAnonCriteria() + func syncNonSyncedEvents() + func logout() +} diff --git a/swift-sdk/Internal/DependencyContainerProtocol.swift b/swift-sdk/Internal/DependencyContainerProtocol.swift index 260dad29d..093df6dbb 100644 --- a/swift-sdk/Internal/DependencyContainerProtocol.swift +++ b/swift-sdk/Internal/DependencyContainerProtocol.swift @@ -122,8 +122,9 @@ extension DependencyContainerProtocol { } func createAnonymousUserManager() -> AnonymousUserManagerProtocol { - AnonymousUserManager(localStorage: localStorage, - dateProvider: dateProvider) + AnonymousUserManager(localStorage: localStorage, + dateProvider: dateProvider, + notificationStateProvider: notificationStateProvider) } private func createTaskScheduler(persistenceContextProvider: IterablePersistenceContextProvider, diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index f3a832aee..9a065c4f8 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -127,7 +127,6 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { anonymousUserMerge.mergeUserUsingEmail(apiClient: apiClient as! ApiClient, destinationUserId: _userId ?? "", destinationEmail: email ?? "", sourceEmail: _email ?? "") ITBInfo() - if email == nil { anonymousUserManager.logout() } @@ -257,7 +256,10 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { mergeNestedObjects: Bool, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Pending { - requestHandler.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects, onSuccess: onSuccess, onFailure: onFailure) + if !isEitherUserIdOrEmailSet() { + anonymousUserManager.trackAnonUpdateUser(dataFields) + } + return requestHandler.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects, onSuccess: onSuccess, onFailure: onFailure) } @discardableResult @@ -283,18 +285,18 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Pending { if !isEitherUserIdOrEmailSet() { - anonymousUserManager.trackAnonUpdateCart(items: items) - } - return requestHandler.updateCart(items: items, onSuccess: onSuccess, onFailure: onFailure) - } - - @discardableResult - func updateCart(items: [CommerceItem], - withUser user: [AnyHashable:Any], - createdAt: Int, - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Pending { - return requestHandler.updateCart(items: items, withUser: user, createdAt: createdAt, onSuccess: onSuccess, onFailure: onFailure) + anonymousUserManager.trackAnonUpdateCart(items: items) + } + return requestHandler.updateCart(items: items, onSuccess: onSuccess, onFailure: onFailure) + } + + @discardableResult + func updateCart(items: [CommerceItem], + withUser user: [AnyHashable:Any], + createdAt: Int, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Pending { + return requestHandler.updateCart(items: items, withUser: user, createdAt: createdAt, onSuccess: onSuccess, onFailure: onFailure) } @discardableResult @@ -306,8 +308,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Pending { if !isEitherUserIdOrEmailSet() { - anonymousUserManager.trackAnonPurchaseEvent(total: total, items: items, dataFields: dataFields) - } + anonymousUserManager.trackAnonPurchaseEvent(total: total, items: items, dataFields: dataFields) + } return requestHandler.trackPurchase(total, items: items, dataFields: dataFields, @@ -316,7 +318,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { onSuccess: onSuccess, onFailure: onFailure) } - + @discardableResult func trackPurchase(_ total: NSNumber, items: [CommerceItem], @@ -333,6 +335,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { onSuccess: onSuccess, onFailure: onFailure) } + @discardableResult func trackPushOpen(_ userInfo: [AnyHashable: Any], @@ -376,19 +379,18 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { dataFields: [AnyHashable: Any]? = nil, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Pending { - requestHandler.track(event: eventName, dataFields: dataFields, onSuccess: onSuccess, onFailure: onFailure) if !isEitherUserIdOrEmailSet() { - anonymousUserManager.trackAnonEvent(name: eventName, dataFields: dataFields) - } - return requestHandler.track(event: eventName, dataFields: dataFields, onSuccess: onSuccess, onFailure: onFailure) - } - - @discardableResult - func track(_ eventName: String, - withBody body: [AnyHashable: Any], - onSuccess: OnSuccessHandler? = nil, - onFailure: OnFailureHandler? = nil) -> Pending { - requestHandler.track(event: eventName, withBody: body, onSuccess: onSuccess, onFailure: onFailure) + anonymousUserManager.trackAnonEvent(name: eventName, dataFields: dataFields) + } + return requestHandler.track(event: eventName, dataFields: dataFields, onSuccess: onSuccess, onFailure: onFailure) + } + + @discardableResult + func track(_ eventName: String, + withBody body: [AnyHashable: Any], + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) -> Pending { + requestHandler.track(event: eventName, withBody: body, onSuccess: onSuccess, onFailure: onFailure) } @discardableResult diff --git a/swift-sdk/Internal/IterableUserDefaults.swift b/swift-sdk/Internal/IterableUserDefaults.swift index 586215387..bd3b8a892 100644 --- a/swift-sdk/Internal/IterableUserDefaults.swift +++ b/swift-sdk/Internal/IterableUserDefaults.swift @@ -78,9 +78,9 @@ class IterableUserDefaults { } } - var criteriaData: [Criteria]? { + var criteriaData: Data? { get { - return criteriaData(withKey: .criteriaData) + return getCriteriaData(withKey: .criteriaData) } set { saveCriteriaData(data: newValue, withKey: .criteriaData) } @@ -118,10 +118,8 @@ class IterableUserDefaults { return nil } - private func saveCriteriaData(data: [Criteria]?, withKey key: UserDefaultsKey) { - if let encodedData = try? JSONEncoder().encode(data) { - userDefaults.set(encodedData, forKey: key.value) - } + private func saveCriteriaData(data: Data?, withKey key: UserDefaultsKey) { + userDefaults.set(data, forKey: key.value) } private func saveEventData(anonymousUserEvents: [[AnyHashable: Any]]?, withKey key: UserDefaultsKey) { @@ -200,6 +198,10 @@ class IterableUserDefaults { userDefaults.array(forKey: key.value) as? [[AnyHashable: Any]] } + private func getCriteriaData(withKey key: UserDefaultsKey) -> Data? { + userDefaults.object(forKey: key.value) as? Data + } + private static func isExpired(expiration: Date?, currentDate: Date) -> Bool { if let expiration = expiration { if expiration.timeIntervalSinceReferenceDate > currentDate.timeIntervalSinceReferenceDate { diff --git a/swift-sdk/Internal/LocalStorage.swift b/swift-sdk/Internal/LocalStorage.swift index c0e47fe63..b94be9774 100644 --- a/swift-sdk/Internal/LocalStorage.swift +++ b/swift-sdk/Internal/LocalStorage.swift @@ -83,7 +83,7 @@ struct LocalStorage: LocalStorageProtocol { } } - var criteriaData: [Criteria]? { + var criteriaData: Data? { get { iterableUserDefaults.criteriaData } set { diff --git a/swift-sdk/Internal/LocalStorageProtocol.swift b/swift-sdk/Internal/LocalStorageProtocol.swift index 5e045eb0d..01a62f9c6 100644 --- a/swift-sdk/Internal/LocalStorageProtocol.swift +++ b/swift-sdk/Internal/LocalStorageProtocol.swift @@ -21,7 +21,7 @@ protocol LocalStorageProtocol { var anonymousUserEvents: [[AnyHashable: Any]]? { get set } - var criteriaData: [Criteria]? { get set } + var criteriaData: Data? { get set } var anonymousSessions: IterableAnonSessionsWrapper? { get set } diff --git a/swift-sdk/Resources/anoncriteria_response.json b/swift-sdk/Resources/anoncriteria_response.json new file mode 100644 index 000000000..67907a8a7 --- /dev/null +++ b/swift-sdk/Resources/anoncriteria_response.json @@ -0,0 +1,130 @@ +{ + "count":2, + "criteriaList":[ + { + "criteriaId":12345, + "searchQuery":{ + "combinator":"And", + "searchQueries":[ + { + "combinator":"And", + "searchQueries":[ + { + "dataType":"purchase", + "searchCombo":{ + "combinator":"And", + "searchQueries":[ + { + "field":"shoppingCartItems.price", + "fieldType":"double", + "comparatorType":"Equals", + "dataType":"purchase", + "id":2, + "value":"4.67" + }, + { + "field":"shoppingCartItems.quantity", + "fieldType":"long", + "comparatorType":"GreaterThan", + "dataType":"purchase", + "id":3, + "valueLong":2, + "value":"2" + }, + { + "field":"total", + "fieldType":"long", + "comparatorType":"GreaterThanOrEqualTo", + "dataType":"purchase", + "id":4, + "valueLong":10, + "value":"10" + } + ] + } + } + ] + }, + { + "combinator":"And", + "searchQueries":[ + { + "dataType":"customEvent", + "searchCombo":{ + "combinator":"Or", + "searchQueries":[ + { + "field":"eventName", + "fieldType":"string", + "comparatorType":"Equals", + "dataType":"customEvent", + "id":9, + "value":"processing_cancelled" + } + ] + } + } + ] + } + ] + } + }, + { + "criteriaId":5678, + "searchQuery":{ + "combinator":"Or", + "searchQueries":[ + { + "combinator":"Or", + "searchQueries":[ + { + "dataType":"user", + "searchCombo":{ + "combinator":"And", + "searchQueries":[ + { + "field":"itblInternal.emailDomain", + "fieldType":"string", + "comparatorType":"Equals", + "dataType":"user", + "id":6, + "value":"gmail.com" + } + ] + } + }, + { + "dataType":"customEvent", + "searchCombo":{ + "combinator":"And", + "searchQueries":[ + { + "field":"eventName", + "fieldType":"string", + "comparatorType":"Equals", + "dataType":"customEvent", + "id":9, + "value":"processing_cancelled" + }, + { + "field":"createdAt", + "fieldType":"date", + "comparatorType":"GreaterThan", + "dataType":"customEvent", + "id":10, + "dateRange":{ + + }, + "isRelativeDate":false, + "value":"1731513963000" + } + ] + } + } + ] + } + ] + } + } + ] +} diff --git a/tests/common/MockLocalStorage.swift b/tests/common/MockLocalStorage.swift index ab148e719..aa1158459 100644 --- a/tests/common/MockLocalStorage.swift +++ b/tests/common/MockLocalStorage.swift @@ -7,6 +7,12 @@ import Foundation @testable import IterableSDK class MockLocalStorage: LocalStorageProtocol { + var anonymousUserEvents: [[AnyHashable : Any]]? + + var criteriaData: Data? + + var anonymousSessions: IterableSDK.IterableAnonSessionsWrapper? + var userId: String? = nil var email: String? = nil diff --git a/tests/unit-tests/AnonymousUserCriteriaMatchTests.swift b/tests/unit-tests/AnonymousUserCriteriaMatchTests.swift new file mode 100644 index 000000000..414551930 --- /dev/null +++ b/tests/unit-tests/AnonymousUserCriteriaMatchTests.swift @@ -0,0 +1,211 @@ +// +// File.swift +// +// +// Created by HARDIK MASHRU on 14/11/23. +// + +import XCTest + +@testable import IterableSDK + +class AnonymousUserCriteriaMatchTests: XCTestCase { + + private let mockDataWithOr = """ + { + "count":1, + "criteriaList":[ + { + "criteriaId":12345, + "searchQuery":{ + "combinator":"Or", + "searchQueries":[ + { + "dataType":"purchase", + "searchCombo":{ + "combinator":"Or", + "searchQueries":[ + { + "field":"shoppingCartItems.price", + "fieldType":"double", + "comparatorType":"Equals", + "dataType":"purchase", + "id":2, + "value":"5.9" + }, + { + "field":"shoppingCartItems.quantity", + "fieldType":"long", + "comparatorType":"GreaterThan", + "dataType":"purchase", + "id":3, + "valueLong":2, + "value":"2" + }, + { + "field":"total", + "fieldType":"long", + "comparatorType":"GreaterThanOrEqualTo", + "dataType":"purchase", + "id":4, + "valueLong":10, + "value":"10" + } + ] + } + } + ] + } + } + ] + } + """ + + private let mockDataWithAnd = """ + { + "count":1, + "criteriaList":[ + { + "criteriaId":12345, + "searchQuery":{ + "combinator":"And", + "searchQueries":[ + { + "combinator":"And", + "searchQueries":[ + { + "dataType":"purchase", + "searchCombo":{ + "combinator":"And", + "searchQueries":[ + { + "field":"shoppingCartItems.price", + "fieldType":"double", + "comparatorType":"Equals", + "dataType":"purchase", + "id":2, + "value":"4.67" + }, + { + "field":"shoppingCartItems.quantity", + "fieldType":"long", + "comparatorType":"GreaterThan", + "dataType":"purchase", + "id":3, + "valueLong":2, + "value":"2" + }, + { + "field":"total", + "fieldType":"long", + "comparatorType":"GreaterThanOrEqualTo", + "dataType":"purchase", + "id":4, + "valueLong":10, + "value":"10" + }, + { + "field":"campaignId", + "fieldType":"long", + "comparatorType":"Equals", + "dataType":"purchase", + "id":11, + "value":"1234" + } + ] + } + }, + { + "combinator":"And", + "searchQueries":[ + { + "dataType":"customEvent", + "searchCombo":{ + "combinator":"Or", + "searchQueries":[ + { + "field":"eventName", + "fieldType":"string", + "comparatorType":"Equals", + "dataType":"customEvent", + "id":9, + "value":"processing_cancelled" + }, + { + "field":"messageId", + "fieldType":"string", + "comparatorType":"Equals", + "dataType":"customEvent", + "id":10, + "value":"1234" + } + ] + } + } + ] + } + ] + } + ] + } + } + ] + } + """ + + override func setUp() { + super.setUp() + } + + func data(from jsonString: String) -> Data? { + return jsonString.data(using: .utf8) + } + + func testCompareDataWithANDCombinatorSuccess() { + let eventItems: [[AnyHashable: Any]] = [[ + "items": [["id": "12", "name": "Mocha", "price": 4.67, "quantity": 3]], + "total": 11.0, + "createdAt": 1699246745093, + "dataType": "purchase", + "dataFields": ["campaignId": 1234] + ], ["dataType": "customEvent", "eventName": "processing_cancelled"]] + let expectedCriteriaId = 12345 + let matchedCriteriaId = CriteriaCompletionChecker(anonymousCriteria: data(from: mockDataWithAnd)!, anonymousEvents: eventItems).getMatchedCriteria() + XCTAssertEqual(matchedCriteriaId, expectedCriteriaId) + } + + func testCompareDataWithANDCombinatorFail() { + let eventItems: [[AnyHashable: Any]] = [[ + "items": [["id": "12", "name": "Mocha", "price": 4.67, "quantity": 3]], + "total": 9.0, + "createdAt": 1699246745093, + "dataType": "purchase" + ]] + let matchedCriteriaId = CriteriaCompletionChecker(anonymousCriteria: data(from: mockDataWithAnd)!, anonymousEvents: eventItems).getMatchedCriteria() + XCTAssertEqual(matchedCriteriaId, nil) + } + + + func testCompareDataWithORCombinatorSuccess() { + let eventItems: [[AnyHashable: Any]] = [[ + "items": [["id": "12", "name": "Mocha", "price": 5.9, "quantity": 1]], + "total": 9.0, + "createdAt": 1699246745093, + "dataType": "purchase" + ]] + let expectedCriteriaId = 12345 + let matchedCriteriaId = CriteriaCompletionChecker(anonymousCriteria: data(from: mockDataWithOr)!, anonymousEvents: eventItems).getMatchedCriteria() + XCTAssertEqual(matchedCriteriaId, expectedCriteriaId) + } + + func testCompareDataWithORCombinatorFail() { + let eventItems: [[AnyHashable: Any]] = [[ + "items": [["id": "12", "name": "Mocha", "price": 2.9, "quantity": 1]], + "total": 9.0, + "createdAt": 1699246745093, + "dataType": "purchase" + ]] + let matchedCriteriaId = CriteriaCompletionChecker(anonymousCriteria: data(from: mockDataWithOr)!, anonymousEvents: eventItems).getMatchedCriteria() + XCTAssertEqual(matchedCriteriaId, nil) + } +} diff --git a/tests/unit-tests/BlankApiClient.swift b/tests/unit-tests/BlankApiClient.swift index 43571ae10..ab0193044 100644 --- a/tests/unit-tests/BlankApiClient.swift +++ b/tests/unit-tests/BlankApiClient.swift @@ -7,6 +7,24 @@ import Foundation @testable import IterableSDK class BlankApiClient: ApiClientProtocol { + + func track(event eventName: String, dataFields: [AnyHashable : Any]?) -> IterableSDK.Pending { + Pending() + } + + func track(event eventName: String, withBody body: [AnyHashable : Any]?) -> IterableSDK.Pending { + Pending() + } + + + func updateCart(items: [IterableSDK.CommerceItem], withUser user: [AnyHashable : Any], createdAt: Int) -> IterableSDK.Pending { + Pending() + } + + func track(purchase total: NSNumber, items: [IterableSDK.CommerceItem], dataFields: [AnyHashable : Any]?, withUser user: [AnyHashable : Any], createdAt: Int) -> IterableSDK.Pending { + Pending() + } + func register(registerTokenInfo: RegisterTokenInfo, notificationsEnabled: Bool) -> Pending { Pending() } @@ -31,10 +49,6 @@ class BlankApiClient: ApiClientProtocol { Pending() } - func track(event eventName: String, dataFields: [AnyHashable : Any]?) -> Pending { - Pending() - } - func updateSubscriptions(_ emailListIds: [NSNumber]?, unsubscribedChannelIds: [NSNumber]?, unsubscribedMessageTypeIds: [NSNumber]?, subscribedMessageTypeIds: [NSNumber]?, campaignId: NSNumber?, templateId: NSNumber?) -> Pending { Pending() }