Migrate API endpoints from CFML to PHP

- Replace all .cfm endpoints with .php (PHP backend migration)
- Update debug strings and test walkthrough documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Pinkyfloyd 2026-03-14 17:16:36 -07:00
parent bbdc5a91c2
commit 8d9fa1caa6
6 changed files with 79 additions and 45 deletions

View file

@ -9,6 +9,9 @@ struct OrderLineItem: Identifiable {
let quantity: Int let quantity: Int
let remark: String let remark: String
let isModifier: Bool let isModifier: Bool
let isCheckedByDefault: Bool
let isInvertedGroup: Bool
let removedDefaults: [String] // Array of item names that were removed from inverted groups
var id: Int { lineItemId } var id: Int { lineItemId }
@ -26,5 +29,21 @@ struct OrderLineItem: Identifiable {
if let b = json["IsModifier"] as? Bool { isModifier = b } if let b = json["IsModifier"] as? Bool { isModifier = b }
else if let i = json["IsModifier"] as? Int { isModifier = i == 1 } else if let i = json["IsModifier"] as? Int { isModifier = i == 1 }
else { isModifier = false } else { isModifier = false }
// Inverted group fields
if let b = json["IsCheckedByDefault"] as? Bool { isCheckedByDefault = b }
else if let i = json["IsCheckedByDefault"] as? Int { isCheckedByDefault = i == 1 }
else { isCheckedByDefault = false }
if let b = json["IsInvertedGroup"] as? Bool { isInvertedGroup = b }
else if let i = json["IsInvertedGroup"] as? Int { isInvertedGroup = i == 1 }
else { isInvertedGroup = false }
// Removed defaults array
if let arr = json["RemovedDefaults"] as? [String] {
removedDefaults = arr
} else {
removedDefaults = []
}
} }
} }

View file

