// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // Client de l'API de production Vrai-Prix (https://www.vrai-prix.com). import Foundation enum APIError: LocalizedError { case server(String) case http(Int) var errorDescription: String? { switch self { case .server(let message): return message case .http(let code): return "Erreur réseau (\(code)). Réessayez." } } } struct VraiPrixAPI { static let shared = VraiPrixAPI() let base = URL(string: "https://www.vrai-prix.com")! private func decode(_ type: T.Type, from data: Data, status: Int) throws -> T { guard (200...299).contains(status) else { if let payload = try? JSONDecoder().decode(APIErrorPayload.self, from: data) { throw APIError.server(payload.error) } throw APIError.http(status) } return try JSONDecoder().decode(type, from: data) } private func get(_ type: T.Type, path: String, query: [URLQueryItem]) async throws -> T { var comps = URLComponents(url: base.appending(path: path), resolvingAgainstBaseURL: false)! comps.queryItems = query let (data, response) = try await URLSession.shared.data(from: comps.url!) let status = (response as? HTTPURLResponse)?.statusCode ?? 0 return try decode(type, from: data, status: status) } // MARK: Endpoints func search(_ q: String) async throws -> [SearchResult] { try await get(SearchResponse.self, path: "/api/search", query: [.init(name: "q", value: q)]).results } func nearby(lat: Double, lng: Double, halfLat: Double, halfLng: Double, limit: Int = 250) async throws -> [NearbyUnit] { try await get(NearbyResponse.self, path: "/api/nearby", query: [ .init(name: "lat", value: String(lat)), .init(name: "lng", value: String(lng)), .init(name: "halfLat", value: String(halfLat)), .init(name: "halfLng", value: String(halfLng)), .init(name: "limit", value: String(limit)), ]).results } func estimate(id: String) async throws -> UnitEstimate { try await get(UnitEstimate.self, path: "/api/estimate", query: [.init(name: "id", value: id)]) } func estimateManual( municipality: String, typeProp: String, floorArea: Double?, yearBuilt: Int?, landArea: Double? ) async throws -> UnitEstimate { var body: [String: Any] = ["municipality": municipality, "typeProp": typeProp] if let floorArea { body["floorArea"] = floorArea } if let yearBuilt { body["yearBuilt"] = yearBuilt } if let landArea { body["landArea"] = landArea } var request = URLRequest(url: base.appending(path: "/api/estimate")) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, response) = try await URLSession.shared.data(for: request) let status = (response as? HTTPURLResponse)?.statusCode ?? 0 return try decode(UnitEstimate.self, from: data, status: status) } /// Télécharge un rapport PDF dans un fichier temporaire et retourne son URL locale. func downloadReport(path: String, query: [URLQueryItem], fileName: String) async throws -> URL { var comps = URLComponents(url: base.appending(path: path), resolvingAgainstBaseURL: false)! comps.queryItems = query let (data, response) = try await URLSession.shared.data(from: comps.url!) let status = (response as? HTTPURLResponse)?.statusCode ?? 0 guard (200...299).contains(status) else { if let payload = try? JSONDecoder().decode(APIErrorPayload.self, from: data) { throw APIError.server(payload.error) } throw APIError.http(status) } let url = FileManager.default.temporaryDirectory.appending(path: fileName) try data.write(to: url) return url } }