<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">

<!---
  Payfrit API Application.cfm (updated 2026-01-22)

  FIX: Provide a DEFAULT datasource so endpoints can call queryExecute()
  without specifying { datasource="payfrit" } every time.

  Token-auth gate for /api endpoints.

  Public allowlist (NO auth):
    - /api/login.cfm
    - /api/logout.cfm
    - /api/auth/login.cfm
    - /api/auth/logout.cfm
    - /api/businesses/list.cfm
    - /api/servicepoints/list.cfm

  Authenticated requests should send:
    - Header: X-User-Token: <token from /api/auth/login.cfm>
    - Header: X-Business-ID: <selected BusinessID> (required for most endpoints)
--->

<!--- OPTION A: default datasource for the whole app (THIS fixes "missing datasource") --->
<cfapplication
  name="payfrit_api"
  sessionmanagement="true"
  clientmanagement="false"
  setclientcookies="false"
  datasource="payfrit"
  showdebugoutput="false"
>
<!--- Preserve struct key casing in JSON serialization (Flutter expects mixed-case keys) --->
<cfset getApplicationSettings().serialization.preserveCaseForStructKey = true>

<!--- Magic OTP bypass for App Store review (set to true to enable 123456 as universal OTP) --->
<cfset application.MAGIC_OTP_ENABLED = false>
<cfset application.MAGIC_OTP_CODE = "123456">

<!--- Initialize Twilio for SMS --->
<cfif NOT structKeyExists(application, "twilioObj")>
  <cftry>
    <cfset application.twilioObj = new library.cfc.twilio() />
    <cfcatch type="any">
      <!--- Twilio component not available, skip initialization --->
    </cfcatch>
  </cftry>
</cfif>

<!--- Stripe Configuration (loads from config/stripe.cfm) --->
<cfinclude template="config/stripe.cfm">

<!--- Environment Configuration (dev vs prod settings) --->
<cfinclude template="config/environment.cfm">

<cfscript>
function apiAbort(payload) {
  var json = serializeJSON(payload);
  try { logPerf(len(json)); } catch (any e) {}
  writeOutput(json);
  abort;
}

function headerValue(name) {
  // Use servlet request object to get headers (CGI scope doesn't expose custom HTTP headers in Lucee)
  try {
    req = getPageContext().getRequest();
    val = req.getHeader(arguments.name);
    if (!isNull(val)) return trim(val);
  } catch (any e) {
    // Fall back to CGI scope
    k = "HTTP_" & ucase(reReplace(arguments.name, "[^A-Za-z0-9]", "_", "all"));
    if (structKeyExists(cgi, k)) return trim(cgi[k]);
  }
  return "";
}

// Determine request path
request._api_scriptName = "";
if (structKeyExists(cgi, "SCRIPT_NAME")) {
  request._api_scriptName = cgi.SCRIPT_NAME;
} else if (structKeyExists(cgi, "PATH_INFO")) {
  request._api_scriptName = cgi.PATH_INFO;
}
request._api_path = lcase(request._api_scriptName);

// Performance tracking - start timer
request._perf_start = getTickCount();
request._perf_queryCount = 0;
request._perf_queryTimeMs = 0;

