Add address types endpoint, fix dev mode SMS skip

- Add /addresses/types.cfm - returns address types list
- Update /addresses/list.cfm - include TypeID in response
- Update /addresses/add.cfm - accept TypeID instead of hardcoded '2'
- Fix loginOTP.cfm and sendOTP.cfm to skip Twilio SMS on dev server

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-28 00:00:01 -08:00
parent 477cf6b8b5
commit cadc66e46a
5 changed files with 67 additions and 22 deletions

View file

@ -31,6 +31,7 @@ try {
data = readJsonBody();
// Required fields
typeId = val(data.TypeID ?: 0);
line1 = trim(data.Line1 ?: "");
city = trim(data.City ?: "");
stateId = val(data.StateID ?: 0);
@ -42,25 +43,26 @@ try {
setAsDefault = (data.SetAsDefault ?: false) == true;
// Validation
if (len(line1) == 0 || len(city) == 0 || stateId <= 0 || len(zipCode) == 0) {
if (typeId <= 0 || len(line1) == 0 || len(city) == 0 || stateId <= 0 || len(zipCode) == 0) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": "missing_fields",
"MESSAGE": "Line1, City, StateID, and ZIPCode are required"
"MESSAGE": "TypeID, Line1, City, StateID, and ZIPCode are required"
}));
abort;
}
// If setting as default, clear other defaults first
// If setting as default, clear other defaults first (for same type)
if (setAsDefault) {
queryExecute("
UPDATE Addresses
SET AddressIsDefaultDelivery = 0
WHERE AddressUserID = :userId
AND (AddressBusinessID = 0 OR AddressBusinessID IS NULL)
AND AddressTypeID LIKE '%2%'
AND AddressTypeID = :typeId
", {
userId: { value: userId, cfsqltype: "cf_sql_integer" }
userId: { value: userId, cfsqltype: "cf_sql_integer" },
typeId: { value: typeId, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
}
@ -88,7 +90,7 @@ try {
:addressId,
:userId,
0,
'2',
:typeId,
:label,
:isDefault,
:line1,
@ -102,6 +104,7 @@ try {
", {
addressId: { value: newAddressId, cfsqltype: "cf_sql_integer" },
userId: { value: userId, cfsqltype: "cf_sql_integer" },
typeId: { value: typeId, cfsqltype: "cf_sql_integer" },
label: { value: label, cfsqltype: "cf_sql_varchar" },
isDefault: { value: setAsDefault ? 1 : 0, cfsqltype: "cf_sql_integer" },
line1: { value: line1, cfsqltype: "cf_sql_varchar" },
@ -124,6 +127,7 @@ try {
"OK": true,
"ADDRESS": {
"AddressID": newAddressId,
"TypeID": typeId,
"Label": len(label) ? label : "Address",
"IsDefault": setAsDefault,
"Line1": line1,

View file

@ -46,26 +46,25 @@ if (userId <= 0) {
}
try {
// Get user's delivery addresses with GROUP BY to show unique addresses only
// Get user's addresses
qAddresses = queryExecute("
SELECT
MIN(a.AddressID) as AddressID,
MAX(a.AddressLabel) as AddressLabel,
MAX(a.AddressIsDefaultDelivery) as AddressIsDefaultDelivery,
a.AddressID,
a.AddressLabel,
a.AddressTypeID,
a.AddressIsDefaultDelivery,
a.AddressLine1,
a.AddressLine2,
a.AddressCity,
a.AddressStateID,
MAX(s.tt_StateAbbreviation) as StateAbbreviation,
MAX(s.tt_StateName) as StateName,
s.tt_StateAbbreviation as StateAbbreviation,
s.tt_StateName as StateName,
a.AddressZIPCode
FROM Addresses a
LEFT JOIN tt_States s ON a.AddressStateID = s.tt_StateID
WHERE a.AddressUserID = :userId
AND a.AddressTypeID LIKE '%2%'
AND a.AddressIsDeleted = 0
GROUP BY a.AddressLine1, a.AddressLine2, a.AddressCity, a.AddressStateID, a.AddressZIPCode
ORDER BY MAX(a.AddressIsDefaultDelivery) DESC, MIN(a.AddressID) DESC
ORDER BY a.AddressIsDefaultDelivery DESC, a.AddressID DESC
", {
userId: { value = userId, cfsqltype = "cf_sql_integer" }
});
@ -74,6 +73,7 @@ try {
for (row in qAddresses) {
arrayAppend(addresses, {
"AddressID": row.AddressID,
"TypeID": val(row.AddressTypeID),
"Label": len(row.AddressLabel) ? row.AddressLabel : "Address",
"IsDefault": row.AddressIsDefaultDelivery == 1,
"Line1": row.AddressLine1,

40
api/addresses/types.cfm Normal file
View file

@ -0,0 +1,40 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="max-age=3600">
<cfscript>
/**
* Get list of address types
* GET: /api/addresses/types.cfm
* Returns: { OK: true, TYPES: [{ ID: 1, Label: "Billing" }, ...] }
*/
try {
qTypes = queryExecute("
SELECT tt_AddressTypeID as ID, tt_AddressType as Label
FROM tt_AddressTypes
ORDER BY tt_AddressTypeID
", {}, { datasource: "payfrit" });
types = [];
for (row in qTypes) {
arrayAppend(types, {
"ID": row.ID,
"Label": row.Label
});
}
writeOutput(serializeJSON({
"OK": true,
"TYPES": types
}));
} catch (any e) {
writeOutput(serializeJSON({
"OK": false,
"ERROR": "server_error",
"MESSAGE": e.message
}));
}
</cfscript>

View file

@ -87,9 +87,11 @@ try {
userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
// Send OTP via Twilio (if available)
// Send OTP via Twilio (skip on dev server)
smsMessage = "Code saved (SMS skipped in dev)";
if (structKeyExists(application, "twilioObj")) {
isDev = findNoCase("dev.payfrit.com", cgi.SERVER_NAME) > 0;
if (!isDev && structKeyExists(application, "twilioObj")) {
try {
smsResult = application.twilioObj.sendSMS(
recipientNumber: "+1" & phone,

View file

@ -130,18 +130,17 @@ try {
}, { datasource: "payfrit" });
}
// Send OTP via Twilio (if available)
// Send OTP via Twilio (skip on dev server)
smsMessage = "Code saved (SMS skipped in dev)";
devOTP = otp; // Return OTP in dev mode for testing
isDev = findNoCase("dev.payfrit.com", cgi.SERVER_NAME) > 0;
if (structKeyExists(application, "twilioObj")) {
if (!isDev && structKeyExists(application, "twilioObj")) {
try {
smsResult = application.twilioObj.sendSMS(
recipientNumber: "+1" & phone,
messageBody: "Your Payfrit verification code is: " & otp
);
smsMessage = smsResult.success ? "Verification code sent" : "SMS failed - please try again";
if (smsResult.success) devOTP = ""; // Don't leak OTP in production
} catch (any smsErr) {
smsMessage = "SMS error: " & smsErr.message;
}
@ -151,7 +150,7 @@ try {
"OK": true,
"UUID": userUUID,
"MESSAGE": smsMessage,
"DEV_OTP": devOTP
"DEV_OTP": otp
}));
} catch (any e) {