AlkantarClanX12

Your IP : 216.73.217.24


Current Path : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/classes/
Upload File :
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);
    }
  }
}

Home - Capital GMC Buick Regina

No data

Welcome to Capital GMC BUICK – REGINA

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!

Ask a Question