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