Complete port of all 163 API endpoints from Lucee/CFML to PHP 8.3. Shared helpers in api/helpers.php (DB, auth, request/response, security). PDO prepared statements throughout. Same JSON response shapes as CFML.
133 lines
4.6 KiB
PHP
133 lines
4.6 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../helpers.php';
|
|
runAuth();
|
|
|
|
/**
|
|
* Look up tax rate by ZIP code
|
|
* Uses Zippopotam.us for city/state, then Claude API for exact rate.
|
|
*
|
|
* GET: ?zip=90001
|
|
*/
|
|
|
|
$response = ['OK' => false, 'taxRate' => 0, 'message' => '', 'city' => '', 'state' => ''];
|
|
|
|
try {
|
|
// Load Claude API key
|
|
$CLAUDE_API_KEY = '';
|
|
$configPath = __DIR__ . '/../../config/claude.json';
|
|
if (file_exists($configPath)) {
|
|
$configData = json_decode(file_get_contents($configPath), true);
|
|
$CLAUDE_API_KEY = $configData['apiKey'] ?? '';
|
|
}
|
|
|
|
// Get ZIP code
|
|
$zipCode = trim($_GET['zip'] ?? $_POST['zip'] ?? '');
|
|
|
|
// Validate ZIP format
|
|
if (!preg_match('/^\d{5}(-\d{4})?$/', $zipCode)) {
|
|
$response['message'] = 'Invalid ZIP code format';
|
|
jsonResponse($response);
|
|
}
|
|
|
|
$zipCode = substr($zipCode, 0, 5);
|
|
|
|
// Zippopotam.us lookup
|
|
$ch = curl_init("https://api.zippopotam.us/us/$zipCode");
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
]);
|
|
$zipResult = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode !== 200) {
|
|
$response['message'] = 'ZIP lookup failed';
|
|
jsonResponse($response);
|
|
}
|
|
|
|
$zipData = json_decode($zipResult, true);
|
|
if (empty($zipData['places'][0])) {
|
|
$response['message'] = 'No location data for ZIP';
|
|
jsonResponse($response);
|
|
}
|
|
|
|
$place = $zipData['places'][0];
|
|
$cityName = $place['place name'] ?? '';
|
|
$stateAbbr = strtoupper($place['state abbreviation'] ?? '');
|
|
$response['city'] = $cityName;
|
|
$response['state'] = $stateAbbr;
|
|
|
|
// State base rates fallback
|
|
$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,
|
|
];
|
|
|
|
if (empty($CLAUDE_API_KEY)) {
|
|
if (isset($stateTaxRates[$stateAbbr])) {
|
|
$response['taxRate'] = $stateTaxRates[$stateAbbr];
|
|
$response['OK'] = true;
|
|
$response['message'] = 'State base rate only (no API key)';
|
|
}
|
|
jsonResponse($response);
|
|
}
|
|
|
|
// Ask Claude for exact tax rate
|
|
$requestBody = json_encode([
|
|
'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.",
|
|
]],
|
|
]);
|
|
|
|
$ch = curl_init('https://api.anthropic.com/v1/messages');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $requestBody,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
"x-api-key: $CLAUDE_API_KEY",
|
|
'anthropic-version: 2023-06-01',
|
|
],
|
|
]);
|
|
$claudeResult = curl_exec($ch);
|
|
$claudeHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($claudeHttpCode === 200) {
|
|
$claudeResponse = json_decode($claudeResult, true);
|
|
if (!empty($claudeResponse['content'][0]['text'])) {
|
|
$rateText = trim($claudeResponse['content'][0]['text']);
|
|
$rateText = preg_replace('/[^0-9.]/', '', $rateText);
|
|
$rateVal = (float)$rateText;
|
|
if ($rateVal > 0 && $rateVal < 20) {
|
|
$response['taxRate'] = $rateVal;
|
|
$response['OK'] = true;
|
|
$response['message'] = "Tax rate for $cityName, $stateAbbr";
|
|
} else {
|
|
$response['message'] = 'Could not parse tax rate';
|
|
}
|
|
}
|
|
} else {
|
|
$response['message'] = 'Claude API error';
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$response['message'] = 'Error: ' . $e->getMessage();
|
|
}
|
|
|
|
jsonResponse($response);
|