Корзина в Битрикс D7 и работа с ней (Legacy и D7 API)

1. Ключевые понятия

FUSER

FUSER (сокращение от "Fuser ID") — уникальный идентификатор покупателя, не зависящий от авторизации пользователя. Он обеспечивает работу корзины для анонимных пользователей и связывает корзину с сессией или пользовательским аккаунтом после входа.

Ключевые моменты о FUSER

  1. Назначение

    • Отслеживает корзину в сессии анонимного пользователя.

    • После авторизации автоматически связывается с ID пользователя (USER_ID), обеспечивая перенос корзины.

    • Хранится в cookies (SALE_UID) или сессии (SALE_USER_ID).

  2. Как получить FUSER Основные методы класса Bitrix\Sale\Fuser:

    use Bitrix\Sale\Fuser;
    
    // Текущий покупатель (создаёт ID, если не существует)
    $fuserId = Fuser::getId(); 
    
    // Для конкретного пользователя (например, после авторизации)
    $userId = 123; 
    $fuserId = Fuser::getIdByUserId($userId);

    Важно! Всегда используйте в сочетании с ID сайта:

    $siteId = \Bitrix\Main\Context::getCurrent()->getSite(); // Для публичной части
    $basket = \Bitrix\Sale\Basket::loadItemsForFUser($fuserId, $siteId);

    Связь FUSER и пользователя

    • Анонимный режим: FUSER генерируется при первом добавлении товара и сохраняется в cookies.

    • Авторизация: Вызов Fuser::getIdByUserId($userId) связывает корзину с аккаунтом, предотвращая дублирование 39.

Примечание: getSite работает только в публичной части.


Методы класса Fuser

  • getId() - Возвращает ID текущего покупателя (создаёт при необходимости)

    $fuserId = Fuser::getId();
  • getIdByUserId() - Возвращает FUSER для указанного пользователя

    $fuserId = Fuser::getIdByUserId(123);
  • getUserIdById() - Возвращает USER_ID по FUSER (для поиска привязанных аккаунтов)

    $userId = Fuser::getUserIdById($fuserId);
  • refreshSessionCurrentId() - Обновляет FUSER в сессии (например, после авторизации)

    Fuser::refreshSessionCurrentId();

Практические примеры

Создание корзины с привязкой к FUSER:

if (\Bitrix\Main\Loader::includeModule('sale')) {
    $siteId = \Bitrix\Main\Context::getCurrent()->getSite();
    $fuserId = \Bitrix\Sale\Fuser::getId(); 
    $basket = \Bitrix\Sale\Basket::loadItemsForFUser($fuserId, $siteId);
}
``` :cite[1]:cite[8].  

**Перенос корзины при авторизации:**  
```php
// После входа пользователя
$userId = $USER->GetID();
$newFuserId = \Bitrix\Sale\Fuser::getIdByUserId($userId); 
$basket->setFUserId($newFuserId);
$basket->save();
``` :cite[3]:cite[6].  

---

#### **Ошибки и решения**  
- **Проблема:** Корзина не сохраняется между сессиями.  
  **Решение:** Убедитесь, что FUSER записан в cookies:  
  ```php
  // Проверка настроек модуля sale
  if (!\Bitrix\Main\Config\Option::get("sale", "encode_fuser_id")) {
      // Включить шифрование FUSER в настройках
  }
  ``` :cite[2]:cite[4].  
- **Проблема:** Дублирование товаров после авторизации.  
  **Решение:** Всегда вызывайте `Fuser::getIdByUserId()` после входа и обновляйте корзину :cite[3].  

---

#### **Итог**  
**FUSER** — системный компонент для управления корзиной, критичный для работы интернет-магазина. Ключевые правила:  
1. Используйте `Fuser::getId()` для анонимных операций.  
2. Привязывайте корзину к аккаунту через `Fuser::getIdByUserId()`.  
3. Всегда передавайте `SITE_ID` при загрузке корзины :cite[1]:cite[8].  
```php
// Минимальный рабочий пример
$basket = \Bitrix\Sale\Basket::loadItemsForFUser(
    \Bitrix\Sale\Fuser::getId(), 
    \Bitrix\Main\Context::getCurrent()->getSite()
);

