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

View file

@ -46,26 +46,25 @@ if (userId <= 0) {
} }
try { try {
// Get user's delivery addresses with GROUP BY to show unique addresses only // Get user's addresses
qAddresses = queryExecute(" qAddresses = queryExecute("
SELECT SELECT
MIN(a.AddressID) as AddressID, a.AddressID,
MAX(a.AddressLabel) as AddressLabel, a.AddressLabel,
MAX(a.AddressIsDefaultDelivery) as AddressIsDefaultDelivery, a.AddressTypeID,
a.AddressIsDefaultDelivery,
a.AddressLine1, a.AddressLine1,
a.AddressLine2, a.AddressLine2,
a.AddressCity, a.AddressCity,
a.AddressStateID, a.AddressStateID,
MAX(s.tt_StateAbbreviation) as StateAbbreviation, s.tt_StateAbbreviation as StateAbbreviation,
MAX(s.tt_StateName) as StateName, s.tt_StateName as StateName,
a.AddressZIPCode a.AddressZIPCode
FROM Addresses a FROM Addresses a
LEFT JOIN tt_States s ON a.AddressStateID = s.tt_StateID LEFT JOIN tt_States s ON a.AddressStateID = s.tt_StateID
WHERE a.AddressUserID = :userId WHERE a.AddressUserID = :userId
AND a.AddressTypeID LIKE '%2%'
AND a.AddressIsDeleted = 0 AND a.AddressIsDeleted = 0
GROUP BY a.AddressLine1, a.AddressLine2, a.AddressCity, a.AddressStateID, a.AddressZIPCode ORDER BY a.AddressIsDefaultDelivery DESC, a.AddressID DESC
ORDER BY MAX(a.AddressIsDefaultDelivery) DESC, MIN(a.AddressID) DESC
", { ", {
userId: { value = userId, cfsqltype = "cf_sql_integer" } userId: { value = userId, cfsqltype = "cf_sql_integer" }
}); });
@ -74,6 +73,7 @@ try {
for (row in qAddresses) { for (row in qAddresses) {
arrayAppend(addresses, { arrayAppend(addresses, {
"AddressID": row.AddressID, "AddressID": row.AddressID,
"TypeID": val(row.AddressTypeID),
"Label": len(row.AddressLabel) ? row.AddressLabel : "Address", "Label": len(row.AddressLabel) ? row.AddressLabel : "Address",
"IsDefault": row.AddressIsDefaultDelivery == 1, "IsDefault": row.AddressIsDefaultDelivery == 1,
"Line1": row.AddressLine1, "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" } userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" }); }, { datasource: "payfrit" });
// Send OTP via Twilio (if available) // Send OTP via Twilio (skip on dev server)
smsMessage = "Code saved (SMS skipped in dev)"; 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 { try {
smsResult = application.twilioObj.sendSMS( smsResult = application.twilioObj.sendSMS(
recipientNumber: "+1" & phone, recipientNumber: "+1" & phone,

View file

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