Stop Approx. distances beside routed values; add Best Value alert mode

Distances (the "Approx. 1.5 mi" report):
- Refuse junk CoreLocation fixes (invalid accuracy, or a coarse >300 m fix
  within 3 km of the position already held; a coarse fix in another city is
  real travel and is still accepted).
- Resolve ONE display basis per render through FuelStore.distanceDisplayOrigin,
  used by rows, radius filter, sorting, savings figures and the Live Activity,
  so a stray fix can no longer re-base the list outside the route cache's 600 m
  ring while the routed values sit unused in the cache.
- Treat an incomplete route set as a state: retry it after 30 s rather than the
  full 10-minute throttle, and repair a stale incomplete flag once every
  in-radius station has a route (nothing left to route, flag stuck down).
- Widget extension resolves the same origin for fetch, radius, ordering and
  labels, and keeps its own fix in fuelboard.widgetLocation instead of
  overwriting the app's GPS fix.

Alerts:
- Best Value as a distinct alert mode on the existing geofence/notification
  pipeline, with a persisted threshold; reverts to cheapest when the feature is
  unconfigured and is not selectable while disabled.

Diagnostics (DEBUG, gated): FB_DIST_FOCUS now prints FB-DIST-ORIGIN (candidate,
accuracy, anchor, offset, resolved branch, served origin), FB_FAKE_FIX feeds a
synthetic fix through the real guard, FB_STALE_INCOMPLETE forces the stale-flag
state, and the route-fill status is surfaced in Settings behind debug mode.
This commit is contained in:
FuelBoard Contributor
2026-10-13 22:04:00 +01:00
parent 2a2823c180
commit 6f177837a5
13 changed files with 1194 additions and 99 deletions
@@ -81,6 +81,65 @@ final class FuelSavingsCalculatorTests: XCTestCase {
let c = FuelSavingsRankedStation(stationID: "c", result: .unavailable("No route"), distanceMiles: 1, pricePencePerLitre: 140)
XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([a, c, b]).map(\.stationID), ["b", "a", "c"])
}
func testBestValueAlertCandidateRequiresConfiguredSavingsAndThreshold() {
let stations = [
station("near", lat: 0, lng: 0, price: 160),
station("cheap", lat: 0, lng: 0, price: 150),
]
let journeys = ["near": 2.0, "cheap": 4.0]
let off = FuelSavingsSettings(enabled: false, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
XCTAssertNil(FuelSavingsCalculator.bestValueAlertCandidate(
stations: stations,
fuel: .e10,
settings: off,
amount: off.preferredAmount,
minimumNetSavingGBP: 1.0,
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
))
let on = FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
XCTAssertNil(FuelSavingsCalculator.bestValueAlertCandidate(
stations: stations,
fuel: .e10,
settings: on,
amount: on.preferredAmount,
minimumNetSavingGBP: 5.0,
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
))
}
func testBestValueAlertCandidatePicksHighestNetSavingAboveThreshold() throws {
let stations = [
station("baseline", lat: 0, lng: 0, price: 160),
station("small", lat: 0, lng: 0, price: 154),
station("best", lat: 0, lng: 0, price: 148),
]
let journeys = ["baseline": 2.0, "small": 4.0, "best": 6.0]
let settings = FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
let candidate = FuelSavingsCalculator.bestValueAlertCandidate(
stations: stations,
fuel: .e10,
settings: settings,
amount: settings.preferredAmount,
minimumNetSavingGBP: 1.0,
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
)
XCTAssertEqual(candidate?.station.id, "best")
XCTAssertEqual(candidate?.result.status, .worthTheDrive)
XCTAssertGreaterThan(candidate?.result.netSavingGBP ?? 0, 1.0)
}
func testBestValueAlertCandidateBreaksTiesByDistanceThenPrice() throws {
let first = FuelSavingsResult(fillLitres: 25, grossSavingGBP: 3, extraDrivingCostGBP: 1, extraDistanceMiles: 2, netSavingGBP: 2, status: .worthTheDrive)
let nearer = FuelSavingsRankedStation(stationID: "nearer", result: .available(first), distanceMiles: 4, pricePencePerLitre: 149)
let farther = FuelSavingsRankedStation(stationID: "farther", result: .available(first), distanceMiles: 8, pricePencePerLitre: 145)
XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([farther, nearer]).first?.stationID, "nearer")
}
private func station(_ id: String, lat: Double, lng: Double, price: Double) -> FuelStation {
FuelStation(id: id, name: id, brand: id, address: "", postcode: "", lat: lat, lng: lng, prices: [.e10: price], priceUpdated: nil)
}
}
final class FuelSavingsSettingsTests: XCTestCase {