Корзина пользователя vs Корзина заказа в Bitrix D7

В корзину по дефолту попадает товар с наименьшей возможной, для пользователя, ценой.

В системе Bitrix существуют два типа корзин:

  1. Пользовательская корзина (активная)

    • Не привязана к заказу

    • Хранится в сессии/FUSER

    • Изменяется до оформления заказа

  2. Корзина заказа (историческая)

    • Привязана к конкретному заказу (ORDER_ID)

    • Фиксируется при оформлении

    • Доступна только для чтения


Важно!

Сохраняем корзину:

\$order->save(); // корзина ПРИВЯЗАНА к заказу \$basket->save(); // корзина НЕ привязана к заказу

Сохранять корзину нужно после всех изменений.

1. Работа с пользовательской корзиной (не привязанной к заказу)

Получение:

use Bitrix\Sale\Basket;
use Bitrix\Sale\Fuser;

$siteId = \Bitrix\Main\Context::getCurrent()->getSite();
$fuserId = Fuser::getId();

// Загрузка активной корзины текущего покупателя
$basket = Basket::loadItemsForFUser($fuserId, $siteId);

Основные операции

Добавление товара:

$item = $basket->createItem('catalog', 123); // ID товара
$item->setFields([
    'QUANTITY' => 2,
    'CURRENCY' => Bitrix\Currency\CurrencyManager::getBaseCurrency(),
    'LID' => $siteId
]);
$basket->save();

Обновление количества:

if ($item = $basket->getItemById(456)) { // ID элемента корзины
    $item->setField('QUANTITY', 3);
    $basket->save();
}

Удаление товара:

if ($item = $basket->getItemById(456)) {
    $item->delete();
    $basket->save();
}

Очистка корзины:

$basket->clearCollection();
$basket->save();

Получение содержимого:

foreach ($basket as $item) {
    echo "Товар: " . $item->getField('NAME') 
         . ", Количество: " . $item->getQuantity();
}

Общая стоимость:

$totalPrice = $basket->getPrice();

Применение купона:

use Bitrix\Sale\DiscountCouponsManager;
DiscountCouponsManager::add("DISCOUNT10");

Сохранение изменений:

// Пример: добавление товара
$item = $basket->createItem('catalog', 123);
$item->setFields([
    'QUANTITY' => 2,
    'CURRENCY' => Bitrix\Currency\CurrencyManager::getBaseCurrency(),
    'LID' => $siteId
]);

// Сохранение в базу
$result = $basket->save();
if (!$result->isSuccess()) {
    $errors = $result->getErrorMessages();
}

Особенности:

  • Хранится в таблице b_sale_basket с ORDER_ID = NULL

  • Автоматически очищается после оформления заказа (настраивается в модуле "Интернет-магазин")


2. Работа с корзиной заказа (привязанной)

Получение через заказ:

use Bitrix\Sale;

$orderId = 789; // ID существующего заказа
$order = Sale\Order::load($orderId);

// Получаем привязанную корзину
$orderBasket = $order->getBasket();

Особенности:

  • Доступна только для чтения (save() недоступен)

  • Данные хранятся в таблицах:

    • b_sale_order - заказ

    • b_sale_basket - элементы корзины (с ORDER_ID = $orderId)

    • b_sale_basket_props - свойства элементов

Экспорт данных:

$items = [];
foreach ($orderBasket as $item) {
    $items[] = [
        'NAME' => $item->getField('NAME'),
        'PRICE' => $item->getPrice(),
        'QUANTITY' => $item->getQuantity(),
        'PROPS' => $item->getPropertyCollection()->getArray()
    ];
}
print_r($items);

3. Конвертация пользовательской корзины в корзину заказа

Происходит автоматически при оформлении. Ручное копирование:

// Создаем новый заказ
$order = Sale\Order::create($siteId, $userId);
$order->setPersonTypeId(1); // Тип плательщика

