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/lead-mapper.php

<?php if (!defined('ABSPATH')) exit;

/**
 * LBT-1468 — Maps the plugin's internal lead bag (LB_Lead_Dto) to the
 * Leadbox Platform lead-intake contract (POST /api/v1/leads).
 *
 * The contract is a FLAT PascalCase payload (frozen from the legacy C# API).
 * Vehicle/attribution context and any unmapped form fields go into the
 * free-form `AdditionalData` bag. This mapper is the single explicit shaping
 * point — it never blindly forwards arbitrary keys to the new backend.
 *
 * Reads exclusively from LB_Lead_Dto::ToArray(); the DTO's magic __get is
 * unreliable for non-blocklisted keys, so direct property access is avoided.
 *
 * @see lead-intake-endpoint-contract.md (Desktop) for the full field contract.
 */
final class LB_Lead_Mapper
{
  /**
   * Top-level OS fields and the legacy DTO key aliases that feed them.
   * First non-empty alias wins.
   *
   * @var array<string, string[]>
   */
  private static $aliases = array(
    'Name'         => array('first_name', 'firstname', 'fname', 'name'),
    'Lastname'     => array('last_name', 'lastname', 'lname'),
    'Email'        => array('email', 'email_address', 'emailaddress'),
    'Phone'        => array('phone', 'phonenumber', 'phone_number', 'telephone', 'mobile'),
    'AddressLine1' => array('address', 'addressline1', 'address_line_1', 'address1'),
    'AddressLine2' => array('addressline2', 'address_line_2', 'address2'),
    'City'         => array('city'),
    'RegionCode'   => array('regioncode', 'region', 'state', 'province'),
    'PostalCode'   => array('postal_code', 'postalcode', 'zip', 'zipcode', 'postal'),
    'Country'      => array('country'),
  );

  /**
   * OS boolean fields and the legacy DTO key that feeds each one.
   * The DTO already stores these as "true"/"false" strings (see
   * send-lead.php process()), so they are coerced to real booleans here.
   * SkipAutoResponder is accepted-but-ignored by OS; forwarded for parity.
   *
   * @var array<string, string>
   */
  private static $bool_fields = array(
    'SkipADF'           => 'skipadf',
    'SkipDelivr'        => 'skipdelivr',
    'SkipShiftDigital'  => 'skipshiftdigital',
    'SkipAutoResponder' => 'skipautoresponder',
    'HoldVehicle'       => 'hold_vehicle',
  );

  /**
   * Keys never forwarded to Leadbox Platform (legacy-only or already represented).
   * Compared case-insensitively. SendADFTo has no OS equivalent (no ADF
   * recipient list); the rest are DTO blocklist leftovers / internal helpers.
   *
   * @var string[]
   */
  private static $drop = array(
    'sendadfto', 'body', 'subject', 'recipient', 'sender', 'use_html', 'form_id',
  );

