InvestmentTrackerApp/InvestmentTracker/ViewModels/SourceDetailViewModel.swift

230 lines
6.3 KiB
Swift

import Foundation
import Combine
import CoreData
@MainActor
class SourceDetailViewModel: ObservableObject {
// MARK: - Published Properties
@Published var source: InvestmentSource
@Published var snapshots: [Snapshot] = []
@Published var metrics: InvestmentMetrics = .empty
@Published var predictions: [Prediction] = []
@Published var predictionResult: PredictionResult?
@Published var isLoading = false
@Published var showingAddSnapshot = false
@Published var showingEditSource = false
@Published var showingPaywall = false
@Published var errorMessage: String?
// MARK: - Chart Data
@Published var chartData: [(date: Date, value: Decimal)] = []
// MARK: - Dependencies
private let snapshotRepository: SnapshotRepository
private let sourceRepository: InvestmentSourceRepository
private let calculationService: CalculationService
private let predictionEngine: PredictionEngine
private let freemiumValidator: FreemiumValidator
private var cancellables = Set<AnyCancellable>()
// MARK: - Initialization
init(
source: InvestmentSource,
snapshotRepository: SnapshotRepository = SnapshotRepository(),
sourceRepository: InvestmentSourceRepository = InvestmentSourceRepository(),
calculationService: CalculationService = .shared,
predictionEngine: PredictionEngine = .shared,
iapService: IAPService
) {
self.source = source
self.snapshotRepository = snapshotRepository
self.sourceRepository = sourceRepository
self.calculationService = calculationService
self.predictionEngine = predictionEngine
self.freemiumValidator = FreemiumValidator(iapService: iapService)
loadData()
setupObservers()
}
// MARK: - Setup
private func setupObservers() {
NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange)
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { [weak self] _ in
self?.refreshData()
}
.store(in: &cancellables)
}
// MARK: - Data Loading
func loadData() {
isLoading = true
refreshData()
isLoading = false
FirebaseService.shared.logScreenView(screenName: "SourceDetail")
}
func refreshData() {
// Fetch snapshots (filtered by freemium limits)
let allSnapshots = snapshotRepository.fetchSnapshots(for: source)
snapshots = freemiumValidator.filterSnapshots(allSnapshots)
// Calculate metrics
metrics = calculationService.calculateMetrics(for: snapshots)
// Prepare chart data
chartData = snapshots
.sorted { $0.date < $1.date }
.map { (date: $0.date, value: $0.decimalValue) }
// Calculate predictions if premium
if freemiumValidator.canViewPredictions() && snapshots.count >= 3 {
predictionResult = predictionEngine.predict(snapshots: snapshots)
predictions = predictionResult?.predictions ?? []
} else {
predictions = []
predictionResult = nil
}
}
// MARK: - Snapshot Actions
func addSnapshot(date: Date, value: Decimal, contribution: Decimal?, notes: String?) {
snapshotRepository.createSnapshot(
for: source,
date: date,
value: value,
contribution: contribution,
notes: notes
)
// Reschedule notification
NotificationService.shared.scheduleReminder(for: source)
// Log analytics
FirebaseService.shared.logSnapshotAdded(sourceName: source.name, value: value)
showingAddSnapshot = false
refreshData()
}
func deleteSnapshot(_ snapshot: Snapshot) {
snapshotRepository.deleteSnapshot(snapshot)
refreshData()
}
func deleteSnapshot(at offsets: IndexSet) {
snapshotRepository.deleteSnapshot(at: offsets, from: snapshots)
refreshData()
}
// MARK: - Source Actions
func updateSource(
name: String,
category: Category,
frequency: NotificationFrequency,
customMonths: Int
) {
sourceRepository.updateSource(
source,
name: name,
category: category,
notificationFrequency: frequency,
customFrequencyMonths: customMonths
)
// Update notification
NotificationService.shared.scheduleReminder(for: source)
showingEditSource = false
}
// MARK: - Predictions
func showPredictions() {
if freemiumValidator.canViewPredictions() {
// Already loaded, just navigate
FirebaseService.shared.logPredictionViewed(
algorithm: predictionResult?.algorithm.rawValue ?? "unknown"
)
} else {
showingPaywall = true
FirebaseService.shared.logPaywallShown(trigger: "predictions")
}
}
// MARK: - Computed Properties
var currentValue: Decimal {
source.latestValue
}
var formattedCurrentValue: String {
currentValue.currencyString
}
var totalReturn: Decimal {
metrics.absoluteReturn
}
var formattedTotalReturn: String {
metrics.formattedAbsoluteReturn
}
var percentageReturn: Decimal {
metrics.percentageReturn
}
var formattedPercentageReturn: String {
metrics.formattedPercentageReturn
}
var isPositiveReturn: Bool {
totalReturn >= 0
}
var categoryName: String {
source.category?.name ?? "Uncategorized"
}
var categoryColor: String {
source.category?.colorHex ?? "#3B82F6"
}
var lastUpdated: String {
source.latestSnapshot?.date.friendlyDescription ?? "Never"
}
var snapshotCount: Int {
snapshots.count
}
var hasEnoughDataForPredictions: Bool {
snapshots.count >= 3
}
var canViewPredictions: Bool {
freemiumValidator.canViewPredictions()
}
var isHistoryLimited: Bool {
!freemiumValidator.isPremium &&
snapshotRepository.fetchSnapshots(for: source).count > snapshots.count
}
var hiddenSnapshotCount: Int {
let allCount = snapshotRepository.fetchSnapshots(for: source).count
return allCount - snapshots.count
}
}