/** * Unified OTP: Send OTP to any phone number (login or signup) * * POST: { "phone": "5551234567" } * * Returns: { OK: true, UUID: "..." } * * Works for any phone number: * - Existing complete account: updates OTP, user will be logged in after verify * - Existing incomplete account: updates OTP, user continues to registration after verify * - New phone: creates user record, user continues to registration after verify */ function apiAbort(required struct payload) { writeOutput(serializeJSON(payload)); abort; } function readJsonBody() { var raw = getHttpRequestData().content; if (isNull(raw)) raw = ""; if (!len(trim(raw))) return {}; try { var data = deserializeJSON(raw); if (isStruct(data)) return data; } catch (any e) {} return {}; } function normalizePhone(required string p) { var x = trim(arguments.p); x = reReplace(x, "[^0-9]", "", "all"); // Remove leading 1 if 11 digits if (len(x) == 11 && left(x, 1) == "1") { x = right(x, 10); } return x; } function generateOTP() { return randRange(100000, 999999); } try { data = readJsonBody(); phone = structKeyExists(data, "phone") ? normalizePhone(data.phone) : ""; if (len(phone) != 10) { apiAbort({ "OK": false, "ERROR": "invalid_phone", "MESSAGE": "Please enter a valid 10-digit phone number" }); } // Check if phone already has ANY account qExisting = queryTimed(" SELECT ID, UUID, FirstName, IsContactVerified, IsActive FROM Users WHERE ContactNumber = :phone LIMIT 1 ", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); otp = generateOTP(); if (structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED && structKeyExists(application, "MAGIC_OTP_CODE") && len(application.MAGIC_OTP_CODE)) { otp = application.MAGIC_OTP_CODE; } userUUID = ""; if (qExisting.recordCount > 0) { // Existing account - just update OTP userUUID = qExisting.UUID; if (!len(trim(userUUID))) { userUUID = replace(createUUID(), "-", "", "all"); } queryTimed(" UPDATE Users SET MobileVerifyCode = :otp, UUID = :uuid WHERE ID = :userId ", { otp: { value: otp, cfsqltype: "cf_sql_varchar" }, uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }, userId: { value: qExisting.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); } else { // New phone - create user record userUUID = replace(createUUID(), "-", "", "all"); queryTimed(" INSERT INTO Users ( ContactNumber, UUID, MobileVerifyCode, IsContactVerified, IsEmailVerified, IsActive, AddedOn, Password, PromoCode ) VALUES ( :phone, :uuid, :otp, 0, 0, 0, :addedOn, '', :promoCode ) ", { phone: { value: phone, cfsqltype: "cf_sql_varchar" }, uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }, otp: { value: otp, cfsqltype: "cf_sql_varchar" }, addedOn: { value: now(), cfsqltype: "cf_sql_timestamp" }, promoCode: { value: randRange(1000000, 9999999), cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); } // Send OTP via Twilio (skip on dev server) smsMessage = "Code saved (SMS skipped in dev)"; isDev = findNoCase("dev.payfrit.com", cgi.SERVER_NAME) > 0; if (!isDev && structKeyExists(application, "twilioObj")) { try { smsResult = application.twilioObj.sendSMS( recipientNumber: "+1" & phone, messageBody: "Your Payfrit code is: " & otp ); smsMessage = smsResult.success ? "Code sent" : "SMS failed - please try again"; } catch (any smsErr) { smsMessage = "SMS error: " & smsErr.message; } } resp = { "OK": true, "UUID": userUUID, "MESSAGE": smsMessage }; if (isDev) { resp["DEV_OTP"] = otp; } writeOutput(serializeJSON(resp)); } catch (any e) { apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message }); }