/** * Environment Configuration * * Controls dev vs production behavior. * This file should have DIFFERENT values on dev vs biz servers. * * dev.payfrit.com: isDevEnvironment = true * biz.payfrit.com: isDevEnvironment = false */ // ============================================ // ENVIRONMENT FLAG - CHANGE PER SERVER // ============================================ isDevEnvironment = true; // Set to FALSE on biz.payfrit.com // ============================================ // ERROR HANDLING // ============================================ // Dev: Show full stack traces // Prod: Show generic "server error" message application.showDetailedErrors = isDevEnvironment; // ============================================ // DEBUG LOGGING // ============================================ // Dev: Log all API requests/responses // Prod: Minimal logging application.debugLogging = isDevEnvironment; application.logDirectory = expandPath("/api/logs/"); // ============================================ // RATE LIMITING // ============================================ // Dev: No rate limits // Prod: Enforce rate limits application.enableRateLimiting = !isDevEnvironment; application.rateLimitPerMinute = 60; // requests per minute per IP // ============================================ // EMAIL HANDLING // ============================================ // Dev: Send all emails to catch-all address // Prod: Send to real recipients application.emailCatchAll = isDevEnvironment ? "dev-emails@payfrit.com" : ""; application.emailEnabled = !isDevEnvironment ? true : false; // Disable emails on dev entirely // ============================================ // MAGIC OTP (for testing) // ============================================ // Dev: Allow magic phone number to bypass OTP // Prod: Disabled application.MAGIC_OTP_ENABLED = isDevEnvironment; application.MAGIC_OTP_CODE = "123456"; application.MAGIC_PHONE_NUMBERS = ["5555555555", "0000000000"]; // ============================================ // STRIPE MODE // ============================================ // Already handled in stripe.cfm, but good to have reference application.stripeTestMode = isDevEnvironment; // ============================================ // API RESPONSE EXTRAS // ============================================ // Dev: Include debug info in API responses (timing, queries, etc) // Prod: Minimal responses application.includeDebugInResponse = isDevEnvironment; // ============================================ // SESSION/TOKEN SETTINGS // ============================================ // Dev: Longer token expiry for easier testing // Prod: Normal expiry application.tokenExpiryHours = isDevEnvironment ? 720 : 24; // 30 days vs 1 day // ============================================ // HELPER FUNCTIONS // ============================================ function isDev() { return structKeyExists(application, "isDevEnvironment") && application.isDevEnvironment; } function logDebug(message, data = {}) { if (!application.debugLogging) return; var logFile = application.logDirectory & "debug_" & dateFormat(now(), "yyyy-mm-dd") & ".log"; var logLine = "[" & timeFormat(now(), "HH:mm:ss") & "] " & message; if (!structIsEmpty(data)) { logLine &= " | " & serializeJSON(data); } try { if (!directoryExists(application.logDirectory)) { directoryCreate(application.logDirectory); } fileAppend(logFile, logLine & chr(10)); } catch (any e) { // Silent fail - don't break app if logging fails } } function apiError(message, detail = "", statusCode = 500) { var response = { "OK": false, "ERROR": "server_error", "MESSAGE": application.showDetailedErrors ? message : "An error occurred" }; if (application.showDetailedErrors && len(detail)) { response["DETAIL"] = detail; response["STACK"] = ""; } return response; } // ============================================ // PERFORMANCE PROFILING // ============================================ application.perfEnabled = true; // Toggle profiling on/off // Initialize perf buffer (once per application start) if (!structKeyExists(application, "perfBuffer")) { application.perfBuffer = []; } /** * Drop-in replacement for queryExecute that tracks query count and time. * Usage: queryTimed("SELECT ...", [...], { datasource: "payfrit" }) */ function queryTimed(required string sql, any params = [], struct options = {}) { var t = getTickCount(); var result = queryExecute(arguments.sql, arguments.params, arguments.options); var elapsed = getTickCount() - t; request._perf_queryCount++; request._perf_queryTimeMs += elapsed; return result; } /** * Flush the in-memory perf buffer to the api_perf_log MySQL table. * Called automatically when buffer reaches 100 entries. */ function flushPerfBuffer() { var batch = []; lock name="payfrit_perfBuffer" timeout="2" type="exclusive" { batch = duplicate(application.perfBuffer); application.perfBuffer = []; } if (arrayLen(batch) == 0) return; try { var sql = "INSERT INTO api_perf_log (Endpoint, TotalMs, DbMs, AppMs, QueryCount, ResponseBytes, BusinessID, UserID, LoggedAt) VALUES "; var rows = []; for (var m in batch) { arrayAppend(rows, "('" & replace(m.endpoint, "'", "''", "all") & "'," & m.totalMs & "," & m.dbMs & "," & m.appMs & "," & m.queryCount & "," & m.responseBytes & "," & m.businessId & "," & m.userId & ",'" & m.loggedAt & "')" ); } sql &= arrayToList(rows, ","); queryExecute(sql, {}, { datasource: "payfrit" }); // Cleanup: delete rows older than 30 days (run occasionally) if (randRange(1, 100) == 1) { queryExecute( "DELETE FROM api_perf_log WHERE LoggedAt < DATE_SUB(NOW(), INTERVAL 30 DAY)", {}, { datasource: "payfrit" } ); } } catch (any e) { // Silent fail - never break the app for profiling } } /** * Log performance metrics for the current request. * Call this at the end of any instrumented endpoint, before the final writeOutput/abort. */ function logPerf(numeric responseBytes = 0) { if (!structKeyExists(application, "perfEnabled") || !application.perfEnabled) return; var totalMs = getTickCount() - request._perf_start; var dbMs = request._perf_queryTimeMs; var metric = { endpoint: structKeyExists(request, "_api_path") ? request._api_path : "unknown", totalMs: totalMs, dbMs: dbMs, appMs: totalMs - dbMs, queryCount: request._perf_queryCount, responseBytes: arguments.responseBytes, businessId: structKeyExists(request, "BusinessID") ? val(request.BusinessID) : 0, userId: structKeyExists(request, "UserID") ? val(request.UserID) : 0, loggedAt: dateTimeFormat(now(), "yyyy-mm-dd HH:nn:ss") }; lock name="payfrit_perfBuffer" timeout="1" type="exclusive" { arrayAppend(application.perfBuffer, metric); if (arrayLen(application.perfBuffer) >= 100) { thread name="perfFlush_#createUUID()#" { flushPerfBuffer(); } } } } // ============================================ // APPLICATION-SCOPE CACHE // ============================================ if (!structKeyExists(application, "cache")) { application.cache = {}; } /** * Get a cached value. Returns null if not found or expired. * Usage: cached = cacheGet("menu_123", 120); * if (!isNull(cached)) { use it } else { query and cachePut } */ function appCacheGet(required string key, numeric ttlSeconds = 300) { if (structKeyExists(application.cache, arguments.key)) { var entry = application.cache[arguments.key]; if (dateDiff("s", entry.cachedAt, now()) < arguments.ttlSeconds) { return entry.data; } structDelete(application.cache, arguments.key); } return javaCast("null", ""); } /** * Store a value in cache. */ function appCachePut(required string key, required any data) { application.cache[arguments.key] = { data: arguments.data, cachedAt: now() }; } /** * Invalidate all cache keys starting with the given prefix. */ function appCacheInvalidate(required string keyPrefix) { var keys = structKeyArray(application.cache); for (var k in keys) { if (findNoCase(arguments.keyPrefix, k) == 1) { structDelete(application.cache, k); } } } // Store in application scope application.isDevEnvironment = isDevEnvironment; application.isDev = isDev; application.logDebug = logDebug; application.apiError = apiError;