function apiAbort(payload) { writeOutput(serializeJSON(payload)); abort; } // Read JSON body for user location function readJsonBody() { var raw = getHttpRequestData().content; if (isNull(raw) OR len(trim(raw)) EQ 0) return {}; try { var data = deserializeJSON(raw); if (isStruct(data)) return data; return {}; } catch (any e) { return {}; } } // Haversine formula to calculate distance in miles function haversineDistance(lat1, lng1, lat2, lng2) { var R = 3959; // Earth radius in miles var dLat = (lat2 - lat1) * 3.14159265359 / 180; var dLng = (lng2 - lng1) * 3.14159265359 / 180; var a = sin(dLat/2) * sin(dLat/2) + cos(lat1 * 3.14159265359 / 180) * cos(lat2 * 3.14159265359 / 180) * sin(dLng/2) * sin(dLng/2); // Clamp a to avoid NaN from sqrt of negative or division by zero if (a < 0) a = 0; if (a > 1) a = 1; var c = 2 * asin(sqr(a)); return R * c; } try { data = readJsonBody(); userLat = structKeyExists(data, "lat") ? val(data.lat) : 0; userLng = structKeyExists(data, "lng") ? val(data.lng) : 0; hasUserLocation = (userLat != 0 AND userLng != 0); // Get businesses with their address coordinates (exclude demo and private) q = queryExecute( " SELECT b.ID, b.Name, a.Latitude, a.Longitude, a.City, a.Line1 FROM Businesses b LEFT JOIN Addresses a ON b.AddressID = a.ID WHERE (b.IsDemo = 0 OR b.IsDemo IS NULL) AND (b.IsPrivate = 0 OR b.IsPrivate IS NULL) ORDER BY b.Name ", [], { datasource = "payfrit" } ); // Convert query -> array of structs with distance rows = []; for (i = 1; i <= q.recordCount; i++) { row = { "BusinessID": q.ID[i], "Name": q.Name[i], "City": isNull(q.City[i]) ? "" : q.City[i], "Line1": isNull(q.Line1[i]) ? "" : q.Line1[i] }; // Calculate distance if we have both user location and business location bizLat = isNull(q.Latitude[i]) ? 0 : val(q.Latitude[i]); bizLng = isNull(q.Longitude[i]) ? 0 : val(q.Longitude[i]); if (hasUserLocation AND bizLat != 0 AND bizLng != 0) { row["DistanceMiles"] = haversineDistance(userLat, userLng, bizLat, bizLng); } else { row["DistanceMiles"] = 99999; // No location = sort to end } arrayAppend(rows, row); } // Sort by distance if user location provided if (hasUserLocation) { arraySort(rows, function(a, b) { return compare(a.DistanceMiles, b.DistanceMiles); }); } // Limit to 20 nearest restaurants if (arrayLen(rows) > 20) { rows = arraySlice(rows, 1, 20); } // Provide BOTH keys to satisfy any Flutter casing expectation writeOutput(serializeJSON({ "OK": true, "ERROR": "", "VERSION": "businesses_list_v5", "BUSINESSES": rows, "Businesses": rows })); abort; } catch (any e) { apiAbort({ "OK": false, "ERROR": "server_error", "DETAIL": e.message }); }