Перейти к основному содержимому

Наличие товара и остатки

ShopStory работает с двумя разными состояниями товара, и их важно не смешивать.

Каталог — какие товары показаны в эфире, с какими названием, изображением и ценой. Источник каталога всегда товарный фид.

Наличие — можно ли купить товар прямо сейчас.

Подключений два, и они сильно разной сложности. Определите своё по двум проверкам ниже.

Какой вариант ваш

Вариант AВариант B
Цена товараодна для всех покупателейзависит от региона, зоны или канала
Версий выгрузки каталогаоднанесколько
Что показывает наличиепризнак available из фидаостаток магазина покупателя на момент эфира
Работа ритейлераотдать фидотдать фид и добавить одну функцию на сайт
Новый код на стороне ShopStoryне нуженне нужен: обработчик входит в опубликованный контракт SDK

Проверок две, и они независимы. Достаточно одной сработавшей, чтобы ваш вариант был B.

  1. Цены. Если каталог выгружается в нескольких версиях по регионам или ценовым зонам — это вариант B. Плеер показывает цену подключённой версии всем зрителям, поэтому версию нужно выбирать осознанно: Несколько версий фида.
  2. Наличие. Откройте выгрузку и поищите available="false". Если в вашем фиде нет ни одного available="false", признак наличия в нём отсутствует — так устроены рекламные выгрузки вроде Yandex smart feed и Google Merchant: отсутствующий товар просто не попадает в файл. Тогда вариант A вам не подойдёт независимо от цен, и увеличение частоты синхронизации ничего не изменит.

Вариант A. Один фид и единые цены

Ничего дополнительно подключать не нужно.

ShopStory синхронизирует фид по согласованному расписанию и берёт из него название, изображение, цену и признак available. Товар с available="false" остаётся в эфире, но действие покупки заблокировано. Проверку в момент покупки выполняет ваша корзина: Добавление в корзину.

Этого достаточно, когда наличие меняется медленнее, чем синхронизируется фид, и не зависит от того, где находится покупатель. Ограничение варианта одно и его важно принять сознательно: между синхронизациями карточка показывает снимок, а не текущий остаток.

Если сработала только проверка про цены, вам нужна из варианта B лишь его первая часть — выбор версии выгрузки. Обработчик наличия при этом не требуется.

Вариант B. Разные цены и живые остатки

Вариант состоит из двух независимых частей, и подключать их можно по отдельности.

Цена и состав каталога приходят из той версии выгрузки, которая подключена к проекту. Обработчик наличия их не меняет. Выбор версии и данные для него — в разделе Несколько версий фида.

Наличие приходит от обработчика наличия — функции на стороне сайта ритейлера, которая обращается к его собственному stock API с выбранным магазином покупателя. Это штатная возможность Web SDK: отдельная разработка на стороне ShopStory для неё не нужна.

Что делает каждая сторона

КтоЧто делает
Сайт ритейлераОпределяет магазин покупателя своими существующими средствами
ShopStoryПередаёт список идентификаторов товаров эфира в обработчик
Сайт ритейлераОбращается к своему stock API и возвращает карту «идентификатор → доступен»
ShopStoryПрименяет карту к кнопкам покупки
Сайт ритейлераСообщает о смене магазина вызовом refreshProductsAvailability()

Код обработчика живёт на сайте ритейлера, рядом с инициализацией плеера. Изменений в товарном фиде, новых полей в каталоге и серверной интеграции с ShopStory он не требует.

Требование к stock API одно: он должен отвечать по всем товарам эфира внутри дедлайна обработчика. Если API отвечает по одному товару за запрос, посчитайте: число товаров в эфире, делённое на допустимую параллельность, умноженное на время ответа. Не укладывается — понадобится пакетный метод или обёртка над существующим.

Чего интеграция не требует:

  • открывать stock API во внешнюю сеть — запрос идёт из браузера покупателя к тому же origin, что и остальные запросы сайта;
  • передавать ShopStory учётные данные, ключи или адреса внутренних сервисов;
  • отдавать ShopStory остатки, справочник магазинов или привязку покупателей к ним;
  • менять товарный фид, добавлять в него поля или увеличивать частоту выгрузки;
  • поддерживать отдельный контракт для записей: live и VOD работают одинаково.

Как устроен процесс

Ключевое свойство схемы: запрос остатка не покидает периметр ритейлера. Он выполняется кодом его сайта, с его пользовательской сессией, к его API. ShopStory получает только результат — доступен товар или нет.

