* @copyright 2021 DIGITAL RETAIL TECHNOLOGIES SL
* @license https://opensource.org/licenses/MIT The MIT License
*
* Don't forget to prefix your containers with your own identifier
* to avoid any conflicts with others containers.
*/
class RetailcrmExport
{
const RETAILCRM_EXPORT_ORDERS_STEP_SIZE_CLI = 5000;
const RETAILCRM_EXPORT_CUSTOMERS_STEP_SIZE_CLI = 5000;
const RETAILCRM_EXPORT_ORDERS_STEP_SIZE_WEB = 50;
const RETAILCRM_EXPORT_CUSTOMERS_STEP_SIZE_WEB = 300;
/**
* @var \RetailcrmProxy|\RetailcrmApiClientV5
*/
static $api;
/**
* @var int
*/
static $ordersOffset;
/**
* @var int
*/
static $customersOffset;
/**
* Initialize inner state
*/
public static function init()
{
static::$api = null;
static::$ordersOffset = self::RETAILCRM_EXPORT_ORDERS_STEP_SIZE_CLI;
static::$customersOffset = self::RETAILCRM_EXPORT_CUSTOMERS_STEP_SIZE_CLI;
}
/**
* Get total count of orders for context shop
*
* @return int
*/
public static function getOrdersCount($skipUploaded = false)
{
$sql = 'SELECT count(o.id_order)
FROM `' . _DB_PREFIX_ . 'orders` o' . ($skipUploaded ? '
LEFT JOIN `' . _DB_PREFIX_ . 'retailcrm_exported_orders` reo ON o.`id_order` = reo.`id_order`
' : '') . '
WHERE 1
' . Shop::addSqlRestriction(false, 'o') . ($skipUploaded ? '
AND (reo.`last_uploaded` IS NULL OR reo.`errors` IS NOT NULL)
' : '');
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
}
/**
* Get orders ids from the database
*
* @param int $start Sets the LIMIT start parameter for sql query
* @param int|null $count Sets the count of orders to get from database
*
* @return Generator
*
* @throws PrestaShopDatabaseException
*/
public static function getOrdersIds($start = 0, $count = null, $skipUploaded = false)
{
if (null === $count) {
$to = static::getOrdersCount($skipUploaded);
$count = $to - $start;
} else {
$to = $start + $count;
}
if (0 < $count) {
$predefinedSql = 'SELECT o.`id_order`
FROM `' . _DB_PREFIX_ . 'orders` o' . ($skipUploaded ? '
LEFT JOIN `' . _DB_PREFIX_ . 'retailcrm_exported_orders` reo ON o.`id_order` = reo.`id_order`
' : '') . '
WHERE 1
' . Shop::addSqlRestriction(false, 'o') . ($skipUploaded ? '
AND (reo.`last_uploaded` IS NULL OR reo.`errors` IS NOT NULL)
' : '') . '
ORDER BY o.`id_order` ASC';
while ($start < $to) {
$offset = ($start + static::$ordersOffset > $to) ? $to - $start : static::$ordersOffset;
if (0 >= $offset) {
break;
}
$sql = $predefinedSql . '
LIMIT ' . (int) $start . ', ' . (int) $offset;
$orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
if (empty($orders)) {
break;
}
foreach ($orders as $order) {
yield $order;
}
$start += $offset;
}
}
}
/**
* @param int $from
* @param int|null $count
*/
public static function exportOrders($from = 0, $count = null, $skipUploaded = false)
{
if (!static::validateState()) {
return;
}
$orders = [];
$orderRecords = static::getOrdersIds($from, $count, $skipUploaded);
$orderBuilder = new RetailcrmOrderBuilder();
$orderBuilder->defaultLangFromConfiguration()->setApi(static::$api);
foreach ($orderRecords as $record) {
$orderBuilder->reset();
$order = new Order($record['id_order']);
$orderCart = new Cart($order->id_cart);
$orderCustomer = new Customer($order->id_customer);
$orderBuilder->setCmsOrder($order);
if (!empty($orderCart->id)) {
$orderBuilder->setCmsCart($orderCart);
} else {
$orderBuilder->setCmsCart(null);
}
if (!empty($orderCustomer->id)) {
$orderBuilder->setCmsCustomer($orderCustomer);
} else {
// TODO
// Caused crash before because of empty RetailcrmOrderBuilder::cmsCustomer.
// Current version *shouldn't* do this, but I suggest more tests for guest customers.
$orderBuilder->setCmsCustomer(null);
}
try {
$orders[] = $orderBuilder->buildOrderWithPreparedCustomer();
} catch (Exception $exception) {
RetailcrmExportOrdersHelper::updateExportState(
$record['id_order'],
null,
[$exception->getMessage()]
);
self::handleError($record['id_order'], $exception);
} catch (Error $exception) {
RetailcrmExportOrdersHelper::updateExportState(
$record['id_order'],
null,
[$exception->getMessage()]
);
self::handleError($record['id_order'], $exception);
}
time_nanosleep(0, 250000000);
if (50 == count($orders)) {
static::$api->ordersUpload($orders);
$orders = [];
}
}
if (count($orders)) {
static::$api->ordersUpload($orders);
}
}
/**
* Get total count of customers for context shop
*
* @param bool $withOrders If set to true, then return total count of customers.
* If set to false, then return count of customers without orders
*
* @return int
*/
public static function getCustomersCount($withOrders = true)
{
$sql = 'SELECT count(c.id_customer)
FROM `' . _DB_PREFIX_ . 'customer` c
WHERE 1
' . Shop::addSqlRestriction(false, 'c');
if (!$withOrders) {
$sql .= '
AND c.id_customer not in (
select o.id_customer
from `' . _DB_PREFIX_ . 'orders` o
WHERE 1
' . Shop::addSqlRestriction(false, 'o') . '
group by o.id_customer
)';
}
return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
}
/**
* Get customers ids from database
*
* @param int $start Sets the LIMIT start parameter for sql query
* @param null $count Sets the count of customers to get from database
* @param bool $withOrders If set to true, then return all customers ids.
* If set to false, then return ids only of customers without orders
* @param bool $returnAddressId If set to true, then also return address id in `id_address`
*
* @return Generator
*
* @throws PrestaShopDatabaseException
*/
public static function getCustomersIds($start = 0, $count = null, $withOrders = true, $returnAddressId = true)
{
if (null === $count) {
$to = static::getCustomersCount($withOrders);
$count = $to - $start;
} else {
$to = $start + $count;
}
if (0 < $count) {
$predefinedSql = 'SELECT c.`id_customer`
' . ($returnAddressId ? ', a.`id_address`' : '') . '
FROM `' . _DB_PREFIX_ . 'customer` c
' . ($returnAddressId ? '
LEFT JOIN
(
SELECT
ad.`id_customer`,
ad.`id_address`
FROM
`' . _DB_PREFIX_ . 'address` ad
INNER JOIN
(
SELECT
`id_customer`,
MAX(`date_add`) AS `date_add`
FROM
`' . _DB_PREFIX_ . 'address`
GROUP BY
id_customer
) ad2
ON
ad2.`id_customer` = ad.`id_customer` AND ad2.`date_add` = ad.`date_add`
ORDER BY
ad.`id_customer` ASC
) a
ON
a.`id_customer` = c.`id_customer`
' : '') . '
WHERE 1
' . Shop::addSqlRestriction(false, 'c') .
($withOrders ? '' : '
AND c.`id_customer` not in (
select o.`id_customer`
from `' . _DB_PREFIX_ . 'orders` o
WHERE 1
' . Shop::addSqlRestriction(false, 'o') . '
group by o.`id_customer`
)') . '
ORDER BY c.`id_customer` ASC';
while ($start < $to) {
$offset = ($start + static::$customersOffset > $to) ? $to - $start : static::$customersOffset;
if (0 >= $offset) {
break;
}
$sql = $predefinedSql . '
LIMIT ' . (int) $start . ', ' . (int) $offset;
$customers = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
if (empty($customers)) {
break;
}
foreach ($customers as $customer) {
yield $customer;
}
$start += $offset;
}
}
}
/**
* @param int $from
* @param int|null $count
*/
public static function exportCustomers($from = 0, $count = null)
{
if (!static::validateState()) {
return;
}
$customers = [];
$customersRecords = RetailcrmExport::getCustomersIds($from, $count, false);
foreach ($customersRecords as $record) {
$customerId = $record['id_customer'];
$addressId = $record['id_address'];
$cmsCustomer = new Customer($customerId);
if (Validate::isLoadedObject($cmsCustomer)) {
if ($addressId) {
$cmsAddress = new Address($addressId);
$addressBuilder = new RetailcrmAddressBuilder();
$address = $addressBuilder
->setAddress($cmsAddress)
->build()
->getDataArray()
;
} else {
$address = [];
}
try {
$customers[] = RetailcrmOrderBuilder::buildCrmCustomer($cmsCustomer, $address);
} catch (Exception $exception) {
self::handleError($customerId, $exception);
} catch (Error $exception) {
self::handleError($customerId, $exception);
}
if (50 == count($customers)) {
static::$api->customersUpload($customers);
$customers = [];
time_nanosleep(0, 250000000);
}
}
}
if (count($customers)) {
static::$api->customersUpload($customers);
}
}
/**
* @param int $id
*
* @return bool
*
* @throws RetailcrmNotFoundException
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
* @throws Exception
*/
public static function exportOrder($id)
{
if (!static::$api) {
return false;
}
$object = new Order($id);
if (!Validate::isLoadedObject($object)) {
throw new RetailcrmNotFoundException('Order not found');
}
$customerId = $object->id_customer;
$customer = new Customer($customerId);
$apiResponse = static::$api->ordersGet($object->id);
$existingOrder = [];
if ($apiResponse->isSuccessful() && $apiResponse->offsetExists('order')) {
$existingOrder = $apiResponse['order'];
}
$orderBuilder = new RetailcrmOrderBuilder();
$crmOrder = $orderBuilder
->defaultLangFromConfiguration()
->setApi(static::$api)
->setCmsOrder($object)
->setCmsCustomer($customer)
->buildOrderWithPreparedCustomer()
;
if (empty($crmOrder)) {
return false;
}
if (empty($existingOrder)) {
try {
$reference = new RetailcrmReferences(static::$api);
$site = $reference->getSite()['code'];
$crmCart = static::$api->cartGet($customerId, $site);
if (!empty($crmCart['cart'])) {
// If the order is from a corporate customer, need to clear the cart for the contact person
if (!empty($crmOrder['contragent']['legalName']) && !empty($crmOrder['contact'])) {
static::$api->cartClear(
[
'clearedAt' => date('Y-m-d H:i:sP'),
'customer' => ['externalId' => $customerId],
],
$site
);
} else {
$crmOrder['isFromCart'] = true;
}
}
} catch (Throwable $exception) {
self::handleError($customerId, $exception);
}
$response = static::$api->ordersCreate($crmOrder);
} else {
$response = static::$api->ordersEdit($crmOrder);
if (empty($existingOrder['payments']) && !empty($crmOrder['payments'])) {
$payment = array_merge(reset($crmOrder['payments']), [
'order' => ['externalId' => $crmOrder['externalId']],
]);
static::$api->ordersPaymentCreate($payment);
}
}
if (!$response->isSuccessful()) {
$errorMsg = '';
if ($response->offsetExists('errorMsg')) {
$errorMsg = $response['errorMsg'] . ': ';
}
if ($response->offsetExists('errors')) {
$errorMsg .= implode('; ', $response['errors']);
}
throw new Exception($errorMsg);
}
return $response->isSuccessful();
}
/**
* @param $orderIds
*
* @return array
*
* @throws Exception
*/
public static function uploadOrders($orderIds)
{
if (!static::$api || !(static::$api instanceof RetailcrmProxy)) {
throw new Exception('Set API key and API URL first');
}
$isSuccessful = true;
$skippedOrders = [];
$uploadedOrders = [];
$errors = [];
foreach ($orderIds as $orderId) {
$id_order = (int) $orderId;
$response = false;
try {
$response = self::exportOrder($id_order);
if ($response) {
$uploadedOrders[] = $id_order;
}
} catch (RetailcrmNotFoundException $e) {
$skippedOrders[] = $id_order;
} catch (Exception $e) {
$errors[$id_order][] = $e->getMessage();
}
$isSuccessful = $isSuccessful ? $response : false;
time_nanosleep(0, 50000000);
}
return [
'success' => $isSuccessful,
'uploadedOrders' => $uploadedOrders,
'skippedOrders' => $skippedOrders,
'errors' => $errors,
];
}
/**
* Returns false if inner state is not correct
*
* @return bool
*/
private static function validateState()
{
if (!static::$api
|| !static::$ordersOffset
|| !static::$customersOffset
) {
return false;
}
return true;
}
/**
* @param int $step
* @param string $entity
* @param bool $skipUploaded
*
* @throws Exception
*/
public static function export($step, $entity = 'order', $skipUploaded = false)
{
--$step;
if (0 > $step) {
throw new Exception('Invalid request data');
}
if ('order' === $entity) {
$stepSize = RetailcrmExport::RETAILCRM_EXPORT_ORDERS_STEP_SIZE_WEB;
RetailcrmExport::$ordersOffset = $stepSize;
RetailcrmExport::exportOrders($step * $stepSize, $stepSize, $skipUploaded);
// todo maybe save current step to database
} elseif ('customer' === $entity) {
$stepSize = RetailcrmExport::RETAILCRM_EXPORT_CUSTOMERS_STEP_SIZE_WEB;
RetailcrmExport::$customersOffset = $stepSize;
RetailcrmExport::exportCustomers($step * $stepSize, $stepSize);
// todo maybe save current step to database
}
}
private static function handleError($entityId, $exception)
{
RetailcrmLogger::writeException('export', $exception, sprintf(
'Error while building %s: %s',
$entityId,
$exception->getMessage()
), true);
RetailcrmLogger::output($exception->getMessage());
}
}