// Клонируем корзину
$clonedBasket = Basket::create($siteId);
foreach ($basket as $item) {
    $newItem = $clonedBasket->createItem($item->getField('MODULE'), $item->getProductId());
    $newItem->setFields($item->getFields());
}

// Привязываем к заказу
$order->setBasket($clonedBasket);
$order->save(); // Теперь это корзина заказа!

4. Сравнение типов корзин

Характеристика Пользовательская корзина Корзина заказа
Статус Активная, изменяемая Зафиксированная
Хранение b_sale_basket (ORDER_ID=NULL) Привязана к заказу
Изменение товаров Разрешено Запрещено
Доступ через Fuser::getId() Order::load()
Автоочистка Да (после оформления) Нет
Сохранение $basket->save(); $order->save();

2. Сравнение Legacy и D7 API

Добавление товара

Legacy:

Add2BasketByProductID(123, 2);

D7:

$basket->createItem('catalog', 123)->setFields(['QUANTITY' => 2]);
$basket->save();

Обновление количества

Legacy:

CSaleBasket::Update(456, ["QUANTITY" => 3]);

D7:

$basket->getItemById(456)->setField('QUANTITY', 3);
$basket->save();

3. Практические примеры

Информация о корзине

$price = $basket->getPrice(); // Цена с учетом скидок
$fullPrice = $basket->getBasePrice(); // Цена без учета скидок
$weight = $basket->getWeight(); // Общий вес корзины
$basket->getVatSum(); // Сумма НДС
$basket->getVatRate(); // Ставка НДС
$basket->getFUserId(); // Id покупателя
$basket->getOrder(); // Заказ, к которому привязана
// Товары
$basketItems = $basket->getBasketItems(); // Все товары
$basketItemsOrderable = $basket->getOrderableItems(); // Только товары, доступные для заказа
$arBasketInfo = $basket->getListOfFormatText(); // Массив товаров в читаемом виде
$arQuantityList = $basket->getQuantityList();// Массив вида basketId - количество

Получить количество товаров в корзине

array_sum($basket->getQuantityList()); // количество товаров в корзине
count($basket->getQuantityList()); // количество позиций в корзине

Копирование корзины

//Если привязана к заказу, то скопирует вместе с заказом
$basket->createClone(); 
//скопирует, только если корзина не привязана к заказу
$basket->copy(); 

Создать заказ из пользовательской корзины

$order = Sale\Order::create($siteId, $userId);
$order->setBasket($basket); // Автоматическая конвертация

// Установка других параметров заказа
$order->setField('CURRENCY', 'RUB');
$order->setField('USER_DESCRIPTION', 'Комментарий');

// Финализация
if ($order->save()->isSuccess()) {
    $basket->clearCollection(); // Очистка пользовательской корзины
    $basket->save();
}

Получить все корзины пользователя (включая исторические)

$dbRes = Sale\Basket::getList([
    'filter' => [
        'FUSER_ID' => Fuser::getId(),
        'ORDER_ID' => null // Только активные корзины
        // Для исторических: '!ORDER_ID' => null
    ],
    'select' => ['ID', 'PRODUCT_ID', 'QUANTITY']
]);

while ($item = $dbRes->fetch()) {
    // Обработка элементов
}

Добавить товар в корзину. Обновить запись и проверить наличие

/** int $productId ID товара */
/** int $quantity количество */
if ($item = $basket->getExistsItem('catalog', $productId)) {
    $item->setField('QUANTITY', $item->getQuantity() + $quantity);
}
else {
    $item = $basket->createItem('catalog', $productId);
    $item->setFields(array(
        'QUANTITY' => $quantity,
        'CURRENCY' => Bitrix\Currency\CurrencyManager::getBaseCurrency(),
        'LID' => Bitrix\Main\Context::getCurrent()->getSite(),
        'PRODUCT_PROVIDER_CLASS' => 'CCatalogProductProvider',
    ));
}
$basket->save();