Live-проверка наличия в выбранном магазинеПрокрутите схему по горизонтали
Список товаров уходит в обработчик resolveProductsAvailability, сигнал обновления — это refreshProductsAvailability(). SDK не обращается к API ритейлера напрямую и не хранит его остатки.

Кто определяет магазин покупателя

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

Магазин определяет сайт ритейлера — теми же средствами, которыми он уже пользуется на собственных карточках товара: выбор города в шапке, cookie, профиль покупателя, определение по IP. Плеер встроен в эту же страницу и попадает в тот же контекст.

Обработчик наличия — это функция в коде сайта ритейлера. Она читает то же состояние, что и остальные его страницы, и сама подставляет магазин в свой запрос. Контекст покупателя не является аргументом обработчика, в ответе не возвращается и на серверы ShopStory не уходит.

Отдельно про поле stockId

В конфигурации SDK есть необязательное поле config.stockId. Это другой, более ранний механизм: постоянный для проекта идентификатор склада или магазина, по которому SDK читает региональное наличие из поля options товарного фида. Он задаётся один раз при инициализации, не меняется при смене города покупателем и к обработчику наличия отношения не имеет.

Если для проекта заданы оба механизма, доступность считается по логическому И: товар покупаем, когда его открыл фид, и не закрыл региональный options, и обработчик не вернул false. Ни один из источников не возвращает в продажу то, что закрыл другой.

Что определяетсяКто определяет
Город, регион и магазин покупателяСайт ритейлера, его существующими средствами
Список товаров текущего эфираShopStory
Запрос к stock API и подстановка магазинаКод сайта ритейлера, с его пользовательской сессией
Сопоставление ответа с товарами эфираShopStory, по feedProductId
Состояние кнопки покупкиShopStory, по ответу обработчика

Сопоставление на стороне ShopStory только товарное: ключи карты ответа сверяются с feedProductId из фида. Это тот же идентификатор, который уже используется в действии корзины, поэтому отдельного сопоставления покупателей, магазинов или сессий не возникает.

Требование к странице

Там, где размещён плеер, магазин уже должен быть определён сайтом. Если на этой странице выбор магазина ещё не сделан, обработчику нечего подставить: наличие остаётся по снимку фида, покупка не блокируется.

Контракт обработчика

Обработчик передаётся вместе с действиями корзины в вызов ShopStorySDK.show().

availability-actions.ts
type ProductAvailabilityMap = Record<string, boolean>;

type ClientActions = {
resolveProductsAvailability?: (
feedProductIds: string[],
callback: (availability: ProductAvailabilityMap) => void,
) => void;
// Обработчики корзины передаются в тот же объект actions
// и описаны отдельно: ./add-to-cart.md#web-sdk-callback
};

feedProductIds — массив строковых идентификаторов в том же формате, что и в фиде. Обрабатывайте ровно тот список, который пришёл в аргументе, и не додумывайте его состав.

Обычно это все товары текущего эфира. Но когда товары добавляются в уже идущий эфир, SDK передаёт только добавленные идентификаторы, чтобы не перезапрашивать то, что уже известно. Полный перечень поводов для вызова — в разделе Когда SDK запрашивает наличие.

Чего не бывает никогда: вызова на один товар при показе карточки. Обработчик всегда получает список.

Контекст покупателя в аргументы не входит — см. Кто определяет магазин покупателя.

Карта ответа

Верните объект, ключи которого — идентификаторы из запроса:

Значение по ключуЧто делает SDK
trueДействие покупки доступно
falseКарточка переходит в состояние «Нет в наличии», кнопка покупки заблокирована, callback корзины не вызывается
Ключ отсутствует в ответеНаличие неизвестно; сохраняется состояние по снимку фида

Возвращайте ключ только для тех товаров, по которым получен достоверный ответ. Не подставляйте false вместо неизвестного результата: ошибка API — это не подтверждённое отсутствие товара.

Обратное тоже важно. Отсутствие ключа означает «не удалось узнать», а не «товар покупателю не положен». Если вы знаете, что товар недоступен — не входит в ассортимент его региона, снят с продажи, недоступен для выбранного способа получения, — возвращайте false. Пропущенный товар останется доступным к покупке по снимку фида.

Ключи, которых не было в запросе, игнорируются. Повторный вызов callback игнорируется.

Пример

