Endpoint report application/core/controllers/mobileapp/QuickController.php · lines 152–198

Test Drive Booking Flow

A line-by-line breakdown of QuickController::bookdriveAction() — the mobile-app endpoint that records a test-drive request, forwards it to the CRM, and emails the sales team — with its full input contract and JSON response shapes.

POST
ControllerQuick
Actionbookdrive
Modulemobileapp
Writes tobook_a_test_drive
CRM lead type3 — "Test Drive Request"
01

Request flow

Six stages run in order; the first two can end the request early without ever reaching the database.

Gate Captcha check Only for brand = chery / exeed. Can exit with HTTP 400 before anything else runs.
Guard Field validation Required-field check, with brand-specific exceptions.
Exit path Failure response Missing fields → 500 JSON, skips every step below.
Persist Save booking BookTestDriveModel::saveBooking() inserts one row.
Integrate Push to CRM Conditional on the post_lead_to_crm setting.
Notify Email sales team sendMailLeadNotification(3), brand-dependent.
Respond 200 JSON Success meta, regardless of the CRM call's own outcome.
02

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->brand is read from setting site_settings.autoline_api_brand.
  • $_POST['subsite_id'] and $_POST['website_id'] are copied in from the SUBSITEID / WEBSITEID HTTP headers.
  • $_POST['store_name'] is looked up from StoresModel using the subsite.
  • If productID is present (falling back to ecomProductId), VehiclemodelsModel::getProductModelByID() fills in model, variant, and variantSKU on $_POST.
  • crmCustomerID is resolved via UserModel::getCrmIDByUserID().
  • If showroomID is set, ShowroomModel::getLocationCode() fills locationCode.
  • For the Chery / Omoda5 combination specifically, crm_api_domain and crm_api_token are swapped for the omoda_* 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.

03

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.

FieldSourceRequiredNotes
SUBSITEIDHTTP headerYesCopied into $_POST['subsite_id'] by the constructor.
WEBSITEIDHTTP headerYesCopied into $_POST['website_id']; used for every settings lookup.
LANGUAGEIDHTTP headerRecommendedDrives the language used for saved copy and error strings.
productIDstring/intYesFalls back to ecomProductId if empty. Drives the model/variant lookup.
phoneNumberstringYesStored as an escaped integer in the booking row.
emailstringConditionalRequired for every brand except ford.
showroomIDintConditionalRequired for every brand except exeed and byd.
subsiteIDintYesClient-supplied field, re-set server-side from session context — see Notes.
GRecaptchaResponsestringConditionalRequired only when brand is chery or exeed and captcha is enabled.
titlestringNoSaved verbatim on the booking row.
firstName / lastNamestringNoSaved on the booking row; used in the notification email body.
commentsstringNoFree text, saved and emailed.
scheduledTimedate stringNoReformatted to Y-m-d before insert; also relayed to the CRM as requestDate.
userIDintNoAny client-sent value is discarded — overwritten from the logged-in session (or 0).
model, variant, variantSKUserver-derivedFilled in from productID during the constructor.
locationCodeserver-derivedFilled in from showroomID during the constructor.
crmCustomerIDserver-derivedResolved from userID via UserModel.
04

Validation rules

A single combined condition (line 164) decides pass/fail — the request fails if any of these are true:

ConditionApplies to
productID is emptyAll brands
phoneNumber is emptyAll brands
showroomID is emptyAll brands except exeed, byd
email is emptyAll brands except ford
subsiteID is emptyAll brands

Any failure short-circuits to the generic errParameterMissing response — the specific missing field is not named back to the caller.

05

Processing steps

The success branch, in execution order.

  1. Captcha gate (brand-conditional) If brand is chery or exeed, verifyCaptchaAction() runs first. It only enforces anything if security_settings.enable_captcha is Yes — otherwise it's a no-op. When enforced, a missing or Google-rejected token ends the request immediately with an HTTP 400 JSON body; bookdriveAction never resumes.
  2. Session context is stamped onto the request userID and subsiteID are overwritten from ObjCommon (the logged-in session), defaulting to 0 when there's no session.
  3. Required-field validation See Validation rules. Failing here skips every remaining step and returns the failure JSON directly.
  4. Booking is persisted BookTestDriveModel::saveBooking($_POST, $languageIDs['Current']) inserts one row into book_a_test_drive. See Database write.
  5. CRM push, conditional on a site setting site_settings.post_lead_to_crm is checked. When Yes, scheduledTime is copied into requestDate and Crm::postLead($_POST, 3, $this->pdtSettings) is called. When not Yes, $crmresponse is simply set to an empty string and no HTTP call happens.
  6. Local status is fixed to success status, responseCode (200), and message are set unconditionally at this point — they do not depend on what the CRM call returned.
  7. Notification email sendMailLeadNotification(3) sends the "Test Drive Request" lead email — unless the brand is omoda or jac, in which case it returns immediately without sending anything.
  8. Response is emitted The meta object is assembled and echoed as pretty-printed JSON, then the request exits.
06

Database write

BookTestDriveModel::saveBooking() runs a single raw INSERT into {DB_PREFIX}book_a_test_drive:

ColumnSourceTransform
title$_POST['title']
firstName$_POST['firstName']
lastName$_POST['lastName']
phoneNumber$_POST['phoneNumber']escaped as integer
email$_POST['email']
productID$_POST['productID']cast to int
userID$_POST['userID']cast to int
requestedTimeserver clockNOW() — not derived from input
languageIDmethod argumentcast 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.

07

CRM integration

Crm::postLead($_POST, 3, $this->pdtSettings) extracts everything out of $_POST and assembles a JSON payload sent as:

POST {crm_api_domain}lead/external-lead
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
}
i

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.

08

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 omoda or jac (or byd with lead type 7), the method returns immediately — no mail is sent.
  • Lead type 3 maps to the string "Test Drive Request", used to fill the {$extra} token in the email template.
  • The template lead-request is loaded, merged with firstName, lastName, email, phoneNumber, model, variant, variantSKU, showroom name, comments, and the request date.
  • Recipients come from the site_settings.lead-notification-email setting — a comma-separated list, each address regex-validated before sending.
09

Output structure

All three outcomes are pretty-printed JSON (JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK), followed by exit.

Success — 200

response body
{
  "meta": {
    "status": "Success",
    "responsecode": 200,
    "message": "<msgTestDriveBookedSuccessfully>",
    "crmresponse": "<raw CRM response, or "" if post_lead_to_crm ≠ Yes>"
  }
}

Validation failure — required field missing

response body
{
  "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

response body · HTTP 400
{
  "meta": {
    "status": "Failure",
    "responsecode": 400,
    "message": "<errParameterMissing or errCaptcha>"
  }
}
10

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.

i

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.