Добавить товар с произвольной (кастомной ценой)

$item->setFields(array(
     'QUANTITY' => $quantity,
     'CURRENCY' => Bitrix\Currency\CurrencyManager::getBaseCurrency(),
     'LID' => Bitrix\Main\Context::getCurrent()->getSite(),
     'PRICE' => $customPrice,
     'CUSTOM_PRICE' => 'Y', // см. сюда!!!
));

В последних версиях модуля catalog

Bitrix\Main\Loader::includeModule("catalog");

$fields = [
    'PRODUCT_ID' => 98, // ID товара, обязательно
    'QUANTITY' => 2, // количество, обязательно
    'PROPS' => [
        ['NAME' => 'Test prop', 'CODE' => 'TEST_PROP', 'VALUE' => 'test value'],
    ],

];
$r = Bitrix\Catalog\Product\Basket::addProduct($fields);
if (!$r->isSuccess()) {
    var_dump($r->getErrorMessages());
}

Данный метод проверяет доступность товара к покупке (при отсутствии будет возвращен результат с ошибкой "Товар отсутствует"), сам проверяет наличие товара в корзине и при наличии увеличивает количество товара в корзине.

Также к товару добавляются свойства корзины, необходимые для обмена с : PRODUCT.XML_ID и CATALOG.XML_ID.

Но при этом нет возможности передать в корзину кастомную цену.

Товары корзины формируем в  массив

$arItems = array();
foreach ($basket as $item) {
     $arItems[] = array(
       "ID" => $item->getProductId(),
       "COUNT" => $item->getQuantity(),
       "SUM" => $item->getFinalPrice(),
       "PRICE" => $item->getPrice()
    );
}

Получить запись по ID и удалить запись из корзины (аналог CSaleBasket::Delete**)**

/** int $id ID записи */
$basket->getItemById($id)->delete();
$basket->save();

Получить товары в корзине, доступные для покупки (CAN_BUY=Y)

$orderBasket = $basket->getOrderableItems();

Методы форматирования корзины для писем и отчётов

$formattedItems = $basket->getListOfFormatText();
var_dump($formattedItems);

/* Результат:
array(2) {
  [ID] => "Кольцо [Размер: 18] - 1 : 14 800 руб."
  [ID] => "Браслет - 4 : 6 000 руб."
}
*/

$quantityData = $basket->getQuantityList(); // Массив количеств
var_dump($quantityData);

/* Результат:
array(2) {
  [ID] => float(1)  // 1 единица товара ID=*
  [ID] => float(4)  // 4 единицs товара ID=*
}
*/

4. Товары в корзине D7 (объект Bitrix\Sale\BasketItem)

1. Архитектура корзины и модель данных

В ядре D7 корзина представлена объектом Bitrix\Sale\Basket, содержащим коллекцию элементов типа Bitrix\Sale\BasketItem. Каждый элемент инкапсулирует данные о товаре, его свойствах и состоянии в корзине. Ключевые особенности 17:

  • Коллекция реализует интерфейсы \ArrayAccess, \Countable, \IteratorAggregate, что позволяет обрабатывать элементы как массив.

  • Для работы требуется подключенный модуль sale:

    if (!\Bitrix\Main\Loader::includeModule('sale')) 
      throw new \Exception('Модуль sale не доступен');
  • Корзина загружается через статический метод:

    $basket = \Bitrix\Sale\Basket::loadItemsForFUser(
      \Bitrix\Sale\Fuser::getId(),
      \Bitrix\Main\Context::getCurrent()->getSite()
    );

2. Получение элементов корзины

2.1. Доступ к коллекции:

// Получение как массива объектов BasketItem
$basketItems = $basket->getBasketItems();

// Итерация через foreach (благодаря IteratorAggregate)
foreach ($basket as $basketItem) {
    $productName = $basketItem->getField('NAME');
    $quantity = $basketItem->getQuantity();
}

2.2. Фильтрация элементов:

  • getOrderableItems() – возвращает товары с CAN_BUY='Y' (исключает отложенные и недоступные).

  • getExistsItem($moduleId, $productId) – проверяет наличие товара в корзине.

