125 lines
5.2 KiB
Text
125 lines
5.2 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfsetting enablecfoutputonly="true">
|
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
|
<cfheader name="Cache-Control" value="no-store">
|
|
|
|
<cfset response = { "OK": false, "taxRate": 0, "message": "", "city": "", "state": "" }>
|
|
|
|
<cftry>
|
|
<!--- Load Claude API Key --->
|
|
<cfset CLAUDE_API_KEY = "">
|
|
<cfset configPath = getDirectoryFromPath(getCurrentTemplatePath()) & "../../config/claude.json">
|
|
<cfif fileExists(configPath)>
|
|
<cfset configData = deserializeJSON(fileRead(configPath))>
|
|
<cfif structKeyExists(configData, "apiKey")>
|
|
<cfset CLAUDE_API_KEY = configData.apiKey>
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<!--- Get ZIP code from URL or form --->
|
|
<cfset zipCode = "">
|
|
<cfif structKeyExists(url, "zip")>
|
|
<cfset zipCode = trim(url.zip)>
|
|
<cfelseif structKeyExists(form, "zip")>
|
|
<cfset zipCode = trim(form.zip)>
|
|
</cfif>
|
|
|
|
<!--- Validate ZIP code format (5 digits or 5+4) --->
|
|
<cfif NOT reFind("^\d{5}(-\d{4})?$", zipCode)>
|
|
<cfset response.message = "Invalid ZIP code format">
|
|
<cfoutput>#serializeJSON(response)#</cfoutput>
|
|
<cfabort>
|
|
</cfif>
|
|
|
|
<!--- Use just the 5-digit portion --->
|
|
<cfset zipCode = left(zipCode, 5)>
|
|
|
|
<!--- Use Zippopotam.us to get city/state from ZIP --->
|
|
<cfhttp url="https://api.zippopotam.us/us/#zipCode#" method="GET" timeout="10" result="httpResult">
|
|
</cfhttp>
|
|
|
|
<cfif NOT httpResult.statusCode CONTAINS "200">
|
|
<cfset response.message = "ZIP lookup failed">
|
|
<cfoutput>#serializeJSON(response)#</cfoutput>
|
|
<cfabort>
|
|
</cfif>
|
|
|
|
<cfset zipData = deserializeJSON(httpResult.fileContent)>
|
|
<cfif NOT structKeyExists(zipData, "places") OR NOT isArray(zipData.places) OR arrayLen(zipData.places) EQ 0>
|
|
<cfset response.message = "No location data for ZIP">
|
|
<cfoutput>#serializeJSON(response)#</cfoutput>
|
|
<cfabort>
|
|
</cfif>
|
|
|
|
<cfset place = zipData.places[1]>
|
|
<cfset cityName = place["place name"]>
|
|
<cfset stateAbbr = uCase(place["state abbreviation"])>
|
|
<cfset response.city = cityName>
|
|
<cfset response.state = stateAbbr>
|
|
|
|
<!--- If no Claude API key, return state-only estimate --->
|
|
<cfif NOT len(CLAUDE_API_KEY)>
|
|
<!--- Fallback: state base rates --->
|
|
<cfset stateTaxRates = {
|
|
"AL": 4, "AK": 0, "AZ": 5.6, "AR": 6.5, "CA": 7.25,
|
|
"CO": 2.9, "CT": 6.35, "DE": 0, "FL": 6, "GA": 4,
|
|
"HI": 4, "ID": 6, "IL": 6.25, "IN": 7, "IA": 6,
|
|
"KS": 6.5, "KY": 6, "LA": 4.45, "ME": 5.5, "MD": 6,
|
|
"MA": 6.25, "MI": 6, "MN": 6.875, "MS": 7, "MO": 4.225,
|
|
"MT": 0, "NE": 5.5, "NV": 6.85, "NH": 0, "NJ": 6.625,
|
|
"NM": 4.875, "NY": 4, "NC": 4.75, "ND": 5, "OH": 5.75,
|
|
"OK": 4.5, "OR": 0, "PA": 6, "RI": 7, "SC": 6,
|
|
"SD": 4.2, "TN": 7, "TX": 6.25, "UT": 6.1, "VT": 6,
|
|
"VA": 5.3, "WA": 6.5, "WV": 6, "WI": 5, "WY": 4, "DC": 6
|
|
}>
|
|
<cfif structKeyExists(stateTaxRates, stateAbbr)>
|
|
<cfset response.taxRate = stateTaxRates[stateAbbr]>
|
|
<cfset response.OK = true>
|
|
<cfset response.message = "State base rate only (no API key)">
|
|
</cfif>
|
|
<cfoutput>#serializeJSON(response)#</cfoutput>
|
|
<cfabort>
|
|
</cfif>
|
|
|
|
<!--- Ask Claude for the exact tax rate --->
|
|
<cfset requestBody = {
|
|
"model": "claude-sonnet-4-20250514",
|
|
"max_tokens": 100,
|
|
"temperature": 0,
|
|
"messages": [{
|
|
"role": "user",
|
|
"content": "What is the current combined sales tax rate (state + county + city + special districts) for #cityName#, #stateAbbr# (ZIP code #zipCode#)? Return ONLY a single number representing the percentage, like 10.25 for 10.25%. No text, no explanation, just the number."
|
|
}]
|
|
}>
|
|
|
|
<cfhttp url="https://api.anthropic.com/v1/messages" method="POST" timeout="30" result="claudeResult">
|
|
<cfhttpparam type="header" name="Content-Type" value="application/json">
|
|
<cfhttpparam type="header" name="x-api-key" value="#CLAUDE_API_KEY#">
|
|
<cfhttpparam type="header" name="anthropic-version" value="2023-06-01">
|
|
<cfhttpparam type="body" value="#serializeJSON(requestBody)#">
|
|
</cfhttp>
|
|
|
|
<cfif claudeResult.statusCode CONTAINS "200">
|
|
<cfset claudeResponse = deserializeJSON(claudeResult.fileContent)>
|
|
<cfif structKeyExists(claudeResponse, "content") AND arrayLen(claudeResponse.content)>
|
|
<cfset rateText = trim(claudeResponse.content[1].text)>
|
|
<!--- Extract just the number --->
|
|
<cfset rateText = reReplace(rateText, "[^0-9.]", "", "all")>
|
|
<cfif isNumeric(rateText) AND val(rateText) GT 0 AND val(rateText) LT 20>
|
|
<cfset response.taxRate = val(rateText)>
|
|
<cfset response.OK = true>
|
|
<cfset response.message = "Tax rate for #cityName#, #stateAbbr#">
|
|
<cfelse>
|
|
<cfset response.message = "Could not parse tax rate">
|
|
</cfif>
|
|
</cfif>
|
|
<cfelse>
|
|
<cfset response.message = "Claude API error">
|
|
</cfif>
|
|
|
|
<cfcatch type="any">
|
|
<cfset response.message = "Error: #cfcatch.message#">
|
|
</cfcatch>
|
|
</cftry>
|
|
|
|
<cfoutput>#serializeJSON(response)#</cfoutput>
|