Request flow
Six stages run in order; the first two can end the request early without ever reaching the database.
BookTestDriveModel::saveBooking() inserts one row.
post_lead_to_crm setting.
sendMailLeadNotification(3), brand-dependent.
Constructor preconditions
Every action on QuickController — including bookdriveAction — runs behind the class constructor (lines 20–58) first. It prepares context that the action reads without re-fetching it:
$this->brandis read from settingsite_settings.autoline_api_brand.$_POST['subsite_id']and$_POST['website_id']are copied in from theSUBSITEID/WEBSITEIDHTTP headers.$_POST['store_name']is looked up fromStoresModelusing the subsite.- If
productIDis present (falling back toecomProductId),VehiclemodelsModel::getProductModelByID()fills inmodel,variant, andvariantSKUon$_POST. crmCustomerIDis resolved viaUserModel::getCrmIDByUserID().- If
showroomIDis set,ShowroomModel::getLocationCode()fillslocationCode. - For the Chery / Omoda5 combination specifically,
crm_api_domainandcrm_api_tokenare swapped for theomoda_*variants of those settings.
Ordering detail: the constructor's CRM-customer lookup reads $_POST['subsiteID'] (camelCase), but that key is only assigned inside the action method itself — see Notes.
Input parameters
Parameters arrive as $_POST fields plus three request headers. "Server-derived" fields are never sent by the client — they're computed during the constructor or the action and then read as if they were part of the request.
| Field | Source | Required | Notes |
|---|---|---|---|
| SUBSITEID | HTTP header | Yes | Copied into $_POST['subsite_id'] by the constructor. |
| WEBSITEID | HTTP header | Yes | Copied into $_POST['website_id']; used for every settings lookup. |
| LANGUAGEID | HTTP header | Recommended | Drives the language used for saved copy and error strings. |
| productID | string/int | Yes | Falls back to ecomProductId if empty. Drives the model/variant lookup. |
| phoneNumber | string | Yes | Stored as an escaped integer in the booking row. |
| string | Conditional | Required for every brand except ford. | |
| showroomID | int | Conditional | Required for every brand except exeed and byd. |
| subsiteID | int | Yes | Client-supplied field, re-set server-side from session context — see Notes. |
| GRecaptchaResponse | string | Conditional | Required only when brand is chery or exeed and captcha is enabled. |
| title | string | No | Saved verbatim on the booking row. |
| firstName / lastName | string | No | Saved on the booking row; used in the notification email body. |
| comments | string | No | Free text, saved and emailed. |
| scheduledTime | date string | No | Reformatted to Y-m-d before insert; also relayed to the CRM as requestDate. |
| userID | int | No | Any client-sent value is discarded — overwritten from the logged-in session (or 0). |
| model, variant, variantSKU | server-derived | — | Filled in from productID during the constructor. |
| locationCode | server-derived | — | Filled in from showroomID during the constructor. |
| crmCustomerID | server-derived | — | Resolved from userID via UserModel. |
Validation rules
A single combined condition (line 164) decides pass/fail — the request fails if any of these are true:
| Condition | Applies to |
|---|---|
productID is empty | All brands |
phoneNumber is empty | All brands |
showroomID is empty | All brands except exeed, byd |
email is empty | All brands except ford |
subsiteID is empty | All brands |
Any failure short-circuits to the generic errParameterMissing response — the specific missing field is not named back to the caller.
Processing steps
The success branch, in execution order.
-
Captcha gate (brand-conditional) If
brandischeryorexeed,verifyCaptchaAction()runs first. It only enforces anything ifsecurity_settings.enable_captchaisYes— otherwise it's a no-op. When enforced, a missing or Google-rejected token ends the request immediately with an HTTP 400 JSON body;bookdriveActionnever resumes. -
Session context is stamped onto the request
userIDandsubsiteIDare overwritten fromObjCommon(the logged-in session), defaulting to0when there's no session. -
Required-field validation See Validation rules. Failing here skips every remaining step and returns the failure JSON directly.
-
Booking is persisted
BookTestDriveModel::saveBooking($_POST, $languageIDs['Current'])inserts one row intobook_a_test_drive. See Database write. -
CRM push, conditional on a site setting
site_settings.post_lead_to_crmis checked. WhenYes,scheduledTimeis copied intorequestDateandCrm::postLead($_POST, 3, $this->pdtSettings)is called. When notYes,$crmresponseis simply set to an empty string and no HTTP call happens. -
Local status is fixed to success
status,responseCode(200), andmessageare set unconditionally at this point — they do not depend on what the CRM call returned. -
Notification email
sendMailLeadNotification(3)sends the "Test Drive Request" lead email — unless the brand isomodaorjac, in which case it returns immediately without sending anything. -
Response is emitted The
metaobject is assembled and echoed as pretty-printed JSON, then the requestexits.
Database write
BookTestDriveModel::saveBooking() runs a single raw INSERT into {DB_PREFIX}book_a_test_drive:
| Column | Source | Transform |
|---|---|---|
| title | $_POST['title'] | — |
| firstName | $_POST['firstName'] | — |
| lastName | $_POST['lastName'] | — |
| phoneNumber | $_POST['phoneNumber'] | escaped as integer |
| $_POST['email'] | — | |
| productID | $_POST['productID'] | cast to int |
| userID | $_POST['userID'] | cast to int |
| requestedTime | server clock | NOW() — not derived from input |
| languageID | method argument | cast to int |
| showroomID | $_POST['showroomID'] | cast to int |
| comments | $_POST['comments'] | — |
| subsiteID | $_POST['subsiteID'] | cast to int |
| scheduledTime | $_POST['scheduledTime'] | reformatted to Y-m-d |
The method returns whatever the database wrapper's setQuery() call returns; bookdriveAction does not inspect or forward that value.
CRM integration
Crm::postLead($_POST, 3, $this->pdtSettings) extracts everything out of $_POST and assembles a JSON payload sent as:
Authorization: Bearer <pdtSettings.crm_api_token> Content-Type: application/json { "leadType": "Test Drive Request", // numeric 3 mapped to this string "businessSource": "Alghanim website", "leadIntent": "Sales", "model": "<uppercased model name>", "firstName": "<firstName>", "lastName": "<lastName>", "phone": "<phoneNumber>", "prefix": "<title>", "email": "<email>", "initialTouchPoint": "Online", "civilId": "", "customerId": "<crmCustomerID>", "showroom": "<locationCode>", "requestedDate": "<scheduledTime, ISO-8601>", "variant": "<variant>", "note": "<comments>" // plus any of: campaignTag, promoterName, exteriorColor, interiorColor, // purchaseHorizon, purchaseMethod, drivingLicence, addressStreet — // included only when present on the request }
Endpoint: the literal path lead/external-lead is appended directly to pdtSettings['crm_api_domain'] — the Chery/Omoda5 branch in the constructor swaps that base domain (and token) before this call happens.
Response shape is not fixed: postLead() just JSON-decodes whatever the CRM returns and casts it to an array. bookdriveAction passes it straight into meta.crmresponse without checking for a status or message key.
Email notification
sendMailLeadNotification(3) is called once the booking is saved, independent of whether the CRM push happened or succeeded.
- Brand short-circuit: if brand is
omodaorjac(orbydwith lead type 7), the method returns immediately — no mail is sent. - Lead type
3maps to the string"Test Drive Request", used to fill the{$extra}token in the email template. - The template
lead-requestis loaded, merged withfirstName,lastName,email,phoneNumber,model,variant,variantSKU, showroom name,comments, and the request date. - Recipients come from the
site_settings.lead-notification-emailsetting — a comma-separated list, each address regex-validated before sending.
Output structure
All three outcomes are pretty-printed JSON (JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK), followed by exit.
Success — 200
{
"meta": {
"status": "Success",
"responsecode": 200,
"message": "<msgTestDriveBookedSuccessfully>",
"crmresponse": "<raw CRM response, or "" if post_lead_to_crm ≠ Yes>"
}
}
Validation failure — required field missing
{
"meta": {
"status": "Failure",
"responsecode": 500,
"message": "<errParameterMissing>"
}
}
Note: this branch never sets crmresponse, so the key is entirely absent from the JSON — unlike the success path, which always includes it.
Captcha failure — chery / exeed only, ends before validation
{
"meta": {
"status": "Failure",
"responsecode": 400,
"message": "<errParameterMissing or errCaptcha>"
}
}
Notes & observations
subsiteID timing: the constructor calls UserModel::getCrmIDByUserID($_POST['userID'], $_POST['subsiteID']) before bookdriveAction assigns $_POST['subsiteID'] (line 162). Unless the client already sent subsiteID in the raw POST body, that lookup runs against an unset key.
crmresponse has no guaranteed shape: Crm::postLead() returns the CRM's raw decoded JSON. Other actions in the same controller (e.g. saveenquiryAction) use Crm::postEnquiry(), which does guarantee a status/message shape and drives the outer response code from it. bookdriveAction does not do this — its responsecode is always 200 on the success branch regardless of what the CRM actually returned.
userID can't be spoofed: whatever the client sends as userID is discarded and replaced with the authenticated session's ID (or 0), before validation or persistence.
Brand-specific required fields: showroomID is optional for exeed/byd; email is optional only for ford. Every other brand needs all five required fields listed in Validation rules.