shopstory-availability-adapter.js
// Пример для stock API, который отвечает по одному товару за запрос.
// Если у вашего API есть пакетный метод, замените checkOne и очередь
// одним запросом со всем списком — остальная часть обработчика не изменится.
const STOCK_CONCURRENCY = 4;
const STOCK_DEADLINE_MS = 4000;

function checkOne(feedProductId, storeId, signal) {
return fetch(
'/api/stock/' + encodeURIComponent(feedProductId) + '?store=' + encodeURIComponent(storeId),
{ credentials: 'same-origin', signal },
)
.then(function (response) {
if (!response.ok) return null;
return response.json();
})
.then(function (data) {
// null => «не смогли узнать»: ключ в карту не попадёт
return data && typeof data.inStock === 'boolean' ? data.inStock : null;
})
.catch(function () {
return null;
});
}

ShopStorySDK.show({
containerElement: document.getElementById('shopstory'),
config: { clientId: 'assigned-application-id' },
cartUrl: '/cart/',
actions: {
resolveProductsAvailability: function (feedProductIds, callback) {
const storeId = readSelectedStoreId();
const controller = new AbortController();
const deadline = setTimeout(function () { controller.abort(); }, STOCK_DEADLINE_MS);

const availability = {};
const queue = feedProductIds.slice();

function worker() {
const id = queue.shift();
if (id === undefined) return Promise.resolve();
return checkOne(id, storeId, controller.signal).then(function (inStock) {
if (inStock !== null) availability[String(id)] = inStock;
return worker();
});
}

const workers = [];
for (let i = 0; i < STOCK_CONCURRENCY; i += 1) workers.push(worker());

Promise.all(workers)
.catch(function () { /* частичный результат сохраняем */ })
.then(function () {
clearTimeout(deadline);
callback(availability);
});
},
},
});

Обратите внимание: обработчик вызывает callback один раз, отдавая то, что успел собрать к дедлайну. Товары, по которым ответа не пришло, в карту не попадают и остаются в состоянии по фиду. Поэтому дедлайн внутри обработчика должен быть меньше тайм-аута SDK — иначе весь ответ потеряется целиком.

Если ваш stock API отвечает по одному товару, оцените заранее: число товаров в эфире, делённое на допустимую параллельность, умноженное на время ответа, должно укладываться в этот дедлайн. Не укладывается — нужен пакетный метод или backend-for-frontend, который его изобразит.

AbortController и setTimeout использованы вместо AbortSignal.timeout() намеренно: последний недоступен в Safari до 16.4, а исключение внутри обработчика SDK не отличит от отказа API.

В этом примере показан только обработчик наличия. Обработчик корзины передаётся в тот же объект actions.

callback({}) — корректный ответ «данных нет». Он не ломает плеер и не помечает товары отсутствующими.

Адаптируйте пример к своей платформе: имена полей, защиту сессии и способ получения выбранного магазина. Пример рассчитан на same-origin endpoint ритейлера. Если stock API находится на другом origin или требует серверной авторизации, вызывайте его через backend-for-frontend ритейлера: учётные данные не должны попадать в JavaScript или в конфигурацию ShopStory.

Смена магазина

Когда покупатель меняет магазин, регион или способ получения, сайт сообщает об этом SDK:

on-store-change.js
function onStoreChanged() {
if (window.ShopStorySDK && window.ShopStorySDK.refreshProductsAvailability) {
window.ShopStorySDK.refreshProductsAvailability();
}
}

Метод делает три вещи: снимает уже применённые ответы, отменяет незавершённые запросы и вызывает обработчик заново. Ответы прежнего магазина на карточку попасть не могут — ни те, что были показаны, ни те, что придут с опозданием.

До прихода новых данных карточки показывают состояние по снимку фида.

Метод ничего не возвращает и безопасен для повторных вызовов — быстрое переключение города подряд не создаёт гонки. Если обработчик не задан, вызов не делает ничего. Обновляйте выбранный магазин на своей стороне до вызова метода, а не после.

Метод относится к плееру, созданному вызовом show(). Контракт рассчитан на один такой экземпляр на странице; сценарий с двумя одновременными плеерами на одной странице не поддержан.

Когда SDK запрашивает наличие

МоментПоведение
Открытие эфира или записиОдин вызов со списком товаров этого эфира
Добавление товаров в идущий эфирОдин вызов со списком добавленных идентификаторов
refreshProductsAvailability()Один вызов со списком товаров текущего эфира

Других поводов нет. Обработчик не вызывается при показе или открытии отдельной карточки, не опрашивается по расписанию и не работает при закрытом плеере. Единственный внутренний таймер — тайм-аут запроса.