@ -1,6 +1,6 @@
import Foundation import Foundation
// MARK: - Tier Status (from tierStatus.cfm) // MARK: - Tier Status (from tierStatus.php)
struct TierStatus: Codable { struct TierStatus: Codable {
var tier: Int var tier: Int
@ -98,7 +98,7 @@ struct TierStatus: Codable {
} }
} }
// MARK: - Ledger Response (from ledger.cfm) // MARK: - Ledger Response (from ledger.php)
struct LedgerResponse: Codable { struct LedgerResponse: Codable {
var entries: [LedgerEntry] var entries: [LedgerEntry]

View file

@ -253,7 +253,7 @@ actor APIService {
// MARK: - Auth // MARK: - Auth
func login(username: String, password: String) async throws -> LoginResponse { func login(username: String, password: String) async throws -> LoginResponse {
let json = try await postJSON("/auth/login.cfm", payload: [ let json = try await postJSON("/auth/login.php", payload: [
"username": username, "username": username,
"password": password "password": password
]) ])
@ -293,7 +293,7 @@ actor APIService {
// MARK: - OTP Login // MARK: - OTP Login
func sendLoginOtp(phone: String) async throws -> SendOtpResponse { func sendLoginOtp(phone: String) async throws -> SendOtpResponse {
let json = try await postJSON("/auth/loginOTP.cfm", payload: [ let json = try await postJSON("/auth/loginOTP.php", payload: [
"phone": phone "phone": phone
]) ])
@ -309,7 +309,7 @@ actor APIService {
} }
func verifyLoginOtp(uuid: String, otp: String) async throws -> LoginResponse { func verifyLoginOtp(uuid: String, otp: String) async throws -> LoginResponse {
let json = try await postJSON("/auth/verifyLoginOTP.cfm", payload: [ let json = try await postJSON("/auth/verifyLoginOTP.php", payload: [
"uuid": uuid, "uuid": uuid,
"otp": otp "otp": otp
]) ])
@ -363,7 +363,7 @@ actor APIService {
throw APIError.serverError("User not logged in") throw APIError.serverError("User not logged in")
} }
let json = try await postJSON("/auth/profile.cfm", payload: [ let json = try await postJSON("/auth/profile.php", payload: [
"UserID": uid "UserID": uid
]) ])
@ -393,7 +393,7 @@ actor APIService {
if let em = email { payload["Email"] = em } if let em = email { payload["Email"] = em }
if let ph = phone { payload["Phone"] = ph } if let ph = phone { payload["Phone"] = ph }
let json = try await postJSON("/auth/profile.cfm", payload: payload) let json = try await postJSON("/auth/profile.php", payload: payload)
guard ok(json) else { guard ok(json) else {
throw APIError.serverError("Failed to update profile: \(err(json))") throw APIError.serverError("Failed to update profile: \(err(json))")
@ -407,7 +407,7 @@ actor APIService {
throw APIError.serverError("User not logged in") throw APIError.serverError("User not logged in")
} }
let json = try await postJSON("/workers/myBusinesses.cfm", payload: [ let json = try await postJSON("/workers/myBusinesses.php", payload: [
"UserID": uid "UserID": uid
]) ])
@ -429,7 +429,7 @@ actor APIService {
payload["CategoryID"] = cid payload["CategoryID"] = cid
} }
let json = try await postJSON("/tasks/listPending.cfm", payload: payload) let json = try await postJSON("/tasks/listPending.php", payload: payload)
guard ok(json) else { guard ok(json) else {
throw APIError.serverError("Failed to load tasks: \(err(json))") throw APIError.serverError("Failed to load tasks: \(err(json))")
} }
@ -440,7 +440,7 @@ actor APIService {
func acceptTask(taskId: Int) async throws { func acceptTask(taskId: Int) async throws {
// Flutter only sends TaskID + BusinessID (no UserID) // Flutter only sends TaskID + BusinessID (no UserID)
let json = try await postJSON("/tasks/accept.cfm", payload: [ let json = try await postJSON("/tasks/accept.php", payload: [
"TaskID": taskId, "TaskID": taskId,
"BusinessID": businessId "BusinessID": businessId
]) ])
@ -463,7 +463,7 @@ actor APIService {
payload["BusinessID"] = businessId payload["BusinessID"] = businessId
} }
let json = try await postJSON("/tasks/listMine.cfm", payload: payload) let json = try await postJSON("/tasks/listMine.php", payload: payload)
guard ok(json) else { guard ok(json) else {
throw APIError.serverError("Failed to load my tasks: \(err(json))") throw APIError.serverError("Failed to load my tasks: \(err(json))")
} }
@ -483,7 +483,7 @@ actor APIService {
if cancelOrder { if cancelOrder {
payload["CancelOrder"] = true payload["CancelOrder"] = true
} }
let json = try await postJSON("/tasks/complete.cfm", payload: payload) let json = try await postJSON("/tasks/complete.php", payload: payload)
guard ok(json) else { guard ok(json) else {
throw APIError.serverError("Failed to complete task: \(err(json))") throw APIError.serverError("Failed to complete task: \(err(json))")
} }
@ -493,14 +493,14 @@ actor APIService {
let payload: [String: Any] = [ let payload: [String: Any] = [
"TaskID": taskId "TaskID": taskId
] ]
let json = try await postJSON("/tasks/completeChat.cfm", payload: payload) let json = try await postJSON("/tasks/completeChat.php", payload: payload)
guard ok(json) else { guard ok(json) else {
throw APIError.serverError("Close chat failed: \(err(json))") throw APIError.serverError("Close chat failed: \(err(json))")
} }
} }
func getTaskDetails(taskId: Int) async throws -> TaskDetails { func getTaskDetails(taskId: Int) async throws -> TaskDetails {
let json = try await postJSON("/tasks/getDetails.cfm", payload: [ let json = try await postJSON("/tasks/getDetails.php", payload: [
"TaskID": taskId "TaskID": taskId
]) ])
guard ok(json) else { guard ok(json) else {
@ -553,7 +553,7 @@ actor APIService {
payload["AfterMessageID"] = after payload["AfterMessageID"] = after
} }
let json = try await postJSON("/chat/getMessages.cfm", payload: payload) let json = try await postJSON("/chat/getMessages.php", payload: payload)
guard ok(json) else { guard ok(json) else {
throw APIError.serverError("Failed to load chat messages") throw APIError.serverError("Failed to load chat messages")
} }
@ -573,7 +573,7 @@ actor APIService {
if let uid = userId { payload["UserID"] = uid } if let uid = userId { payload["UserID"] = uid }
if let st = senderType { payload["SenderType"] = st } if let st = senderType { payload["SenderType"] = st }
let json = try await postJSON("/chat/sendMessage.cfm", payload: payload) let json = try await postJSON("/chat/sendMessage.php", payload: payload)
guard ok(json) else { guard ok(json) else {
throw APIError.serverError("Failed to send message") throw APIError.serverError("Failed to send message")
} }
@ -582,7 +582,7 @@ actor APIService {
} }
func markChatMessagesRead(taskId: Int, readerType: String) async throws { func markChatMessagesRead(taskId: Int, readerType: String) async throws {
let json = try await postJSON("/chat/markRead.cfm", payload: [ let json = try await postJSON("/chat/markRead.php", payload: [
"TaskID": taskId, "TaskID": taskId,
"ReaderType": readerType "ReaderType": readerType
]) ])
@ -594,7 +594,7 @@ actor APIService {
// MARK: - Payout / Tier Endpoints // MARK: - Payout / Tier Endpoints
func getTierStatus() async throws -> TierStatus { func getTierStatus() async throws -> TierStatus {
let json = try await postJSON("/workers/tierStatus.cfm", payload: [ let json = try await postJSON("/workers/tierStatus.php", payload: [
"UserID": userId ?? 0 "UserID": userId ?? 0
]) ])
guard ok(json) else { guard ok(json) else {
@ -605,7 +605,7 @@ actor APIService {
} }
func createStripeAccount() async throws -> String { func createStripeAccount() async throws -> String {
let json = try await postJSON("/workers/createAccount.cfm", payload: [ let json = try await postJSON("/workers/createAccount.php", payload: [
"UserID": userId ?? 0 "UserID": userId ?? 0
]) ])
guard ok(json) else { guard ok(json) else {
@ -615,7 +615,7 @@ actor APIService {
} }
func getOnboardingLink() async throws -> String { func getOnboardingLink() async throws -> String {
let json = try await postJSON("/workers/onboardingLink.cfm", payload: [ let json = try await postJSON("/workers/onboardingLink.php", payload: [
"UserID": userId ?? 0 "UserID": userId ?? 0
]) ])
guard ok(json) else { guard ok(json) else {
@ -625,7 +625,7 @@ actor APIService {
} }
func getEarlyUnlockUrl() async throws -> String { func getEarlyUnlockUrl() async throws -> String {
let json = try await postJSON("/workers/earlyUnlock.cfm", payload: [ let json = try await postJSON("/workers/earlyUnlock.php", payload: [
"UserID": userId ?? 0 "UserID": userId ?? 0
]) ])
guard ok(json) else { guard ok(json) else {
@ -635,7 +635,7 @@ actor APIService {
} }
func getLedger() async throws -> LedgerResponse { func getLedger() async throws -> LedgerResponse {
let json = try await postJSON("/workers/ledger.cfm", payload: [ let json = try await postJSON("/workers/ledger.php", payload: [
"UserID": userId ?? 0 "UserID": userId ?? 0
]) ])
guard ok(json) else { guard ok(json) else {
@ -648,7 +648,7 @@ actor APIService {
// MARK: - Avatar // MARK: - Avatar
func getAvatarUrl() async throws -> String? { func getAvatarUrl() async throws -> String? {
let json = try await getJSON("/auth/avatar.cfm") let json = try await getJSON("/auth/avatar.php")
print("[Avatar] Response: \(json)") print("[Avatar] Response: \(json)")
guard ok(json) else { guard ok(json) else {
print("[Avatar] Response not OK") print("[Avatar] Response not OK")
@ -681,7 +681,7 @@ actor APIService {
/// Get avatar URL for any user by their userId /// Get avatar URL for any user by their userId
func getUserAvatarUrl(userId: Int) async throws -> String? { func getUserAvatarUrl(userId: Int) async throws -> String? {
// Try the avatar endpoint with UserID // Try the avatar endpoint with UserID
let json = try await postJSON("/auth/avatar.cfm", payload: ["UserID": userId]) let json = try await postJSON("/auth/avatar.php", payload: ["UserID": userId])
print("[Avatar] getUserAvatarUrl(\(userId)) Response: \(json)") print("[Avatar] getUserAvatarUrl(\(userId)) Response: \(json)")
let data = json["DATA"] as? [String: Any] ?? json let data = json["DATA"] as? [String: Any] ?? json
@ -712,7 +712,7 @@ actor APIService {
// MARK: - Beacon Sharding // MARK: - Beacon Sharding
func resolveServicePoint(uuid: String, major: Int, minor: Int) async throws -> Int { func resolveServicePoint(uuid: String, major: Int, minor: Int) async throws -> Int {
let json = try await postJSON("/beacon-sharding/resolve_servicepoint.cfm", payload: [ let json = try await postJSON("/beacon-sharding/resolve_servicepoint.php", payload: [
"UUID": uuid, "UUID": uuid,
"Major": major, "Major": major,
"Minor": minor "Minor": minor
@ -732,7 +732,7 @@ actor APIService {
} }
func resolveBusiness(uuid: String, major: Int) async throws -> (businessId: Int, businessName: String) { func resolveBusiness(uuid: String, major: Int) async throws -> (businessId: Int, businessName: String) {
let json = try await postJSON("/beacon-sharding/resolve_business.cfm", payload: [ let json = try await postJSON("/beacon-sharding/resolve_business.php", payload: [
"UUID": uuid, "UUID": uuid,
"Major": major "Major": major
]) ])
@ -774,7 +774,7 @@ actor APIService {
// MARK: - About Info // MARK: - About Info
func getAboutInfo() async throws -> AboutInfo { func getAboutInfo() async throws -> AboutInfo {
let json = try await getJSON("app/about.cfm") let json = try await getJSON("app/about.php")
guard ok(json) else { guard ok(json) else {
throw APIError.serverError(err(json)) throw APIError.serverError(err(json))

View file

@ -309,15 +309,15 @@ struct BusinessSelectionScreen: View {
var text = "=== RAW API RESPONSES ===\n\n" var text = "=== RAW API RESPONSES ===\n\n"
text += "UserID: \(uid), BusinessID: \(bid)\n\n" text += "UserID: \(uid), BusinessID: \(bid)\n\n"
text += "--- /workers/myBusinesses.cfm ---\n" text += "--- /workers/myBusinesses.php ---\n"
let bizRaw = await APIService.shared.debugRawJSON( let bizRaw = await APIService.shared.debugRawJSON(
"/workers/myBusinesses.cfm", payload: ["UserID": uid]) "/workers/myBusinesses.php", payload: ["UserID": uid])
text += bizRaw + "\n\n" text += bizRaw + "\n\n"
if bid > 0 { if bid > 0 {
text += "--- /tasks/listPending.cfm ---\n" text += "--- /tasks/listPending.php ---\n"
let taskRaw = await APIService.shared.debugRawJSON( let taskRaw = await APIService.shared.debugRawJSON(
"/tasks/listPending.cfm", payload: ["BusinessID": bid]) "/tasks/listPending.php", payload: ["BusinessID": bid])
text += taskRaw + "\n\n" text += taskRaw + "\n\n"
} }

View file

@ -518,14 +518,29 @@ struct TaskDetailScreen: View {
} }
ForEach(modifiers) { mod in ForEach(modifiers) { mod in
// For inverted groups: show removed defaults as "NO {name}"
if mod.isInvertedGroup && !mod.removedDefaults.isEmpty {
ForEach(mod.removedDefaults, id: \.self) { removedName in
Text("- NO \(removedName)")
.font(.caption)
.foregroundColor(.red)
}
}
let children = d.lineItems.filter { $0.parentLineItemId == mod.lineItemId } let children = d.lineItems.filter { $0.parentLineItemId == mod.lineItemId }
if !children.isEmpty { if !children.isEmpty {
ForEach(children) { child in ForEach(children) { child in
Text("- \(mod.itemName): \(child.itemName)") // Skip default items in inverted groups (they're unchanged)
.font(.caption) if mod.isInvertedGroup && child.isCheckedByDefault {
.foregroundColor(.secondary) // Don't display - this is an unchanged default
} else {
Text("- \(mod.itemName): \(child.itemName)")
.font(.caption)
.foregroundColor(.secondary)
}
} }
} else { } else if !mod.isInvertedGroup {
// Regular modifier (not inverted group)
Text("- \(mod.itemName)") Text("- \(mod.itemName)")
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)

View file

@ -461,7 +461,7 @@ const sections = [
preconditions: 'Logged in, user has businesses.', preconditions: 'Logged in, user has businesses.',
action: 'After login, observe Business Selection screen.', action: 'After login, observe Business Selection screen.',
expected: 'Nav title "Select Business". List of businesses with: initial letter in colored square, business name, address/city, status badge (Pending/Active/Inactive), task count or green checkmark. Toolbar: refresh button and ellipsis menu.', expected: 'Nav title "Select Business". List of businesses with: initial letter in colored square, business name, address/city, status badge (Pending/Active/Inactive), task count or green checkmark. Toolbar: refresh button and ellipsis menu.',
onFail: 'Screenshot. Check API response from /workers/myBusinesses.cfm.' onFail: 'Screenshot. Check API response from /workers/myBusinesses.php.'
}, },
{ {
id: 'BS02', id: 'BS02',
@ -532,7 +532,7 @@ const sections = [
title: 'Toolbar menu — Debug API', title: 'Toolbar menu — Debug API',
preconditions: 'Business Selection screen.', preconditions: 'Business Selection screen.',
action: 'Tap ellipsis menu → "Debug API".', action: 'Tap ellipsis menu → "Debug API".',
expected: 'Modal sheet appears showing raw JSON from /workers/myBusinesses.cfm and /tasks/listPending.cfm. Monospaced text, "Done" button to dismiss.', expected: 'Modal sheet appears showing raw JSON from /workers/myBusinesses.php and /tasks/listPending.php. Monospaced text, "Done" button to dismiss.',
onFail: 'Screenshot.' onFail: 'Screenshot.'
}, },
{ {
@ -570,7 +570,7 @@ const sections = [
preconditions: 'Selected a business with pending tasks.', preconditions: 'Selected a business with pending tasks.',
action: 'Observe the task list screen.', action: 'Observe the task list screen.',
expected: 'Nav title = business name. List of task cards with: color bar, category icon (chat = bubble icon), title, location (mappin or bicycle icon), category badge, chat badge if applicable, time ago label.', expected: 'Nav title = business name. List of task cards with: color bar, category icon (chat = bubble icon), title, location (mappin or bicycle icon), category badge, chat badge if applicable, time ago label.',
onFail: 'Screenshot. Check /tasks/listPending.cfm response.' onFail: 'Screenshot. Check /tasks/listPending.php response.'
}, },
{ {
id: 'TL02', id: 'TL02',
@ -655,7 +655,7 @@ const sections = [
preconditions: 'Tapped a task from task list.', preconditions: 'Tapped a task from task list.',
action: 'Observe the task detail screen.', action: 'Observe the task detail screen.',
expected: 'Loading spinner, then content: customer section (avatar/name/phone), location section (table or delivery), nav bar colored with category color. "Accept Task" button at bottom.', expected: 'Loading spinner, then content: customer section (avatar/name/phone), location section (table or delivery), nav bar colored with category color. "Accept Task" button at bottom.',
onFail: 'Screenshot. Check /tasks/getDetails.cfm response.' onFail: 'Screenshot. Check /tasks/getDetails.php response.'
}, },
{ {
id: 'TD02', id: 'TD02',
@ -750,7 +750,7 @@ const sections = [
title: 'Accept Task — confirm', title: 'Accept Task — confirm',
preconditions: 'Accept alert shown.', preconditions: 'Accept alert shown.',
action: 'Tap "Accept".', action: 'Tap "Accept".',
expected: 'API call to /tasks/accept.cfm. On success, screen dismisses back to task list. Task removed from pending list.', expected: 'API call to /tasks/accept.php. On success, screen dismisses back to task list. Task removed from pending list.',
onFail: 'Screenshot. Check API response.' onFail: 'Screenshot. Check API response.'
}, },
{ {
@ -782,7 +782,7 @@ const sections = [
title: 'Complete Task — confirm', title: 'Complete Task — confirm',
preconditions: 'Complete alert shown.', preconditions: 'Complete alert shown.',
action: 'Tap "Complete".', action: 'Tap "Complete".',
expected: 'API call to /tasks/complete.cfm. On success, dismisses back to My Tasks. Task moves to Completed filter.', expected: 'API call to /tasks/complete.php. On success, dismisses back to My Tasks. Task moves to Completed filter.',
onFail: 'Screenshot.' onFail: 'Screenshot.'
}, },
{ {
@ -844,7 +844,7 @@ const sections = [
preconditions: 'Worker has accepted tasks.', preconditions: 'Worker has accepted tasks.',
action: 'Select Active tab.', action: 'Select Active tab.',
expected: 'Shows tasks with status Accepted/In Progress. Each card shows: color bar, title, location, category badge, time ago, status badge.', expected: 'Shows tasks with status Accepted/In Progress. Each card shows: color bar, title, location, category badge, time ago, status badge.',
onFail: 'Screenshot. Verify /tasks/listMine.cfm?FilterType=active response.' onFail: 'Screenshot. Verify /tasks/listMine.php?FilterType=active response.'
}, },
{ {
id: 'MT03', id: 'MT03',
@ -1008,7 +1008,7 @@ const sections = [
title: 'HTTP fallback when WebSocket disconnected', title: 'HTTP fallback when WebSocket disconnected',
preconditions: 'WebSocket not connected.', preconditions: 'WebSocket not connected.',
action: 'Send a message.', action: 'Send a message.',
expected: 'Message sent via HTTP POST to /chat/sendMessage.cfm. Messages poll via 3-second timer.', expected: 'Message sent via HTTP POST to /chat/sendMessage.php. Messages poll via 3-second timer.',
onFail: 'Note if message fails or gets lost.' onFail: 'Note if message fails or gets lost.'
}, },
{ {
@ -1030,7 +1030,7 @@ const sections = [
preconditions: 'Navigated to Account from menu.', preconditions: 'Navigated to Account from menu.',
action: 'Observe Account screen.', action: 'Observe Account screen.',
expected: 'Nav title "Account". Loading spinner, then: Payout Status card, Activation card, Earnings card, Logout button. FAB in bottom-right.', expected: 'Nav title "Account". Loading spinner, then: Payout Status card, Activation card, Earnings card, Logout button. FAB in bottom-right.',
onFail: 'Screenshot. Check /workers/tierStatus.cfm response.' onFail: 'Screenshot. Check /workers/tierStatus.php response.'
}, },
{ {
id: 'AC02', id: 'AC02',
@ -1085,7 +1085,7 @@ const sections = [
title: 'Early unlock button', title: 'Early unlock button',
preconditions: 'Activation not complete.', preconditions: 'Activation not complete.',
action: 'Tap "Complete activation now (optional)".', action: 'Tap "Complete activation now (optional)".',
expected: 'API call to /workers/earlyUnlock.cfm. Opens URL in Safari. Reloads after return.', expected: 'API call to /workers/earlyUnlock.php. Opens URL in Safari. Reloads after return.',
onFail: 'Screenshot.' onFail: 'Screenshot.'
}, },
{ {