%PDF- %PDF-
| Direktori : /home/bitrix/www/bitrix/modules/sale/lib/delivery/tracking/ |
| Current File : /home/bitrix/www/bitrix/modules/sale/lib/delivery/tracking/manager.php |
<?php
namespace Bitrix\Sale\Delivery\Tracking;
use Bitrix\Main\ArgumentNullException;
use Bitrix\Main\Config\Option;
use Bitrix\Main\Error;
use Bitrix\Main\Event;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\SystemException;
use Bitrix\Sale\Delivery\Services;
use Bitrix\Sale\Internals\ShipmentTable;
use Bitrix\Sale\Order;
use Bitrix\Sale\PropertyValueCollection;
use Bitrix\Sale\Result;
use Bitrix\Sale\Shipment;
Loc::loadMessages(__FILE__);
/**
* Class Statuses
* @package Bitrix\Sale\Delivery\Tracking
*
* Possible internals variants of tracking statuses
*/
class Statuses
{
const NO_INFORMATION = 0; //No tracking (may be yet)
const WAITING_SHIPMENT = 10;
const ON_THE_WAY = 20;
const ARRIVED = 30;
const HANDED = 40; //shipping definitely finished
const PROBLEM = 50;
const UNKNOWN = 60; //Incorrect status mapping made by tracking handler.
const RETURNED = 70;
}
/**
* Class StatusResult
* @package Bitrix\Sale\Delivery\Tracking
*/
class StatusResult extends Result
{
/** @var int */
public $status;
/** @var string */
public $description;
/** @var string */
public $trackingNumber;
/** @var int */
public $lastChangeTimestamp;
}
/**
* Class StatusChangeEventParam
* @package Bitrix\Sale\Delivery\Tracking
*/
class StatusChangeEventParam
{
/** @var int */
public $status;
/** @var string */
public $description;
/** @var string */
public $trackingNumber;
/** @var int */
public $lastChangeTimestamp;
/** @var int */
public $orderId;
/** @var int */
public $shipmentId;
/** @var int */
public $deliveryId;
}
/**
* Class Manager
* @package Bitrix\Sale\Delivery\Tracking
*
* Singleton
*
* All job with tracking numbers must must be done through this class
*/
class Manager
{
protected static $instance = null;
protected static $classNames = null;
//If status didn't changed for a long time let's stop update it.
protected static $activeStatusLiveTime = 5184000; //60 days
/** @var bool */
protected $isClone = false;
protected function __clone(){}
protected function __construct()
{
self::initClassNames();
}
/**
* @return static
*/
public static function getInstance()
{
if (self::$instance === null)
self::$instance = new static();
return self::$instance;
}
/**
* @param int $status
* @return string
*/
public static function getStatusName($status)
{
$statusesNames = self::getStatusesList();
$status = intval($status);
if(empty($statusesNames[$status]))
return Loc::getMessage("SALE_DTM_STATUS_NAME_UNKNOWN");
return $statusesNames[$status];
}
/**
* @return array
*/
public static function getStatusesList()
{
return array(
Statuses::NO_INFORMATION => Loc::getMessage("SALE_DTM_STATUS_NAME_NO_INFORMATION"),
Statuses::WAITING_SHIPMENT => Loc::getMessage("SALE_DTM_STATUS_NAME_WAITING_SHIPMENT"),
Statuses::ON_THE_WAY => Loc::getMessage("SALE_DTM_STATUS_NAME_ON_THE_WAY"),
Statuses::ARRIVED => Loc::getMessage("SALE_DTM_STATUS_NAME_ARRIVED"),
Statuses::HANDED => Loc::getMessage("SALE_DTM_STATUS_NAME_HANDED"),
Statuses::PROBLEM => Loc::getMessage("SALE_DTM_STATUS_NAME_PROBLEM"),
Statuses::UNKNOWN => Loc::getMessage("SALE_DTM_STATUS_NAME_UNKNOWN"),
Statuses::RETURNED => Loc::getMessage("SALE_DTM_STATUS_NAME_RETURNED"),
);
}
/**
* @param int $deliveryId Delivery service id.
* @param string $trackingNumber Trcking number.
* @return string Url were we can see tracking information.
* @throws ArgumentNullException
*/
public function getTrackingUrl($deliveryId, $trackingNumber = '')
{
if(!$deliveryId)
return '';
$trackingObject = $this->getTrackingObjectByDeliveryId($deliveryId);
if(!$trackingObject)
return '';
return $trackingObject->getTrackingUrl($trackingNumber);
}
/**
* @param int $shipmentId
* @param string $trackingNumber if changed
* @return StatusResult
* @throws ArgumentNullException
* @throws SystemException
* @throws \Bitrix\Main\ArgumentException
*/
public function getStatusByShipmentId($shipmentId, $trackingNumber = '')
{
if(intval($shipmentId) <= 0)
throw new ArgumentNullException('shipmentId');
$result = new StatusResult();
$res = ShipmentTable::getList(array(
'filter' => array(
'ID'=>$shipmentId
),
'select' => array(
'ID', 'ORDER_ID', 'DELIVERY_ID', 'TRACKING_STATUS', 'TRACKING_NUMBER'
)
));
if(!$shipment = $res->fetch())
{
$result->addError(new Error("Can't find shipment with id:\"".$shipmentId.'"'));
return $result;
}
if(strlen($trackingNumber) > 0 && $trackingNumber != $shipment['TRACKING_NUMBER'])
$shipment['TRACKING_NUMBER'] = $trackingNumber;
if(strlen($shipment['TRACKING_NUMBER']) <= 0)
return $result;
$result = $this->getStatus($shipment);
if($result->isSuccess())
{
if($shipment['TRACKING_STATUS'] != $result->status)
{
$eventParams = new StatusChangeEventParam();
$eventParams->orderId = $shipment['ORDER_ID'];
$eventParams->shipmentId = $shipmentId;
$eventParams->status = $result->status;
$eventParams->trackingNumber = $shipment['TRACKING_NUMBER'];
$eventParams->description = $result->description;
$eventParams->lastChangeTimestamp = $result->lastChangeTimestamp;
$eventParams->deliveryId = $shipment['DELIVERY_ID'];
$res = $this->processStatusChange(array($eventParams));
if(!$res)
$result->addErrors($res->getErrors());
}
}
return $result;
}
/**
* Returns mapping tracking statuses to shipment statuses.
* @return array
* @throws ArgumentNullException
*/
protected static function getMappedStatuses()
{
$result = unserialize(Option::get('sale', 'tracking_map_statuses',''));
if(!is_array($result))
$result = array();
return $result;
}
protected static function getCheckPeriod()
{
return (int)Option::get('sale', 'tracking_check_period', '24');
}
/**
* @param $trackingNumber
* @param $deliveryId
* @return StatusResult
* @throws ArgumentNullException
* @throws SystemException
*/
protected function getStatus($shipment)
{
$result = new \Bitrix\Sale\Result();
if(intval($shipment['DELIVERY_ID']) <= 0)
throw new ArgumentNullException('deliveryId');
$trackingObject = $this->getTrackingObjectByDeliveryId($shipment['DELIVERY_ID']);
if(!$trackingObject)
return $result;
return $trackingObject->getStatusShipment($shipment);
}
/**
* @param $deliveryId
* @return Base|null
* @throws ArgumentNullException
* @throws SystemException
* @throws \Bitrix\Main\ArgumentException
*/
public function getTrackingObjectByDeliveryId($deliveryId)
{
if(intval($deliveryId) <= 0)
throw new ArgumentNullException('deliveryId');
$result = null;
$deliveryService = Services\Manager::getObjectById($deliveryId);
if(!$deliveryService)
return null;
$class = $deliveryService->getTrackingClass();
if(strlen($class) > 0)
{
$result = $this->createTrackingObject(
$class,
$deliveryService->getTrackingParams(),
$deliveryService
);
}
return $result;
}
/**
* @param string $className Class name delivered from \Bitrix\Sale\Delivery\Tracking\Base
* @param array $params
* @return Base
* @throws ArgumentNullException
* @throws SystemException
*/
protected function createTrackingObject($className, array $params, Services\Base $deliveryService)
{
if(strlen($className) <= 0)
throw new ArgumentNullException('className');
if(!class_exists($className))
throw new SystemException('Class "'.$className.'" does not exist!');
if(get_parent_class($className) != 'Bitrix\Sale\Delivery\Tracking\Base')
throw new SystemException($className.' is not inherited from \"\Bitrix\Sale\Delivery\Tracking\Base\"!');
return new $className($params, $deliveryService);
}
/**
* Starts statuses refreshing
*/
public static function startRefreshingStatuses()
{
$manager = self::getInstance();
$result = $manager->updateStatuses();
if(!$result->isSuccess())
{
$eventLog = new \CEventLog;
$eventLog->Add(array(
"SEVERITY" => \CEventLog::SEVERITY_ERROR,
"AUDIT_TYPE_ID" => 'SALE_DELIVERY_TRACKING_REFRESHING_STATUS_ERROR',
"MODULE_ID" => "sale",
"ITEM_ID" => time(),
"DESCRIPTION" => implode('\n', $result->getErrorMessages())
));
}
$data = $result->getData();
if(!empty($data))
$manager->processStatusChange($data);
return '\Bitrix\Sale\Delivery\Tracking\Manager::startRefreshingStatuses();';
}
/**
* @return Result
* @throws ArgumentNullException
* @throws \Bitrix\Main\ArgumentException
* todo: timelimit
*/
protected function updateStatuses()
{
$result = new Result();
$checkPeriod = self::getCheckPeriod();
if($checkPeriod <= 0)
return $result;
$lastChage = \Bitrix\Main\Type\DateTime::createFromTimestamp(time()-self::$activeStatusLiveTime);
$lastUpdate = \Bitrix\Main\Type\DateTime::createFromTimestamp(time()-$checkPeriod*60*60);
$dbRes = ShipmentTable::getList(array(
'filter' => array(
'!=TRACKING_NUMBER' => false,
'!=DELIVERY_ID' => false,
array(
'LOGIC' => 'OR',
array(
'LOGIC' => 'AND',
array('!=TRACKING_STATUS' => Statuses::HANDED),
array('!=TRACKING_STATUS' => Statuses::RETURNED),
),
array('=TRACKING_STATUS' => false)
),
array(
'LOGIC' => 'OR',
array(
'LOGIC' => 'AND',
array('=TRACKING_LAST_CHANGE' => false),
array('>=DATE_INSERT' => $lastChage),
),
array('>=TRACKING_LAST_CHANGE' => $lastChage)
),
array(
'LOGIC' => 'OR',
array('=TRACKING_LAST_CHECK' => false),
array('<=TRACKING_LAST_CHECK' => $lastUpdate)
)
),
'select' => array(
'ID', 'ORDER_ID', 'DELIVERY_ID', 'TRACKING_STATUS', 'TRACKING_NUMBER'
),
'order' => array(
'DELIVERY_ID' => 'ASC'
)
));
$deliveryId = 0;
$shipmentsData = array();
while($shipment = $dbRes->fetch())
{
if(!isset($shipmentsData[$shipment['DELIVERY_ID']]))
$shipmentsData[$shipment['DELIVERY_ID']] = array();
if(strlen($shipment['TRACKING_NUMBER']) <= 0)
continue;
$shipmentsData[$shipment['DELIVERY_ID']][$shipment['TRACKING_NUMBER']] = array(
'SHIPMENT_ID' => $shipment['ID'],
'ORDER_ID' => $shipment['ORDER_ID'],
'DELIVERY_ID' => $shipment['DELIVERY_ID'],
'TRACKING_STATUS' => $shipment['TRACKING_STATUS']
);
if($shipment['DELIVERY_ID'] != $deliveryId && $deliveryId > 0)
{
$res = $this->processStatusesByDelivery($deliveryId, $shipmentsData[$deliveryId]);
if($res->isSuccess())
{
$data = $res->getData();
if(!empty($data))
$result->setData(array_merge($result->getData(), $data));
}
else
{
$result->addErrors($res->getErrors());
}
$deliveryId = $shipment['DELIVERY_ID'];
}
if($deliveryId <= 0)
$deliveryId = $shipment['DELIVERY_ID'];
}
if($deliveryId > 0)
{
$res = $this->processStatusesByDelivery($deliveryId, $shipmentsData[$deliveryId]);
if($res->isSuccess())
{
$data = $res->getData();
if(!empty($data))
$result->setData(array_merge($result->getData(), $data));
}
else
{
$result->addErrors($res->getErrors());
}
}
return $result;
}
protected function processStatusesByDelivery($deliveryId, $shipmentsData)
{
$result = new Result();
$trackingObject = $this->getTrackingObjectByDeliveryId($deliveryId);
if($trackingObject)
{
$statusResults = $trackingObject->getStatusesShipment($shipmentsData);
$eventsParams = array();
/** @var StatusResult $statusResult */
foreach($statusResults as $number => $statusResult)
{
$eventParams = null;
if(empty($shipmentsData[$number]))
continue;
if(!$statusResult->isSuccess())
{
$eventLog = new \CEventLog;
$eventLog->Add(array(
"SEVERITY" => \CEventLog::SEVERITY_ERROR,
"AUDIT_TYPE_ID" => 'SALE_DELIVERY_TRACKING_STATUS_RESULT',
"MODULE_ID" => "sale",
"ITEM_ID" => $shipmentsData[$number]['SHIPMENT_ID'],
"DESCRIPTION" => implode('\n', $statusResult->getErrorMessages())
));
continue;
}
if(($statusResult->status != $shipmentsData[$number]['TRACKING_STATUS']))
{
$eventParams = new StatusChangeEventParam();
$eventParams->orderId = $shipmentsData[$number]['ORDER_ID'];
$eventParams->shipmentId = $shipmentsData[$number]['SHIPMENT_ID'];
$eventParams->status = $statusResult->status;
$eventParams->trackingNumber = $number;
$eventParams->description = $statusResult->description;
$eventParams->lastChangeTimestamp = $statusResult->lastChangeTimestamp;
$eventParams->deliveryId = $deliveryId;
$eventsParams[] = $eventParams;
}
$res = $this->updateShipment(
$shipmentsData[$number]['SHIPMENT_ID'],
$statusResult
);
if(!$res->isSuccess())
{
$eventLog = new \CEventLog;
$eventLog->Add(array(
"SEVERITY" => \CEventLog::SEVERITY_ERROR,
"AUDIT_TYPE_ID" => 'SALE_DELIVERY_TRACKING_UPDATE_SHIPMENT',
"MODULE_ID" => "sale",
"ITEM_ID" => $shipmentsData[$number]['SHIPMENT_ID'],
"DESCRIPTION" => implode('\n', $res->getErrorMessages())
));
}
}
$result->setData($eventsParams);
}
return $result;
}
/**
* @param StatusChangeEventParam[] $params
* @return Result
* @throws ArgumentNullException
* @throws \Bitrix\Main\NotSupportedException
*/
protected function processStatusChange($params)
{
$result = new Result();
foreach($params as $param)
{
if(intval($param->status) <= 0 && strlen($param->description) <= 0)
continue;
$mappedStatuses = $this->getMappedStatuses();
if(!empty($mappedStatuses[$param->status]))
{
/** @var Order $order */
$order = Order::load($param->orderId);
$shipmentCollection = null;
$oShipment = null;
if($order)
{
/** @var \Bitrix\Sale\ShipmentCollection $shipmentCollection */
$shipmentCollection = $order->getShipmentCollection();
}
if($shipmentCollection)
{
/** @var Shipment $oShipment */
$oShipment = $shipmentCollection->getItemById($param->shipmentId);
}
if($oShipment)
{
$res = $oShipment->setField('STATUS_ID', $mappedStatuses[$param->status]);
if($res->isSuccess())
{
$res = $order->save();
if(!$res->isSuccess())
$result->addErrors($res->getErrors());
}
else
{
$result->addErrors($res->getErrors());
}
}
}
}
$this->sendOnStatusesChangedEvent($params);
$this->sendStatusChangedMail($params);
return $result;
}
/**
* @param StatusChangeEventParam[] $params
* @return bool|int
*/
protected function sendStatusChangedMail($params)
{
if(empty($params))
return true;
/** @var StatusChangeEventParam[] $paramsByShipmentId */
$paramsByShipmentId = array();
foreach($params as $status)
$paramsByShipmentId[$status->shipmentId] = $status;
$res = ShipmentTable::getList(array(
'filter' => array(
'=ID' => array_keys($paramsByShipmentId)
),
'select' => array(
'DELIVERY_NAME',
'SITE_ID' => 'ORDER.LID',
'SITE_NAME' => 'SITE.NAME',
'SHIPMENT_NO' => 'ID',
'SHIPMENT_DATE' => 'DATE_INSERT',
'ORDER_NO' => 'ORDER.ACCOUNT_NUMBER',
'ORDER_DATE' => 'ORDER.DATE_INSERT',
'USER_NAME' => 'ORDER.USER.NAME',
'USER_LAST_NAME' => 'ORDER.USER.LAST_NAME',
'EMAIL' => 'ORDER.USER.EMAIL'
),
'runtime' => array(
'SITE' => array(
'data_type' => 'Bitrix\Main\SiteTable',
'reference' => array(
'ref.LID' => 'this.ORDER.LID',
),
'join_type' => 'left'
),
)
));
$event = new \CEvent;
while($data = $res->fetch())
{
$userEmail = '';
$userName = '';
$order = Order::load($paramsByShipmentId[$data['SHIPMENT_NO']]->orderId);
/** @var PropertyValueCollection $propertyCollection */
if ($propertyCollection = $order->getPropertyCollection())
{
if ($propUserEmail = $propertyCollection->getUserEmail())
$userEmail = $propUserEmail->getValue();
if ($propPayerName = $propertyCollection->getPayerName())
$userName = $propPayerName->getValue();
}
if(empty($userEmail))
$userEmail = $data['EMAIL'];
if(empty($userName))
$userName = $data["USER_NAME"].((strlen($data["USER_NAME"])<=0 || strlen($data["USER_LAST_NAME"])<=0) ? "" : " ").$data["USER_LAST_NAME"];
$siteFields = \CAllEvent::GetSiteFieldsArray($data['SITE_ID']);
$fields = array(
'SITE_NAME' => $data['SITE_NAME'],
'ORDER_NO' => $data['ORDER_NO'],
'ORDER_DATE' => $data['ORDER_DATE']->toString(),
'ORDER_USER' => $userName,
'SHIPMENT_NO' => $data['SHIPMENT_NO'],
'SHIPMENT_DATE' => $data['SHIPMENT_DATE']->toString(),
'EMAIL' => $userEmail,
'STATUS_NAME' => self::getStatusName($paramsByShipmentId[$data['SHIPMENT_NO']]->status),
'STATUS_DESCRIPTION' => $paramsByShipmentId[$data['SHIPMENT_NO']]->description,
'TRACKING_NUMBER' => $paramsByShipmentId[$data['SHIPMENT_NO']]->trackingNumber,
'DELIVERY_NAME' => $data['DELIVERY_NAME'],
"ORDER_ACCOUNT_NUMBER_ENCODE" => urlencode(urlencode($data['ORDER_NO'])),
"SALE_EMAIL" => Option::get("sale", "order_email", "order@".$_SERVER["SERVER_NAME"]),
);
$fields['ORDER_DETAIL_URL'] = Loc::getMessage(
'SALE_DTM_ORDER_DETAIL_URL',
array(
'#A1#' => '<a href="http://'.$siteFields['SERVER_NAME'].'/personal/order/detail/'.$fields['ORDER_ACCOUNT_NUMBER_ENCODE'].'/">',
'#A2#' => '</a>'
)
).'.';
$trackingUrl = self::getTrackingUrl(
$paramsByShipmentId[$data['SHIPMENT_NO']]->deliveryId,
$paramsByShipmentId[$data['SHIPMENT_NO']]->trackingNumber
);
$deliveryTrackingUrl = '';
if(strlen($trackingUrl) > 0)
{
$deliveryTrackingUrl = Loc::getMessage(
'SALE_DTM_SHIPMENT_STATUS_TRACKING_URL',
array(
'#A1#' => '<a href="'.$trackingUrl.'">',
'#A2#' => '</a>'
)
).".<br><br>";
}
$fields['DELIVERY_TRACKING_URL'] = $deliveryTrackingUrl;
$event->Send("SALE_ORDER_SHIPMENT_STATUS_CHANGED", $data['SITE_ID'], $fields, "N");
}
return true;
}
/**
* @param StatusChangeEventParam[] $params
* @throws SystemException
*/
protected function sendOnStatusesChangedEvent(array $params)
{
$event = new Event('sale', 'onSaleShipmentsTrackingStatusesChanged', $params);
$event->send();
}
/**
* @throws SystemException
* @throws \Bitrix\Main\LoaderException
*
* For custom handlers use (for example in init.php)
* paste code like this:
*
* function addCustomDeliveryTrackingServices()
* {
* return new \Bitrix\Main\EventResult(
* \Bitrix\Main\EventResult::SUCCESS,
* array(
* '\Custom\Name\Space\TrackingHandlerClass' => '/custom/path/tracking_handler_class.php'
* ),
* 'sale'
* );
* }
*
* $eventManager->addEventHandler('sale', 'onSaleDeliveryTrackingClassNamesBuildList', 'addCustomDeliveryTrackingServices');
*/
protected function initClassNames()
{
if(self::$classNames !== null)
return true;
Services\Manager::getHandlersList();
$classes = array(
'\Bitrix\Sale\Delivery\Tracking\RusPost' => 'lib/delivery/tracking/rus_post.php',
);
\Bitrix\Main\Loader::registerAutoLoadClasses('sale', $classes);
$event = new Event('sale', 'onSaleDeliveryTrackingClassNamesBuildList');
$event->send();
$resultList = $event->getResults();
if (is_array($resultList) && !empty($resultList))
{
$customClasses = array();
foreach ($resultList as $eventResult)
{
$params = $eventResult->getParameters();
if(!empty($params) && is_array($params))
$customClasses = array_merge($customClasses, $params);
}
if(!empty($customClasses))
{
\Bitrix\Main\Loader::registerAutoLoadClasses(null, $customClasses);
$classes = array_merge($customClasses, $classes);
}
}
self::$classNames = array_keys($classes);
return true;
}
/**
* Returns list of known class names
* @return array
*/
public function getClassNames()
{
return self::$classNames;
}
/**
* @param int $shipmentId
* @param StatusResult $params
* @param bool|false $isStatusChanged
* @return Result
* @throws ArgumentNullException
* @throws \Exception
*/
public function updateShipment($shipmentId, StatusResult $params)
{
if($shipmentId <= 0)
throw new ArgumentNullException('id');
$updParams = array();
if($params->status !== null)
$updParams['TRACKING_STATUS'] = $params->status;
$updParams['TRACKING_LAST_CHECK'] = new \Bitrix\Main\Type\DateTime();
if(intval($params->lastChangeTimestamp) > 0)
{
$updParams['TRACKING_LAST_CHANGE'] = \Bitrix\Main\Type\DateTime::createFromTimestamp(
$params->lastChangeTimestamp
);
}
else
{
$updParams['TRACKING_LAST_CHANGE'] = null;
}
if($params->trackingNumber !== null)
$updParams['TRACKING_NUMBER'] = $params->trackingNumber;
$updParams['TRACKING_DESCRIPTION'] = $params->description;
if(!$params->isSuccess())
$updParams['TRACKING_DESCRIPTION'] .= ' '.implode(', ', $params->getErrorMessages());
return ShipmentTable::update($shipmentId, $updParams);
}
/**
* @internal
* @param \SplObjectStorage $cloneEntity
*
* @return Manager
*/
public function createClone(\SplObjectStorage $cloneEntity)
{
if ($this->isClone() && $cloneEntity->contains($this))
{
return $cloneEntity[$this];
}
$trackingClone = clone $this;
$trackingClone->isClone = true;
if (!$cloneEntity->contains($this))
{
$cloneEntity[$this] = $trackingClone;
}
return $trackingClone;
}
/**
* @return bool
*/
public function isClone()
{
return $this->isClone;
}
}