/** * Send OTP to phone number for signup * * POST: { "phone": "5551234567" } * * Returns: { OK: true, UUID: "..." } or { OK: false, ERROR: "..." } * * If phone already has verified account, returns error. * If phone has unverified account, resends OTP. * Otherwise creates new user record with OTP. */ 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 a COMPLETE account (verified AND has profile info) // An account is only "complete" if they have a first name (meaning they finished signup) qExisting = queryExecute(" SELECT UserID, UserUUID, UserFirstName FROM Users WHERE UserContactNumber = :phone AND UserIsContactVerified > 0 AND UserFirstName IS NOT NULL AND LENGTH(TRIM(UserFirstName)) > 0 LIMIT 1 ", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); if (qExisting.recordCount > 0) { apiAbort({ "OK": false, "ERROR": "phone_exists", "MESSAGE": "This phone number already has an account. Please login instead." }); } // Check for incomplete account with this phone (verified but no profile, OR unverified) // These accounts can be reused for signup qIncomplete = queryExecute(" SELECT UserID, UserUUID FROM Users WHERE UserContactNumber = :phone AND (UserIsContactVerified = 0 OR UserFirstName IS NULL OR LENGTH(TRIM(UserFirstName)) = 0) LIMIT 1 ", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); otp = generateOTP(); userUUID = ""; if (qIncomplete.recordCount > 0) { // Update existing incomplete record with new OTP and reset for re-registration userUUID = qIncomplete.UserUUID; queryExecute(" UPDATE Users SET UserMobileVerifyCode = :otp, UserIsContactVerified = 0, UserIsActive = 0 WHERE UserID = :userId ", { otp: { value: otp, cfsqltype: "cf_sql_varchar" }, userId: { value: qIncomplete.UserID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); } else { // Create new user record userUUID = replace(createUUID(), "-", "", "all"); queryExecute(" INSERT INTO Users ( UserContactNumber, UserUUID, UserMobileVerifyCode, UserIsContactVerified, UserIsEmailVerified, UserIsActive, UserAddedOn, UserPassword, UserPromoCode ) 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 smsResult = application.twilioObj.sendSMS( recipientNumber: "+1" & phone, messageBody: "Your Payfrit verification code is: " & otp ); smsStatus = smsResult.success ? "sent" : "failed: " & smsResult.message; writeOutput(serializeJSON({ "OK": true, "UUID": userUUID, "MESSAGE": smsResult.success ? "Verification code sent" : "SMS failed but code created - contact support", "SMS_STATUS": smsStatus })); } catch (any e) { apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message }); }