// 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 Halifax, West Yorkshire (53.7270, -1.8575) /// so the app and widget render meaningful distances for the user 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: "h1", name: "Tesco Express Halifax", brand: "Tesco", address: "12 Clare Road", postcode: "HX1 2HX", lat: 53.7200, lng: -1.8630, prices: [.e10: 138.9, .e5: 146.9, .diesel: 145.9], priceUpdated: nil), FuelStation(id: "h2", name: "Shell Skircoat Road", brand: "Shell", address: "245 Skircoat Road", postcode: "HX3 0HP", lat: 53.7120, lng: -1.8710, prices: [.e10: 142.9, .e5: 149.9, .diesel: 148.9], priceUpdated: nil), FuelStation(id: "h3", name: "Morrisons Halifax", brand: "Morrisons", address: "Haugh Shaw Road", postcode: "HX1 3TU", lat: 53.7265, lng: -1.8580, prices: [.e10: 137.9, .e5: 144.9, .diesel: 144.9], priceUpdated: nil), FuelStation(id: "h4", name: "BP Queensbury", brand: "BP", address: "316 High Street, Queensbury", postcode: "BD13 2NB", lat: 53.7550, lng: -1.8450, prices: [.e10: 140.9, .diesel: 147.9], priceUpdated: nil), FuelStation(id: "h5", name: "Asda Halifax", brand: "Asda", address: "Ovenden Way", postcode: "HX2 8DD", lat: 53.7340, lng: -1.8890, prices: [.e10: 136.9, .e5: 143.9, .diesel: 143.9], priceUpdated: nil), FuelStation(id: "h6", name: "Esso Sowerby Bridge", brand: "Esso", address: "Wharf Street", postcode: "HX6 2AH", lat: 53.7080, lng: -1.9080, prices: [.e10: 141.9, .e5: 148.9, .diesel: 147.9], priceUpdated: nil), FuelStation(id: "h7", name: "Sainsbury's Elland", brand: "Sainsbury's", address: "Southgate", postcode: "HX5 0PA", lat: 53.6860, lng: -1.8380, prices: [.e10: 139.9, .e5: 147.9, .diesel: 146.9], priceUpdated: nil), FuelStation(id: "h8", name: "Gulf Brighouse", brand: "Gulf", address: "Bradford Road", postcode: "HD6 1RW", lat: 53.7000, lng: -1.7850, prices: [.e10: 143.9, .e5: 151.9, .diesel: 149.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." } }