/** * 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; if (!structKeyExists(application, "perfBuffer")) { application.perfBuffer = []; } /** * Drop-in replacement for queryExecute() that tracks query count and time. * Opt-in: use in endpoints where you want accurate DB time breakdown. */ function queryTimed(required string sql, any params = [], struct options = {}) { if (!structKeyExists(request, "_perf_queryCount")) { request._perf_queryCount = 0; request._perf_queryTimeMs = 0; } 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 ApiPerfLogs MySQL table. * Thread-safe: duplicates buffer under lock, then inserts outside lock. */ function flushPerfBuffer() { // Auto-create table if it doesn't exist if (!structKeyExists(application, "_perfTableChecked")) { try { queryExecute(" CREATE TABLE IF NOT EXISTS ApiPerfLogs ( ID INT AUTO_INCREMENT PRIMARY KEY, Endpoint VARCHAR(255) NOT NULL, TotalMs INT NOT NULL DEFAULT 0, DbMs INT NOT NULL DEFAULT 0, AppMs INT NOT NULL DEFAULT 0, QueryCount INT NOT NULL DEFAULT 0, ResponseBytes INT NOT NULL DEFAULT 0, BusinessID INT NOT NULL DEFAULT 0, UserID INT NOT NULL DEFAULT 0, LoggedAt DATETIME NOT NULL, INDEX idx_loggedat (LoggedAt), INDEX idx_endpoint (Endpoint) ) ENGINE=InnoDB ", {}, { datasource: "payfrit" }); application._perfTableChecked = true; } catch (any e) {} } var batch = []; lock name="payfrit_perfBuffer" timeout="2" type="exclusive" { if (structKeyExists(application, "perfBuffer")) { batch = duplicate(application.perfBuffer); application.perfBuffer = []; } } if (arrayLen(batch) == 0) return; try { var sql = "INSERT INTO ApiPerfLogs (Endpoint, TotalMs, DbMs, AppMs, QueryCount, ResponseBytes, BusinessID, UserID, LoggedAt) VALUES "; var rows = []; for (var m in batch) { arrayAppend(rows, "('" & replace(m.endpoint, "'", "''", "all") & "'," & val(m.totalMs) & "," & val(m.dbMs) & "," & val(m.appMs) & "," & val(m.queryCount) & "," & val(m.responseBytes) & "," & val(m.businessId) & "," & val(m.userId) & ",'" & m.loggedAt & "')" ); } sql &= arrayToList(rows, ","); queryExecute(sql, {}, { datasource: "payfrit" }); // Cleanup old data (1% chance per flush) if (randRange(1, 100) == 1) { queryExecute( "DELETE FROM ApiPerfLogs 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. * Called automatically from apiAbort(). */ function logPerf(numeric responseBytes = 0) { if (!structKeyExists(application, "perfEnabled") || !application.perfEnabled) return; if (!structKeyExists(request, "_perf_start")) return; // Safety valve: don't let buffer grow unbounded if flush is failing if (structKeyExists(application, "perfBuffer") && arrayLen(application.perfBuffer) > 1000) return; var totalMs = getTickCount() - request._perf_start; var dbMs = structKeyExists(request, "_perf_queryTimeMs") ? request._perf_queryTimeMs : 0; var metric = { endpoint: structKeyExists(request, "_api_path") ? request._api_path : "unknown", totalMs: totalMs, dbMs: dbMs, appMs: totalMs - dbMs, queryCount: structKeyExists(request, "_perf_queryCount") ? request._perf_queryCount : 0, 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") }; var shouldFlush = false; lock name="payfrit_perfBuffer" timeout="1" type="exclusive" { arrayAppend(application.perfBuffer, metric); if (arrayLen(application.perfBuffer) >= 100) { shouldFlush = true; } } if (shouldFlush) { thread name="perfFlush_#createUUID()#" { flushPerfBuffer(); } } } // Store in application scope application.isDevEnvironment = isDevEnvironment; application.baseUrl = isDevEnvironment ? "https://dev.payfrit.com" : "https://biz.payfrit.com"; application.isDev = isDev; application.logDebug = logDebug; application.apiError = apiError; application.queryTimed = queryTimed; application.logPerf = logPerf; application.flushPerfBuffer = flushPerfBuffer;