Add addresses debug endpoint

This commit is contained in:
John Mizerek 2026-01-14 22:28:16 -08:00
parent f98eaa4ba1
commit 307c443f56
3 changed files with 216 additions and 82 deletions

View file

@ -1,4 +1,4 @@
<cfsetting showdebugoutput="false">
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<!---
@ -34,7 +34,12 @@
<!--- Initialize Twilio for SMS --->
<cfif NOT structKeyExists(application, "twilioObj")>
<cfset application.twilioObj = new library.cfc.twilio() />
<cftry>
<cfset application.twilioObj = new library.cfc.twilio() />
<cfcatch type="any">
<!--- Twilio component not available, skip initialization --->
</cfcatch>
</cftry>
</cfif>
<!--- Stripe Configuration (loads from config/stripe.cfm) --->
@ -91,6 +96,7 @@ if (len(request._api_path)) {
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/addresses/states.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/addresses/debug.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/debug/", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/admin/", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/orders/getOrCreateCart.cfm", request._api_path)) request._api_isPublic = true;

View file

@ -24,11 +24,20 @@ CLAUDE_API_KEY = application.claudeApiKey ?: "";
// If not in application scope, try to read from config
if (!len(CLAUDE_API_KEY)) {
try {
configPath = expandPath("/biz.payfrit.com/config/claude.json");
// Try relative path from current file location first
configPath = getDirectoryFromPath(getCurrentTemplatePath()) & "../../config/claude.json";
if (fileExists(configPath)) {
configData = deserializeJSON(fileRead(configPath));
CLAUDE_API_KEY = configData.apiKey ?: "";
}
// Fallback to expandPath if relative path didn't work
if (!len(CLAUDE_API_KEY)) {
configPath = expandPath("/biz.payfrit.com/config/claude.json");
if (fileExists(configPath)) {
configData = deserializeJSON(fileRead(configPath));
CLAUDE_API_KEY = configData.apiKey ?: "";
}
}
} catch (any e) {
// Config file doesn't exist or is invalid
}
@ -47,7 +56,7 @@ try {
// Get form fields for file uploads (file0, file1, file2, etc.)
formFields = form.keyArray();
for (fieldName in formFields) {
if (reFindNoCase("^file\d+$", fieldName) && len(form[fieldName])) {
if (reFindNoCase("^file[0-9]+$", fieldName) && len(form[fieldName])) {
// This is a file field
arrayAppend(uploadedFiles, fieldName);
}
@ -67,18 +76,56 @@ try {
}
for (fieldName in uploadedFiles) {
// Upload the file
uploadResult = fileUpload(
uploadDir,
fieldName,
"image/jpeg,image/png,image/gif,image/webp,application/pdf",
"makeunique"
);
try {
// Upload the file
uploadResult = fileUpload(
uploadDir,
fieldName,
"image/jpeg,image/png,image/gif,image/webp,application/pdf",
"makeunique"
);
} catch (any uploadErr) {
throw(message="File upload failed: " & uploadErr.message, detail="Field: " & fieldName);
}
if (uploadResult.fileWasSaved) {
filePath = uploadResult.serverDirectory & "/" & uploadResult.serverFile;
fileExt = lCase(uploadResult.serverFileExt);
// For images, resize if too large (max 1600px on longest side)
if (listFindNoCase("jpg,jpeg,png,gif,webp", fileExt)) {
try {
img = imageRead(filePath);
} catch (any readErr) {
throw(message="Image read failed: " & readErr.message, detail="File: " & filePath);
}
imgWidth = img.width;
imgHeight = img.height;
maxDimension = 1600;
if (imgWidth > maxDimension || imgHeight > maxDimension) {
try {
if (imgWidth > imgHeight) {
newWidth = maxDimension;
newHeight = int(imgHeight * (maxDimension / imgWidth));
} else {
newHeight = maxDimension;
newWidth = int(imgWidth * (maxDimension / imgHeight));
}
imageResize(img, newWidth, newHeight);
} catch (any resizeErr) {
throw(message="Image resize failed: " & resizeErr.message, detail="Dimensions: " & imgWidth & "x" & imgHeight);
}
}
// Re-save with good quality compression
try {
imageWrite(img, filePath, 0.8);
} catch (any writeErr) {
throw(message="Image write failed: " & writeErr.message, detail="File: " & filePath);
}
}
// Read file and convert to base64
fileContent = fileReadBinary(filePath);
base64Content = toBase64(fileContent);
@ -90,14 +137,26 @@ try {
else if (fileExt == "webp") mediaType = "image/webp";
else if (fileExt == "pdf") mediaType = "application/pdf";
arrayAppend(imageDataArray, {
"type": "image",
"source": {
"type": "base64",
"media_type": mediaType,
"data": base64Content
}
});
// Claude API uses different structure for PDFs vs images
if (fileExt == "pdf") {
arrayAppend(imageDataArray, {
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": base64Content
}
});
} else {
arrayAppend(imageDataArray, {
"type": "image",
"source": {
"type": "base64",
"media_type": mediaType,
"data": base64Content
}
});
}
// Clean up temp file
fileDelete(filePath);
@ -111,56 +170,7 @@ try {
// Build the prompt for Claude
systemPrompt = "You are an expert at extracting structured menu data from restaurant menu images. Your task is to analyze menu images and return the data in a specific JSON format. Be thorough and extract ALL items, categories, and any modifier patterns you can identify.";
userPrompt = "Please analyze these menu images and extract all the information into the following JSON structure:
{
""business"": {
""name"": ""Restaurant name if visible"",
""address"": ""Full address if visible"",
""phone"": ""Phone number(s) if visible"",
""hours"": ""Business hours if visible""
},
""categories"": [
{
""name"": ""Category name"",
""itemCount"": 0
}
],
""modifiers"": [
{
""name"": ""Modifier template name (e.g., 'Size', 'Protein Choice', 'Toast Type')"",
""required"": true or false,
""options"": [
{ ""name"": ""Option name"", ""price"": 0.00 }
]
}
],
""items"": [
{
""name"": ""Item name"",
""description"": ""Item description if any"",
""price"": 0.00,
""category"": ""Category this item belongs to"",
""modifiers"": [""Name of applicable modifier template""]
}
]
}
Important guidelines:
1. Extract EVERY menu item you can see, with accurate prices
2. Group items into their categories as shown on the menu
3. Look for modifier patterns like:
- Size options (Small/Regular/Large)
- Protein choices (Chicken/Beef/Shrimp)
- Bread choices (Wheat/Sourdough/White/Rye)
- Add-ons with prices (Add bacon +$2.00)
- Required choices (served with choice of side)
4. For modifiers that appear on multiple items, create a reusable template
5. Assign the appropriate modifier templates to each item
6. If a price has a '+' it's usually an upcharge modifier option
7. Pay attention to category headers and section dividers
Return ONLY the JSON object, no other text.";
userPrompt = "Please analyze these menu images and extract all the information into a JSON object with these keys: business (with name, address, phone, hours), categories (array with name and itemCount), modifiers (array with name, required boolean, and options array), and items (array with name, description, price, category, and modifiers array). Extract EVERY menu item with accurate prices. Group items by category. Look for modifier patterns like sizes, protein choices, bread choices, and add-ons. Create reusable modifier templates for patterns that appear on multiple items. Return ONLY valid JSON, no other text.";
// Build the messages array with images
messagesContent = [];
@ -176,14 +186,7 @@ Return ONLY the JSON object, no other text.";
"text": userPrompt
});
// Call Claude API
httpService = new http();
httpService.setMethod("POST");
httpService.setUrl("https://api.anthropic.com/v1/messages");
httpService.addParam(type="header", name="Content-Type", value="application/json");
httpService.addParam(type="header", name="x-api-key", value=CLAUDE_API_KEY);
httpService.addParam(type="header", name="anthropic-version", value="2023-06-01");
// Build request body
requestBody = {
"model": "claude-sonnet-4-20250514",
"max_tokens": 8192,
@ -196,16 +199,51 @@ Return ONLY the JSON object, no other text.";
]
};
httpService.addParam(type="body", value=serializeJSON(requestBody));
// Write request body to temp file (Lucee's HTTP client has issues with large payloads)
tempRequestFile = getTempDirectory() & "claude_request_" & createUUID() & ".json";
tempResponseFile = getTempDirectory() & "claude_response_" & createUUID() & ".json";
fileWrite(tempRequestFile, serializeJSON(requestBody));
httpResult = httpService.send().getPrefix();
try {
// Call Claude API using cfhttp tag (more reliable than http component)
httpResult = {};
http url="https://api.anthropic.com/v1/messages" method="POST" timeout=300 result="httpResult" {
httpparam type="header" name="Content-Type" value="application/json";
httpparam type="header" name="x-api-key" value=CLAUDE_API_KEY;
httpparam type="header" name="anthropic-version" value="2023-06-01";
httpparam type="body" value=serializeJSON(requestBody);
}
if (httpResult.statusCode != "200 OK" && httpResult.statusCode != 200) {
throw(message="Claude API error: " & httpResult.statusCode, detail=httpResult.fileContent);
httpStatusCode = httpResult.statusCode ?: "0";
httpFileContent = httpResult.fileContent ?: "";
// Normalize status code
if (isNumeric(httpStatusCode)) {
httpStatusCode = int(httpStatusCode);
} else if (findNoCase("200", httpStatusCode)) {
httpStatusCode = 200;
}
if (httpStatusCode != 200) {
// Try to parse error message from Claude's response
errorMsg = "Claude API error: " & httpResult.statusCode;
try {
errorData = deserializeJSON(httpFileContent);
if (structKeyExists(errorData, "error") && structKeyExists(errorData.error, "message")) {
errorMsg = errorMsg & " - " & errorData.error.message;
}
} catch (any e) {
// Use raw response if can't parse
}
throw(message=errorMsg, detail=httpFileContent);
}
} finally {
// Clean up temp files
if (fileExists(tempRequestFile)) fileDelete(tempRequestFile);
}
// Parse Claude's response
claudeResponse = deserializeJSON(httpResult.fileContent);
claudeResponse = deserializeJSON(httpFileContent);
if (!structKeyExists(claudeResponse, "content") || !arrayLen(claudeResponse.content)) {
throw(message="Empty response from Claude");
@ -266,6 +304,11 @@ Return ONLY the JSON object, no other text.";
if (len(e.detail)) {
response.DETAIL = e.detail;
}
// Add debug info
if (structKeyExists(e, "tagContext") && arrayLen(e.tagContext) > 0) {
response.DEBUG_LINE = e.tagContext[1].line;
response.DEBUG_TEMPLATE = e.tagContext[1].template;
}
}
writeOutput(serializeJSON(response));

85
api/setup/testUpload.cfm Normal file
View file

@ -0,0 +1,85 @@
<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfset response = structNew()>
<cfset response["OK"] = false>
<cftry>
<!--- Step 1: Check form fields --->
<cfset response["step"] = "checking form fields">
<cfset formFields = form.keyArray()>
<cfset response["formFields"] = formFields>
<!--- Step 2: Find file fields --->
<cfset response["step"] = "finding file fields">
<cfset uploadedFiles = arrayNew(1)>
<cfloop array="#formFields#" index="fieldName">
<cfif reFindNoCase("^file[0-9]+$", fieldName)>
<cfset arrayAppend(uploadedFiles, fieldName)>
</cfif>
</cfloop>
<cfset response["uploadedFiles"] = uploadedFiles>
<cfif arrayLen(uploadedFiles) EQ 0>
<cfthrow message="No files found">
</cfif>
<!--- Step 3: Create temp directory --->
<cfset response["step"] = "creating temp dir">
<cfset uploadDir = getTempDirectory() & "test_uploads/">
<cfif NOT directoryExists(uploadDir)>
<cfdirectory action="create" directory="#uploadDir#">
</cfif>
<cfset response["uploadDir"] = uploadDir>
<!--- Step 4: Upload first file --->
<cfset response["step"] = "uploading file">
<cfset fieldName = uploadedFiles[1]>
<cffile action="upload" destination="#uploadDir#" filefield="#fieldName#"
accept="image/jpeg,image/png,image/gif,image/webp,application/pdf"
nameconflict="makeunique" result="uploadResult">
<cfset response["uploadResult"] = uploadResult.fileWasSaved>
<cfset response["serverFile"] = uploadResult.serverFile>
<cfset response["serverFileExt"] = uploadResult.serverFileExt>
<!--- Step 5: Read the file --->
<cfset response["step"] = "reading file">
<cfset filePath = uploadResult.serverDirectory & "/" & uploadResult.serverFile>
<cfset response["filePath"] = filePath>
<cfset response["fileExists"] = fileExists(filePath)>
<!--- Step 6: Try image operations --->
<cfset fileExt = lCase(uploadResult.serverFileExt)>
<cfif listFindNoCase("jpg,jpeg,png,gif,webp", fileExt)>
<cfset response["step"] = "reading image">
<cfimage action="read" source="#filePath#" name="img">
<cfset response["imageWidth"] = img.width>
<cfset response["imageHeight"] = img.height>
<cfset response["step"] = "converting to base64">
<cffile action="readbinary" file="#filePath#" variable="fileContent">
<cfset base64Content = toBase64(fileContent)>
<cfset response["base64Length"] = len(base64Content)>
</cfif>
<!--- Step 7: Cleanup --->
<cfset response["step"] = "cleanup">
<cffile action="delete" file="#filePath#">
<cfset response["OK"] = true>
<cfset response["step"] = "complete">
<cfcatch type="any">
<cfset response["MESSAGE"] = cfcatch.message>
<cfif len(cfcatch.detail)>
<cfset response["DETAIL"] = cfcatch.detail>
</cfif>
<cfif structKeyExists(cfcatch, "tagContext") AND arrayLen(cfcatch.tagContext) GT 0>
<cfset response["DEBUG_LINE"] = cfcatch.tagContext[1].line>
<cfset response["DEBUG_TEMPLATE"] = cfcatch.tagContext[1].template>
</cfif>
</cfcatch>
</cftry>
<cfoutput>#serializeJSON(response)#</cfoutput>