Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
184 lines
5.4 KiB
PHP
184 lines
5.4 KiB
PHP
<?php
|
|
/**
|
|
* Public quote route handlers.
|
|
*
|
|
* These endpoints do not require authentication. They allow prospects
|
|
* to create, view, update, and submit quotes using an access token.
|
|
*/
|
|
|
|
// Deny direct access
|
|
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__)) {
|
|
http_response_code(403);
|
|
exit('Direct access denied.');
|
|
}
|
|
|
|
require_once __DIR__ . '/../helpers.php';
|
|
require_once __DIR__ . '/../db.php';
|
|
require_once __DIR__ . '/../services/quote_service.php';
|
|
require_once __DIR__ . '/../services/email_service.php';
|
|
|
|
/**
|
|
* POST /quotes
|
|
*
|
|
* Create a new quote draft. Returns the quote ID, access token, status, and
|
|
* a success message. HTTP 201 on success.
|
|
*/
|
|
function handle_create_quote(): void
|
|
{
|
|
$data = get_json_body();
|
|
$ip = get_client_ip();
|
|
$ua = get_user_agent();
|
|
$db = get_db();
|
|
|
|
// Validate employee_count if provided
|
|
if (isset($data['employee_count'])) {
|
|
$data['employee_count'] = (int)$data['employee_count'];
|
|
if ($data['employee_count'] < 1) {
|
|
error_response('employee_count must be >= 1', 422);
|
|
}
|
|
}
|
|
|
|
$quote = create_quote($db, $data, $ip, $ua);
|
|
|
|
json_response([
|
|
'id' => $quote['id'],
|
|
'access_token' => $quote['access_token'],
|
|
'status' => $quote['status'],
|
|
'message' => 'Quote created successfully. Use the access_token to access your quote.',
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* GET /quotes/{token}
|
|
*
|
|
* Retrieve a quote by its access token. Returns the full quote with items.
|
|
*/
|
|
function handle_get_quote(string $token): void
|
|
{
|
|
$db = get_db();
|
|
$quote = get_quote_by_token($db, $token);
|
|
$response = build_quote_response($db, $quote);
|
|
json_response($response);
|
|
}
|
|
|
|
/**
|
|
* PUT /quotes/{token}
|
|
*
|
|
* Update a draft quote's fields and/or replace all items.
|
|
*/
|
|
function handle_update_quote(string $token): void
|
|
{
|
|
$data = get_json_body();
|
|
$ip = get_client_ip();
|
|
$db = get_db();
|
|
|
|
$quote = update_quote($db, $token, $data, $ip);
|
|
$response = build_quote_response($db, $quote);
|
|
json_response($response);
|
|
}
|
|
|
|
/**
|
|
* POST /quotes/{token}/items
|
|
*
|
|
* Add a single item to a draft quote. HTTP 201 on success.
|
|
*/
|
|
function handle_add_item(string $token): void
|
|
{
|
|
$data = get_json_body();
|
|
$ip = get_client_ip();
|
|
$db = get_db();
|
|
|
|
// Validate required item fields
|
|
$errors = validate_required($data, ['category', 'product_code', 'product_name', 'unit_price']);
|
|
if (!empty($errors)) {
|
|
error_response('Validation error', 422, $errors);
|
|
}
|
|
|
|
$quote = add_item($db, $token, $data, $ip);
|
|
$response = build_quote_response($db, $quote);
|
|
json_response($response, 201);
|
|
}
|
|
|
|
/**
|
|
* DELETE /quotes/{token}/items/{item_id}
|
|
*
|
|
* Remove an item from a draft quote.
|
|
*/
|
|
function handle_remove_item(string $token, string $item_id): void
|
|
{
|
|
$ip = get_client_ip();
|
|
$db = get_db();
|
|
|
|
$quote = remove_item($db, $token, $item_id, $ip);
|
|
$response = build_quote_response($db, $quote);
|
|
json_response($response);
|
|
}
|
|
|
|
/**
|
|
* POST /quotes/{token}/submit
|
|
*
|
|
* Submit a draft quote with contact information. Sends an email notification
|
|
* to the admin (best-effort -- email failure does not fail the submission).
|
|
*/
|
|
function handle_submit_quote(string $token): void
|
|
{
|
|
$data = get_json_body();
|
|
$ip = get_client_ip();
|
|
$db = get_db();
|
|
|
|
// Validate required submission fields
|
|
$errors = validate_required($data, ['company_name', 'contact_name', 'contact_email']);
|
|
if (!empty($errors)) {
|
|
error_response('Validation error', 422, $errors);
|
|
}
|
|
|
|
if (!validate_email($data['contact_email'])) {
|
|
error_response('Invalid email address', 422, ["Field 'contact_email' is not a valid email."]);
|
|
}
|
|
|
|
// Submit the quote (updates DB)
|
|
$quote = submit_quote($db, $token, $data, $ip);
|
|
|
|
// Send email notification (best-effort, do not fail the request)
|
|
try {
|
|
$items_raw = fetch_items_for_quote($db, $quote['id']);
|
|
$items_data = array_map(function ($item) {
|
|
return [
|
|
'service_name' => $item['product_name'],
|
|
'billing_frequency' => $item['billing_frequency'],
|
|
'unit_price' => $item['unit_price'],
|
|
'quantity' => (int)$item['quantity'],
|
|
];
|
|
}, $items_raw);
|
|
|
|
$html = build_quote_notification_html(
|
|
$data['company_name'],
|
|
$data['contact_name'],
|
|
$data['contact_email'],
|
|
$data['contact_phone'] ?? null,
|
|
number_format((float)$quote['monthly_total'], 2, '.', ''),
|
|
number_format((float)$quote['setup_total'], 2, '.', ''),
|
|
$items_data,
|
|
$data['notes'] ?? null
|
|
);
|
|
|
|
$subject = "New Quote Submission: {$data['company_name']} - \$" .
|
|
number_format((float)$quote['monthly_total'], 2, '.', '') . "/mo";
|
|
|
|
$sent = send_email(ADMIN_NOTIFICATION_EMAIL, $subject, $html);
|
|
|
|
// Update notification record with result
|
|
$notif_status = $sent ? 'sent' : 'failed';
|
|
$notif_error = $sent ? null : 'Graph API send failed';
|
|
update_notification_status($db, $quote['id'], $notif_status, $notif_error);
|
|
|
|
} catch (\Throwable $e) {
|
|
app_log('ERROR', '[ERROR] Failed to send quote notification email: ' . $e->getMessage());
|
|
// Do not fail the submission
|
|
}
|
|
|
|
// Return the full quote response
|
|
$response = build_quote_response($db, $quote);
|
|
json_response($response);
|
|
}
|