3. Методы объекта BasketItem: данные элемента

$item = $basketItems[0];
$item->getId();         // ID записи в корзине
$item->getProductId();  // ID товара
$item->getQuantity();   // Количество
$item->getWeight();     // Вес
$item->getField('NAME');// Любое поле товара в корзине
$item->canBuy();        // true, если доступно для покупки
$item->isDelay();       // true, если отложено
// Все поля элемента корзины
$item->getFields();               // Вариант 1
$item->getFieldsCatalogProduct(); // Вариант 2

$item->getField('PRICE') // Получение значения любого поля

$item->getBasketCode() // Код корзины
$item->getFUserId()    // id владельца корзины
$item->getCurrency()   // Код валюты

$item->getFinalPrice();   // Сумма
$item->getPrice();        // Цена за единицу, цена с учетом скидок
$item->getBasePrice()     // Цена без учета скидок
$item->getDefaultPrice()  // Цена по умолчанию
$item->getDiscountPrice() // Величина скидки
$item->isCustomPrice()    // Цена указана вручную (без использования провайдера)

$item->getVatRate()          // Ставка НДС
$item->getPriceWithVat()     // Цена с НДС
$item->getBasePriceWithVat() // Базовая цена с НДС
$item->getInitialPrice()     // Исходная цена без НДС 
$item->getVat()              // НДС
$item->isVatInPrice()        // Цена включает НДС

$item->getCallbackFunction() // Поле "CALLBACK_FUNC"
$item->getProviderEntity()   // Объект провайдера
$item->getProvider()         // Класс провайдера

$item->getAvailableFields() // Массив кодов всех полей
$item->getSettableFields()  // Массив кодов изменяемых полей

$item->getCalculatedFields()           // Массив кодов генерируемых полей
$item->isCalculatedField('FIELD_NAME') // Поле является генерируемым

$item->isBarcodeMulti()

$item->getPropertyCollection(); // Свойства товара в корзине, коллекция объектов Sale\BasketPropertyItem, см. ниже
$item->getCollection();         // Корзина, в которой лежит товар 

Получить товар в корзине по id позиции в корзине (не путать с id товара)

$item = $basket->getItemById($id);

получить товар в корзине по id товара

$item = $basket->getExistsItem('catalog', $productId); // Вернет товар, если он без свойств в корзине
$item = $basket->getExistsItem('catalog', $productId, $properties); // Вернет товар, с такими же $properties

Вернуть товар в корзине по productId

если товаров несколько, только с разными свойствами, вернет первый

function GetExistsBasketItem($basket,$moduleId,$productId){
   $result = false;
   if(!empty($productId) && (intval($productId)>0) && (intval($productId)==$productId) && ($moduleId!='')){
      foreach ($basket as $item) {
         if($productId == $item->getProductId() && ($item->getField('MODULE') == $moduleId)){
            $result = $item;
            break;
         }
      }
   }
   return $result;
}
$item = GetExistsBasketItem($basket,'catalog',$productID); // Вернет false, если нет такого

Получить название единицы измерения товара

$arProductIds = array($productId1,$productId2); // ID товаров
$arMeasure = \Bitrix\Catalog\ProductTable::getCurrentRatioWithMeasure($arProductIds);  

echo $arMeasure[$productId1]['MEASURE']['SYMBOL_RUS'];
echo $arMeasure[$productId2]['MEASURE']['SYMBOL_RUS'];

Действия над товаром

$item->setField('QUANTITY', $quantity); // Изменение поля
$item->setFields(array(
    'QUANTITY' => $quantity,
    'CUSTOM_PRICE' => $customPrice,
)); // Изменение полей

$item->delete(); // Удаление
$item->save();   // Сохранение изменения, можно использовать и $basket->save();

4. Свойства товаров в корзине ( объект Sale\BasketPropertiesCollection)

