Модуль Интернет-магазин связывает заказ, профиль покупателя, статистику и внутренний счет через идентификатор зарегистрированного пользователя USER_ID.
Через API разработчик может:
-
найти заказы и профили пользователя,
-
подставить сохраненные контактные данные в новый заказ,
-
получить статистику покупок,
-
проверить баланс,
-
получить историю операций по внутреннему счету.
Для серверных сценариев требуется зарегистрированный пользователь. Покупатель корзины FUSER_ID, свойства конкретного заказа, платежные системы, партнерская программа и аффилиаты относятся к другим подсистемам.
Как различать данные покупателя
Пользователь, заказ, профиль и внутренний счет хранят разные данные. Совпадение USER_ID связывает их, но не превращает в один объект.
|
|| Данные | Основной API | Что хранит ||
|| Пользователь сайта | Bitrix\Main\UserTable | Учетную запись пользователя. Идентификатор записи передают в USER_ID ||
|| Заказ | Bitrix\Sale\Order | Покупку и снимок данных, которые действовали при оформлении ||
|| Профиль покупателя | Bitrix\Sale\OrderUserProperties, Bitrix\Sale\Internals\UserPropsTable | Именованный набор значений свойств для пользователя и типа плательщика ||
|| Значение профиля | Bitrix\Sale\Internals\UserPropsValueTable | Связь профиля со свойством заказа и сохраненное значение ||
|| Статистика покупателя | Bitrix\Sale\BuyerStatistic | Агрегаты заказов по пользователю, сайту и валюте ||
|| Внутренний счет | CSaleUserAccount | Баланс пользователя отдельно для каждой валюты ||
|| Транзакция | Bitrix\Sale\Internals\UserTransactTable | Изменение внутреннего счета ||
|#
Идентификаторы пользователя и покупателя корзины имеют разное назначение:
-
USER_IDв заказе — идентификатор пользователя сайта, -
FUSER_ID— идентификатор покупателя корзины. Он может относиться к зарегистрированному пользователю или неавторизованному посетителю. Подробнее о связиFUSER_IDс корзиной — Работа с корзиной.
Профиль не заменяет свойства заказа. Значения профиля можно подставить при оформлении, но после сохранения заказ хранит собственную копию значений. Позднее изменение профиля не меняет созданные заказы.
Внутренний счет также не является платежной системой. Счет хранит баланс, а внутренняя платежная система создает оплату заказа средствами с этого баланса. Для создания оплаты используйте внутренний счет как платежную систему.
Подготовить данные
Установите и подключите модуль sale. Замените демонстрационные значения на данные проекта:
-
$userId— положительный идентификатор зарегистрированного пользователя, -
$siteId— идентификатор сайта, напримерs1, -
$currency— трехсимвольный код валюты, напримерRUB, -
$personTypeId— идентификатор типа плательщика, которому принадлежат свойства профиля, -
$profileId— идентификатор профиля, который принадлежит$userIdи$personTypeId.
use Bitrix\Main\Loader;
if (!Loader::includeModule('sale'))
{
throw new \RuntimeException('Не удалось подключить модуль sale');
}
$userId = 42;
$siteId = 's1';
$currency = 'RUB';
$personTypeId = 1;
$profileId = 15;
В публичном разделе получайте $userId из текущей авторизованной сессии и разрешайте пользователю читать и менять только собственные профили. Не принимайте произвольный USER_ID из HTTP-запроса.
Административные и фоновые операции должны отдельно проверять служебное право проекта. ORM-запрос с фильтром по USER_ID не заменяет авторизацию.
Найти заказы и профили пользователя
Заказы хранят историю покупок, а профили — сохраненные контактные и адресные данные для новых оформлений. Получайте эти данные отдельными запросами по одному USER_ID.
Получить заказы
Метод Order::getList() читает сохраненные заказы. Фильтр =USER_ID выбирает заказы конкретного пользователя, а =LID ограничивает результат сайтом.
use Bitrix\Sale\Order;
$orders = Order::getList([
'select' => [
'ID',
'ACCOUNT_NUMBER',
'DATE_INSERT',
'STATUS_ID',
'PRICE',
'CURRENCY',
'PAYED',
],
'filter' => [
'=USER_ID' => $userId,
'=LID' => $siteId,
],
'order' => [
'DATE_INSERT' => 'DESC',
'ID' => 'DESC',
],
'limit' => 50,
]);
$orderList = $orders->fetchAll();
Поля заказа имеют разное назначение:
-
ID— внутренний идентификатор заказа, -
ACCOUNT_NUMBER— номер, который показывают покупателю. Он может отличаться отID.
Для постраничного списка задавайте limit и offset, а сортировку дополняйте полем ID, чтобы порядок оставался однозначным.
Метод Order::getList() не возвращает архивные заказы. Если история должна включать архивные записи, добавьте отдельный запрос к архиву по статье Архив заказов.
Получить список профилей
Таблица UserPropsTable хранит заголовки профилей без сохраненных значений. Фильтруйте список одновременно по пользователю и типу плательщика.
use Bitrix\Sale\Internals\UserPropsTable;
$profiles = UserPropsTable::getList([
'select' => [
'ID',
'NAME',
'USER_ID',
'PERSON_TYPE_ID',
'DATE_UPDATE',
],
'filter' => [
'=USER_ID' => $userId,
'=PERSON_TYPE_ID' => $personTypeId,
],
'order' => [
'DATE_UPDATE' => 'DESC',
'ID' => 'DESC',
],
]);
$profileList = $profiles->fetchAll();
Поля профиля:
|
|| Поле | Тип | Назначение ||
|| ID | integer | Идентификатор профиля ||
|| NAME | string | Название профиля длиной от одного до 255 символов ||
|| USER_ID | integer | Идентификатор владельца профиля ||
|| PERSON_TYPE_ID | integer | Идентификатор типа плательщика ||
|| DATE_UPDATE | datetime | Дата изменения профиля ||
|| XML_ID | string | Внешний идентификатор длиной до 50 символов ||
|| VERSION_1C | string | Версия данных обмена с 1С длиной до 15 символов ||
|#
Если известен $profileId, дополнительно проверьте его владельца и тип плательщика через OrderUserProperties::checkCorrect().
use Bitrix\Sale\OrderUserProperties;
if (!OrderUserProperties::checkCorrect($profileId, $personTypeId, $userId))
{
throw new \RuntimeException('Профиль не принадлежит пользователю и типу плательщика');
}
Такая проверка нужна перед чтением, изменением и удалением профиля по идентификатору из запроса.
Получить сохраненные значения профиля
Метод OrderUserProperties::getProfileValues() возвращает массив, в котором ключ — идентификатор свойства заказа, а значение — данные из профиля.
use Bitrix\Sale\OrderUserProperties;
if (!OrderUserProperties::checkCorrect($profileId, $personTypeId, $userId))
{
throw new \RuntimeException('Профиль не найден');
}
$profileValues = OrderUserProperties::getProfileValues($profileId);
Метод учитывает тип свойства. Например, он преобразует сериализованные множественные значения в массивы и загружает сведения о файлах для свойств типа FILE.
Если нужны строки ORM вместе с идентификаторами значений профиля, используйте UserPropsValueTable.
use Bitrix\Sale\Internals\UserPropsValueTable;
$valueRows = UserPropsValueTable::getList([
'select' => [
'ID',
'USER_PROPS_ID',
'ORDER_PROPS_ID',
'NAME',
'VALUE',
],
'filter' => [
'=USER_PROPS_ID' => $profileId,
'=USER_PROPERTY.USER_ID' => $userId,
'=USER_PROPERTY.PERSON_TYPE_ID' => $personTypeId,
],
'order' => [
'ID' => 'ASC',
],
])->fetchAll();
Поле ORDER_PROPS_ID обозначает настройку свойства заказа. Коды и типы свойств на разных проектах могут отличаться. Не связывайте профиль с фиксированными идентификаторами из демонстрационного проекта.
Подставить профиль в новый заказ
Профиль хранит значения по идентификаторам настроек свойств. Чтобы применить профиль, выполните три шага:
-
Создайте заказ для того же пользователя.
-
Назначьте тип плательщика.
-
Перенесите значения в коллекцию свойств заказа.
use Bitrix\Sale\Order;
use Bitrix\Sale\OrderUserProperties;
// Проверяем, что профиль принадлежит пользователю и типу плательщика
if (!OrderUserProperties::checkCorrect($profileId, $personTypeId, $userId))
{
throw new \RuntimeException('Профиль не найден');
}
// Получаем сохраненные значения свойств профиля
$profileValues = OrderUserProperties::getProfileValues($profileId);
// Создаем заказ в памяти и назначаем тип плательщика
$order = Order::create($siteId, $userId, $currency);
$personTypeResult = $order->setPersonTypeId($personTypeId);
if (!$personTypeResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $personTypeResult->getErrorMessages())
);
}
$propertyCollection = $order->getPropertyCollection();
// Переносим значения профиля в свойства нового заказа
foreach ($profileValues as $orderPropertyId => $value)
{
$propertyValue = $propertyCollection->getItemByOrderPropertyId(
(int)$orderPropertyId
);
if ($propertyValue === null)
{
continue;
}
$setValueResult = $propertyValue->setValue($value);
if (!$setValueResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $setValueResult->getErrorMessages())
);
}
}
Пример создает заказ только в памяти и заполняет его свойства. Чтобы создать полноценный заказ, добавьте корзину, оплаты и отгрузки, затем вызовите Order::save() по инструкции Создание заказа.
Если в заказе нет свойства из профиля, пример пропускает это значение. Устаревшие данные профиля не прерывают заполнение заказа. Обязательные свойства после переноса проверяйте по коллекции и результату сохранения заказа.
Создать или обновить профиль
Для создания и обновления профиля сначала сопоставьте коды свойств с их идентификаторами. Затем передайте значения методу классического ядра, который учитывает типы свойств профиля.
Найти свойства для профиля
В профиль можно сохранять активные свойства выбранного типа плательщика с флагом USER_PROPS = 'Y'. Получите их через OrderPropsTable и сопоставьте по символьному коду. Конкретные коды задает проект.
use Bitrix\Sale\Internals\OrderPropsTable;
$properties = OrderPropsTable::getList([
'select' => [
'ID',
'CODE',
'NAME',
'TYPE',
'MULTIPLE',
],
'filter' => [
'=PERSON_TYPE_ID' => $personTypeId,
'=ACTIVE' => 'Y',
'=USER_PROPS' => 'Y',
],
'order' => [
'SORT' => 'ASC',
'ID' => 'ASC',
],
]);
$profilePropertyIds = [];
while ($property = $properties->fetch())
{
if ($property['CODE'] !== '')
{
$profilePropertyIds[$property['CODE']] = (int)$property['ID'];
}
}
Перед созданием массива значений проверьте, что нужные коды найдены. В примере ниже используются условные коды EMAIL, PHONE и ADDRESS. Замените их на коды свойств выбранного типа плательщика.
$requiredCodes = ['EMAIL', 'PHONE', 'ADDRESS'];
foreach ($requiredCodes as $requiredCode)
{
if (!isset($profilePropertyIds[$requiredCode]))
{
throw new \RuntimeException(
"Свойство {$requiredCode} недоступно для профиля"
);
}
}
$profileData = [
$profilePropertyIds['EMAIL'] => 'buyer@example.com',
$profilePropertyIds['PHONE'] => '+7 900 000-00-00',
$profilePropertyIds['ADDRESS'] => 'Калининград, ул. Примерная, д. 1',
];
Поле TYPE определяет формат значения, а MULTIPLE — возможность хранить несколько значений. Не преобразуйте сложные значения вручную, если сохраняете профиль через DoSaveUserProfile().
Сохранить профиль
Классы ядра D7 позволяют читать и изменять отдельные строки профиля. Для создания или обновления профиля нужно также сопоставлять свойства и преобразовывать их значения. Для этого используйте метод классического ядра CSaleOrderUserProps::DoSaveUserProfile(), который:
-
проверяет владельца и тип плательщика существующего профиля,
-
выбирает активные свойства с флагом
USER_PROPS = 'Y', -
преобразует значения списков, файлов и множественных свойств,
-
создает недостающие строки и обновляет существующие.
Передайте подготовленный $profileData с идентификаторами свойств заказа в ключах.
$errors = [];
$savedProfileId = \CSaleOrderUserProps::DoSaveUserProfile(
$userId,
$profileId,
'Основной адрес',
$personTypeId,
$profileData,
$errors
);
if ($savedProfileId === false)
{
$messages = array_column($errors, 'TEXT');
throw new \RuntimeException(
$messages
? implode('; ', $messages)
: 'Не удалось сохранить профиль'
);
}
Чтобы создать профиль, передайте 0 вместо $profileId. Метод вернет идентификатор созданного или обновленного профиля. Если массив значений пуст, метод DoSaveUserProfile() вернет переданный идентификатор без ошибки.
Возвращенный идентификатор подтверждает создание или наличие заголовка профиля, но не гарантирует запись каждого значения. DoSaveUserProfile() не проверяет результаты всех внутренних операций добавления и обновления.
Для значений, которые обязательны для текущего сценария, после сохранения вызовите getProfileValues() и сравните результат с ожидаемыми значениями. Например для электронной почты, телефона или адреса доставки.
Не записывайте VALUE напрямую через ORM, если сценарий должен поддерживать местоположения, файлы, списки или множественные свойства. DoSaveUserProfile() выполняет необходимые преобразования.
Внимание
Создание профиля не добавляет пользователя в стандартный административный список Магазин > Покупатели. Список строится по записям BuyerStatistic, которые появляются после пересчета статистики покупателя. Флаг активности пользователя ACTIVE не определяет его присутствие в этом списке.
Обновить профиль перед созданием заказа
Если пользователь изменил контактные или адресные данные, сначала сохраните профиль, затем заново получите его значения и перенесите их в новый заказ. Такой порядок исключает расхождение между профилем и данными создаваемого заказа.
Следующий пример продолжает сценарий сохранения профиля. Переменная $savedProfileId содержит идентификатор созданного или обновленного профиля. Передайте его в следующий вызов DoSaveUserProfile() вместо исходного $profileId.
use Bitrix\Sale\OrderUserProperties;
$profileId = $savedProfileId;
$profileData[$profilePropertyIds['PHONE']] = '+7 900 111-22-33';
$profileData[$profilePropertyIds['ADDRESS']] = 'Калининград, ул. Новая, д. 2';
$errors = [];
$savedProfileId = \CSaleOrderUserProps::DoSaveUserProfile(
$userId,
$profileId,
'Основной адрес',
$personTypeId,
$profileData,
$errors
);
if ($savedProfileId === false)
{
$messages = array_column($errors, 'TEXT');
throw new \RuntimeException(
$messages
? implode('; ', $messages)
: 'Не удалось обновить профиль'
);
}
$profileValues = OrderUserProperties::getProfileValues($savedProfileId);
Массив $profileValues содержит значения после обновления. Сравните измененные поля с $profileData, прежде чем переносить профиль в новый заказ.
Не обновляйте профиль после каждого фонового пересчета заказа. Сохраняйте его только по явному действию пользователя или по правилу проекта. Иначе параллельные оформления могут перезаписать один профиль разными данными.
Удалить профиль
Метод классического ядра CSaleOrderUserProps::Delete() удаляет профиль и все связанные значения. Перед вызовом обязательно проверьте владельца и тип плательщика.
Не используйте ORM-метод UserPropsTable::delete() для этого сценария. В UserPropsTable нет специальной логики каскадного удаления значений профиля. Метод CSaleOrderUserProps::Delete() сначала удаляет связанные строки значений, а затем заголовок профиля.
use Bitrix\Sale\OrderUserProperties;
if (!OrderUserProperties::checkCorrect($profileId, $personTypeId, $userId))
{
throw new \RuntimeException('Профиль не найден');
}
if (!\CSaleOrderUserProps::Delete($profileId))
{
throw new \RuntimeException('Не удалось удалить профиль');
}
Удаление профиля не меняет свойства уже сохраненных заказов.
Получить статистику покупателя
Класс BuyerStatistic группирует статистику по пользователю, сайту и валюте. Одна запись содержит дату заказа, сумму оплаченных средств и счетчики оплаченных заказов.
|
|| Поле | Тип | Назначение ||
|| USER_ID | integer | Идентификатор пользователя ||
|| LID | string | Идентификатор сайта ||
|| CURRENCY | string | Валюта агрегатов ||
|| LAST_ORDER_DATE | datetime | Дата заказа, сохраненная в статистике ||
|| SUM_PAID | float | Сумма оплат заказов в указанной валюте ||
|| COUNT_FULL_PAID_ORDER | integer | Счетчик полностью оплаченных заказов ||
|| COUNT_PART_PAID_ORDER | integer | Счетчик заказов с ненулевой оплаченной суммой ||
|#
Метод BuyerStatistic::calculate() пересчитывает запись пользователя по заказам и архиву заказов и возвращает Bitrix\Main\Result. Если заказов нет, результат содержит предупреждение. Поэтому проверяйте ошибки и предупреждения отдельно.
use Bitrix\Sale\BuyerStatistic;
// Пересчитываем статистику для пользователя, валюты и сайта
$calculateResult = BuyerStatistic::calculate(
$userId,
$currency,
$siteId
);
if (!$calculateResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $calculateResult->getErrorMessages())
);
}
// Отдельно проверяем предупреждение об отсутствии заказов
if (
$calculateResult instanceof \Bitrix\Sale\Result
&& $calculateResult->hasWarnings()
)
{
throw new \RuntimeException(
implode('; ', $calculateResult->getWarningMessages())
);
}
// Читаем сохраненную запись после успешного пересчета
$statistic = BuyerStatistic::getList([
'select' => [
'USER_ID',
'LID',
'CURRENCY',
'LAST_ORDER_DATE',
'SUM_PAID',
'COUNT_FULL_PAID_ORDER',
'COUNT_PART_PAID_ORDER',
],
'filter' => [
'=USER_ID' => $userId,
'=LID' => $siteId,
'=CURRENCY' => $currency,
],
'limit' => 1,
])->fetch();
Для обычного чтения не запускайте пересчет на каждом HTTP-запросе. Сначала прочитайте сохраненную запись статистики. Пересчет подходит для обслуживания данных, импорта или восстановления статистики после контролируемых изменений.
Не складывайте SUM_PAID из разных валют. Получайте отдельную строку для каждой комбинации пользователя, сайта и валюты.
Работать с внутренним счетом
Внутренний счет хранится отдельно для каждой пары «пользователь — валюта».
Для чтения и изменения счета используйте класс классического API CSaleUserAccount. В D7 есть ORM-класс журнала транзакций Bitrix\Sale\Internals\UserTransactTable, но нет класса для безопасного изменения баланса.
Проверить баланс
Метод CSaleUserAccount::GetByUserID() возвращает счет пользователя в указанной валюте или false, если счет еще не создан.
В публичном запросе получайте идентификатор пользователя из авторизованной сессии. Пользователь может читать только собственный баланс и запускать разрешенный платежный сценарий.
$account = \CSaleUserAccount::GetByUserID($userId, $currency);
$balance = $account
? (float)$account['CURRENT_BUDGET']
: 0.0;
Основные поля счета:
-
ID— идентификатор счета, -
USER_ID— идентификатор владельца, -
CURRENT_BUDGET— текущий баланс в валютеCURRENCY, -
CURRENCY— валюта счета, -
LOCKED— признак блокировки счета во время изменения, -
TIMESTAMP_X— дата изменения записи, -
NOTES— примечание к счету.
Отсутствие записи означает, что счет в этой валюте не создан. Для отображения можно считать его баланс нулевым, но не создавайте пустой счет только ради чтения.
Пополнить или уменьшить баланс
Метод CSaleUserAccount::UpdateAccount() принимает изменение баланса, а не новое итоговое значение.
Пополнение, ручная корректировка и служебное списание требуют отдельной серверной авторизации. Не меняйте таблицу внутреннего счета и не добавляйте транзакцию вручную. Метод UpdateAccount() использует совместимый механизм блокировки, изменяет баланс и затем пытается создать запись в журнале.
-
Положительная сумма пополняет счет.
-
Отрицательная сумма уменьшает счет.
-
Ноль не меняет баланс и не должен использоваться как операция изменения счета.
$amount = 1500.0;
$description = 'MANUAL';
$orderId = 0;
$notes = 'Пополнение по обращению покупателя';
$updated = \CSaleUserAccount::UpdateAccount(
$userId,
$amount,
$currency,
$description,
$orderId,
$notes
);
if (!$updated)
{
$exception = $GLOBALS['APPLICATION']->GetException();
throw new \RuntimeException(
$exception
? $exception->GetString()
: 'Не удалось изменить внутренний счет'
);
}
Как работает изменение счета
Метод проверяет пользователя и валюту, вызывает логическую блокировку счета, обновляет или создает запись, пытается добавить транзакцию и снимает блокировку. Если передан положительный $orderId, транзакция связывается с заказом.
Метод UpdateAccount() использует только логическую блокировку. Она не блокирует строку базы данных и не открывает транзакцию вокруг обновления баланса и записи в журнал. Метод также не проверяет, добавлена ли транзакция.
Успешный возврат подтверждает изменение счета, но не гарантирует появление строки журнала. Если проект требует строгой финансовой сверки, после операции проверяйте баланс и связанную транзакцию. Сбои передавайте в отдельный процесс согласования.
Как списывать средства
Для списания передайте отрицательную сумму. Не вычисляйте новое значение по схеме «прочитать баланс — вычесть — записать». Между чтением и записью другой процесс может изменить счет.
Если несколько процессов одновременно изменяют счет, добавьте защиту от параллельной записи: логической блокировки UpdateAccount() недостаточно.
Метод UpdateAccount() не запрещает отрицательный итоговый баланс. Используйте отрицательную дельту только после проверки прав и только если правила проекта допускают отрицательный остаток. Если средства списываются в оплату заказа, используйте внутреннюю платежную систему. Ее сценарий проверяет доступность средств и сохраняет связь с оплатой.
Оплату заказа средствами внутреннего счета проводите через объект оплаты и внутреннюю платежную систему. Не заменяйте этот сценарий прямым отрицательным UpdateAccount(): оплата должна остаться связанной с заказом, платежом и его жизненным циклом.
Прочитать историю транзакций
Класс UserTransactTable подходит для чтения журнала через ORM. Всегда фильтруйте записи по владельцу. При необходимости добавьте валюту, заказ или платеж.
use Bitrix\Sale\Internals\UserTransactTable;
$transactions = UserTransactTable::getList([
'select' => [
'ID',
'USER_ID',
'TRANSACT_DATE',
'AMOUNT',
'CURRENCY',
'DEBIT',
'ORDER_ID',
'PAYMENT_ID',
'DESCRIPTION',
'NOTES',
],
'filter' => [
'=USER_ID' => $userId,
'=CURRENCY' => $currency,
],
'order' => [
'TRANSACT_DATE' => 'DESC',
'ID' => 'DESC',
],
'limit' => 50,
]);
$transactionList = $transactions->fetchAll();
Для следующей страницы активного журнала используйте дату и ID последней полученной транзакции как границу выборки, а не только offset. Новые транзакции могут сдвинуть границу выборки. При сортировке по убыванию выберите строки с более ранней датой или с меньшим ID при совпадении даты.
Следующий пример продолжает предыдущий и использует полученный массив $transactionList.
use Bitrix\Sale\Internals\UserTransactTable;
$lastTransaction = $transactionList[array_key_last($transactionList)] ?? null;
if ($lastTransaction === null)
{
throw new \RuntimeException('Следующая страница отсутствует');
}
$cursorDate = $lastTransaction['TRANSACT_DATE'];
$cursorId = (int)$lastTransaction['ID'];
$nextPage = UserTransactTable::getList([
'select' => [
'ID',
'TRANSACT_DATE',
'AMOUNT',
'CURRENCY',
'DEBIT',
'DESCRIPTION',
],
'filter' => [
'=USER_ID' => $userId,
'=CURRENCY' => $currency,
[
'LOGIC' => 'OR',
['<TRANSACT_DATE' => $cursorDate],
[
'=TRANSACT_DATE' => $cursorDate,
'<ID' => $cursorId,
],
],
],
'order' => [
'TRANSACT_DATE' => 'DESC',
'ID' => 'DESC',
],
'limit' => 50,
])->fetchAll();
Объедините дату и ID последней строки в маркер страницы, который клиент хранит и возвращает серверу без изменения. При следующем запросе снова примените на сервере фильтры владельца и валюты.
Поля транзакции:
|
|| Поле | Тип | Назначение ||
|| TRANSACT_DATE | datetime | Дата финансовой операции ||
|| AMOUNT | float | Абсолютная сумма операции ||
|| CURRENCY | string | Валюта операции длиной до трех символов ||
|| DEBIT | boolean | Направление операции: Y в хранилище означает пополнение, N — списание ||
|| ORDER_ID | integer | Идентификатор связанного заказа, если он есть ||
|| PAYMENT_ID | integer | Идентификатор связанной оплаты, если он есть ||
|| DESCRIPTION | string | Код или описание операции длиной до 255 символов ||
|| NOTES | text | Дополнительное пояснение ||
|| EMPLOYEE_ID | integer | Идентификатор пользователя, от имени которого выполнено изменение, если он определен ||
|#
Не восстанавливайте баланс простым суммированием строк журнала в пользовательском запросе. Для текущего значения используйте CSaleUserAccount::GetByUserID(). Журнал нужен для истории, аудита и связи изменения с заказом или оплатой.
Не изменяйте и не удаляйте строки UserTransactTable в коде проекта. Исправляйте баланс компенсирующей операцией через UpdateAccount(), чтобы история оставалась последовательной.
Для компенсации ошибочной операции:
-
Найдите исходную транзакцию по
IDи проверьте ее владельца, валюту и сумму. -
Авторизуйте служебное исправление и убедитесь, что эта транзакция еще не компенсирована.
-
Вызовите
UpdateAccount()в той же валюте и передайте сумму исходной операции с противоположным знаком. -
Сохраните в данных проекта связь компенсации с исходной транзакцией.
-
Повторно прочитайте баланс и последние строки журнала.
Таблица UserTransactTable не содержит отдельного поля для идентификатора компенсируемой транзакции. Такую связь и защиту от повторной компенсации храните в собственной таблице проекта.
События внутреннего счета
Добавление и обновление записи счета вызывают события классического API. Регистрируйте обработчики через Bitrix\Main\EventManager::addEventHandlerCompatible().
|
|| Событие | При каком действии вызывается | Параметры обработчика ||
|| OnBeforeUserAccountAdd | Перед добавлением счета. Обработчик может вернуть false и отменить операцию | Массив полей $fields по ссылке ||
|| OnAfterUserAccountAdd | После добавления счета | Идентификатор счета $id, массив записанных полей $fields ||
|| OnBeforeUserAccountUpdate | Перед обновлением счета. Обработчик может вернуть false и отменить операцию | Идентификатор счета $id, массив изменяемых полей $fields по ссылке ||
|| OnAfterUserAccountUpdate | После обновления счета | Идентификатор счета $id, массив записанных полей $fields ||
|#
Один вызов UpdateAccount() может вызвать несколько событий обновления. Механизм совместимости отдельно устанавливает и снимает признак блокировки, а между этими действиями меняет баланс. Не считайте одно событие обновления эквивалентом одной финансовой операции. Для интеграции сверяйте пользователя, валюту, итоговый баланс и журнал транзакций.
Ошибки и ограничения
Проверяйте результат каждой операции и учитывайте состояние связанных объектов.
|
|| Ситуация | Причина | Действие ||
|| Профиль не найден по известному ID | Профиль удален, принадлежит другому пользователю или другому типу плательщика | Проверить OrderUserProperties::checkCorrect() и не раскрывать данные чужого профиля ||
|| В заказ перенесены не все значения | Настройка свойства удалена или недоступна выбранному типу плательщика | Пропустить отсутствующее свойство и отдельно проверить обязательные свойства заказа ||
|| Изменение профиля не повлияло на старый заказ | Заказ хранит собственные значения свойств | Изменять профиль для будущих оформлений, а существующий заказ обновлять через его коллекцию свойств ||
|| BuyerStatistic::calculate() вернул предупреждение | У пользователя нет заказов для указанного сайта и валюты | Обработать отсутствие статистики как отдельное состояние ||
|| Баланс равен нулю при отсутствии счета | Для пары пользователя и валюты еще нет записи | Не создавать счет при чтении, показать нулевой баланс ||
|| UpdateAccount() вернул false | Некорректный пользователь, пустая валюта, ошибка блокировки или записи | Получить исключение приложения и не повторять операцию без анализа причины ||
|| Повторный запрос дважды изменил баланс | Прикладной код повторно изменил счет | Хранить внешний идентификатор операции в своей таблице и проверять его до UpdateAccount() ||
|| История показывает другую валюту | У пользователя несколько счетов | Фильтровать баланс и транзакции по одному коду валюты ||
|#
Как защитить баланс от повторного изменения
Изменение баланса не защищено от повторного выполнения: каждый успешный вызов UpdateAccount() применяет новую дельту. Метод пытается добавить транзакцию, но его успешный возврат не подтверждает запись журнала.
Если внешний сервис может повторить запрос, зарегистрируйте уникальный идентификатор операции в собственной таблице до изменения счета и не запускайте повторно уже принятую операцию. После сбоя сверяйте статус операции с журналом счета. Не используйте текст DESCRIPTION как единственную защиту от повторов.
Как учесть параллельное изменение профиля
Профиль также может измениться между чтением и сохранением. Если несколько процессов редактируют один профиль, задайте в проекте правило владельца изменения или храните собственную версию данных. Поле DATE_UPDATE подходит для обнаружения изменения, но встроенной оптимистической блокировки профиля API не предоставляет.
Как отделить операции от уведомлений
Показанные операции с профилями, статистикой и счетом не создают пользовательские уведомления как результат вызова. Уведомления о заказе и оплате относятся к жизненному циклу соответствующих объектов. Обработку собственной интеграции запускайте только после проверки результата вызова API.