Current File : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/classes/os-api.php
<?php if (!defined('ABSPATH')) exit;
/**
* LBT-1468 — HTTP client for the Leadbox Platform lead-intake endpoint
* (POST {url}/api/v1/leads, Bearer auth).
*
* Mirrors LB_API_Client::CreateLead()'s return contract exactly — the
* standardized { success, data, count, message, error_code, timestamp } array —
* so the router and the Ninja Forms action can consume either client without
* branching on which backend handled the lead.
*
* @see lead-intake-endpoint-contract.md (Desktop) for status/error mapping.
*/
final class LB_OS_API_Client
{
/** Matches the legacy client's 5s budget; OS pipeline is sub-second + async fan-out. */
const TIMEOUT_SECONDS = 5;
private $url;
private $token;
public function __construct($url, $token)
{
$this->url = $url;
$this->token = $token;
}
public static function getInstance()
{
$settings = LB_API_Settings::getInstance();
return new LB_OS_API_Client(
$settings->getLeadboxOsLeadIntakeUrl(),
$settings->getLeadboxOsLeadIntakeToken()
);
}
/**
* Same shape as LB_API_Client::create_response().
*/
private function create_response($success, $data, $message = null, $error_code = null, $http_status = null, $latency_ms = null)
{
return array(
'success' => $success,
'data' => $data,
'count' => is_array($data) ? count($data) : 0,
'message' => $message,
'error_code' => $error_code,
// LBT-1468 — surfaced for the dispatch log so the router can record the
// real HTTP status and latency without re-deriving them.
'http_status' => $http_status,
'latency_ms' => $latency_ms,
'timestamp' => time(),
);
}
/**
* POST a lead to Leadbox Platform.
*
* @param LB_Lead_Dto $lead
* @param bool $blocking When false, fire-and-forget (used by the 'both'
* dual-write mode so Leadbox Platform never blocks the legacy
* authoritative POST). Non-blocking cannot read a status.
* @return array Standardized response.
*/
public function CreateLead($lead, $blocking = true)
{
if (false === ($lead instanceof LB_Lead_Dto)) {
throw new Exception('$lead needs to be instance of Lead Object');
}
// The URL always resolves to a valid base (configured value or the default
// constant), so only the per-site token can be missing.
if (empty($this->token)) {
$this->log_error('Leadbox Platform lead-intake not configured', array(
'has_token' => false,
));
return $this->create_response(
false,
null,
'Leadbox Platform lead-intake token is not configured.',
'os_not_configured'
);
}
// LBT-1468 — resolve the vehicle's stock number from its (website) id so the
// Platform can match the vehicle by stock, which is stable across systems.
// Best-effort: never throws, never blocks the dispatch on failure.
self::maybe_enrich_stock($lead);
$payload = LB_Lead_Mapper::toLeadboxOs($lead);
$endpoint = $this->url . '/api/v1/leads';
$args = array(
'timeout' => self::TIMEOUT_SECONDS,
'blocking' => (bool) $blocking,
// LBT-1468 — relax TLS verification only for loopback endpoints so local
// dev can target a Platform over http(s)://localhost with a self-signed or
// no certificate. Production hosts are never loopback, so they keep
// verification on.
'sslverify' => !LB_API_Settings::isLoopbackHost(parse_url($this->url, PHP_URL_HOST)),
'headers' => array(
'Authorization' => 'Bearer ' . $this->token,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
'body' => wp_json_encode($payload),
);
$start = microtime(true);
$result = wp_remote_post($endpoint, $args);
$latency = (int) round((microtime(true) - $start) * 1000);
// Transport-level failure (DNS, timeout, refused).
if (is_wp_error($result)) {
$this->log_error('Leadbox Platform lead-intake connection failed', array(
'endpoint' => $endpoint,
'latency_ms' => $latency,
'error' => $result->get_error_message(),
));
return $this->create_response(
false,
null,
'Unable to connect to Leadbox Platform. Please try again later.',
'network_error',
null,
$latency
);
}
// Fire-and-forget: WordPress returns immediately with no readable status.
if (!$blocking) {
return $this->create_response(true, null, 'Dispatched to Leadbox Platform (non-blocking).', null);
}
$status = (int) wp_remote_retrieve_response_code($result);
$body = wp_remote_retrieve_body($result);
$decoded = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$decoded = array();
}
return $this->map_response($status, $decoded, $latency, $endpoint);
}
/**
* Map an HTTP status + decoded body to the standardized response shape.
*/
private function map_response($status, $decoded, $latency, $endpoint)
{
$message = isset($decoded['message']) ? $decoded['message'] : null;
if ($status >= 200 && $status < 300) {
$data = isset($decoded['data']) ? $decoded['data'] : $decoded;
$this->log_info('Leadbox Platform lead dispatched', array(
'status' => $status,
'latency_ms' => $latency,
));
return $this->create_response(true, $data, $message, null, $status, $latency);
}
switch ($status) {
case 400:
$error_code = 'spam_rejected';
break;
case 401:
$error_code = 'auth_failed';
break;
case 403:
$error_code = 'location_mismatch';
break;
case 422:
// OS distinguishes unresolvable-location from field validation via `error`.
$error_code = (isset($decoded['error']) && $decoded['error'] === 'location_unresolved')
? 'location_unresolved'
: 'validation_failed';
break;
case 429:
$error_code = 'rate_limited';
break;
default:
$error_code = ($status >= 500) ? 'server_error' : ('http_error_' . $status);
break;
}
if ($message === null) {
$message = 'Leadbox Platform rejected the lead (HTTP ' . $status . ').';
}
$this->log_error('Leadbox Platform lead failed', array(
'endpoint' => $endpoint,
'status' => $status,
'error_code' => $error_code,
'latency_ms' => $latency,
'message' => $message,
));
return $this->create_response(false, null, $message, $error_code, $status, $latency);
}
/**
* LBT-1468 — Best-effort enrichment: when the lead carries a numeric website
* vehicle id but no stock number, resolve the vehicle's `stocknumber` and add
* it to the DTO. The mapper then forwards it as AdditionalData.stock, which the
* Platform resolves the vehicle by — stable across systems, unlike the website's
* vehicle id (which does not match the Platform's own inventory ids).
*
* Resolution is FILE-FIRST: the per-vehicle inventory file
* (uploads/data/{id}.json) is the same source that renders the VDP, so its id
* matches the one we derive here, and it needs no external call (no SaaS API
* latency/timeout, no credentials). Only when that file is unavailable or
* carries no stock do we fall back to the inventory API (VehicleDetail).
*
* Never throws and never blocks the dispatch on failure (the lead is still
* sent, just without a resolved stock).
*
* @param LB_Lead_Dto $lead
* @return void
*/
private static function maybe_enrich_stock($lead)
{
if (!($lead instanceof LB_Lead_Dto)) {
return;
}
$vehicle_id = null;
$referrer = null;
foreach ($lead->ToArray() as $key => $value) {
$lc = strtolower($key);
if (($lc === 'stocknumber' || $lc === 'stock') && self::value_present($value)) {
return; // A stock is already present — nothing to resolve.
}
if ($lc === 'vehicleid' && is_numeric($value)) {
$vehicle_id = (int) $value;
}
if ($lc === 'referrer' && is_string($value)) {
$referrer = $value;
}
}
// Forms embedded on a VDP (clean permalink, no `?vehicle_id=`) submit an
// empty `vehicleid`. Fall back to the vehicle id in the page URL the plugin
// sends as `referrer`: the VDP slug ends in `-{id}` (see get_vdp_slug()).
if ($vehicle_id === null && $referrer !== null) {
$vehicle_id = self::vehicle_id_from_referrer($referrer);
}
if ($vehicle_id === null) {
return; // No vehicle id to resolve a stock from.
}
// 1) File-first: read the stock from the local inventory file (no external
// call, no timeout, same source as the rendered VDP).
$stock = self::stocknumber_from_local_inventory($vehicle_id);
$source = 'local_inventory_file';
// 2) Fallback to the inventory API only when the local file is missing or
// carries no stock (e.g. sites that render inventory straight from the API).
if ($stock === '' && class_exists('LB_API_Client')) {
try {
$resp = LB_API_Client::getInstance()->VehicleDetail($vehicle_id);
} catch (Exception $e) {
$resp = null;
}
if (is_array($resp) && !empty($resp['success']) && isset($resp['data'])) {
$stock = self::extract_stocknumber($resp['data']);
$source = 'inventory_api';
}
}
if ($stock === '') {
return;
}
// Merge into the DTO; LB_Lead_Mapper maps `stocknumber` -> AdditionalData.stock.
$lead->FromArray(array('stocknumber' => $stock));
if (function_exists('leadbox_logger')) {
leadbox_logger()->info('Leadbox Platform lead enriched with stock from vehicle id', array(
'vehicle_id' => $vehicle_id,
'source' => $source,
));
}
}
/**
* LBT-1468 — Resolve a vehicle's stock number from the local inventory file
* (uploads/data/{id}.json) via LB_Inventory_Manager, reusing the same reader
* that renders the VDPs. Returns '' when the manager is unavailable, the file
* is missing, or it carries no stock. Never throws.
*
* @param int $vehicle_id Website/file vehicle id (the per-vehicle file name).
* @return string Trimmed stock number, or '' if not resolvable from a file.
*/
private static function stocknumber_from_local_inventory($vehicle_id)
{
if (!class_exists('LB_Inventory_Manager')) {
return '';
}
try {
$vehicle = LB_Inventory_Manager::getInstance()->get_vehicle_by_id($vehicle_id);
} catch (Exception $e) {
return '';
}
return self::extract_stocknumber($vehicle);
}
/**
* Extract the vehicle id from a Leadbox VDP URL. Pure (no WordPress) so it is
* unit-testable. The VDP slug is built as
* `/{view|vue}/{condition}-{year}-{make}-{model}-{id}/` (see get_vdp_slug()),
* so the id is the last `-`-delimited token of the final path segment.
*
* @param mixed $referrer The page URL the lead was submitted from.
* @return int|null The vehicle id, or null if the URL is not a VDP slug.
*/
public static function vehicle_id_from_referrer($referrer)
{
if (!is_string($referrer) || $referrer === '') {
return null;
}
$path = parse_url($referrer, PHP_URL_PATH);
if (!is_string($path) || $path === '') {
return null;
}
$segments = array_values(array_filter(explode('/', $path), 'strlen'));
if (empty($segments)) {
return null;
}
$tokens = explode('-', end($segments));
$candidate = end($tokens);
return ctype_digit((string) $candidate) ? (int) $candidate : null;
}
/**
* Pull a stock number out of a vehicle-detail payload. Pure (no WordPress) so
* it is unit-testable. Handles the vehicle object at the top level or nested
* under a common wrapper key. Returns a trimmed string, or '' if absent.
*
* @param mixed $vehicle Decoded vehicle-detail data.
* @return string
*/
public static function extract_stocknumber($vehicle)
{
if (!is_array($vehicle)) {
return '';
}
if (isset($vehicle['stocknumber']) && is_scalar($vehicle['stocknumber'])) {
return trim((string) $vehicle['stocknumber']);
}
foreach (array('data', 'vehicle', 'result') as $k) {
if (isset($vehicle[$k]['stocknumber']) && is_scalar($vehicle[$k]['stocknumber'])) {
return trim((string) $vehicle[$k]['stocknumber']);
}
}
return '';
}
/**
* A value is present if non-null and not an empty / whitespace-only string.
*/
private static function value_present($value)
{
if ($value === null) {
return false;
}
if (is_string($value)) {
return trim($value) !== '';
}
return !empty($value);
}
private function log_info($message, $context)
{
if (function_exists('leadbox_logger')) {
leadbox_logger()->info($message, $context);
}
}
private function log_error($message, $context)
{
if (function_exists('leadbox_logger')) {
leadbox_logger()->error($message, $context);
}
}
}
Thank you for choosing Capital GMC Buick | Regina, your premier certified Buick and GMC dealership proudly serving drivers in Regina and the surrounding communities. Whether you’re searching for a brand-new Buick or GMC vehicle or a meticulously inspected pre-owned model, we have a diverse selection to match your needs and lifestyle.
Beyond our impressive inventory, we offer a seamless and stress-free financing experience through our well-connected finance centre, where our team of experts is dedicated to securing the best loan or lease options for you, quickly, transparently, and hassle-free.
But our commitment to you doesn’t stop at the sale. Our state-of-the-art service centre is staffed with skilled Buick and GMC technicians who use the latest equipment and genuine OEM parts to keep your vehicle running at its best. From routine maintenance to complex repairs, we’ve got you covered.
Experience top-tier customer service, quality vehicles, and expert care, all in one place. Visit Capital GMC Buick | Regina today or call us at 306-205-8072 with any questions. We’re here to help!
Have a question or need assistance? Fill out the form and we will reach out to you as soon as possible.
Schedule a Visit
Let us know when you are coming and how we can assist you. We can ensure someone will be on hand to help you out at the desired date and time.
Find a Career
Have a look at our list of available positions and apply online today to join our team!
Opportunities to Grow
The auto industry is constantly changing and we want to continue to grow. We offer growth, leadership & mentorship programs to allow our staff to grow with us.
Competitive Salary
We have a significant earning potential with incentive-based pay in most roles. We also offer an employee referral bonus with paid bonuses.
Health & Dental
We offer a comprehensive benefits package including extended health, dental, and vision care. We also include paramedical, life insurance, paid sick leave, short & long-term disability coverages.
Vacation
We value our employees and want everyone to take their vacation time. We offer a minimum of 2 weeks vacation each year.
Training & Development
We have many opportunities for paid education and training in-house as well as training from the Manufacturer.
$10,000 Cash Giveaway – Terms & Conditions
All October long, stop by Capital GMC Buick Cadillac, to enter for your chance to win $10,000 cash. No purchase is required, but entries must be made in-store.
The contest is open to residents of Saskatchewan who are 18+. Dealership employees and their households are not eligible. Entries will be accepted from October 1 to October 31, 2025. A random draw will take place on November 1, 2025.
The prize is one $10,000 award, paid by cheque, and must be accepted as awarded. Winner will be contacted by phone or email and must respond within 7 days or another entry may be drawn. Odds of winning depend on the number of entries received.By entering, you agree that Capital Automotive Group may use your name and photo for winner announcements. The contest is governed by the laws of Saskatchewan.