  /**
   * @param LB_Lead_Dto $lead
   * @return array PascalCase payload ready to JSON-encode for POST /api/v1/leads.
   */
  public static function toLeadboxOs(LB_Lead_Dto $lead)
  {
    $data = $lead->ToArray();

    // Case-insensitive lookup: lowercased key => original key.
    $ci = array();
    foreach ($data as $key => $value) {
      $ci[strtolower($key)] = $key;
    }

    $payload  = array();
    $consumed = array(); // lowercased keys already placed into top-level fields

    // 1. Aliased core contact fields.
    foreach (self::$aliases as $field => $alias_list) {
      foreach ($alias_list as $alias) {
        if (isset($ci[$alias]) && self::has_value($data[$ci[$alias]])) {
          $payload[$field]   = self::clean($data[$ci[$alias]]);
          $consumed[$alias]  = true;
          break;
        }
      }
    }

    // 2. Message / Comments. Message falls back to comments when absent.
    $message  = self::lookup($data, $ci, 'message');
    $comments = self::lookup($data, $ci, 'comments');
    $consumed['message']  = true;
    $consumed['comments'] = true;
    if (!self::has_value($message) && self::has_value($comments)) {
      $message = $comments;
    }
    if (self::has_value($message)) {
      $payload['Message'] = self::clean($message);
    }
    if (self::has_value($comments)) {
      $payload['Comments'] = self::clean($comments);
    }

    // 3. Source (default "website") and Type (lowercased so OS flow routing
    //    matches "emailafriend"/"financing"; default "general").
    $source = self::lookup($data, $ci, 'source');
    $consumed['source'] = true;
    $payload['Source'] = self::has_value($source) ? self::clean($source) : 'website';

    $type = self::lookup($data, $ci, 'type');
    $consumed['type'] = true;
    $payload['Type'] = self::has_value($type) ? strtolower(self::clean($type)) : 'general';

    // 4. LocationId — omitted by default (OS resolves it from the location-bound
    //    API key). Promoted to top-level only when a form explicitly supplies a
    //    numeric value, so multi-location sites still work.
    foreach (array('location_id', 'locationid') as $loc_key) {
      $consumed[$loc_key] = true;
      if (!isset($payload['LocationId']) && isset($ci[$loc_key]) && is_numeric($data[$ci[$loc_key]])) {
        $payload['LocationId'] = (int) $data[$ci[$loc_key]];
      }
    }

    // 5. VehicleId (numeric only).
    $consumed['vehicleid'] = true;
    if (isset($ci['vehicleid']) && is_numeric($data[$ci['vehicleid']])) {
      $payload['VehicleId'] = (int) $data[$ci['vehicleid']];
    }

    // 6. Notification override + email-a-friend fields (forwarded when present).
    $consumed['sendto'] = true;
    if (isset($ci['sendto']) && self::has_value($data[$ci['sendto']])) {
      $payload['SendTo'] = self::clean($data[$ci['sendto']]);
    }
    foreach (array('FriendName' => array('friend_name', 'friendname'), 'FriendEmail' => array('friend_email', 'friendemail')) as $field => $alias_list) {
      foreach ($alias_list as $alias) {
        $consumed[$alias] = true;
        if (!isset($payload[$field]) && isset($ci[$alias]) && self::has_value($data[$ci[$alias]])) {
          $payload[$field] = self::clean($data[$ci[$alias]]);
        }
      }
    }

    // 7. Boolean flags (string "true"/"false" -> real bool). Only when present.
    foreach (self::$bool_fields as $field => $src_key) {
      $consumed[$src_key] = true;
      if (isset($ci[$src_key])) {
        $payload[$field] = self::to_bool($data[$ci[$src_key]]);
      }
    }

    // 8. AdditionalData — everything of value not already mapped or dropped.
    $additional = array();
    foreach ($data as $key => $value) {
      $lc = strtolower($key);
      if (isset($consumed[$lc]) || in_array($lc, self::$drop, true) || !self::has_value($value)) {
        continue;
      }
      // stocknumber is the documented vehicle-resolution key inside AdditionalData.
      if ($lc === 'stocknumber') {
        $additional['stock'] = self::clean($value);
        continue;
      }
      $additional[$key] = is_scalar($value) ? self::clean($value) : $value;
    }
    if (!empty($additional)) {
      $payload['AdditionalData'] = $additional;
    }

    return $payload;
  }

  /**
   * @param array $data
   * @param array $ci    lowercased-key => original-key map
   * @param string $lc_key lowercased key to look up
   * @return mixed|null
   */
  private static function lookup($data, $ci, $lc_key)
  {
    return isset($ci[$lc_key]) ? $data[$ci[$lc_key]] : null;
  }

  /**
   * A value is meaningful if it is non-null and not an empty string / empty array.
   * "0" is considered a value.
   */
  private static function has_value($value)
  {
    if ($value === null) {
      return false;
    }
    if (is_string($value)) {
      return trim($value) !== '';
    }
    if (is_array($value)) {
      return count($value) > 0;
    }
    return true;
  }

  private static function clean($value)
  {
    return is_scalar($value) ? trim((string) $value) : $value;
  }

  /**
   * Coerce legacy truthy representations ("true"/"1"/"yes"/"on"/true) to bool.
   */
  private static function to_bool($value)
  {
    if (is_bool($value)) {
      return $value;
    }
    return in_array(strtolower(trim((string) $value)), array('1', 'true', 'yes', 'on'), true);
  }
}