import Foundation /// Errors from API calls enum APIError: LocalizedError { case network(Error) case httpError(Int) case decodingError(Error) case serverError(String) case unauthorized case noData var errorDescription: String? { switch self { case .network(let e): return "Network error: \(e.localizedDescription)" case .httpError(let code): return "HTTP \(code)" case .decodingError(let e): return "Decode error: \(e.localizedDescription)" case .serverError(let msg): return msg case .unauthorized: return "Session expired. Please log in again." case .noData: return "No data received" } } } /// Lightweight API client — all Payfrit backend calls actor APIClient { static let shared = APIClient() private let session: URLSession private init() { let config = URLSessionConfiguration.default config.timeoutIntervalForResource = APIConfig.readTimeout config.timeoutIntervalForRequest = APIConfig.connectTimeout self.session = URLSession(configuration: config) } // MARK: - Auth struct OTPResponse: Codable { let uuid: String? let UUID: String? var otpUUID: String { uuid ?? UUID ?? "" } } func sendOTP(phone: String) async throws -> String { let body: [String: Any] = ["ContactNumber": phone] let data = try await post(path: "/auth/loginOTP.php", body: body) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success, let payload = resp.data else { throw APIError.serverError(resp.message ?? "Failed to send OTP") } return payload.otpUUID } struct LoginResponse: Codable { let token: String? let Token: String? let userId: String? let UserID: String? var authToken: String { token ?? Token ?? "" } var authUserId: String { userId ?? UserID ?? "" } } func verifyOTP(uuid: String, code: String) async throws -> (token: String, userId: String) { let body: [String: Any] = ["UUID": uuid, "Code": code] let data = try await post(path: "/auth/verifyLoginOTP.php", body: body) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success, let payload = resp.data else { throw APIError.serverError(resp.message ?? "Invalid OTP") } return (payload.authToken, payload.authUserId) } // MARK: - Businesses func listBusinesses(token: String) async throws -> [Business] { let data = try await post(path: "/businesses/list.php", body: [:], token: token) let resp = try JSONDecoder().decode(APIResponse<[Business]>.self, from: data) guard resp.success else { throw APIError.serverError(resp.message ?? "Failed to load businesses") } return resp.data ?? [] } // MARK: - Service Points func listServicePoints(businessId: String, token: String) async throws -> [ServicePoint] { let body: [String: Any] = ["BusinessID": businessId] let data = try await post(path: "/servicepoints/list.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse<[ServicePoint]>.self, from: data) guard resp.success else { throw APIError.serverError(resp.message ?? "Failed to load service points") } return resp.data ?? [] } func createServicePoint(name: String, businessId: String, token: String) async throws -> ServicePoint { let body: [String: Any] = ["Name": name, "BusinessID": businessId] let data = try await post(path: "/servicepoints/save.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success, let sp = resp.data else { throw APIError.serverError(resp.message ?? "Failed to create service point") } return sp } // MARK: - Beacon Sharding struct NamespaceResponse: Codable { let uuid: String? let UUID: String? let major: Int? let Major: Int? var shardUUID: String { uuid ?? UUID ?? "" } var shardMajor: Int { major ?? Major ?? 0 } } func allocateBusinessNamespace(businessId: String, token: String) async throws -> (uuid: String, major: Int) { let body: [String: Any] = ["BusinessID": businessId] let data = try await post(path: "/beacon-sharding/allocate_business_namespace.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success, let ns = resp.data else { throw APIError.serverError(resp.message ?? "Failed to allocate namespace") } return (ns.shardUUID, ns.shardMajor) } struct MinorResponse: Codable { let minor: Int? let Minor: Int? var allocated: Int { minor ?? Minor ?? 0 } } func allocateMinor(businessId: String, servicePointId: String, token: String) async throws -> Int { let body: [String: Any] = ["BusinessID": businessId, "ServicePointID": servicePointId] let data = try await post(path: "/beacon-sharding/allocate_servicepoint_minor.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success, let m = resp.data else { throw APIError.serverError(resp.message ?? "Failed to allocate minor") } return m.allocated } func registerBeaconHardware( businessId: String, servicePointId: String, uuid: String, major: Int, minor: Int, macAddress: String?, beaconType: String, token: String ) async throws { var body: [String: Any] = [ "BusinessID": businessId, "ServicePointID": servicePointId, "UUID": uuid, "Major": major, "Minor": minor, "BeaconType": beaconType ] if let mac = macAddress { body["MacAddress"] = mac } let data = try await post(path: "/beacon-sharding/register_beacon_hardware.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success else { throw APIError.serverError(resp.message ?? "Failed to register beacon") } } func verifyBeaconBroadcast( uuid: String, major: Int, minor: Int, token: String ) async throws { let body: [String: Any] = ["UUID": uuid, "Major": major, "Minor": minor] let data = try await post(path: "/beacon-sharding/verify_beacon_broadcast.php", body: body, token: token) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success else { throw APIError.serverError(resp.message ?? "Failed to verify broadcast") } } struct ResolveResponse: Codable { let businessName: String? let BusinessName: String? var name: String { businessName ?? BusinessName ?? "Unknown" } } func resolveBusiness(uuid: String, major: Int, token: String) async throws -> String { let body: [String: Any] = ["UUID": uuid, "Major": major] let data = try await post(path: "/beacon-sharding/resolve_business.php", body: body, token: token) let resp = try JSONDecoder().decode(APIResponse.self, from: data) return resp.data?.name ?? "Unknown" } // MARK: - Service Point Management func deleteServicePoint(servicePointId: String, businessId: String, token: String) async throws { let body: [String: Any] = ["ID": servicePointId, "BusinessID": businessId] let data = try await post(path: "/servicepoints/delete.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success else { throw APIError.serverError(resp.message ?? "Failed to delete service point") } } func updateServicePoint(servicePointId: String, name: String, businessId: String, token: String) async throws { let body: [String: Any] = ["ID": servicePointId, "Name": name, "BusinessID": businessId] let data = try await post(path: "/servicepoints/save.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success else { throw APIError.serverError(resp.message ?? "Failed to update service point") } } // MARK: - Beacon Management struct BeaconListResponse: Codable { let id: String? let ID: String? let uuid: String? let UUID: String? let major: Int? let Major: Int? let minor: Int? let Minor: Int? let macAddress: String? let MacAddress: String? let beaconType: String? let BeaconType: String? let servicePointId: String? let ServicePointID: String? let isVerified: Bool? let IsVerified: Bool? } func listBeacons(businessId: String, token: String) async throws -> [BeaconListResponse] { let body: [String: Any] = ["BusinessID": businessId] let data = try await post(path: "/beacon-sharding/list_beacons.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse<[BeaconListResponse]>.self, from: data) guard resp.success else { throw APIError.serverError(resp.message ?? "Failed to list beacons") } return resp.data ?? [] } func decommissionBeacon(beaconId: String, businessId: String, token: String) async throws { let body: [String: Any] = ["ID": beaconId, "BusinessID": businessId] let data = try await post(path: "/beacon-sharding/decommission_beacon.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success else { throw APIError.serverError(resp.message ?? "Failed to decommission beacon") } } func lookupByMac(macAddress: String, token: String) async throws -> BeaconListResponse? { let body: [String: Any] = ["MacAddress": macAddress] let data = try await post(path: "/beacon-sharding/lookup_by_mac.php", body: body, token: token) let resp = try JSONDecoder().decode(APIResponse.self, from: data) return resp.data } func getBeaconStatus(uuid: String, major: Int, minor: Int, token: String) async throws -> BeaconListResponse? { let body: [String: Any] = ["UUID": uuid, "Major": major, "Minor": minor] let data = try await post(path: "/beacon-sharding/beacon_status.php", body: body, token: token) let resp = try JSONDecoder().decode(APIResponse.self, from: data) return resp.data } // MARK: - Beacon Config (server-configured values) struct BeaconConfigResponse: Codable { let uuid: String? let UUID: String? let major: Int? let Major: Int? let minor: Int? let Minor: Int? let txPower: Int? let TxPower: Int? let measuredPower: Int? let MeasuredPower: Int? let advInterval: Int? let AdvInterval: Int? var configUUID: String { uuid ?? UUID ?? "" } var configMajor: Int { major ?? Major ?? 0 } var configMinor: Int { minor ?? Minor ?? 0 } var configTxPower: Int { txPower ?? TxPower ?? 1 } var configMeasuredPower: Int { measuredPower ?? MeasuredPower ?? -100 } var configAdvInterval: Int { advInterval ?? AdvInterval ?? 2 } } func getBeaconConfig(businessId: String, servicePointId: String, token: String) async throws -> BeaconConfigResponse { let body: [String: Any] = ["BusinessID": businessId, "ServicePointID": servicePointId] let data = try await post(path: "/beacon-sharding/get_beacon_config.php", body: body, token: token, businessId: businessId) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success, let config = resp.data else { throw APIError.serverError(resp.message ?? "Failed to get beacon config") } return config } // MARK: - User Profile struct UserProfile: Codable { let id: String? let ID: String? let firstName: String? let FirstName: String? let lastName: String? let LastName: String? let contactNumber: String? let ContactNumber: String? } func getProfile(token: String) async throws -> UserProfile { let data = try await post(path: "/users/profile.php", body: [:], token: token) let resp = try JSONDecoder().decode(APIResponse.self, from: data) guard resp.success, let profile = resp.data else { throw APIError.serverError(resp.message ?? "Failed to load profile") } return profile } // MARK: - Internal private struct EmptyData: Codable {} private struct APIResponse: Codable { let success: Bool let message: String? let data: T? enum CodingKeys: String, CodingKey { case success = "Success" case message = "Message" case data = "Data" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // Handle both bool and int/string for Success if let b = try? container.decode(Bool.self, forKey: .success) { success = b } else if let i = try? container.decode(Int.self, forKey: .success) { success = i != 0 } else { success = false } message = try? container.decode(String.self, forKey: .message) data = try? container.decode(T.self, forKey: .data) } } private func post( path: String, body: [String: Any], token: String? = nil, businessId: String? = nil ) async throws -> Data { guard let url = URL(string: APIConfig.baseURL + path) else { throw APIError.serverError("Invalid URL: \(path)") } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") if let token = token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.setValue(token, forHTTPHeaderField: "X-User-Token") } if let bizId = businessId { request.setValue(bizId, forHTTPHeaderField: "X-Business-Id") } request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, response) = try await session.data(for: request) if let http = response as? HTTPURLResponse { if http.statusCode == 401 { throw APIError.unauthorized } guard (200...299).contains(http.statusCode) else { throw APIError.httpError(http.statusCode) } } return data } }