/** * NotificationDeliveryDAO.swift * * Data Access Object (DAO) for NotificationDelivery Core Data entity * Provides CRUD operations and query helpers for notification delivery tracking * * @author Matthew Raymer * @version 1.0.0 * @created 2025-12-08 */ import Foundation import CoreData /** * Extension providing DAO methods for NotificationDelivery entity * * This extension adds CRUD operations and query helpers to the * auto-generated NotificationDelivery Core Data class. */ extension NotificationDelivery { // MARK: - Constants private static let TAG = "DNP-NOTIFICATION-DELIVERY-DAO" // MARK: - Create/Insert Methods /** * Create a new NotificationDelivery entity in the given context * * @param context Core Data managed object context * @param id Unique delivery identifier * @param notificationId Associated notification content ID * @param notificationContent Associated NotificationContent entity * @param timesafariDid TimeSafari device ID * @param deliveryTimestamp When delivery occurred * @param deliveryStatus Delivery status string * @param deliveryMethod Delivery method string * @param deliveryAttemptNumber Attempt number (1-based) * @param deliveryDurationMs Duration of delivery in milliseconds * @param userInteractionType Type of user interaction (if any) * @param userInteractionTimestamp When user interacted * @param userInteractionDurationMs Duration of interaction in milliseconds * @param errorCode Error code (if delivery failed) * @param errorMessage Error message (if delivery failed) * @param deviceInfo Device information JSON string * @param networkInfo Network information JSON string * @param batteryLevel Battery level (0-100, -1 if unknown) * @param dozeModeActive Whether device was in doze mode * @param exactAlarmPermission Whether exact alarm permission granted * @param notificationPermission Whether notification permission granted * @param metadata Additional metadata (JSON string) * @return Created NotificationDelivery entity */ static func create( in context: NSManagedObjectContext, id: String, notificationId: String, notificationContent: NotificationContentEntity? = nil, timesafariDid: String? = nil, deliveryTimestamp: Date, deliveryStatus: String? = nil, deliveryMethod: String? = nil, deliveryAttemptNumber: Int32 = 1, deliveryDurationMs: Int64 = 0, userInteractionType: String? = nil, userInteractionTimestamp: Date? = nil, userInteractionDurationMs: Int64 = 0, errorCode: String? = nil, errorMessage: String? = nil, deviceInfo: String? = nil, networkInfo: String? = nil, batteryLevel: Int32 = -1, dozeModeActive: Bool = false, exactAlarmPermission: Bool = false, notificationPermission: Bool = false, metadata: String? = nil ) -> NotificationDelivery { let entity = NotificationDelivery(context: context) entity.id = id entity.notificationId = notificationId entity.notificationContent = notificationContent entity.timesafariDid = timesafariDid entity.deliveryTimestamp = deliveryTimestamp entity.deliveryStatus = deliveryStatus entity.deliveryMethod = deliveryMethod entity.deliveryAttemptNumber = deliveryAttemptNumber entity.deliveryDurationMs = deliveryDurationMs entity.userInteractionType = userInteractionType entity.userInteractionTimestamp = userInteractionTimestamp entity.userInteractionDurationMs = userInteractionDurationMs entity.errorCode = errorCode entity.errorMessage = errorMessage entity.deviceInfo = deviceInfo entity.networkInfo = networkInfo entity.batteryLevel = batteryLevel entity.dozeModeActive = dozeModeActive entity.exactAlarmPermission = exactAlarmPermission entity.notificationPermission = notificationPermission entity.metadata = metadata print("\(Self.TAG): Created NotificationDelivery with id: \(id)") return entity } /** * Create from dictionary representation * * @param context Core Data managed object context * @param dict Dictionary with delivery data * @param notificationContent Optional associated NotificationContent entity * @return Created NotificationDelivery entity or nil */ static func create( in context: NSManagedObjectContext, from dict: [String: Any], notificationContent: NotificationContentEntity? = nil ) -> NotificationDelivery? { guard let id = dict["id"] as? String, let notificationId = dict["notificationId"] as? String else { print("\(Self.TAG): Missing required fields") return nil } // Convert deliveryTimestamp from epoch milliseconds or Date let deliveryTimestamp: Date if let timeMillis = dict["deliveryTimestamp"] as? Int64 { deliveryTimestamp = DailyNotificationDataConversions.dateFromEpochMillis(timeMillis) } else if let timeDate = dict["deliveryTimestamp"] as? Date { deliveryTimestamp = timeDate } else { print("\(Self.TAG): Missing or invalid 'deliveryTimestamp' field") return nil } // Convert userInteractionTimestamp if present let userInteractionTimestamp: Date? if let interactionMillis = dict["userInteractionTimestamp"] as? Int64 { userInteractionTimestamp = DailyNotificationDataConversions.dateFromEpochMillis(interactionMillis) } else if let interactionDate = dict["userInteractionTimestamp"] as? Date { userInteractionTimestamp = interactionDate } else { userInteractionTimestamp = nil } let entity = NotificationDelivery(context: context) entity.id = id entity.notificationId = notificationId entity.notificationContent = notificationContent entity.timesafariDid = dict["timesafariDid"] as? String entity.deliveryTimestamp = deliveryTimestamp entity.deliveryStatus = dict["deliveryStatus"] as? String entity.deliveryMethod = dict["deliveryMethod"] as? String entity.deliveryAttemptNumber = DailyNotificationDataConversions.int32FromInt( dict["deliveryAttemptNumber"] as? Int ?? 1 ) entity.deliveryDurationMs = dict["deliveryDurationMs"] as? Int64 ?? 0 entity.userInteractionType = dict["userInteractionType"] as? String entity.userInteractionTimestamp = userInteractionTimestamp entity.userInteractionDurationMs = dict["userInteractionDurationMs"] as? Int64 ?? 0 entity.errorCode = dict["errorCode"] as? String entity.errorMessage = dict["errorMessage"] as? String entity.deviceInfo = dict["deviceInfo"] as? String entity.networkInfo = dict["networkInfo"] as? String entity.batteryLevel = DailyNotificationDataConversions.int32FromInt( dict["batteryLevel"] as? Int ?? -1 ) entity.dozeModeActive = dict["dozeModeActive"] as? Bool ?? false entity.exactAlarmPermission = dict["exactAlarmPermission"] as? Bool ?? false entity.notificationPermission = dict["notificationPermission"] as? Bool ?? false entity.metadata = dict["metadata"] as? String return entity } // MARK: - Read/Query Methods /** * Fetch NotificationDelivery by ID * * @param context Core Data managed object context * @param id Delivery ID * @return NotificationDelivery entity or nil */ static func fetch( by id: String, in context: NSManagedObjectContext ) -> NotificationDelivery? { let request: NSFetchRequest = NotificationDelivery.fetchRequest() request.predicate = NSPredicate(format: "id == %@", id) request.fetchLimit = 1 do { let results = try context.fetch(request) return results.first } catch { print("\(Self.TAG): Error fetching by id: \(error.localizedDescription)") return nil } } /** * Query by notificationId * * @param context Core Data managed object context * @param notificationId Notification content ID * @return Array of NotificationDelivery entities */ static func queryByNotificationId( _ notificationId: String, in context: NSManagedObjectContext ) -> [NotificationDelivery] { let request: NSFetchRequest = NotificationDelivery.fetchRequest() request.predicate = NSPredicate(format: "notificationId == %@", notificationId) request.sortDescriptors = [NSSortDescriptor(key: "deliveryTimestamp", ascending: false)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by notificationId: \(error.localizedDescription)") return [] } } /** * Query by deliveryTimestamp range * * @param context Core Data managed object context * @param startDate Start date (inclusive) * @param endDate End date (inclusive) * @return Array of NotificationDelivery entities */ static func query( deliveryTimestampBetween startDate: Date, and endDate: Date, in context: NSManagedObjectContext ) -> [NotificationDelivery] { let request: NSFetchRequest = NotificationDelivery.fetchRequest() request.predicate = NSPredicate( format: "deliveryTimestamp >= %@ AND deliveryTimestamp <= %@", startDate as NSDate, endDate as NSDate ) request.sortDescriptors = [NSSortDescriptor(key: "deliveryTimestamp", ascending: false)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by deliveryTimestamp range: \(error.localizedDescription)") return [] } } /** * Query by deliveryStatus * * @param context Core Data managed object context * @param deliveryStatus Delivery status string * @return Array of NotificationDelivery entities */ static func queryByDeliveryStatus( _ deliveryStatus: String, in context: NSManagedObjectContext ) -> [NotificationDelivery] { let request: NSFetchRequest = NotificationDelivery.fetchRequest() request.predicate = NSPredicate(format: "deliveryStatus == %@", deliveryStatus) request.sortDescriptors = [NSSortDescriptor(key: "deliveryTimestamp", ascending: false)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by deliveryStatus: \(error.localizedDescription)") return [] } } /** * Query by timesafariDid * * @param context Core Data managed object context * @param timesafariDid TimeSafari device ID * @return Array of NotificationDelivery entities */ static func queryByTimesafariDid( _ timesafariDid: String, in context: NSManagedObjectContext ) -> [NotificationDelivery] { let request: NSFetchRequest = NotificationDelivery.fetchRequest() request.predicate = NSPredicate(format: "timesafariDid == %@", timesafariDid) request.sortDescriptors = [NSSortDescriptor(key: "deliveryTimestamp", ascending: false)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by timesafariDid: \(error.localizedDescription)") return [] } } // MARK: - Update Methods /** * Update delivery status * * @param status New delivery status */ func updateDeliveryStatus(_ status: String) { self.deliveryStatus = status } /** * Record user interaction * * @param interactionType Type of interaction * @param timestamp When interaction occurred * @param durationMs Duration of interaction in milliseconds */ func recordUserInteraction( type: String, timestamp: Date, durationMs: Int64 ) { self.userInteractionType = type self.userInteractionTimestamp = timestamp self.userInteractionDurationMs = durationMs } // MARK: - Delete Methods /** * Delete NotificationDelivery by ID * * @param context Core Data managed object context * @param id Delivery ID * @return true if deleted, false otherwise */ static func delete( by id: String, in context: NSManagedObjectContext ) -> Bool { guard let entity = fetch(by: id, in: context) else { return false } context.delete(entity) do { try context.save() print("\(Self.TAG): Deleted NotificationDelivery with id: \(id)") return true } catch { print("\(Self.TAG): Error deleting: \(error.localizedDescription)") context.rollback() return false } } /** * Delete all NotificationDelivery entities for a notification * * @param context Core Data managed object context * @param notificationId Notification content ID * @return Number of entities deleted */ static func deleteAll( for notificationId: String, in context: NSManagedObjectContext ) -> Int { let deliveries = queryByNotificationId(notificationId, in: context) let count = deliveries.count for delivery in deliveries { context.delete(delivery) } do { try context.save() print("\(Self.TAG): Deleted \(count) NotificationDelivery entities for notification: \(notificationId)") return count } catch { print("\(Self.TAG): Error deleting all: \(error.localizedDescription)") context.rollback() return 0 } } /** * Delete all NotificationDelivery entities * * @param context Core Data managed object context * @return Number of entities deleted */ static func deleteAll( in context: NSManagedObjectContext ) -> Int { let request: NSFetchRequest = NotificationDelivery.fetchRequest() let deleteRequest = NSBatchDeleteRequest(fetchRequest: request) do { let result = try context.execute(deleteRequest) as? NSBatchDeleteResult try context.save() let count = result?.result as? Int ?? 0 print("\(Self.TAG): Deleted \(count) NotificationDelivery entities") return count } catch { print("\(Self.TAG): Error deleting all: \(error.localizedDescription)") context.rollback() return 0 } } }