// FuelPriceProvider.swift — data-source seam for FuelBoard. // // The official UK Fuel Finder API (api.fuelfinder.service.gov.uk) is the live // source for station-level prices, but it requires GOV.UK One Login + OAuth 2.0 // client credentials. For the scaffold we ship SampleFuelProvider (realistic // stations around a location, works offline), plus the FuelFinderProvider // skeleton with the exact integration steps documented inline so wiring the // live API later is a drop-in: change `activeProvider` to `.fuelFinder`. import Foundation // MARK: - Provider protocol protocol FuelPriceProviding { /// Fetch stations with prices. `location` may be nil (sort by price only). /// Throws on failure so callers can fall back to cached/sample data. func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] } enum FuelPriceProvider { static let active: FuelPriceProviding = SampleFuelProvider() } // MARK: - Sample provider (default for the scaffold) /// Ships realistic stations around Cambridge (52.2053, 0.1218) so the app and /// widget render meaningful data with zero setup. Prices in pence/litre. struct SampleFuelProvider: FuelPriceProviding { func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] { try await Task.sleep(nanoseconds: 300_000_000) // simulate fetch return Self.sampleStations } static let sampleStations: [FuelStation] = [ FuelStation(id: "s1", name: "Shell Cambridge Retail Park", brand: "Shell", address: "12 Retail Park Way", postcode: "CB1 3EW", lat: 52.1955, lng: 0.1380, prices: [.e10: 142.9, .e5: 149.9, .diesel: 148.9], priceUpdated: nil), FuelStation(id: "s2", name: "Tesco Express Hills Road", brand: "Tesco", address: "245 Hills Road", postcode: "CB2 8RP", lat: 52.1809, lng: 0.1398, prices: [.e10: 138.9, .e5: 146.9, .diesel: 145.9], priceUpdated: nil), FuelStation(id: "s3", name: "BP Milton Road", brand: "BP", address: "161 Milton Road", postcode: "CB4 1XE", lat: 52.2160, lng: 0.1410, prices: [.e10: 140.9, .diesel: 147.9], priceUpdated: nil), FuelStation(id: "s4", name: "Morrisons Newmarket Road", brand: "Morrisons", address: "Newmarket Road", postcode: "CB5 8AA", lat: 52.2164, lng: 0.1599, prices: [.e10: 137.9, .e5: 144.9, .diesel: 144.9], priceUpdated: nil), FuelStation(id: "s5", name: "Sainsbury's Coldhams Lane", brand: "Sainsbury's", address: "Coldhams Lane", postcode: "CB1 3HY", lat: 52.2025, lng: 0.1589, prices: [.e10: 139.9, .e5: 147.9, .diesel: 146.9], priceUpdated: nil), FuelStation(id: "s6", name: "Esso Cherry Hinton", brand: "Esso", address: "Cherry Hinton Road", postcode: "CB1 9AP", lat: 52.1854, lng: 0.1657, prices: [.e10: 141.9, .e5: 148.9, .diesel: 147.9], priceUpdated: nil), FuelStation(id: "s7", name: "Gulf Fen Road", brand: "Gulf", address: "Fen Road", postcode: "CB4 1UN", lat: 52.2201, lng: 0.1470, prices: [.e10: 143.9, .e5: 151.9, .diesel: 149.9], priceUpdated: nil), FuelStation(id: "s8", name: "Asda Beehive Centre", brand: "Asda", address: "Coldhams Lane", postcode: "CB1 3ER", lat: 52.1995, lng: 0.1641, prices: [.e10: 136.9, .e5: 143.9, .diesel: 143.9], priceUpdated: nil), ] } // MARK: - Fuel Finder API provider (integration point) /// Live provider skeleton for the official GOV.UK Fuel Finder API. /// /// To activate: /// 1. Sign in at developer.fuel-finder.service.gov.uk with GOV.UK One Login, /// register an app, and generate client credentials (client_id + secret). /// 2. Implement token fetch: POST to the token endpoint with OAuth 2.0 /// client_credentials grant → access_token (expires in 1h). /// 3. GET prices with `Authorization: Bearer ` and decode the /// stations JSON into [FuelStation]. /// /// Endpoints (per gov.uk docs): /// base: https://api.fuelfinder.service.gov.uk /// auth: base + /v1/token /// prices: base + /v1/prices (full snapshot) /// price: base + /v1/prices/GB-12345 (single station) /// /// Then switch `FuelPriceProvider.active` to `FuelFinderProvider(...)`. struct FuelFinderProvider: FuelPriceProviding { let baseURL = URL(string: "https://api.fuelfinder.service.gov.uk")! let clientID: String let clientSecret: String func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] { // TODO: OAuth token → GET /v1/prices → map to FuelStation. // The live API requires authentication; see notes above. throw FuelProviderError.notImplemented } } enum FuelProviderError: LocalizedError { case notImplemented var errorDescription: String? { "Live Fuel Finder API not wired yet — activate with GOV.UK One Login credentials." } }