import Foundation enum ApiServiceError: Error, LocalizedError { case sessionExpired(String?) var errorDescription: String? { switch self { case .sessionExpired(let message): return message ?? "Sessao expirada. Faca login novamente." } } } struct ApiEnvelope: Decodable { let error: Bool let code: String? let message: String? let result: T? } final class ApiService { private let client: ApiClient private var tokenStore: TokenStore init(client: ApiClient = ApiClient(), tokenStore: TokenStore = DefaultTokenStore()) { self.client = client self.tokenStore = tokenStore } private func send(_ req: ApiRequest) async throws -> T { do { return try await client.send(req) } catch let error as NetworkError { if case .unauthorized(let message) = error { tokenStore.clear() throw ApiServiceError.sessionExpired(message) } throw error } } // MARK: - Auth func registerCustomer(name: String, email: String, phoneNumber: String, birthDate: String? = nil) async throws -> ApiEnvelope { var payload: [String: String] = [ "name": name, "email": email, "phoneNumber": phoneNumber ] if let birthDate, birthDate.isEmpty == false { payload["birthDate"] = birthDate } let body = try JSONEncoder().encode(payload) let req = ApiRequest(path: "/api/customer", method: "POST", module: .customer, requiresAuth: false, body: body) return try await send(req) } func requestOtp(email: String, phoneNumber: String) async throws -> ApiEnvelope { let body = try JSONEncoder().encode(["email": email, "phoneNumber": phoneNumber]) let req = ApiRequest(path: "/api/customer/login", method: "POST", module: .customer, requiresAuth: false, body: body) return try await send(req) } func validateOtp(email: String, phoneNumber: String, otp: String) async throws -> ApiEnvelope { let body = try JSONEncoder().encode(["email": email, "phoneNumber": phoneNumber, "otp": otp]) let req = ApiRequest(path: "/api/customer/login", method: "POST", module: .customer, requiresAuth: false, body: body) let response: ApiEnvelope = try await send(req) if let token = response.result?.token { tokenStore.jwt = token } return response } func profile() async throws -> ApiEnvelope { let req = ApiRequest(path: "/api/customer/profile", method: "GET", module: .customer, requiresAuth: true) return try await send(req) } // MARK: - Stores func listStores(lat: Double, lng: Double, category: String? = nil, search: String? = nil) async throws -> ApiEnvelope<[StoreSummary]> { var items = [ URLQueryItem(name: "lat", value: String(lat)), URLQueryItem(name: "lng", value: String(lng)) ] if let category { items.append(URLQueryItem(name: "category", value: category)) } if let search { items.append(URLQueryItem(name: "search", value: search)) } let req = ApiRequest(path: "/api/app/stores", method: "GET", module: .app, requiresAuth: true, queryItems: items) return try await send(req) } } // MARK: - DTOs struct EmptyResult: Decodable {} struct RegistrationResult: Decodable { let id: String? let name: String? let email: String? } struct LoginResult: Decodable { let token: String let customer: CustomerProfile? } struct CustomerProfile: Decodable { let id: String let name: String let email: String let phoneNumber: String? let profilePicture: String? let addressBook: [CustomerAddress]? enum CodingKeys: String, CodingKey { case id case name case email case phoneNumber case profilePicture case addressBook = "address_book" } } struct CustomerAddress: Decodable { let id: String? let label: String? let address: String? let number: String? let complement: String? let neighborhood: String? let city: String? let state: String? let zipCode: String? let latLong: [Double]? enum CodingKeys: String, CodingKey { case id case label case address case number case complement case neighborhood case city case state case zipCode case latLong = "lat_long" } } struct StoreSummary: Decodable { let id: String let name: String let logo: String? let cover: String? let category: String? let rating: Double? let deliveryTime: String? let deliveryFee: Double? let distance: Double? let isOpen: Bool? let statusLabel: String? }