import Foundation import CoreData import Combine class InvestmentSourceRepository: ObservableObject { private let context: NSManagedObjectContext @Published private(set) var sources: [InvestmentSource] = [] init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) { self.context = context fetchSources() setupNotificationObserver() } private func setupNotificationObserver() { NotificationCenter.default.addObserver( self, selector: #selector(contextDidChange), name: .NSManagedObjectContextObjectsDidChange, object: context ) } @objc private func contextDidChange(_ notification: Notification) { fetchSources() } // MARK: - Fetch func fetchSources() { let request: NSFetchRequest = InvestmentSource.fetchRequest() request.sortDescriptors = [ NSSortDescriptor(keyPath: \InvestmentSource.name, ascending: true) ] do { sources = try context.fetch(request) } catch { print("Failed to fetch sources: \(error)") sources = [] } } func fetchSource(by id: UUID) -> InvestmentSource? { let request: NSFetchRequest = InvestmentSource.fetchRequest() request.predicate = NSPredicate(format: "id == %@", id as CVarArg) request.fetchLimit = 1 return try? context.fetch(request).first } func fetchSources(for category: Category) -> [InvestmentSource] { let request: NSFetchRequest = InvestmentSource.fetchRequest() request.predicate = NSPredicate(format: "category == %@", category) request.sortDescriptors = [ NSSortDescriptor(keyPath: \InvestmentSource.name, ascending: true) ] return (try? context.fetch(request)) ?? [] } func fetchActiveSources() -> [InvestmentSource] { sources.filter { $0.isActive } } func fetchSourcesNeedingUpdate() -> [InvestmentSource] { sources.filter { $0.needsUpdate } } // MARK: - Create @discardableResult func createSource( name: String, category: Category, notificationFrequency: NotificationFrequency = .monthly, customFrequencyMonths: Int = 1 ) -> InvestmentSource { let source = InvestmentSource(context: context) source.name = name source.category = category source.notificationFrequency = notificationFrequency.rawValue source.customFrequencyMonths = Int16(customFrequencyMonths) save() return source } // MARK: - Update func updateSource( _ source: InvestmentSource, name: String? = nil, category: Category? = nil, notificationFrequency: NotificationFrequency? = nil, customFrequencyMonths: Int? = nil, isActive: Bool? = nil ) { if let name = name { source.name = name } if let category = category { source.category = category } if let frequency = notificationFrequency { source.notificationFrequency = frequency.rawValue } if let customMonths = customFrequencyMonths { source.customFrequencyMonths = Int16(customMonths) } if let isActive = isActive { source.isActive = isActive } save() } func toggleActive(_ source: InvestmentSource) { source.isActive.toggle() save() } // MARK: - Delete func deleteSource(_ source: InvestmentSource) { context.delete(source) save() } func deleteSource(at offsets: IndexSet) { for index in offsets { guard index < sources.count else { continue } context.delete(sources[index]) } save() } // MARK: - Statistics var sourceCount: Int { sources.count } var activeSourceCount: Int { sources.filter { $0.isActive }.count } var totalValue: Decimal { sources.reduce(Decimal.zero) { $0 + $1.latestValue } } func topSources(limit: Int = 5) -> [InvestmentSource] { sources .sorted { $0.latestValue > $1.latestValue } .prefix(limit) .map { $0 } } // MARK: - Freemium Check var canAddMoreSources: Bool { // This will be checked against FreemiumValidator true } // MARK: - Save private func save() { guard context.hasChanges else { return } do { try context.save() fetchSources() } catch { print("Failed to save context: \(error)") } } }