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