// Public allowlist
request._api_isPublic = false;
if (len(request._api_path)) {
  if (findNoCase("/api/login.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/logout.cfm", request._api_path)) request._api_isPublic = true;

  if (findNoCase("/api/auth/login.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/logout.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/avatar.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/sendOTP.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/verifyOTP.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/loginOTP.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/verifyLoginOTP.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/sendLoginOTP.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/verifyEmailOTP.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/auth/completeProfile.cfm", request._api_path)) request._api_isPublic = true;

  if (findNoCase("/api/businesses/list.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/businesses/get.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/businesses/getChildren.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/businesses/update.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/businesses/updateHours.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/businesses/updateTabs.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/servicepoints/get.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/servicepoints/save.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/servicepoints/delete.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/servicepoints/reassign_all.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/beacons/list.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/beacons/get.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/beacons/save.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/beacons/delete.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/beacons/reassign_all.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/beacons/lookup.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/assignments/list.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/assignments/save.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/assignments/delete.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/clearAllData.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/clearOrders.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/addresses/states.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/addresses/debug.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/addresses/list.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/addresses/add.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/addresses/delete.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/addresses/setDefault.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/debug/", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/dev/", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/orders/getOrCreateCart.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/orders/getCart.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/orders/setLineItem.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/orders/submit.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/orders/listForKDS.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/orders/updateStatus.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/orders/checkStatusUpdate.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/orders/getDetail.cfm", request._api_path)) request._api_isPublic = true;

  if (findNoCase("/api/users/search.cfm", request._api_path)) request._api_isPublic = true;

  if (findNoCase("/api/tasks/listPending.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/accept.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/listMine.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/complete.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/completeChat.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/getDetails.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/callserver", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/createChat.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/expirestalechats.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/listCategories.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/saveCategory.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/deleteCategory.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/seedCategories.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/listAllTypes.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/listTypes.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/saveType.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/deleteType.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/reorderTypes.cfm", request._api_path)) request._api_isPublic = true;

  // Chat endpoints
  if (findNoCase("/api/chat/getMessages.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/chat/sendMessage.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/chat/markRead.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/chat/getActiveChat.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/chat/closechat.cfm", request._api_path)) request._api_isPublic = true;

  // Token validation (for WebSocket server)
  if (findNoCase("/api/auth/validateToken.cfm", request._api_path)) request._api_isPublic = true;

  // Worker app endpoints
  if (findNoCase("/api/workers/myBusinesses.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/workers/tierStatus.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/workers/createAccount.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/workers/onboardingLink.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/workers/earlyUnlock.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/workers/ledger.cfm", request._api_path)) request._api_isPublic = true;

  // Portal endpoints
  if (findNoCase("/api/portal/stats.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/portal/myBusinesses.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/portal/team.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/portal/searchUser.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/portal/addTeamMember.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/portal/reassign_employees.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/businesses/setHiring.cfm", request._api_path)) request._api_isPublic = true;

  // Order history (auth handled in endpoint)
  if (findNoCase("/api/orders/history.cfm", request._api_path)) request._api_isPublic = true;

  // User profile (auth handled in endpoint)
  if (findNoCase("/api/auth/profile.cfm", request._api_path)) request._api_isPublic = true;

  // Menu builder endpoints
  if (findNoCase("/api/menu/getForBuilder.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/saveFromBuilder.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/updateStations.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/menus.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/uploadHeader.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/uploadItemPhoto.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/listCategories.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/menu/saveCategory.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/businesses/saveBrandColor.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/tasks/create.cfm", request._api_path)) request._api_isPublic = true;

  // Debug endpoints
  if (findNoCase("/api/debug/checkToken.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/debug/headers.cfm", request._api_path)) request._api_isPublic = true;

  // Admin endpoints (protected by localhost check in each file)
  if (findNoCase("/api/admin/resetTestData.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/debugTasks.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/testTaskType.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/testTaskInsert.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/debugBusinesses.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/setupStations.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/setupModifierTemplates.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/migrateModifierTemplates.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/deleteOrphanModifiers.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/fixShakeFlavors.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/eliminateCategories.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/cleanupCategories.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/deleteOrphans.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/switchBeacons.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/randomizePrices.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/debugTemplateLinks.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/fixBigDeansCategories.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/migrateToCategories.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/addItemCategoryColumn.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/checkBigDeans.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/updateBigDeans.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/debugBigDeansMenu.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/listTables.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/describeTable.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/setupBigDeansInfo.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/beaconStatus.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/updateBeaconMapping.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/setupBigDeansStations.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/copyDrinksToBigDeans.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/debugDrinkStructure.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/addDrinkModifiers.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/add_task_columns.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/add_service_category.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/createChatMessagesTable.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/addTaskSourceColumns.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/debugChatMessages.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/cleanupDuplicateEmployees.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/debugEmployees.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/debugUserByPhone.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/admin/setEmployeeActive.cfm", request._api_path)) request._api_isPublic = true;

  // Setup/Import endpoints
  if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/setup/analyzeMenu.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/setup/analyzeMenuImages.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/setup/analyzeMenuUrl.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/setup/uploadSavedPage.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/setup/saveWizard.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/setup/downloadImages.cfm", request._api_path)) request._api_isPublic = true;

  // Stations endpoints
  if (findNoCase("/api/stations/list.cfm", request._api_path)) request._api_isPublic = true;

  // Ratings endpoints (setup + token-based submission)
  if (findNoCase("/api/ratings/setup.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/ratings/submit.cfm", request._api_path)) request._api_isPublic = true;

  // App info endpoints (public, no auth needed)
  if (findNoCase("/api/app/about.cfm", request._api_path)) request._api_isPublic = true;

  // Grant endpoints (SP-SM)
  if (findNoCase("/api/grants/create.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/grants/list.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/grants/get.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/grants/update.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/grants/revoke.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/grants/accept.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/grants/decline.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/grants/searchBusiness.cfm", request._api_path)) request._api_isPublic = true;

  // Beacon sharding endpoints
  if (findNoCase("/api/beacon-sharding/get_shard_pool.cfm", request._api_path)) request._api_isPublic = true;

  // Stripe endpoints
  if (findNoCase("/api/stripe/onboard.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/stripe/status.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/stripe/createPaymentIntent.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/stripe/webhook.cfm", request._api_path)) request._api_isPublic = true;
  if (findNoCase("/api/stripe/createTipCheckout.cfm", request._api_path)) request._api_isPublic = true;
}

// Carry session values into request (if present)
if (!structKeyExists(request, "UserID") && structKeyExists(session, "UserID")) {
  request.UserID = Duplicate(session.UserID);
}
if (!structKeyExists(request, "BusinessID") && structKeyExists(session, "BusinessID")) {
  request.BusinessID = Duplicate(session.BusinessID);
}

// Token auth: X-User-Token -> request.UserID
request._api_userToken = headerValue("X-User-Token");
if (len(request._api_userToken)) {
  try {
    request._api_qTok = queryExecute(
      "SELECT UserID FROM UserTokens WHERE Token = ? LIMIT 1",
      [ { value = request._api_userToken, cfsqltype = "cf_sql_varchar" } ],
      { datasource = "payfrit" }
    );

    if (request._api_qTok.recordCount EQ 1) {
      request.UserID = request._api_qTok.UserID;
      session.UserID = request._api_qTok.UserID;
    }
  } catch (any e) {
    // ignore; treated as unauthenticated
  }
}

// Business header: X-Business-ID -> request.BusinessID
request._api_hdrBiz = headerValue("X-Business-ID");
if (len(request._api_hdrBiz) && isNumeric(request._api_hdrBiz)) {
  request.BusinessID = int(request._api_hdrBiz);
  session.BusinessID = request.BusinessID;
}


// Enforce auth (except public)
if (!request._api_isPublic) {
  if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
    apiAbort({
      "OK": false,
      "ERROR": "not_logged_in",
      "DEBUG_PATH": request._api_path,
      "DEBUG_IS_PUBLIC": request._api_isPublic,
      "DEBUG_SCRIPT_NAME": structKeyExists(cgi, "SCRIPT_NAME") ? cgi.SCRIPT_NAME : "N/A",
      "DEBUG_PATH_INFO": structKeyExists(cgi, "PATH_INFO") ? cgi.PATH_INFO : "N/A",
      "DEBUG_HAS_TOKEN": len(request._api_userToken) > 0,
      "DEBUG_TOKEN_PREFIX": len(request._api_userToken) > 8 ? left(request._api_userToken, 8) : request._api_userToken
    });
  }
  if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
    apiAbort({ "OK": false, "ERROR": "no_business_selected" });
  }
}
</cfscript>

