diff --git a/PayfritBeacon/Models/Business.swift b/PayfritBeacon/Models/Business.swift index 9988954..e2fe1e0 100644 --- a/PayfritBeacon/Models/Business.swift +++ b/PayfritBeacon/Models/Business.swift @@ -6,11 +6,42 @@ struct Business: Identifiable, Codable, Hashable { let imageExtension: String? enum CodingKeys: String, CodingKey { - case id = "ID" - case name = "BusinessName" + case businessId = "BusinessID" + case name = "Name" + // Fallbacks for alternate API shapes + case altId = "ID" + case altName = "BusinessName" case imageExtension = "ImageExtension" } + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + // BusinessID (from list endpoint) or ID (from other endpoints), always as String + if let bid = try? container.decode(Int.self, forKey: .businessId) { + id = String(bid) + } else if let bid = try? container.decode(String.self, forKey: .businessId) { + id = bid + } else if let aid = try? container.decode(Int.self, forKey: .altId) { + id = String(aid) + } else if let aid = try? container.decode(String.self, forKey: .altId) { + id = aid + } else { + id = "" + } + // Name (from list endpoint) or BusinessName (from other endpoints) + name = (try? container.decode(String.self, forKey: .name)) + ?? (try? container.decode(String.self, forKey: .altName)) + ?? "" + imageExtension = try? container.decode(String.self, forKey: .imageExtension) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .businessId) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(imageExtension, forKey: .imageExtension) + } + var headerImageURL: URL? { guard let ext = imageExtension, !ext.isEmpty else { return nil } return URL(string: "\(APIConfig.imageBaseURL)/businesses/\(id)/header.\(ext)") diff --git a/PayfritBeacon/Models/ServicePoint.swift b/PayfritBeacon/Models/ServicePoint.swift index e9a5291..f22c8b4 100644 --- a/PayfritBeacon/Models/ServicePoint.swift +++ b/PayfritBeacon/Models/ServicePoint.swift @@ -6,8 +6,38 @@ struct ServicePoint: Identifiable, Codable, Hashable { let businessId: String enum CodingKeys: String, CodingKey { - case id = "ID" + case servicePointId = "ServicePointID" + case altId = "ID" case name = "Name" case businessId = "BusinessID" } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + // ServicePointID (from list/save endpoints) or ID (fallback) + if let sid = try? container.decode(Int.self, forKey: .servicePointId) { + id = String(sid) + } else if let sid = try? container.decode(String.self, forKey: .servicePointId) { + id = sid + } else if let aid = try? container.decode(Int.self, forKey: .altId) { + id = String(aid) + } else if let aid = try? container.decode(String.self, forKey: .altId) { + id = aid + } else { + id = "" + } + name = (try? container.decode(String.self, forKey: .name)) ?? "" + if let bid = try? container.decode(Int.self, forKey: .businessId) { + businessId = String(bid) + } else { + businessId = (try? container.decode(String.self, forKey: .businessId)) ?? "" + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .servicePointId) + try container.encode(name, forKey: .name) + try container.encode(businessId, forKey: .businessId) + } } diff --git a/PayfritBeacon/Services/APIClient.swift b/PayfritBeacon/Services/APIClient.swift index 6c3522c..d5323c0 100644 --- a/PayfritBeacon/Services/APIClient.swift +++ b/PayfritBeacon/Services/APIClient.swift @@ -110,72 +110,105 @@ actor APIClient { // MARK: - Businesses + /// API returns: { "OK": true, "BUSINESSES": [...], "Businesses": [...] } + private struct BusinessListResponse: Codable { + let OK: Bool + let ERROR: String? + let BUSINESSES: [Business]? + let Businesses: [Business]? + + var businesses: [Business] { BUSINESSES ?? 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") + let resp = try JSONDecoder().decode(BusinessListResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.ERROR ?? "Failed to load businesses") } - return resp.data ?? [] + return resp.businesses } // MARK: - Service Points + /// API returns: { "OK": true, "SERVICEPOINTS": [...], "GRANTED_SERVICEPOINTS": [...] } + private struct ServicePointListResponse: Codable { + let OK: Bool + let ERROR: String? + let SERVICEPOINTS: [ServicePoint]? + } + 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") + let resp = try JSONDecoder().decode(ServicePointListResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.ERROR ?? "Failed to load service points") } - return resp.data ?? [] + return resp.SERVICEPOINTS ?? [] + } + + /// API returns: { "OK": true, "SERVICEPOINT": { ... } } + private struct ServicePointSaveResponse: Codable { + let OK: Bool + let ERROR: String? + let SERVICEPOINT: ServicePoint? } 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") + let resp = try JSONDecoder().decode(ServicePointSaveResponse.self, from: data) + guard resp.OK, let sp = resp.SERVICEPOINT else { + throw APIError.serverError(resp.ERROR ?? "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 } + /// API returns: { "OK": true, "BeaconShardUUID": "...", "BeaconMajor": 5 } + private struct AllocateNamespaceResponse: Codable { + let OK: Bool + let ERROR: String? + let MESSAGE: String? + let BeaconShardUUID: String? + let BeaconMajor: Int? } 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") + let resp = try JSONDecoder().decode(AllocateNamespaceResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to allocate namespace") } - return (ns.shardUUID, ns.shardMajor) + return (resp.BeaconShardUUID ?? "", resp.BeaconMajor ?? 0) } - struct MinorResponse: Codable { - let minor: Int? - let Minor: Int? - var allocated: Int { minor ?? Minor ?? 0 } + /// API returns: { "OK": true, "BeaconMinor": 3 } + private struct AllocateMinorResponse: Codable { + let OK: Bool + let ERROR: String? + let MESSAGE: String? + let BeaconMinor: Int? } 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") + let resp = try JSONDecoder().decode(AllocateMinorResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to allocate minor") } - return m.allocated + return resp.BeaconMinor ?? 0 + } + + /// API returns: { "OK": true, "BeaconHardwareID": 42, ... } + private struct OKResponse: Codable { + let OK: Bool + let ERROR: String? + let MESSAGE: String? } func registerBeaconHardware( @@ -198,9 +231,9 @@ actor APIClient { ] 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") + let resp = try JSONDecoder().decode(OKResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to register beacon") } } @@ -212,23 +245,24 @@ actor APIClient { ) 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") + let resp = try JSONDecoder().decode(OKResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to verify broadcast") } } - struct ResolveResponse: Codable { - let businessName: String? + /// API returns: { "OK": true, "BusinessName": "...", "BusinessID": 5 } + private struct ResolveBusinessResponse: Codable { + let OK: Bool + let ERROR: 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" + let resp = try JSONDecoder().decode(ResolveBusinessResponse.self, from: data) + return resp.BusinessName ?? "Unknown" } // MARK: - Service Point Management @@ -236,24 +270,24 @@ actor APIClient { 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") + let resp = try JSONDecoder().decode(OKResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "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") + let resp = try JSONDecoder().decode(OKResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to update service point") } } // MARK: - Beacon Management - struct BeaconListResponse: Codable { + struct BeaconListItem: Codable { let id: String? let ID: String? let uuid: String? @@ -272,125 +306,135 @@ actor APIClient { let IsVerified: Bool? } - func listBeacons(businessId: String, token: String) async throws -> [BeaconListResponse] { + /// API returns: { "OK": true, "BEACONS": [...] } + private struct BeaconListAPIResponse: Codable { + let OK: Bool + let ERROR: String? + let BEACONS: [BeaconListItem]? + } + + func listBeacons(businessId: String, token: String) async throws -> [BeaconListItem] { 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") + let data = try await post(path: "/beacons/list.php", body: body, token: token, businessId: businessId) + let resp = try JSONDecoder().decode(BeaconListAPIResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.ERROR ?? "Failed to list beacons") } - return resp.data ?? [] + return resp.BEACONS ?? [] } 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") + let data = try await post(path: "/beacons/wipe.php", body: body, token: token, businessId: businessId) + let resp = try JSONDecoder().decode(OKResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to decommission beacon") } } - func lookupByMac(macAddress: String, token: String) async throws -> BeaconListResponse? { + func lookupByMac(macAddress: String, token: String) async throws -> BeaconListItem? { 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 + let data = try await post(path: "/beacons/lookupByMac.php", body: body, token: token) + // This may return a single beacon object or OK: false + struct LookupResponse: Codable { + let OK: Bool + let ID: String? + let UUID: String? + let Major: Int? + let Minor: Int? + let MacAddress: String? + let BeaconType: String? + let ServicePointID: String? + let IsVerified: Bool? + } + let resp = try JSONDecoder().decode(LookupResponse.self, from: data) + guard resp.OK, resp.ID != nil else { return nil } + return BeaconListItem( + id: resp.ID, ID: resp.ID, + uuid: resp.UUID, UUID: resp.UUID, + major: resp.Major, Major: resp.Major, + minor: resp.Minor, Minor: resp.Minor, + macAddress: resp.MacAddress, MacAddress: resp.MacAddress, + beaconType: resp.BeaconType, BeaconType: resp.BeaconType, + servicePointId: resp.ServicePointID, ServicePointID: resp.ServicePointID, + isVerified: resp.IsVerified, IsVerified: resp.IsVerified + ) } - func getBeaconStatus(uuid: String, major: Int, minor: Int, token: String) async throws -> BeaconListResponse? { + func getBeaconStatus(uuid: String, major: Int, minor: Int, token: String) async throws -> BeaconListItem? { 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 + let data = try await post(path: "/beacon-sharding/verify_beacon_broadcast.php", body: body, token: token) + let resp = try JSONDecoder().decode(OKResponse.self, from: data) + guard resp.OK else { return nil } + // The verify endpoint confirms status but doesn't return full beacon details + return BeaconListItem( + id: nil, ID: nil, + uuid: uuid, UUID: uuid, + major: major, Major: major, + minor: minor, Minor: minor, + macAddress: nil, MacAddress: nil, + beaconType: nil, BeaconType: nil, + servicePointId: nil, ServicePointID: nil, + isVerified: true, IsVerified: true + ) } // MARK: - Beacon Config (server-configured values) + /// API returns: { "OK": true, "UUID": "...", "Major": 5, "Minor": 3, ... } struct BeaconConfigResponse: Codable { - let uuid: String? + let OK: Bool + let ERROR: String? + let MESSAGE: 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 } + var configUUID: String { UUID ?? "" } + var configMajor: Int { Major ?? 0 } + var configMinor: Int { Minor ?? 0 } + var configTxPower: Int { TxPower ?? 1 } + var configMeasuredPower: Int { MeasuredPower ?? -100 } + var configAdvInterval: Int { 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") + let resp = try JSONDecoder().decode(BeaconConfigResponse.self, from: data) + guard resp.OK else { + throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to get beacon config") } - return config + return resp } // MARK: - User Profile + /// Note: /users/profile.php endpoint may not exist on server. + /// Using a flat response decoder matching the standard API format. struct UserProfile: Codable { - let id: String? - let ID: String? - let firstName: String? + let OK: Bool? + let ID: IntOrString? let FirstName: String? - let lastName: String? let LastName: String? - let contactNumber: String? let ContactNumber: String? + + var userId: String { ID?.stringValue ?? "" } + var firstName: String { FirstName ?? "" } + var lastName: String { LastName ?? "" } } 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 + let resp = try JSONDecoder().decode(UserProfile.self, from: data) + return resp } // 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],