Полученный ответ живёт до смены магазина, смены эфира или закрытия плеера. Срока давности у него нет: ответ не «протухает» сам по себе, потому что состояние, придуманное таймером, было бы хуже честного ответа мерчанта. Если наличие должно обновляться чаще, вызывайте refreshProductsAvailability() из своего кода.

Тайм-аут обработчика — 5 секунд, значение фиксировано. Он ограничивает ожидание одного вызова, не частоту вызовов.

Приоритет источников

Обработчик может только снять доступность. Товар, закрытый фидом, остаётся недоступным независимо от ответа обработчика; товар, открытый фидом, обработчик может закрыть.

Снимок фидаОтвет обработчикаДействие покупки
ДоступенtrueДоступно
ДоступенfalseЗаблокировано, «Нет в наличии»
Доступенключ отсутствуетДоступно, по снимку фида
НедоступенлюбойЗаблокировано

Правило одностороннее намеренно. Оно исключает случай, когда live-ответ возвращает в продажу позицию, снятую с ассортимента или ограниченную к обороту.

Ограничение, о котором нужно знать заранее

Если available в вашей выгрузке уже привязан к одному складу или городу, обработчик не сможет открыть товар покупателю из другого места: фид закрыл позицию, и правило это не отменяет.

Разделение ответственности здесь жёсткое: фид передаёт ассортиментную доступность, а привязку к магазину покупателя — обработчик. Если ваша выгрузка смешивает их в одном поле, маппинг фида корректируется при подключении. Проверьте это до оценки работ.

Каталог тоже остаётся за фидом: обработчик не может добавить товар в эфир, изменить его цену, название или изображение. Он влияет только на состояние действия покупки.

Это важно для каталогов с ценовыми зонами: если у вас несколько версий выгрузки, плеер показывает цену и состав той версии, которая подключена к проекту. Обработчик наличия этого не меняет — см. Несколько версий фида.

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

Проверка наличия не должна ломать просмотр. Поведение рассчитано на то, что stock API ритейлера временно недоступен.

Что произошлоПоведение SDK
Обработчик не заданНаличие берётся из фида; ничего не вызывается
Обработчик выбросил синхронное исключениеПерехвачено, состояние карточек не меняется, видео продолжается
Обработчик вернул отклонённый промис и не вызвал callbackПерехватить это SDK не может: сработает тайм-аут. Обрабатывайте отказы внутри обработчика и всегда вызывайте callback
callback не вызван за тайм-аутОтвет считается отсутствующим; карточки остаются в состоянии по фиду
Возвращена пустая картаСостояние по фиду сохраняется
Часть ключей отсутствуетПрименяются только полученные значения
callback вызван повторноУчитывается первый вызов, остальные игнорируются
Ответ пришёл после тайм-аутаОтбрасывается, даже если он корректный
Ответ пришёл после refreshProductsAvailability()Отбрасывается целиком
Ответ пришёл после смены эфира или закрытия плеераОтбрасывается

Обновление наличия не отменяет уже запущенное добавление в корзину. Начатая операция доводится до своего результата — успеха, ошибки или тайм-аута, — даже если в это время карточка сменила состояние доступности.

Карточки отрисовываются сразу по данным фида и обновляются при получении ответа. Первый показ плеера и старт видео не ждут stock API.

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

Между показом карточки и оформлением заказа товар может закончиться. Обработчик наличия повышает точность интерфейса, но окончательное решение принимает система ритейлера при изменении корзины и оформлении заказа: Добавление в корзину.

Что ShopStory не делает

Эти границы важны для оценки нагрузки и информационной безопасности:

  • не обращается к stock API ритейлера напрямую и не хранит его учётные данные;
  • не запрашивает и не хранит остатки магазинов и товаров за пределами текущего эфира;
  • не записывает пользовательский остаток в общее поле feedProductAvailable: ответ обработчика живёт в памяти открытой вкладки, относится только к этому покупателю, не уходит на серверы ShopStory и не сохраняется;
  • не передаёт в обработчик персональные данные покупателя и не получает их в ответе — ответ это карта «идентификатор → доступность», и ничего больше;
  • не требует открывать stock API во внешнюю сеть и не требует сетевого доступа со стороны ShopStory;
  • не опрашивает API по таймеру и не создаёт фоновый трафик при закрытом плеере.

