payfrit-beacon-ios/PayfritBeacon/Api.swift
John Pinkyfloyd 2ec195243c Migrate API endpoints from CFML to PHP
- Replace all .cfm endpoints with .php (PHP backend migration)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-14 17:17:02 -07:00

643 lines
25 KiB
Swift

import Foundation
class Api {
static let shared = Api()
// DEV toggle: driven by DEV compiler flag (set in build configuration)
#if DEV
static let IS_DEV = true
#else
static let IS_DEV = false
#endif
private static var BASE_URL: String {
IS_DEV ? "https://dev.payfrit.com/api" : "https://biz.payfrit.com/api"
}
private let session: URLSession
private var authToken: String?
private init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 30
session = URLSession(configuration: config)
}
func setAuthToken(_ token: String?) {
authToken = token
}
func getAuthToken() -> String? {
return authToken
}
private func buildRequest(endpoint: String) -> URLRequest {
let url = URL(string: "\(Api.BASE_URL)\(endpoint)")!
var request = URLRequest(url: url)
if let token = authToken {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue(token, forHTTPHeaderField: "X-User-Token")
}
return request
}
private func postRequest(endpoint: String, body: [String: Any], extraHeaders: [String: String] = [:]) async throws -> [String: Any] {
var request = buildRequest(endpoint: endpoint)
request.httpMethod = "POST"
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: body)
for (key, value) in extraHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw ApiException("Invalid response")
}
guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
throw ApiException("Request failed: \(httpResponse.statusCode)")
}
guard !data.isEmpty else {
throw ApiException("Empty response")
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw ApiException("Invalid JSON response")
}
return json
}
private func getRequest(endpoint: String) async throws -> [String: Any] {
var request = buildRequest(endpoint: endpoint)
request.httpMethod = "GET"
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw ApiException("Invalid response")
}
guard httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
throw ApiException("Request failed: \(httpResponse.statusCode)")
}
guard !data.isEmpty else {
throw ApiException("Empty response")
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw ApiException("Invalid JSON response")
}
return json
}
// =========================================================================
// AUTH
// =========================================================================
func sendLoginOtp(phone: String) async throws -> OtpResponse {
let json = try await postRequest(endpoint: "/auth/loginOTP.php", body: ["Phone": phone])
guard let uuid = (json["UUID"] as? String) ?? (json["uuid"] as? String), !uuid.isEmpty else {
throw ApiException("Server error - please try again")
}
return OtpResponse(uuid: uuid)
}
func verifyLoginOtp(uuid: String, otp: String) async throws -> LoginResponse {
let json = try await postRequest(endpoint: "/auth/verifyLoginOTP.php", body: [
"UUID": uuid,
"OTP": otp
])
let ok = parseBool(json["OK"] ?? json["ok"])
if !ok {
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Invalid code"
throw ApiException(error)
}
return LoginResponse(
userId: parseIntValue(json["UserID"] ?? json["USERID"]) ?? 0,
token: ((json["Token"] ?? json["TOKEN"] ?? json["token"]) as? String) ?? "",
userFirstName: ((json["UserFirstName"] ?? json["USERFIRSTNAME"]) as? String) ?? ""
)
}
// =========================================================================
// BUSINESSES
// =========================================================================
func listBusinesses() async throws -> [Business] {
let json = try await postRequest(endpoint: "/businesses/list.php", body: [:])
guard let businesses = (json["BUSINESSES"] ?? json["businesses"] ?? json["Items"] ?? json["ITEMS"]) as? [[String: Any]] else {
return []
}
return businesses.compactMap { b in
guard let businessId = parseIntValue(b["BusinessID"] ?? b["BUSINESSID"]) else {
return nil
}
let name = ((b["BusinessName"] ?? b["BUSINESSNAME"] ?? b["Name"] ?? b["NAME"]) as? String) ?? ""
let headerImageExtension = (b["HeaderImageExtension"] ?? b["HEADERIMAGEEXTENSION"]) as? String
return Business(businessId: businessId, name: name, headerImageExtension: headerImageExtension)
}
}
// =========================================================================
// BEACONS
// =========================================================================
func listAllBeacons() async throws -> [String: Int] {
let json = try await getRequest(endpoint: "/beacons/list_all.php")
guard let items = (json["ITEMS"] ?? json["items"]) as? [[String: Any]] else {
return [:]
}
var result: [String: Int] = [:]
for item in items {
guard let uuid = ((item["UUID"] ?? item["uuid"] ?? item["BeaconUUID"] ?? item["BEACONUUID"]) as? String)?
.replacingOccurrences(of: "-", with: "").uppercased(),
let beaconId = parseIntValue(item["BeaconID"] ?? item["BEACONID"]) else {
continue
}
result[uuid] = beaconId
}
return result
}
func lookupBeacons(uuids: [String]) async throws -> [BeaconLookupResult] {
if uuids.isEmpty { return [] }
let json = try await postRequest(endpoint: "/beacons/lookup.php", body: ["UUIDs": uuids])
guard let beacons = (json["BEACONS"] ?? json["beacons"]) as? [[String: Any]] else {
return []
}
return beacons.compactMap { b in
guard let uuid = ((b["UUID"] ?? b["uuid"]) as? String)?
.replacingOccurrences(of: "-", with: "").uppercased() else {
return nil
}
return BeaconLookupResult(
uuid: uuid,
beaconId: parseIntValue(b["BeaconID"] ?? b["BEACONID"]) ?? 0,
businessId: parseIntValue(b["BusinessID"] ?? b["BUSINESSID"]) ?? 0,
businessName: ((b["BusinessName"] ?? b["BUSINESSNAME"]) as? String) ?? "",
servicePointName: ((b["ServicePointName"] ?? b["SERVICEPOINTNAME"]) as? String) ?? "",
beaconName: ((b["BeaconName"] ?? b["BEACONNAME"] ?? b["Name"] ?? b["NAME"]) as? String) ?? ""
)
}
}
func listBeacons(businessId: Int) async throws -> [BeaconInfo] {
let json = try await postRequest(
endpoint: "/beacons/list.php",
body: ["BusinessID": businessId],
extraHeaders: ["X-Business-Id": String(businessId)]
)
guard let beacons = (json["BEACONS"] ?? json["beacons"]) as? [[String: Any]] else {
return []
}
return beacons.compactMap { b in
let beaconId = parseIntValue(b["BeaconID"] ?? b["BEACONID"] ?? b["ID"]) ?? 0
let name = ((b["Name"] ?? b["NAME"] ?? b["BeaconName"] ?? b["BEACONNAME"]) as? String) ?? ""
let uuid = ((b["UUID"] ?? b["uuid"]) as? String)?
.replacingOccurrences(of: "-", with: "").uppercased() ?? ""
let isActive = parseBool(b["IsActive"] ?? b["ISACTIVE"] ?? true)
return BeaconInfo(beaconId: beaconId, name: name, uuid: uuid, isActive: isActive)
}
}
func saveBeacon(businessId: Int, name: String, uuid: String, macAddress: String? = nil) async throws -> SavedBeacon {
var params: [String: Any] = [
"BusinessID": businessId,
"Name": name,
"UUID": uuid
]
if let mac = macAddress, !mac.isEmpty {
params["MACAddress"] = mac
}
let json = try await postRequest(
endpoint: "/beacons/save.php",
body: params,
extraHeaders: ["X-Business-Id": String(businessId)]
)
if !parseBool(json["OK"] ?? json["ok"]) {
throw ApiException(((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to save beacon")
}
let beacon = (json["BEACON"] ?? json["beacon"]) as? [String: Any]
return SavedBeacon(
beaconId: parseIntValue(beacon?["BeaconID"] ?? beacon?["BEACONID"] ?? beacon?["ID"]) ?? 0,
name: (beacon?["Name"] ?? beacon?["NAME"]) as? String ?? name,
uuid: uuid,
macAddress: (beacon?["MACAddress"] ?? beacon?["MACADDRESS"]) as? String
)
}
func lookupByMac(macAddress: String) async throws -> MacLookupResult? {
let json = try await postRequest(endpoint: "/beacons/lookupByMac.php", body: ["MACAddress": macAddress])
if !parseBool(json["OK"] ?? json["ok"]) {
return nil
}
guard let beacon = (json["BEACON"] ?? json["beacon"]) as? [String: Any] else {
return nil
}
return MacLookupResult(
beaconId: parseIntValue(beacon["BeaconID"] ?? beacon["BEACONID"]) ?? 0,
businessId: parseIntValue(beacon["BusinessID"] ?? beacon["BUSINESSID"]) ?? 0,
businessName: ((beacon["BusinessName"] ?? beacon["BUSINESSNAME"]) as? String) ?? "",
beaconName: ((beacon["BeaconName"] ?? beacon["BEACONNAME"]) as? String) ?? "",
uuid: ((beacon["UUID"] ?? beacon["uuid"]) as? String) ?? "",
macAddress: ((beacon["MACAddress"] ?? beacon["MACADDRESS"]) as? String) ?? "",
servicePointName: ((beacon["ServicePointName"] ?? beacon["SERVICEPOINTNAME"]) as? String) ?? ""
)
}
func wipeBeacon(businessId: Int, beaconId: Int) async throws -> Bool {
let json = try await postRequest(
endpoint: "/beacons/wipe.php",
body: ["BusinessID": businessId, "BeaconID": beaconId],
extraHeaders: ["X-Business-Id": String(businessId)]
)
if !parseBool(json["OK"] ?? json["ok"]) {
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to wipe beacon"
let message = (json["MESSAGE"] ?? json["message"]) as? String
throw ApiException(message ?? error)
}
return true
}
// =========================================================================
// BEACON SHARDING / PROVISIONING
// =========================================================================
/// Get beacon config for a service point (UUID, Major, Minor to write to beacon)
func getBeaconConfig(businessId: Int, servicePointId: Int) async throws -> BeaconConfigResponse {
DebugLog.shared.log("[API] getBeaconConfig businessId=\(businessId) servicePointId=\(servicePointId)")
let json = try await postRequest(
endpoint: "/beacon-sharding/get_beacon_config.php",
body: ["BusinessID": businessId, "ServicePointID": servicePointId],
extraHeaders: ["X-Business-Id": String(businessId)]
)
DebugLog.shared.log("[API] getBeaconConfig response keys: \(json.keys.sorted())")
DebugLog.shared.log("[API] getBeaconConfig full response: \(json)")
if !parseBool(json["OK"] ?? json["ok"] ?? json["Ok"]) {
let error = ((json["ERROR"] ?? json["error"] ?? json["Error"]) as? String) ?? "Failed to get beacon config"
throw ApiException(error)
}
guard let uuid = (json["UUID"] ?? json["uuid"] ?? json["Uuid"] ?? json["BeaconUUID"] ?? json["BEACONUUID"]) as? String else {
throw ApiException("Invalid beacon config response - no UUID. Keys: \(json.keys.sorted())")
}
guard let major = parseIntValue(json["MAJOR"] ?? json["major"] ?? json["Major"] ?? json["BeaconMajor"] ?? json["BEACONMAJOR"]) else {
throw ApiException("Invalid beacon config response - no Major. Keys: \(json.keys.sorted())")
}
guard let minor = parseIntValue(json["MINOR"] ?? json["minor"] ?? json["Minor"] ?? json["BeaconMinor"] ?? json["BEACONMINOR"]) else {
throw ApiException("Invalid beacon config response - no Minor. Keys: \(json.keys.sorted())")
}
// Parse new fields from updated endpoint
let measuredPower = parseIntValue(json["MeasuredPower"] ?? json["MEASUREDPOWER"] ?? json["measuredPower"]) ?? -59
let advInterval = parseIntValue(json["AdvInterval"] ?? json["ADVINTERVAL"] ?? json["advInterval"]) ?? 2
let txPower = parseIntValue(json["TxPower"] ?? json["TXPOWER"] ?? json["txPower"]) ?? 1
let servicePointName = (json["ServicePointName"] ?? json["SERVICEPOINTNAME"] ?? json["servicePointName"]) as? String ?? ""
let businessName = (json["BusinessName"] ?? json["BUSINESSNAME"] ?? json["businessName"]) as? String ?? ""
DebugLog.shared.log("[API] getBeaconConfig parsed: uuid=\(uuid) major=\(major) minor=\(minor) measuredPower=\(measuredPower) advInterval=\(advInterval) txPower=\(txPower)")
return BeaconConfigResponse(
uuid: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
major: UInt16(major),
minor: UInt16(minor),
measuredPower: Int8(clamping: measuredPower),
advInterval: UInt8(clamping: advInterval),
txPower: UInt8(clamping: txPower),
servicePointName: servicePointName,
businessName: businessName
)
}
/// Register beacon hardware after provisioning
func registerBeaconHardware(businessId: Int, servicePointId: Int, uuid: String, major: UInt16, minor: UInt16, hardwareId: String, macAddress: String? = nil) async throws -> Bool {
var body: [String: Any] = [
"BusinessID": businessId,
"ServicePointID": servicePointId,
"UUID": uuid,
"Major": major,
"Minor": minor,
"HardwareID": hardwareId
]
if let mac = macAddress, !mac.isEmpty {
body["MACAddress"] = mac
}
DebugLog.shared.log("[API] registerBeaconHardware body: \(body)")
let json = try await postRequest(
endpoint: "/beacon-sharding/register_beacon_hardware.php",
body: body,
extraHeaders: ["X-Business-Id": String(businessId)]
)
DebugLog.shared.log("[API] registerBeaconHardware response: \(json)")
if !parseBool(json["OK"] ?? json["ok"]) {
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to register beacon"
throw ApiException(error)
}
return true
}
/// Resolve beacon ownership by UUID and Major
func resolveBusiness(uuid: String, major: UInt16) async throws -> ResolveBusinessResponse {
let json = try await postRequest(
endpoint: "/beacon-sharding/resolve_business.php",
body: ["UUID": uuid, "Major": Int(major)]
)
if !parseBool(json["OK"] ?? json["ok"]) {
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to resolve business"
throw ApiException(error)
}
guard let businessId = parseIntValue(json["BusinessID"] ?? json["BUSINESSID"] ?? json["businessId"]) else {
throw ApiException("Invalid resolve response - no BusinessID")
}
let businessName = (json["BusinessName"] ?? json["BUSINESSNAME"] ?? json["businessName"]) as? String ?? ""
return ResolveBusinessResponse(businessId: businessId, businessName: businessName)
}
/// Verify beacon is broadcasting expected values
func verifyBeaconBroadcast(businessId: Int, uuid: String, major: UInt16, minor: UInt16) async throws -> Bool {
let json = try await postRequest(
endpoint: "/beacon-sharding/verify_beacon_broadcast.php",
body: [
"BusinessID": businessId,
"UUID": uuid,
"Major": major,
"Minor": minor
],
extraHeaders: ["X-Business-Id": String(businessId)]
)
return parseBool(json["OK"] ?? json["ok"])
}
/// Allocate beacon namespace for a business (shard + major)
func allocateBusinessNamespace(businessId: Int) async throws -> BusinessNamespace {
let json = try await postRequest(
endpoint: "/beacon-sharding/allocate_business_namespace.php",
body: ["BusinessID": businessId],
extraHeaders: ["X-Business-Id": String(businessId)]
)
// Debug log
print("[API] allocateBusinessNamespace response: \(json)")
if !parseBool(json["OK"] ?? json["ok"]) {
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to allocate namespace"
throw ApiException(error)
}
// API returns BeaconShardUUID and BeaconMajor
guard let uuid = (json["BeaconShardUUID"] ?? json["BEACONSHARDUUID"] ?? json["UUID"] ?? json["uuid"]) as? String else {
throw ApiException("Invalid namespace response - no UUID")
}
guard let major = parseIntValue(json["BeaconMajor"] ?? json["BEACONMAJOR"] ?? json["MAJOR"] ?? json["Major"]) else {
throw ApiException("Invalid namespace response - no Major")
}
return BusinessNamespace(
shardId: parseIntValue(json["ShardID"] ?? json["SHARDID"]) ?? 0,
uuid: uuid,
uuidClean: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
major: UInt16(major),
alreadyAllocated: parseBool(json["AlreadyAllocated"] ?? json["ALREADYALLOCATED"])
)
}
/// List service points for a business (for beacon assignment)
func listServicePoints(businessId: Int) async throws -> [ServicePoint] {
let json = try await postRequest(
endpoint: "/servicepoints/list.php",
body: ["BusinessID": businessId],
extraHeaders: ["X-Business-Id": String(businessId)]
)
guard let items = (json["SERVICEPOINTS"] ?? json["servicepoints"] ?? json["ITEMS"] ?? json["items"]) as? [[String: Any]] else {
return []
}
return items.compactMap { sp in
guard let spId = parseIntValue(sp["ServicePointID"] ?? sp["SERVICEPOINTID"] ?? sp["ID"]) else {
return nil
}
let name = ((sp["Name"] ?? sp["NAME"] ?? sp["ServicePointName"] ?? sp["SERVICEPOINTNAME"]) as? String) ?? "Table \(spId)"
let hasBeacon = parseBool(sp["HasBeacon"] ?? sp["HASBEACON"])
let beaconMinor = parseIntValue(sp["BeaconMinor"] ?? sp["BEACONMINOR"])
return ServicePoint(servicePointId: spId, name: name, hasBeacon: hasBeacon, beaconMinor: beaconMinor != nil ? UInt16(beaconMinor!) : nil)
}
}
/// Create/save a service point (auto-allocates minor)
func saveServicePoint(businessId: Int, name: String, servicePointId: Int? = nil) async throws -> ServicePoint {
var body: [String: Any] = ["BusinessID": businessId, "Name": name]
if let spId = servicePointId {
body["ServicePointID"] = spId
}
DebugLog.shared.log("[API] saveServicePoint businessId=\(businessId) name=\(name) servicePointId=\(String(describing: servicePointId))")
let json = try await postRequest(
endpoint: "/servicepoints/save.php",
body: body,
extraHeaders: ["X-Business-Id": String(businessId)]
)
DebugLog.shared.log("[API] saveServicePoint response: \(json)")
if !parseBool(json["OK"] ?? json["ok"]) {
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to save service point"
throw ApiException(error)
}
// Response has SERVICEPOINT object containing the data
let sp = (json["SERVICEPOINT"] ?? json["servicepoint"]) as? [String: Any] ?? json
let spId = parseIntValue(sp["ServicePointID"] ?? sp["SERVICEPOINTID"]) ?? servicePointId ?? 0
let beaconMinor = parseIntValue(sp["BeaconMinor"] ?? sp["BEACONMINOR"])
DebugLog.shared.log("[API] saveServicePoint parsed: spId=\(spId) beaconMinor=\(String(describing: beaconMinor))")
if spId == 0 {
DebugLog.shared.log("[API] WARNING: servicePointId is 0! Full sp dict: \(sp)")
}
return ServicePoint(
servicePointId: spId,
name: name,
hasBeacon: beaconMinor != nil,
beaconMinor: beaconMinor != nil ? UInt16(beaconMinor!) : nil
)
}
/// Create a new service point (legacy - calls save)
func createServicePoint(businessId: Int, name: String) async throws -> ServicePoint {
return try await saveServicePoint(businessId: businessId, name: name)
}
/// Delete a service point
func deleteServicePoint(businessId: Int, servicePointId: Int) async throws {
let json = try await postRequest(
endpoint: "/servicepoints/delete.php",
body: ["BusinessID": businessId, "ServicePointID": servicePointId],
extraHeaders: ["X-Business-Id": String(businessId)]
)
if !parseBool(json["OK"] ?? json["ok"]) {
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to delete service point"
throw ApiException(error)
}
}
// =========================================================================
// HELPERS
// =========================================================================
private func parseBool(_ value: Any?) -> Bool {
switch value {
case nil:
return false
case let b as Bool:
return b
case let n as NSNumber:
return n.intValue != 0
case let s as String:
return ["true", "1"].contains(s.lowercased())
default:
return false
}
}
private func parseIntValue(_ value: Any?) -> Int? {
if let n = value as? NSNumber {
return n.intValue
}
if let s = value as? String, let i = Int(s) {
return i
}
return nil
}
}
struct ApiException: LocalizedError {
let message: String
init(_ message: String) { self.message = message }
var errorDescription: String? { message }
}
// =========================================================================
// DATA MODELS
// =========================================================================
struct OtpResponse {
let uuid: String
}
struct LoginResponse {
let userId: Int
let token: String
let userFirstName: String
}
struct Business: Identifiable {
var id: Int { businessId }
let businessId: Int
let name: String
let headerImageExtension: String?
}
struct BeaconLookupResult {
let uuid: String
let beaconId: Int
let businessId: Int
let businessName: String
let servicePointName: String
let beaconName: String
}
struct BeaconInfo {
let beaconId: Int
let name: String
let uuid: String
let isActive: Bool
}
struct SavedBeacon {
let beaconId: Int
let name: String
let uuid: String
let macAddress: String?
}
struct MacLookupResult {
let beaconId: Int
let businessId: Int
let businessName: String
let beaconName: String
let uuid: String
let macAddress: String
let servicePointName: String
}
struct BeaconConfigResponse {
let uuid: String // 32-char hex, no dashes
let major: UInt16
let minor: UInt16
let measuredPower: Int8 // RSSI@1m (e.g., -59)
let advInterval: UInt8 // Advertising interval (raw value, e.g., 2 = 200ms)
let txPower: UInt8 // TX power level
let servicePointName: String
let businessName: String
}
struct ResolveBusinessResponse {
let businessId: Int
let businessName: String
}
struct ServicePoint: Identifiable {
var id: Int { servicePointId }
let servicePointId: Int
let name: String
let hasBeacon: Bool
var beaconMinor: UInt16?
}
struct BusinessNamespace {
let shardId: Int
let uuid: String // Original UUID from API (with dashes, as-is)
let uuidClean: String // 32-char hex, no dashes, uppercase (for BLE provisioning)
let major: UInt16
let alreadyAllocated: Bool
}