Add cheapest-favourite price-drop alerts
This commit is contained in:
@@ -79,6 +79,7 @@ struct TieChoice {
|
||||
/// entering the winner's circle — not at the forecourt.
|
||||
@MainActor
|
||||
final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate, UNUserNotificationCenterDelegate {
|
||||
private static let favouriteDropThresholdPence: Double = 1.0
|
||||
@Published var monitoredStationIDs: [String] = []
|
||||
@Published var lastAlert: String?
|
||||
@Published private(set) var lastTestResult: String?
|
||||
@@ -218,6 +219,46 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
|
||||
var isEnabled: Bool { enabled }
|
||||
|
||||
/// Compares the latest refreshed favourites against the persisted cheapest-
|
||||
/// favourite baseline for one fuel and schedules a notification when the
|
||||
/// winner changes or the current winner drops by a meaningful amount.
|
||||
func evaluateFavouritePriceDropAlert(
|
||||
favourites: [FavouriteEntry],
|
||||
previousSnapshots: [FuelType: FavouriteAlertSnapshot],
|
||||
monitoredFuel: FuelType,
|
||||
enabled: Bool
|
||||
) {
|
||||
var snapshots = previousSnapshots
|
||||
guard let current = FuelStore.cheapestFavourite(in: favourites, fuel: monitoredFuel),
|
||||
let currentPrice = current.station.prices[monitoredFuel] else {
|
||||
snapshots.removeValue(forKey: monitoredFuel)
|
||||
FuelStore.saveFavouriteAlertSnapshots(snapshots)
|
||||
return
|
||||
}
|
||||
|
||||
let currentSnapshot = FavouriteAlertSnapshot(
|
||||
fuel: monitoredFuel,
|
||||
stationID: current.station.id,
|
||||
stationName: current.station.name,
|
||||
price: currentPrice
|
||||
)
|
||||
|
||||
defer {
|
||||
snapshots[monitoredFuel] = currentSnapshot
|
||||
FuelStore.saveFavouriteAlertSnapshots(snapshots)
|
||||
}
|
||||
|
||||
guard enabled else { return }
|
||||
guard let previous = previousSnapshots[monitoredFuel] else { return }
|
||||
|
||||
let winnerChanged = previous.stationID != currentSnapshot.stationID
|
||||
let priceDropped = previous.stationID == currentSnapshot.stationID
|
||||
&& currentSnapshot.price <= previous.price - Self.favouriteDropThresholdPence
|
||||
|
||||
guard winnerChanged || priceDropped else { return }
|
||||
fireFavouriteDropAlert(current: current, previous: previous, price: currentPrice, winnerChanged: winnerChanged)
|
||||
}
|
||||
|
||||
/// Records a stage in the live alert trace, newest first, capped at 8.
|
||||
private func logAlert(_ kind: AlertLogEntry.Kind, _ text: String) {
|
||||
alertLog.insert(AlertLogEntry(date: Date(), kind: kind, text: text), at: 0)
|
||||
@@ -452,6 +493,31 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
lastAlert = "\(brand) · \(String(format: "%.1fp", price)) · \(distanceText) away"
|
||||
}
|
||||
|
||||
private func fireFavouriteDropAlert(
|
||||
current: FavouriteEntry,
|
||||
previous: FavouriteAlertSnapshot,
|
||||
price: Double,
|
||||
winnerChanged: Bool
|
||||
) {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.sound = .default
|
||||
let fuel = current.fuel
|
||||
let previousPriceText = String(format: "%.1fp", previous.price)
|
||||
let currentPriceText = String(format: "%.1fp", price)
|
||||
|
||||
if winnerChanged {
|
||||
content.title = "New cheapest \(fuel.displayName.lowercased()) favourite"
|
||||
content.body = "\(current.station.name) is now your cheapest favourite at \(currentPriceText), ahead of \(previous.stationName)."
|
||||
} else {
|
||||
content.title = "Cheapest favourite just dropped"
|
||||
content.body = "\(current.station.name) fell from \(previousPriceText) to \(currentPriceText) for \(fuel.displayName.lowercased())."
|
||||
}
|
||||
|
||||
addAlertRequest(content: content, station: current.station)
|
||||
logAlert(.fired, "favourite alert scheduled — \(current.station.name) · \(currentPriceText)")
|
||||
lastAlert = "Favourite · \(current.station.name) · \(currentPriceText)"
|
||||
}
|
||||
|
||||
/// Adds a notification whose tap opens directions to `station`. The
|
||||
/// station's coordinates ride in `userInfo` so the tap handler can route
|
||||
/// to Apple Maps (or an in-app fallback), and a map snapshot with a pin is
|
||||
|
||||
Reference in New Issue
Block a user