- DX-Smart auth now tries multiple passwords in sequence (555555, dx1234, 000000) instead of hardcoding a single password. Matches Android behavior for better compatibility across firmware versions. - Added ProvisioningError enum with structured error codes (CONNECTION_FAILED, AUTH_FAILED, SERVICE_NOT_FOUND, WRITE_FAILED, etc.) matching Android's BeaconConfig error codes. All fail() calls now tagged with codes for better debugging and error reporting. - Added ProvisioningResult.failureWithCode case and handling in ScanView. - Added missing API endpoints that Android has: - getBusiness() - single business fetch - getBusinessName() - cached business name lookup - allocateServicePointMinor() - minor value allocation - Fixed stray print() in Api.swift to use DebugLog.shared.log() for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
702 lines
27 KiB
Swift
702 lines
27 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)
|
|
}
|
|
}
|
|
|
|
/// Get a single business by ID
|
|
func getBusiness(businessId: Int) async throws -> Business? {
|
|
let json = try await postRequest(
|
|
endpoint: "/businesses/get.php",
|
|
body: ["BusinessID": businessId],
|
|
extraHeaders: ["X-Business-Id": String(businessId)]
|
|
)
|
|
|
|
guard parseBool(json["OK"] ?? json["ok"]) else {
|
|
return nil
|
|
}
|
|
|
|
guard let b = (json["BUSINESS"] ?? json["business"]) as? [String: Any],
|
|
let id = 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: id, name: name, headerImageExtension: headerImageExtension)
|
|
}
|
|
|
|
// Business name cache (matches Android's caching behavior)
|
|
private var businessNameCache: [Int: String] = [:]
|
|
|
|
/// Get business name with caching (avoids repeated API calls)
|
|
func getBusinessName(businessId: Int) async throws -> String {
|
|
if let cached = businessNameCache[businessId] {
|
|
return cached
|
|
}
|
|
if let business = try await getBusiness(businessId: businessId) {
|
|
businessNameCache[business.businessId] = business.name
|
|
return business.name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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)?
|
|
.normalizedUUID,
|
|
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)?
|
|
.normalizedUUID 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)?
|
|
.normalizedUUID ?? ""
|
|
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.normalizedUUID,
|
|
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
|
|
DebugLog.shared.log("[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.normalizedUUID,
|
|
major: UInt16(major),
|
|
alreadyAllocated: parseBool(json["AlreadyAllocated"] ?? json["ALREADYALLOCATED"])
|
|
)
|
|
}
|
|
|
|
/// Allocate a minor value for a service point (auto-assigns next available minor)
|
|
func allocateServicePointMinor(businessId: Int, servicePointId: Int) async throws -> UInt16 {
|
|
let json = try await postRequest(
|
|
endpoint: "/beacon-sharding/allocate_servicepoint_minor.php",
|
|
body: ["BusinessID": businessId, "ServicePointID": servicePointId],
|
|
extraHeaders: ["X-Business-Id": String(businessId)]
|
|
)
|
|
|
|
DebugLog.shared.log("[API] allocateServicePointMinor response: \(json)")
|
|
|
|
if !parseBool(json["OK"] ?? json["ok"]) {
|
|
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to allocate minor"
|
|
throw ApiException(error)
|
|
}
|
|
|
|
guard let minor = parseIntValue(json["MINOR"] ?? json["minor"] ?? json["Minor"] ?? json["BeaconMinor"] ?? json["BEACONMINOR"]) else {
|
|
throw ApiException("Invalid response - no Minor value")
|
|
}
|
|
|
|
return UInt16(minor)
|
|
}
|
|
|
|
/// 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
|
|
}
|