Верхняя граница нагрузки на stock API считается просто: один вызов обработчика на открытие эфира, один на смену магазина и один на добавление товаров в идущий эфир. Размер вызова — число товаров, по которым запрашивается наличие. Вызовов при показе отдельной карточки нет.

Поверхности

ПоверхностьLive-проверка наличия
Web SDK, каталог и плеер через show()Обработчик в actions, метод refreshProductsAvailability()
Public API без SDKИнтерфейс реализует клиент; наличие проверяет его собственный код
Product mini-player, standalone PiP bundleОбработчик задаётся при инициализации bundle; объект actions из show() в него не наследуется
Mobile SDKПриложение возвращает наличие по контракту поставленной версии пакета: Mobile SDK

Обработчик не переносится между поверхностями: заданный в show() на одной странице, он не действует в плеере, открытом из standalone PiP, и в нативном приложении. Каждая поверхность получает его в своей точке инициализации, которая фиксируется в integration handoff проекта.

Public API без SDK

GET /v3/streams возвращает снимок фида в body.products[].feedProductAvailable. Значение содержит булев признак и не передаёт количество единиц, остаток магазина или время обновления товара; serverTime — время ответа API, а не время синхронизации.

Отдельного endpoint проверки остатков в Public API нет: остаток по магазину — данные ритейлера, и правильное место их запроса — его собственный код. При интеграции через Public API проверку наличия выполняет интерфейс клиента до показа действия покупки.

Данные для подключения

Обработчик — штатная возможность SDK, но стыковка с конкретным stock API требует согласования:

Что нужноЗачем
Соответствие идентификаторовfeedProductId должен однозначно определять позицию, которую принимает stock API: товар, вариант или SKU
Пакетный метод APIПроверка списка товаров одним запросом; при его отсутствии ограничение параллельных запросов остаётся в адаптере сайта
Источник выбранного магазинаОткуда код сайта берёт текущий магазин, регион или способ получения
Значение результатаКакое поле ответа означает возможность купить; что означают ноль, отрицательное значение и отсутствие позиции в ответе
Семантика available в фидеЧто означает false в вашей выгрузке; от этого зависят правила импорта, а не поведение обработчика
Лимиты и время ответаДля настройки тайм-аута и срока актуальности
Тестовые данныеТовары с наличием и без него минимум в двух магазинах

Точные URL, схемы запросов, учётные данные, лимиты и правила магазинов не публикуются: они фиксируются в закрытом приложении к интеграции. Порядок согласования — в разделе Кастомные commerce-интеграции.

Проверка перед production

  1. Обработчик получает те же строковые идентификаторы, что переданы в фиде, без числового преобразования и потери ведущих нулей.
  2. Товар с наличием показывает доступное действие покупки, товар без наличия — состояние «Нет в наличии».
  3. Товар, закрытый фидом, остаётся заблокированным, даже когда обработчик вернул true.
  4. Смена магазина обновляет карточки, а ответ для прежнего магазина на них не попадает.
  5. Недоступный stock API не блокирует покупку целиком, не показывает ложное «Нет в наличии» и не прерывает видео.
  6. Тайм-аут обработчика не задерживает первый показ карточек и старт воспроизведения.
  7. Добавление в корзину во время обновления наличия завершается ожидаемым результатом.
  8. Endpoint остатков не отдаёт данные без действующей пользовательской сессии и не раскрывает внутренние ошибки.
  9. Поведение проверено на desktop и mobile web в целевых браузерах проекта.

Диагностика

СимптомЧто проверить
Все товары показаны как недоступныеВозвращает ли обработчик false вместо пустой карты при ошибке API
Наличие не обновляетсяЗадан ли resolveProductsAvailability в том же вызове show(), что и остальные actions
Наличие не меняется при смене магазинаВызывается ли refreshProductsAvailability() в обработчике смены магазина на сайте
Показано наличие чужого магазинаПорядок вызова: сначала обновите выбранный магазин на сайте, затем вызовите метод
Часть товаров не обновиласьСовпадение ключей ответа с переданными идентификаторами и их строковый тип
Товар доступен по API, но кнопка заблокированаЗначение available в фиде: обработчик не возвращает в продажу закрытый фидом товар
Карточки обновляются с заметной задержкойВремя ответа stock API и размер пакетного запроса

При обращении в поддержку передайте URL страницы, applicationId, время, идентификаторы проверяемых товаров и выбранный магазин. Учётные данные, cookies и персональные данные покупателя в обращение не включайте.

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