Files
claudetools/projects/msp-tools/quote-wizard/php-api/api/routes/quotes.php
Mike Swanson fa15b03180 sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00
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>
2026-03-10 19:59:08 -07:00

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);
}