import "dart:convert"; import "package:http/http.dart" as http; import "../models/cart.dart"; import "../models/menu_item.dart"; import "../models/order_history.dart"; import "../models/restaurant.dart"; import "../models/service_point.dart"; import "../models/user_profile.dart"; import "auth_storage.dart"; class ApiRawResponse { final int statusCode; final String rawBody; final Map? json; const ApiRawResponse({ required this.statusCode, required this.rawBody, required this.json, }); } class LoginResponse { final int userId; final String userFirstName; final String token; const LoginResponse({ required this.userId, required this.userFirstName, required this.token, }); factory LoginResponse.fromJson(Map json) { return LoginResponse( userId: (json["UserID"] as num).toInt(), userFirstName: (json["UserFirstName"] as String?) ?? "", token: (json["Token"] as String?) ?? "", ); } } class Api { static String? _userToken; // MVP hardcode static int _mvpBusinessId = 17; static void setAuthToken(String? token) => _userToken = token; static void clearAuthToken() => _userToken = null; static void setBusinessId(int? businessId) { if (businessId != null && businessId > 0) { _mvpBusinessId = businessId; } } static void clearCookies() { // no-op } static String get baseUrl { const v = String.fromEnvironment("PAYFRIT_API_BASE_URL"); if (v.isEmpty) { return "https://biz.payfrit.com/api"; } return v; } static Uri _u(String path) { final b = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl; final p = path.startsWith("/") ? path : "/$path"; return Uri.parse("$b$p"); } static Map _headers({required bool json, int? businessIdOverride}) { final h = {}; if (json) h["Content-Type"] = "application/json; charset=utf-8"; final tok = _userToken; if (tok != null && tok.isNotEmpty) { h["X-User-Token"] = tok; } final int bid = (businessIdOverride != null && businessIdOverride > 0) ? businessIdOverride : _mvpBusinessId; h["X-Business-ID"] = bid.toString(); return h; } static Map? _tryDecodeJsonMap(String body) { try { final decoded = jsonDecode(body); if (decoded is Map) return decoded; } catch (_) { // If JSON parsing fails, try to extract JSON from the body // (handles cases where debug output is appended after JSON) final jsonStart = body.indexOf('{'); final jsonEnd = body.lastIndexOf('}'); if (jsonStart >= 0 && jsonEnd > jsonStart) { try { final jsonPart = body.substring(jsonStart, jsonEnd + 1); final decoded = jsonDecode(jsonPart); if (decoded is Map) return decoded; } catch (_) {} } } return null; } static Future _getRaw(String path, {int? businessIdOverride}) async { final url = _u(path); final resp = await http.get(url, headers: _headers(json: false, businessIdOverride: businessIdOverride)); final body = resp.body; final j = _tryDecodeJsonMap(body); return ApiRawResponse(statusCode: resp.statusCode, rawBody: body, json: j); } static Future _postRaw( String path, Map payload, { int? businessIdOverride, }) async { final url = _u(path); final resp = await http.post( url, headers: _headers(json: true, businessIdOverride: businessIdOverride), body: jsonEncode(payload), ); final body = resp.body; final j = _tryDecodeJsonMap(body); return ApiRawResponse(statusCode: resp.statusCode, rawBody: body, json: j); } static bool _ok(Map j) => j["OK"] == true || j["ok"] == true; static String _err(Map j) => (j["ERROR"] ?? j["error"] ?? "").toString(); static List? _pickArray(Map j, List keys) { for (final k in keys) { final v = j[k]; if (v is List) return v; } return null; } static Map _requireJson(ApiRawResponse raw, String label) { final j = raw.json; if (j == null) { throw StateError("$label request failed: ${raw.statusCode}\nNon-JSON response."); } return j; } // ------------------------- // Authentication // ------------------------- static Future login({ required String username, required String password, }) async { final raw = await _postRaw( "/auth/login.cfm", { "username": username, "password": password, }, ); final j = _requireJson(raw, "Login"); if (!_ok(j)) { final err = _err(j); if (err == "bad_credentials") { throw StateError("Invalid email/phone or password"); } else if (err == "missing_fields") { throw StateError("Username and password are required"); } else { throw StateError("Login failed: $err"); } } final response = LoginResponse.fromJson(j); // Store token for future requests setAuthToken(response.token); return response; } static Future logout() async { setAuthToken(null); clearCookies(); await AuthStorage.clearAuth(); } // ------------------------- // Businesses (legacy model name: Restaurant) // ------------------------- static Future listRestaurantsRaw() async { return _getRaw("/businesses/list.cfm", businessIdOverride: _mvpBusinessId); } static Future> listRestaurants() async { final raw = await listRestaurantsRaw(); final j = _requireJson(raw, "Businesses"); if (!_ok(j)) { throw StateError( "Businesses API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["Businesses", "BUSINESSES"]); if (arr == null) { throw StateError("Businesses JSON missing Businesses array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(Restaurant.fromJson(e)); } else if (e is Map) { out.add(Restaurant.fromJson(e.cast())); } } return out; } // ------------------------- // Service Points // ------------------------- static Future> listServicePoints({required int businessId}) async { // CRITICAL: endpoint is behaving like it reads JSON body, not query/header. final raw = await _postRaw( "/servicepoints/list.cfm", {"BusinessID": businessId}, businessIdOverride: businessId, ); final j = _requireJson(raw, "ServicePoints"); if (!_ok(j)) { throw StateError( "ServicePoints API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["ServicePoints", "SERVICEPOINTS"]); if (arr == null) { throw StateError("ServicePoints JSON missing ServicePoints array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(ServicePoint.fromJson(e)); } else if (e is Map) { out.add(ServicePoint.fromJson(e.cast())); } } return out; } // ------------------------- // Menu Items // ------------------------- static Future> listMenuItems({required int businessId}) async { final raw = await _postRaw( "/menu/items.cfm", {"BusinessID": businessId}, businessIdOverride: businessId, ); final j = _requireJson(raw, "MenuItems"); if (!_ok(j)) { throw StateError( "MenuItems API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["Items", "ITEMS"]); if (arr == null) { throw StateError("MenuItems JSON missing Items array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(MenuItem.fromJson(e)); } else if (e is Map) { out.add(MenuItem.fromJson(e.cast())); } } return out; } // ------------------------- // Cart & Orders // ------------------------- static Future getOrCreateCart({ required int userId, required int businessId, required int servicePointId, required int orderTypeId, }) async { final raw = await _postRaw( "/orders/getOrCreateCart.cfm", { "OrderUserID": userId, "BusinessID": businessId, "OrderServicePointID": servicePointId, "OrderTypeID": orderTypeId, }, businessIdOverride: businessId, ); final j = _requireJson(raw, "GetOrCreateCart"); if (!_ok(j)) { throw StateError( "GetOrCreateCart API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } return Cart.fromJson(j); } static Future getCart({required int orderId}) async { final raw = await _postRaw( "/orders/getCart.cfm", {"OrderID": orderId}, ); final j = _requireJson(raw, "GetCart"); if (!_ok(j)) { throw StateError( "GetCart API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } return Cart.fromJson(j); } static Future setLineItem({ required int orderId, required int parentOrderLineItemId, required int itemId, required bool isSelected, int quantity = 1, String? remark, }) async { final raw = await _postRaw( "/orders/setLineItem.cfm", { "OrderID": orderId, "ParentOrderLineItemID": parentOrderLineItemId, "ItemID": itemId, "IsSelected": isSelected, "Quantity": quantity, if (remark != null && remark.isNotEmpty) "Remark": remark, }, ); final j = _requireJson(raw, "SetLineItem"); print('[API] setLineItem response: OK=${j["OK"]}, ERROR=${_err(j)}, orderId=$orderId, itemId=$itemId, parentLI=$parentOrderLineItemId'); if (!_ok(j)) { throw StateError( "SetLineItem failed: ${_err(j)} - ${(j["MESSAGE"] ?? "").toString()}", ); } return Cart.fromJson(j); } /// Set the order type (delivery/takeaway) on an existing cart static Future setOrderType({ required int orderId, required int orderTypeId, int? addressId, }) async { final raw = await _postRaw( "/orders/setOrderType.cfm", { "OrderID": orderId, "OrderTypeID": orderTypeId, if (addressId != null) "AddressID": addressId, }, ); final j = _requireJson(raw, "SetOrderType"); if (!_ok(j)) { throw StateError( "SetOrderType failed: ${_err(j)} - ${(j["MESSAGE"] ?? "").toString()}", ); } return Cart.fromJson(j); } static Future submitOrder({required int orderId}) async { final raw = await _postRaw( "/orders/submit.cfm", {"OrderID": orderId}, ); final j = _requireJson(raw, "SubmitOrder"); if (!_ok(j)) { throw StateError( "SubmitOrder API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } } static Future> checkOrderStatus({ required int orderId, required int lastKnownStatusId, }) async { final raw = await _postRaw( "/orders/checkStatusUpdate.cfm", { "OrderID": orderId, "LastKnownStatusID": lastKnownStatusId, }, ); final j = _requireJson(raw, "CheckOrderStatus"); if (!_ok(j)) { throw StateError( "CheckOrderStatus API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } return j; } // ------------------------- // Beacons // ------------------------- static Future> listAllBeacons() async { final raw = await _getRaw("/beacons/list_all.cfm"); final j = _requireJson(raw, "ListAllBeacons"); if (!_ok(j)) { throw StateError( "ListAllBeacons API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["items", "ITEMS"]); if (arr == null) return {}; final Map uuidToBeaconId = {}; for (final e in arr) { if (e is! Map) continue; final item = e is Map ? e : e.cast(); final uuid = (item["BeaconUUID"] ?? item["BEACONUUID"] ?? "").toString().trim(); final beaconId = item["BeaconID"] ?? item["BEACONID"]; if (uuid.isNotEmpty && beaconId is num) { uuidToBeaconId[uuid] = beaconId.toInt(); } } return uuidToBeaconId; } static Future getBusinessFromBeacon({ required int beaconId, }) async { final raw = await _postRaw( "/beacons/getBusinessFromBeacon.cfm", {"BeaconID": beaconId}, ); final j = _requireJson(raw, "GetBusinessFromBeacon"); if (!_ok(j)) { throw StateError( "GetBusinessFromBeacon API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } final beacon = j["BEACON"] as Map? ?? {}; final business = j["BUSINESS"] as Map? ?? {}; final servicePoint = j["SERVICEPOINT"] as Map? ?? {}; return BeaconBusinessMapping( beaconId: _parseInt(beacon["BeaconID"]) ?? 0, beaconName: (beacon["BeaconName"] as String?) ?? "", businessId: _parseInt(business["BusinessID"]) ?? 0, businessName: (business["BusinessName"] as String?) ?? "", servicePointId: _parseInt(servicePoint["ServicePointID"]) ?? 0, servicePointName: (servicePoint["ServicePointName"] as String?) ?? "", ); } static int? _parseInt(dynamic value) { if (value == null) return null; if (value is int) return value; if (value is num) return value.toInt(); if (value is String) { if (value.isEmpty) return null; return int.tryParse(value); } return null; } /// Check if user has pending orders at a business (for pickup detection) static Future> getPendingOrdersForUser({ required int userId, required int businessId, }) async { final raw = await _getRaw( "/orders/getPendingForUser.cfm?UserID=$userId&BusinessID=$businessId", ); final j = _requireJson(raw, "GetPendingOrdersForUser"); if (!_ok(j)) { return []; // Return empty list on error } final arr = _pickArray(j, const ["ORDERS", "orders"]); if (arr == null) return []; return arr.map((e) { final item = e is Map ? e : (e as Map).cast(); return PendingOrder.fromJson(item); }).toList(); } /// Get list of states/provinces for address forms static Future> getStates() async { final raw = await _getRaw("/addresses/states.cfm"); final j = _requireJson(raw, "GetStates"); if (!_ok(j)) { return []; } final arr = _pickArray(j, const ["STATES", "states"]); if (arr == null) return []; return arr.map((e) { final item = e is Map ? e : (e as Map).cast(); return StateInfo.fromJson(item); }).toList(); } /// Get user's delivery addresses static Future> getDeliveryAddresses() async { final raw = await _getRaw("/addresses/list.cfm"); final j = _requireJson(raw, "GetDeliveryAddresses"); if (!_ok(j)) { return []; } final arr = _pickArray(j, const ["ADDRESSES", "addresses"]); if (arr == null) return []; return arr.map((e) { final item = e is Map ? e : (e as Map).cast(); return DeliveryAddress.fromJson(item); }).toList(); } /// Add a new delivery address static Future addDeliveryAddress({ required String line1, required String city, required int stateId, required String zipCode, String? line2, String? label, bool setAsDefault = false, }) async { final raw = await _postRaw("/addresses/add.cfm", { "Line1": line1, "City": city, "StateID": stateId, "ZIPCode": zipCode, if (line2 != null) "Line2": line2, if (label != null) "Label": label, "SetAsDefault": setAsDefault, }); final j = _requireJson(raw, "AddDeliveryAddress"); if (!_ok(j)) { throw StateError("AddDeliveryAddress failed: ${_err(j)}"); } final addressData = j["ADDRESS"] as Map? ?? {}; return DeliveryAddress.fromJson(addressData); } /// Delete a delivery address static Future deleteDeliveryAddress(int addressId) async { final raw = await _postRaw("/addresses/delete.cfm", { "AddressID": addressId, }); final j = _requireJson(raw, "DeleteDeliveryAddress"); if (!_ok(j)) { throw StateError("DeleteDeliveryAddress failed: ${_err(j)}"); } } /// Set an address as the default delivery address static Future setDefaultDeliveryAddress(int addressId) async { final raw = await _postRaw("/addresses/setDefault.cfm", { "AddressID": addressId, }); final j = _requireJson(raw, "SetDefaultDeliveryAddress"); if (!_ok(j)) { throw StateError("SetDefaultDeliveryAddress failed: ${_err(j)}"); } } /// Get user's avatar URL static Future getAvatar() async { print('[API] getAvatar: token=${_userToken != null ? "${_userToken!.substring(0, 8)}..." : "NULL"}'); final raw = await _getRaw("/auth/avatar.cfm"); print('[API] getAvatar response: ${raw.rawBody}'); final j = _requireJson(raw, "GetAvatar"); if (!_ok(j)) { return const AvatarInfo(hasAvatar: false, avatarUrl: null); } return AvatarInfo( hasAvatar: j["HAS_AVATAR"] == true, avatarUrl: j["AVATAR_URL"] as String?, ); } /// Debug: Check token status with server static Future debugCheckToken() async { try { final raw = await _getRaw("/debug/checkToken.cfm"); print('[API] debugCheckToken response: ${raw.rawBody}'); } catch (e) { print('[API] debugCheckToken error: $e'); } } /// Upload user avatar image static Future uploadAvatar(String filePath) async { // First check token status await debugCheckToken(); final uri = _u("/auth/avatar.cfm"); final request = http.MultipartRequest("POST", uri); // Add auth headers final tok = _userToken; print('[API] uploadAvatar: token=${tok != null ? "${tok.substring(0, 8)}..." : "NULL"}'); if (tok != null && tok.isNotEmpty) { request.headers["X-User-Token"] = tok; } request.headers["X-Business-ID"] = _mvpBusinessId.toString(); // Add the file request.files.add(await http.MultipartFile.fromPath("avatar", filePath)); final streamedResponse = await request.send(); final response = await http.Response.fromStream(streamedResponse); print('[API] uploadAvatar response: ${response.statusCode} - ${response.body}'); final j = _tryDecodeJsonMap(response.body); if (j == null) { throw StateError("UploadAvatar: Invalid JSON response"); } if (!_ok(j)) { throw StateError("UploadAvatar failed: ${_err(j)}"); } return j["AVATAR_URL"] as String? ?? ""; } /// Get order history for current user static Future getOrderHistory({int limit = 20, int offset = 0}) async { print('[API] getOrderHistory: token=${_userToken != null ? "${_userToken!.substring(0, 8)}..." : "NULL"}'); final raw = await _getRaw("/orders/history.cfm?limit=$limit&offset=$offset"); print('[API] getOrderHistory response: ${raw.rawBody.substring(0, raw.rawBody.length > 200 ? 200 : raw.rawBody.length)}'); final j = _requireJson(raw, "GetOrderHistory"); if (!_ok(j)) { throw StateError("GetOrderHistory failed: ${_err(j)}"); } final ordersJson = j["ORDERS"] as List? ?? []; final orders = ordersJson .map((e) => OrderHistoryItem.fromJson(e as Map)) .toList(); return OrderHistoryResponse( orders: orders, totalCount: (j["TOTAL_COUNT"] as num?)?.toInt() ?? orders.length, ); } /// Get user profile static Future getProfile() async { final raw = await _getRaw("/auth/profile.cfm"); final j = _requireJson(raw, "GetProfile"); if (!_ok(j)) { throw StateError("GetProfile failed: ${_err(j)}"); } final userData = j["USER"] as Map? ?? {}; return UserProfile.fromJson(userData); } /// Update user profile static Future updateProfile({String? firstName, String? lastName}) async { final body = {}; if (firstName != null) body["firstName"] = firstName; if (lastName != null) body["lastName"] = lastName; final raw = await _postRaw("/auth/profile.cfm", body); final j = _requireJson(raw, "UpdateProfile"); if (!_ok(j)) { throw StateError("UpdateProfile failed: ${_err(j)}"); } final userData = j["USER"] as Map? ?? {}; return UserProfile.fromJson(userData); } } class OrderHistoryResponse { final List orders; final int totalCount; const OrderHistoryResponse({ required this.orders, required this.totalCount, }); } class BeaconBusinessMapping { final int beaconId; final String beaconName; final int businessId; final String businessName; final int servicePointId; final String servicePointName; const BeaconBusinessMapping({ required this.beaconId, required this.beaconName, required this.businessId, required this.businessName, required this.servicePointId, required this.servicePointName, }); } class PendingOrder { final int orderId; final String orderUuid; final int orderTypeId; final String orderTypeName; final int statusId; final String statusName; final String submittedOn; final int servicePointId; final String servicePointName; final String businessName; final double subtotal; const PendingOrder({ required this.orderId, required this.orderUuid, required this.orderTypeId, required this.orderTypeName, required this.statusId, required this.statusName, required this.submittedOn, required this.servicePointId, required this.servicePointName, required this.businessName, required this.subtotal, }); factory PendingOrder.fromJson(Map json) { return PendingOrder( orderId: (json["OrderID"] ?? json["ORDERID"] ?? 0) as int, orderUuid: (json["OrderUUID"] ?? json["ORDERUUID"] ?? "") as String, orderTypeId: (json["OrderTypeID"] ?? json["ORDERTYPEID"] ?? 0) as int, orderTypeName: (json["OrderTypeName"] ?? json["ORDERTYPENAME"] ?? "") as String, statusId: (json["OrderStatusID"] ?? json["ORDERSTATUSID"] ?? 0) as int, statusName: (json["StatusName"] ?? json["STATUSNAME"] ?? "") as String, submittedOn: (json["SubmittedOn"] ?? json["SUBMITTEDON"] ?? "") as String, servicePointId: (json["ServicePointID"] ?? json["SERVICEPOINTID"] ?? 0) as int, servicePointName: (json["ServicePointName"] ?? json["SERVICEPOINTNAME"] ?? "") as String, businessName: (json["BusinessName"] ?? json["BUSINESSNAME"] ?? "") as String, subtotal: ((json["Subtotal"] ?? json["SUBTOTAL"] ?? 0) as num).toDouble(), ); } bool get isReady => statusId == 3; bool get isPreparing => statusId == 2; bool get isSubmitted => statusId == 1; } class DeliveryAddress { final int addressId; final String label; final bool isDefault; final String line1; final String line2; final String city; final int stateId; final String stateAbbr; final String stateName; final String zipCode; final String displayText; const DeliveryAddress({ required this.addressId, required this.label, required this.isDefault, required this.line1, required this.line2, required this.city, required this.stateId, required this.stateAbbr, required this.stateName, required this.zipCode, required this.displayText, }); factory DeliveryAddress.fromJson(Map json) { return DeliveryAddress( addressId: (json["AddressID"] ?? json["ADDRESSID"] ?? 0) as int, label: (json["Label"] ?? json["LABEL"] ?? "Address") as String, isDefault: (json["IsDefault"] ?? json["ISDEFAULT"] ?? false) == true, line1: (json["Line1"] ?? json["LINE1"] ?? "") as String, line2: (json["Line2"] ?? json["LINE2"] ?? "") as String, city: (json["City"] ?? json["CITY"] ?? "") as String, stateId: (json["StateID"] ?? json["STATEID"] ?? 0) as int, stateAbbr: (json["StateAbbr"] ?? json["STATEABBR"] ?? "") as String, stateName: (json["StateName"] ?? json["STATENAME"] ?? "") as String, zipCode: (json["ZIPCode"] ?? json["ZIPCODE"] ?? "") as String, displayText: (json["DisplayText"] ?? json["DISPLAYTEXT"] ?? "") as String, ); } } class AvatarInfo { final bool hasAvatar; final String? avatarUrl; const AvatarInfo({ required this.hasAvatar, required this.avatarUrl, }); } class StateInfo { final int stateId; final String abbr; final String name; const StateInfo({ required this.stateId, required this.abbr, required this.name, }); factory StateInfo.fromJson(Map json) { return StateInfo( stateId: (json["StateID"] ?? json["STATEID"] ?? 0) as int, abbr: (json["Abbr"] ?? json["ABBR"] ?? "") as String, name: (json["Name"] ?? json["NAME"] ?? "") as String, ); } }