<!-- Canonical: https://docs.shopstory.live/docs/guide/add-to-cart -->

# Добавление товара в корзину

ShopStory Web SDK может передать нажатие «Купить» функции ритейлера. Этот сценарий добавления без перехода на карточку товара называется Short Flow. Корзина, пользовательская сессия, окончательная проверка остатка и результат операции остаются в системе ритейлера.

```text
«Купить» → callback ритейлера → проверка товара и остатка → изменение корзины
          → true только после подтверждённого успеха
```

Если callback не настроен, кнопка работает как ссылка на карточку товара.

Клик и подтверждённое добавление учитываются отдельно: [схема клика, корзины и доставки события](<https://docs.shopstory.live/docs/guide/analytics#analytics-ownership-caption>).

Мобильное приложение

В Mobile SDK приложение получает типизированное действие со строковым `feedProductId`. Оно проверяет каталог, текущий остаток и результат cart-команды в своей commerce-системе. Обработка результата для каждой платформы описана в документации к поставленной версии SDK: [Mobile SDK](<https://docs.shopstory.live/docs/guide/mobile-sdk>).

## Web SDK callback

Этот callback передаётся в основной экземпляр `ShopStorySDK.show()`. Standalone PiP bundle его не наследует. Если пользователь открывает player из product mini-player, direct cart требует отдельного контракта, описанного в разделе [Интеграция с интернет-магазином](<https://docs.shopstory.live/docs/guide/custom-commerce-integrations>).

cart-actions.ts

```typescript
type CartActions = {
  addProductToCartById?: (
    feedProductId: string,
    callback: (success: boolean) => void,
  ) => void;
  addProductToCart?: (
    vendorCode: string,
    callback: (success: boolean) => void,
  ) => void;
};

type ShopStoryOptions = {
  actions?: CartActions;
  cartUrl?: string;
};
```

Если заданы обе функции и у товара есть `feedProductId`, SDK сначала использует `addProductToCartById`. Для интеграций по `vendorCode` используется `addProductToCart`.

Callback получает только один товарный идентификатор. Количество, выбранный магазин, регион и способ получения в его аргументы не входят. Для прямой корзины `feedProductId` должен однозначно определять продаваемый offer или SKU; commerce endpoint получает остальной контекст из текущей пользовательской сессии и состояния сайта. Если выбор варианта на PDP меняет SKU, передавайте отдельный `feedProductId` для выбранного offer либо выполняйте явный mapping на стороне ритейлера.

Идентификатор передаётся как строка и не имеет универсальной внутренней структуры:

- не преобразовывайте его в JavaScript `Number`;
- не удаляйте ведущие нули;
- не предполагайте, что это внутренний ID конкретной CMS;
- используйте сопоставление, согласованное при подключении товарного фида.

## Результат Web SDK callback

| Событие | Поведение SDK |
| --- | --- |
| `callback(true)` | Кнопка переходит в состояние «В корзине» |
| `callback(false)` | Показывается повторяемая ошибка «Не удалось добавить» |
| Синхронное исключение | Показывается повторяемая ошибка |
| Callback не вызван за 10 секунд | Показывается повторяемая ошибка |
| Callback вызван повторно | Повторные вызовы игнорируются |

`callback(false)` не означает «нет в наличии». Причиной может быть тайм-аут, ошибка сети, истёкшая сессия, отказ API или недоступный товар. Недоступность товара — отдельное состояние карточки, и задаётся оно [наличием](<https://docs.shopstory.live/docs/guide/product-availability>), а не результатом корзины.

Boolean callback поддерживает только состояния успеха и повторяемой ошибки. Предложение другого магазина и выбор способа получения требуют согласованного commerce-контракта; не выводите причину только из `callback(false)`.

Вызывайте `callback(true)` только после того, как система ритейлера подтвердила изменение текущей корзины. HTTP `2xx` сам по себе не доказывает бизнес-успех.

Если задан `cartUrl`, повторный клик по состоянию «В корзине» открывает этот URL в новой вкладке. Относительный путь разрешается относительно текущего origin сайта.

## Пример адаптера корзины

Адаптируйте пример к API своей платформы: замените названия CSRF-заголовка и полей ответа.

shopstory-cart-adapter.js

```javascript
ShopStorySDK.show({
  containerElement: document.getElementById('shopstory'),
  config: { clientId: 'assigned-application-id' },
  cartUrl: '/cart/',
  actions: {
    addProductToCartById: function (feedProductId, callback) {
      let completed = false;
      const finish = function (success) {
        if (completed) return;
        completed = true;
        callback(success === true);
      };

      fetch('/api/cart/items', {
        method: 'POST',
        credentials: 'same-origin',
        signal: AbortSignal.timeout(8000),
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': readPlatformCsrfToken(),
        },
        body: JSON.stringify({ feedProductId, quantity: 1 }),
      })
        .then(async function (response) {
          if (!response.ok) return finish(false);

          const result = await response.json();
          finish(
            result.added === true &&
            String(result.item?.feedProductId) === feedProductId
          );
        })
        .catch(function () {
          finish(false);
        });
    },
  },
});
```

Не копируйте имя `X-CSRF-Token`, если ваша CMS использует другой механизм. Для 1С-Битрикс, Shopify и других платформ применяйте их штатную защиту сессии и операций изменения данных.

`AbortSignal.timeout()` должен входить в принятую browser matrix. Для более старого браузера используйте `AbortController` и таймер из frontend-инфраструктуры проекта.

Пример рассчитан на same-origin endpoint: браузер отправляет действующую cookie только своему origin, а сервер проверяет штатный CSRF token. Если commerce API находится на другом origin или требует server credentials, вызывайте его через backend-for-frontend ритейлера. Не передавайте такие credentials в JavaScript или конфигурацию ShopStory.

В примере merchant API получает 8 секунд, а SDK ждёт callback 10 секунд. После тайм-аута результат может быть неопределённым: сервер мог принять запрос до обрыва соединения. Сначала проверьте корзину, затем решайте, повторять ли запрос.

В проектном расширенном commerce-контракте используется отдельный lifecycle операции: [схема неизвестного исхода и блокировки](<https://docs.shopstory.live/docs/guide/custom-commerce-integrations#unknown-cart-outcome-caption>). Его обработчики и `operationId` не входят в стандартный boolean callback выше.

Для этого контракта [сравнение двух SKU](<https://docs.shopstory.live/docs/guide/custom-commerce-integrations#sku-cart-states-caption>) объясняет, почему «Купить» и «В корзине» относятся к выбранному варианту. [Схема возврата из магазина](<https://docs.shopstory.live/docs/guide/custom-commerce-integrations#commerce-return-refresh-caption>) показывает повторную проверку условий без нового добавления.

## Требования к endpoint корзины

Endpoint ритейлера должен:

1. Принимать только ожидаемый HTTP-метод и content type.
2. Проверять пользовательскую сессию, CSRF-токен и допустимый origin.
3. Валидировать product key и количество на сервере.
4. Проверять текущую доступность товара в том контексте, который влияет на покупку: страна, магазин, способ получения или склад.
5. Возвращать успех только после подтверждённого изменения корзины.
6. Не включать внутреннюю ошибку, stack trace или учётные данные в публичный ответ.
7. Обрабатывать повторный запрос идемпотентно или сверять корзину после неопределённого результата до повторного изменения.

CORS не защищает запрос с cookie от CSRF. Одной cookie сессии недостаточно.

## Остатки

Состояние кнопки покупки определяется до клика. Базовый источник — `feedProductAvailable`, снимок из фида. Если для проекта настроен [обработчик наличия](<https://docs.shopstory.live/docs/guide/product-availability>), состояние кнопки определяет его ответ по выбранному магазину. При недоступном товаре действие покупки заблокировано и callback корзины не вызывается.

Callback корзины и обработчик наличия решают разные задачи и дополняют друг друга: первый подтверждает результат уже выполненного добавления, второй показывает наличие заранее. Ни один из них не резервирует товар — окончательную проверку выполняет cart-команда ритейлера.

Приоритет источников, контекст магазина, тайм-ауты и поведение при недоступном stock API описаны в разделе [Наличие товара и остатки](<https://docs.shopstory.live/docs/guide/product-availability>). Условия согласования нестандартных commerce-контрактов — в разделе [Интеграция с интернет-магазином](<https://docs.shopstory.live/docs/guide/custom-commerce-integrations>).

## Проверка перед запуском

- Сопоставление идентификатора проверено на обычном товаре и SKU-варианте.
- Идентификатор остаётся строкой на всём пути.
- Endpoint отклоняет запрос без действующей сессии или CSRF-защиты.
- Текущий остаток проверяется до подтверждения покупки.
- `callback(true)` вызывается только при фактическом успехе.
- Mobile action обрабатывается нативным приложением по контракту поставленного пакета, без подмены Web callback.
- Ошибка, exception и тайм-аут оставляют возможность повторить действие.
- После неопределённого тайм-аута состояние корзины сверяется до повтора.
- Callback вызывается не более одного раза.
- Клик по `cartUrl` открывает правильную корзину.
- Событие успеха не отправляется при бизнес-ошибке внутри HTTP `2xx`.

## Диагностика

| Симптом | Что проверить |
| --- | --- |
| Открывается карточка товара | Передан ли `actions` в `ShopStorySDK.show()` |
| «Не удалось добавить» | HTTP-ответ, бизнес-поле успеха, сессию, CSRF и тайм-аут |
| Добавлен другой SKU | Сопоставление `feedProductId`/`vendorCode`; отсутствие числового преобразования |
| Корзина открывается пустой | Работал ли endpoint с той же пользовательской сессией |
| Двойное добавление | Защиту кнопки и идемпотентность merchant endpoint |

## Связанные разделы

- [Web SDK](<https://docs.shopstory.live/docs/guide/web-sdk>)
- [Mini-player](<https://docs.shopstory.live/docs/guide/mini-player>)
- [Интеграция с интернет-магазином](<https://docs.shopstory.live/docs/guide/custom-commerce-integrations>)
- [Безопасность](<https://docs.shopstory.live/docs/operations/security>)
