payfrit-app/lib/services/api.dart
2025-12-28 12:28:45 -08:00

176 lines
4.8 KiB
Dart

import "dart:convert";
import "package:http/http.dart" as http;
import "../models/restaurant.dart";
import "../models/service_point.dart";
class Api {
static String get baseUrl {
const v = String.fromEnvironment("API_BASE_URL");
if (v.isEmpty) {
throw StateError(
"API_BASE_URL is not set. Example (Android emulator): "
"--dart-define=API_BASE_URL=http://10.0.2.2:8888/biz.payfrit.com/api",
);
}
return v;
}
static Uri _u(String path) {
final normalizedBase =
baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
final normalizedPath = path.startsWith("/") ? path : "/$path";
return Uri.parse("$normalizedBase$normalizedPath");
}
static Future<_ApiResponse> _getJson(String path) async {
final url = _u(path);
final resp = await http.get(url);
final body = resp.body;
Map<String, dynamic>? json;
try {
final decoded = jsonDecode(body);
if (decoded is Map<String, dynamic>) {
json = decoded;
}
} catch (_) {
// leave json null
}
// ignore: avoid_print
print("API GET => $url");
// ignore: avoid_print
print("STATUS => ${resp.statusCode}");
// ignore: avoid_print
print("BODY => ${body.length > 1200 ? body.substring(0, 1200) + '' : body}");
return _ApiResponse(
statusCode: resp.statusCode,
rawBody: body,
json: json,
);
}
static Future<_ApiResponse> _postJson(String path, Map<String, dynamic> payload) async {
final url = _u(path);
final resp = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: jsonEncode(payload),
);
final body = resp.body;
Map<String, dynamic>? json;
try {
final decoded = jsonDecode(body);
if (decoded is Map<String, dynamic>) {
json = decoded;
}
} catch (_) {
// leave json null
}
// ignore: avoid_print
print("API POST => $url");
// ignore: avoid_print
print("BODY IN => ${jsonEncode(payload)}");
// ignore: avoid_print
print("STATUS => ${resp.statusCode}");
// ignore: avoid_print
print("BODY OUT => ${body.length > 1200 ? body.substring(0, 1200) + '' : body}");
return _ApiResponse(
statusCode: resp.statusCode,
rawBody: body,
json: json,
);
}
static void _throwIfApiError(Map<String, dynamic> data, String context) {
if (data.containsKey("OK") && data["OK"] is bool && (data["OK"] as bool) == false) {
final err = data["ERROR"];
final detail = data["DETAIL"];
final msg = data["MESSAGE"];
throw StateError(
"$context API returned OK=false"
"${err != null ? "\nERROR: $err" : ""}"
"${msg != null ? "\nMESSAGE: $msg" : ""}"
"${detail != null ? "\nDETAIL: $detail" : ""}",
);
}
}
static Future<_ApiResponse> listRestaurantsRaw() async {
return _getJson("/businesses/list.cfm");
}
static Future<List<Restaurant>> listRestaurants() async {
final r = await listRestaurantsRaw();
if (r.statusCode != 200) {
throw StateError("Restaurants request failed: ${r.statusCode}\n${r.rawBody}");
}
final data = r.json;
if (data == null) {
throw StateError("Restaurants response was not JSON.\n${r.rawBody}");
}
_throwIfApiError(data, "Restaurants");
final rows = data["Businesses"];
if (rows is! List) {
throw StateError(
"Restaurants JSON missing Businesses array.\nKeys present: ${data.keys.toList()}\nRaw: ${r.rawBody}",
);
}
return rows
.whereType<Map<String, dynamic>>()
.map((e) => Restaurant.fromJson(e))
.toList();
}
static Future<_ApiResponse> listServicePointsRaw({required int businessId}) async {
return _postJson("/servicepoints/list.cfm", {"BusinessID": businessId});
}
static Future<List<ServicePoint>> listServicePoints({required int businessId}) async {
final r = await listServicePointsRaw(businessId: businessId);
if (r.statusCode != 200) {
throw StateError("ServicePoints request failed: ${r.statusCode}\n${r.rawBody}");
}
final data = r.json;
if (data == null) {
throw StateError("ServicePoints response was not JSON.\n${r.rawBody}");
}
_throwIfApiError(data, "ServicePoints");
// STRICT: hump case
final rows = data["ServicePoints"];
if (rows is! List) {
throw StateError(
"ServicePoints JSON missing ServicePoints array.\nKeys present: ${data.keys.toList()}\nRaw: ${r.rawBody}",
);
}
return rows
.whereType<Map<String, dynamic>>()
.map((e) => ServicePoint.fromJson(e))
.toList();
}
}
class _ApiResponse {
final int statusCode;
final String rawBody;
final Map<String, dynamic>? json;
const _ApiResponse({
required this.statusCode,
required this.rawBody,
required this.json,
});
}