/** * NotificationContent.swift * * Data structure for notification content * * @author Matthew Raymer * @version 1.0.0 */ import Foundation /** * Data structure representing notification content * * This class encapsulates all the information needed for a notification * including scheduling, content, and metadata. */ class NotificationContent: Codable { // MARK: - Properties let id: String let title: String? let body: String? let scheduledTime: Int64 // milliseconds since epoch (matches Android long) let fetchedAt: Int64 // milliseconds since epoch (matches Android long) let url: String? let payload: [String: Any]? let etag: String? // Phase 2: Delivery tracking properties var deliveryStatus: String? // e.g., "scheduled", "delivered", "missed", "error" var lastDeliveryAttempt: Int64? // milliseconds since epoch (matches Android long) // MARK: - Codable Support enum CodingKeys: String, CodingKey { case id case title case body case scheduledTime case fetchedAt case url case payload case etag case deliveryStatus case lastDeliveryAttempt } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) title = try container.decodeIfPresent(String.self, forKey: .title) body = try container.decodeIfPresent(String.self, forKey: .body) scheduledTime = try container.decode(Int64.self, forKey: .scheduledTime) fetchedAt = try container.decode(Int64.self, forKey: .fetchedAt) url = try container.decodeIfPresent(String.self, forKey: .url) // payload is encoded as JSON string if let payloadString = try? container.decodeIfPresent(String.self, forKey: .payload), let payloadData = payloadString.data(using: .utf8), let payloadDict = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any] { payload = payloadDict } else { payload = nil } etag = try container.decodeIfPresent(String.self, forKey: .etag) deliveryStatus = try container.decodeIfPresent(String.self, forKey: .deliveryStatus) lastDeliveryAttempt = try container.decodeIfPresent(Int64.self, forKey: .lastDeliveryAttempt) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(body, forKey: .body) try container.encode(scheduledTime, forKey: .scheduledTime) try container.encode(fetchedAt, forKey: .fetchedAt) try container.encodeIfPresent(url, forKey: .url) // Encode payload as JSON string if let payload = payload, let payloadData = try? JSONSerialization.data(withJSONObject: payload), let payloadString = String(data: payloadData, encoding: .utf8) { try container.encode(payloadString, forKey: .payload) } try container.encodeIfPresent(etag, forKey: .etag) try container.encodeIfPresent(deliveryStatus, forKey: .deliveryStatus) try container.encodeIfPresent(lastDeliveryAttempt, forKey: .lastDeliveryAttempt) } // MARK: - Initialization /** * Initialize notification content * * @param id Unique notification identifier * @param title Notification title * @param body Notification body text * @param scheduledTime When notification should fire (milliseconds since epoch) * @param fetchedAt When content was fetched (milliseconds since epoch) * @param url URL for content fetching * @param payload Additional payload data * @param etag ETag for HTTP caching * @param deliveryStatus Delivery status (optional, Phase 2) * @param lastDeliveryAttempt Last delivery attempt timestamp (optional, Phase 2) */ init(id: String, title: String?, body: String?, scheduledTime: Int64, fetchedAt: Int64, url: String?, payload: [String: Any]?, etag: String?, deliveryStatus: String? = nil, lastDeliveryAttempt: Int64? = nil) { self.id = id self.title = title self.body = body self.scheduledTime = scheduledTime self.fetchedAt = fetchedAt self.url = url self.payload = payload self.etag = etag self.deliveryStatus = deliveryStatus self.lastDeliveryAttempt = lastDeliveryAttempt } // MARK: - Convenience Methods /** * Get scheduled time as Date * * @return Scheduled time as Date object */ func getScheduledTimeAsDate() -> Date { return Date(timeIntervalSince1970: Double(scheduledTime) / 1000.0) } /** * Get fetched time as Date * * @return Fetched time as Date object */ func getFetchedTimeAsDate() -> Date { return Date(timeIntervalSince1970: Double(fetchedAt) / 1000.0) } /** * Check if notification is scheduled for today * * @return true if scheduled for today */ func isScheduledForToday() -> Bool { let scheduledDate = getScheduledTimeAsDate() let today = Date() let calendar = Calendar.current return calendar.isDate(scheduledDate, inSameDayAs: today) } /** * Check if notification is scheduled for tomorrow * * @return true if scheduled for tomorrow */ func isScheduledForTomorrow() -> Bool { let scheduledDate = getScheduledTimeAsDate() let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date() let calendar = Calendar.current return calendar.isDate(scheduledDate, inSameDayAs: tomorrow) } /** * Check if notification is in the future * * @return true if scheduled time is in the future */ func isInTheFuture() -> Bool { let currentTime = Int64(Date().timeIntervalSince1970 * 1000) return scheduledTime > currentTime } /** * Get age of content at scheduled time * * @return Age in seconds at scheduled time */ func getAgeAtScheduledTime() -> TimeInterval { return Double(scheduledTime - fetchedAt) / 1000.0 } /** * Convert to dictionary representation * * @return Dictionary representation of notification content */ func toDictionary() -> [String: Any] { var dict: [String: Any] = [ "id": id, "title": title ?? "", "body": body ?? "", "scheduledTime": scheduledTime, "fetchedAt": fetchedAt, "url": url ?? "", "payload": payload ?? [:], "etag": etag ?? "" ] // Phase 2: Add delivery tracking properties if present if let deliveryStatus = deliveryStatus { dict["deliveryStatus"] = deliveryStatus } if let lastDeliveryAttempt = lastDeliveryAttempt { dict["lastDeliveryAttempt"] = lastDeliveryAttempt } return dict } /** * Create from dictionary representation * * @param dict Dictionary representation * @return NotificationContent instance */ static func fromDictionary(_ dict: [String: Any]) -> NotificationContent? { guard let id = dict["id"] as? String else { return nil } // Handle both Int64 and TimeInterval (Double) for backward compatibility let scheduledTime: Int64 if let time = dict["scheduledTime"] as? Int64 { scheduledTime = time } else if let time = dict["scheduledTime"] as? Double { scheduledTime = Int64(time) } else { return nil } let fetchedAt: Int64 if let time = dict["fetchedAt"] as? Int64 { fetchedAt = time } else if let time = dict["fetchedAt"] as? Double { fetchedAt = Int64(time) } else { return nil } // Handle lastDeliveryAttempt (can be Int64 or Double/TimeInterval) let lastDeliveryAttempt: Int64? if let attempt = dict["lastDeliveryAttempt"] as? Int64 { lastDeliveryAttempt = attempt } else if let attempt = dict["lastDeliveryAttempt"] as? Double { lastDeliveryAttempt = Int64(attempt) } else { lastDeliveryAttempt = nil } return NotificationContent( id: id, title: dict["title"] as? String, body: dict["body"] as? String, scheduledTime: scheduledTime, fetchedAt: fetchedAt, url: dict["url"] as? String, payload: dict["payload"] as? [String: Any], etag: dict["etag"] as? String, deliveryStatus: dict["deliveryStatus"] as? String, lastDeliveryAttempt: lastDeliveryAttempt ) } }