/** Sale\BasketItem $item объект товара в корзине */
$basketPropertyCollection = $item->getPropertyCollection(); 
$basketPropertyCollection->getPropertyValues(); // Возвращает массив свойств
$basketPropertyCollection = $item->getPropertyCollection(); // \Bitrix\Sale\BasketPropertyItem
foreach ($basketPropertyCollection as $basketProperty){ 
    $name = $basketProperty->getField('NAME')); // любое поле свойства 'NAME','CODE','VALUE','SORT','XML_ID'   
} 
$basketPropertyCollection->getPropertyValues(); // возвращает массив свойств
$prolong = $basketItem->getPropertyCollection()->getPropertyValues()["prolong"]["NAME"];// можно сразу к свойству обратиться

Добавить новое свойство или изменить существующие можно следующим образом

$basketPropertyCollection->setProperty(array(
    array(
       'NAME' => 'Цвет',
       'CODE' => 'COLOR',
       'VALUE' => 'Кофе с молоком',
       'SORT' => 100,
    ),
));
$basketPropertyCollection->save();

Пример удаления свойства

foreach ($basketPropertyCollection as $propertyItem) {
    if ($propertyItem->getField('CODE') == 'COLOR') {
        $propertyItem->delete();
        break;
    }
}
$basketPropertyCollection->save();

5. ORM-классы

Обращаться напрямую к таблице корзины, без использования объектов можно с использованием ORM-класса Bitrix\Sale\Internals\BasketTable

Прямой доступ к данным корзины без использования объектной модели осуществляется через ORM-классы.

Этот подход обеспечивает максимальную производительность при массовых операциях, но требует ручного управления данными.

Перебрать товары в корзине текущего пользователя

$basketRes = Sale\Internals\BasketTable::getList(array(
    'filter' => array(
        'FUSER_ID' => Sale\Fuser::getId(), 
        'ORDER_ID' => null,
        'LID' => SITE_ID,
        'CAN_BUY' => 'Y',
    )
));

while ($item = $basketRes->fetch()) {
    var_dump($item);
}

Получить количество и сумму товаров в корзине текущего юзера

$result = Sale\Internals\BasketTable::getList(array(
    'filter' => array(
        'FUSER_ID' => Sale\Fuser::getId(), 
        'ORDER_ID' => null,
        'LID' => SITE_ID,
        'CAN_BUY' => 'Y',
    ),
    'select' => array('BASKET_COUNT', 'BASKET_SUM'),
    'runtime' => array(
        new \Bitrix\Main\Entity\ExpressionField('BASKET_COUNT', 'COUNT(*)'),
        new \Bitrix\Main\Entity\ExpressionField('BASKET_SUM', 'SUM(PRICE*QUANTITY)'),
    )
))->fetch();

Получить свойства товаров в корзине

Используем класс Bitrix\Sale\Internals\BasketPropertyTable

$basketPropRes = Sale\Internals\BasketPropertyTable::getList(array(
   'filter' => array(
      "BASKET_ID" => $basketItemId,
   ),
));

while ($property = $basketPropRes->fetch()) {
   var_dump($property);
}

6. Скидки

Считать скидки нужно ПОСЛЕ привязки корзины к заказу

Если скидка произвольная (не правило корзины и не скидка на товар)

задаете 3 поля: CUSTOM_PRICE = 'Y' PRICE = цена со скидкой DISCOUNT_PRICE - величина скидки

Важное условие - сумма PRICE и DISCOUNT_PRICE должна давать значение из поля BASE_PRICE

В публичной части

Этот код даст цены без учета скидок торгового каталога (если они еще используются у вас на проекте).

$basket = \Bitrix\Sale\Basket::loadItemsForFUser(\Bitrix\Sale\Fuser::getId(), SITE_ID);
$discounts = \Bitrix\Sale\Discount::buildFromBasket($basket, new \Bitrix\Sale\Discount\Context\Fuser($basket->getFUserId(true)));
$discounts->calculate();
$result = $discounts->getApplyResult(true);
$prices = $result['PRICES']['BASKET'];

Если используем скидки торгового каталога (скидки на товар)

В этом случае необходимо еще выполнять refresh корзины после ее загрузки из базы:

$basket = \Bitrix\Sale\Basket::loadItemsForFUser(\Bitrix\Sale\Fuser::getId(), SITE_ID);

$basket->refreshData(array('PRICE', 'COUPONS'));

$discounts = \Bitrix\Sale\Discount::buildFromBasket($basket, new \Bitrix\Sale\Discount\Context\Fuser($basket->getFUserId(true)));
$discounts->calculate();
$result = $discounts->getApplyResult(true);
$prices = $result['PRICES']['BASKET'];

Чтобы применить скидку

$order->doFinalAction(true);

Если на странице присутствуют другие компоненты, работающие с корзиной, например малая корзина, то в начало кода следует добавить

\Bitrix\Sale\Compatible\DiscountCompatibility::stopUsageCompatible();

Пример создания заказа с применением скидок

/**
 * Создаем виртуальный заказ для расчета стоимости доставок, скидок и купонов.
 */
public static function createVirtualOrder(array $preparedData): object | array
{
    if (empty($preparedData)) {
        return self::createErrorResponse('Передана пустая data');
    }

    try {
        \Bitrix\Main\Loader::includeModule("sale");
        \Bitrix\Main\Loader::includeModule("catalog");

        //dd($preparedData);

        global $USER;

        $propertyValues = $preparedData['properties'];

        $siteId = Context::getCurrent()->getSite();
        $currencyCode = CurrencyManager::getBaseCurrency();

        // СОЗДАЕМ НОВЫЙ ЗАКАЗ.
        //$userId = $USER->isAuthorized() ? $USER->GetID() : \CSaleUser::GetAnonymousUserID();
        //$userId = $USER->isAuthorized() ? $USER->GetID() : \Bitrix\Sale\Fuser::getId();
        $userId = \Bitrix\Sale\Fuser::getId();

        $basketUser = \Bitrix\Sale\Basket::loadItemsForFUser($userId, $siteId);

        $basket = $basketUser->copy();

        $order = Order::create($siteId, $userId);
        $order->setPersonTypeId($preparedData['person_type_id']);
        $order->setField('CURRENCY', $currencyCode);
        $order->setBasket($basket);

        $shipmentCollection = $order->getShipmentCollection();
        $shipment = $shipmentCollection->createItem();
        $service = Delivery\Services\Manager::getById($preparedData['delivery_id']);
        $shipment->setFields([
            'DELIVERY_ID' => $service['ID'],
            'DELIVERY_NAME' => $service['NAME'],
        ]);
        $shipmentItemCollection = $shipment->getShipmentItemCollection();
        $shipment->setField('CURRENCY', $currencyCode);

        foreach ($order->getBasket()->getOrderableItems() as $basketItem) {
            $shipmentItem = $shipmentItemCollection->createItem($basketItem);
            $shipmentItem->setQuantity($basketItem->getQuantity());
        }

        if ($preparedData['delivery_cost'] > 0) {
            $shipment->setField('BASE_PRICE_DELIVERY', $preparedData['delivery_cost']);
            $shipment->setField('PRICE_DELIVERY', $preparedData['delivery_cost']);
            $shipment->setField('CUSTOM_PRICE_DELIVERY', 'Y');
        }

        $paymentCollection = $order->getPaymentCollection();
        $payment = $paymentCollection->createItem();
        $paySystemService = PaySystem\Manager::getObjectById($preparedData['pay_system_id']);
        $payment->setFields([
            'PAY_SYSTEM_ID' => $paySystemService->getField("PAY_SYSTEM_ID"),
            'PAY_SYSTEM_NAME' => $paySystemService->getField("NAME"),
        ]);

        $discounts = $order->getDiscount();
        $discounts->setApplyResult(true);
        $discounts->calculate();
        $order->refreshData();

        $order->doFinalAction(true);

        foreach ($order->getPropertyCollection() as $prop) {
            /**
             * @var PropertyValue $prop
             */
            if ($prop->isUtil()) {
                continue;
            }

            $value = $propertyValues[$prop->getField('CODE')] ?? null;

            if (empty($value)) {
                $value = $prop->getProperty()['DEFAULT_VALUE'];
            }

            if (!empty($value)) {
                $prop->setValue($value);
            }
        }

        return $order;
    } catch (\Exception  $e) {
        return self::createErrorResponse($e->getMessage());
    }
}

Критические замечания

  1. Не путать контексты:

    • Пользовательская корзина = FUSER_ID + ORDER_ID=NULL

    • Корзина заказа = ORDER_ID != NULL

  2. Безопасность: При работе с корзиной заказа всегда проверяйте права:

    if (!$order->getUserId() == $currentUserId) {
       throw new Exception("Доступ запрещен");
    }
  3. Производительность: Для заказов используйте оптимизированные запросы:

    $basket = Sale\Basket::loadItemsForOrder($orderId);
  4. Всегда проверяйте модуль:

    if (!CModule::IncludeModule("sale")) die();
  5. Обработка ошибок в D7:

    try {
       $basket->save();
    } catch (\Exception $e) {
       $error = $e->getMessage();
    }
  6. Перенос корзины при авторизации:

    // После входа пользователя
    $newFuserId = Fuser::getIdByUserId($USER->GetID());
    $basket->setFUserId($newFuserId);
    $basket->save();

Скрипты, сниппеты

Получить заказ, позицию в корзине и добавить свойство

use Bitrix\Sale;
Bitrix\Main\Loader::includeModule("sale");

function UpdateOrderDealerToClient($orderId,$email,$basketItemId=""){
    $result = "";
    if (!empty($orderId) && !empty($email)){
        $rsUser = CUser::GetByLogin($email);
        if ($arUser = $rsUser->Fetch()) {
            $order = Sale\Order::load($orderId);
            if (!empty($order)){
                $basket = $order->getBasket();
                if (!empty($basketItemId)) {
                    $item = $basket->getItemById($basketItemId);
                    $basketPropertyCollection = $item->getPropertyCollection();
                    $arProps = []; //сюда соберем ВСЕ имеющиеся свойства
                    foreach ($basketPropertyCollection as $basketProperty) {
                        $arProps[] = [
                            'NAME' => $basketProperty->getField('NAME'),
                            'CODE' => $basketProperty->getField('CODE'),
                            'VALUE' => $basketProperty->getField('VALUE'),
                            'SORT' => $basketProperty->getField('SORT'),
                        ];
                    }
                    //добавим новое
                    $arProps [] = array(
                        'NAME' => 'Customer: ' . $email,
                        'CODE' => 'client',
                        'VALUE' => $arUser["ID"],
                        'SORT' => 100,
                    );
                    //сохраним
                    $basketPropertyCollection->setProperty($arProps);
                    $basketPropertyCollection->save();
                }
                else{
                    $items = $basket->getBasketItems();
                    foreach ($items as $item){
                        $basketPropertyCollection = $item->getPropertyCollection();
                        $arProps = []; //сюда соберем ВСЕ имеющиеся свойства
                        foreach ($basketPropertyCollection as $basketProperty) {
                            $arProps[] = [
                                'NAME' => $basketProperty->getField('NAME'),
                                'CODE' => $basketProperty->getField('CODE'),
                                'VALUE' => $basketProperty->getField('VALUE'),
                                'SORT' => $basketProperty->getField('SORT'),

                            ];

                        }
                        //добавим новое
                        $arProps [] = array(
                            'NAME' => 'Customer: ' . $email,
                            'CODE' => 'client',
                            'VALUE' => $arUser["ID"],
                            'SORT' => 100,
                        );
                        //сохраним
                        $basketPropertyCollection->setProperty($arProps);
                        $basketPropertyCollection->save();
                    }
                }
                $order->save();
                $result = "ok";

            } else {
                $result = "order not found";
            }
        } else {
            $result = "user not found";
        }
    } else {
        $result = "empty orderId or email";
    }
    return $result;
}