Внешние скидки
Внешняя скидка позволяет рассчитывать скидку на сервере вашего приложения. InSales отправляет POST-запрос с JSON заказа на указанный вами URL, а обработчик возвращает сумму и название скидки либо список ошибок.
Запрос выполняется при открытии корзины или изменении данных, от которых зависит скидка. Повторные запросы зависят от настроек скидки и версии оформления заказа; эти условия описаны ниже.
Настройте скидку в бэк-офисе: Клиенты → Скидки → Внешние скидки, адрес https://SHOP/admin2/external_discounts. Те же поля доступны через /admin/external_discounts.json.
На витрине строка скидки появляется в корзине и на оформлении. Название из ответа сохраняется в созданном заказе.
Настройка
У скидки есть следующие настройки:
url— адрес скрипта,httpилиhttps. Адрес из частной сети сохранить нельзя: и бэк-офис, иPOST /admin/external_discounts.jsonотвечают422с{"url":["Недопустимый URL"]}. Повтор того же URL даёт422и{"url":["уже существует"]}.description— название в бэк-офисе. Оно также используется на витрине, если обработчик не вернул название скидки. Максимальная длина — 200 символов. При превышении API возвращает422,{"description":["Описание слишком большой длины (не может быть больше, чем 200 символов)"]}.order_lines_depend— пересчитывать скидку при изменении состава корзины.observed_fields— id дополнительных полей заказа. Пока выбранное поле пустое, магазин не отправляет запрос. После заполнения значение передаётся вfields_values. При изменении значения магазин отправляет новый запрос.active— скидка включена. Выключенная запись запрос не отправляет.
В форме бэк-офиса это поля «URL», «Описание», «Зависит от состава корзины», «Зависит от дополнительных полей заказа» и «Включено». Галочка «Включено» стоит по умолчанию.
Если зависимость от состава корзины выключена и ни одно выбранное дополнительное поле не заполнено, магазин не запрашивает расчёт скидки. Сумма корзины не меняется.
API
Авторизация та же, что у остального Admin API: HTTP Basic, логин — идентификатор приложения, пароль — пароль приложения в этом магазине.
GET /admin/external_discounts.json
GET /admin/external_discounts/:id.json
POST /admin/external_discounts.json
PUT /admin/external_discounts/:id.json
DELETE /admin/external_discounts/:id.json
Создание:
{
"external_discount": {
"url": "https://discount.example/calculate",
"description": "Скидка по карте",
"order_lines_depend": true,
"observed_fields": [185],
"active": true
}
}
Ответ 201 возвращает запись с id. Обновление отвечает 200 и той же записью. Удаление отвечает 200 и {"status":"ok"}, следующий GET этой записи — 404.
Запрос
InSales отправляет на url POST-запрос с заголовком Content-Type: application/json, без отдельной авторизации. Тело содержит данные заказа, даже если покупатель ещё работает с корзиной и заказ не создан.
До создания заказа поля id, number и key пустые. Товары передаются в массиве order_lines. Для расчёта суммы товаров умножьте sale_price на quantity в каждой строке и сложите результаты. В повторном запросе items_price, total_price заказа и массив discounts уже могут содержать ранее применённую внешнюю скидку, а sale_price строки остаётся ценой до неё.
Ниже приведены поля запроса, относящиеся к расчёту скидки для корзины из двух позиций.
{
"id": null,
"number": null,
"key": null,
"currency_code": "RUR",
"locale": "ru",
"items_price": 266.0,
"total_price": 266.0,
"discount": null,
"discounts": [],
"fields_values": [],
"client": {
"id": null,
"email": null,
"phone": null,
"registered": false
},
"order_lines": [
{
"title": "товар 2",
"product_id": 569,
"variant_id": 2176487162,
"sku": null,
"quantity": 1,
"sale_price": 200.0,
"full_sale_price": 200.0,
"total_price": 200.0,
"full_total_price": 200.0,
"discounts_amount": 0.0,
"vat": -1,
"weight": null,
"barcode": null,
"unit": "pce"
},
{
"title": "товар 6",
"product_id": 601,
"variant_id": 2176487170,
"sku": null,
"quantity": 1,
"sale_price": 66.0,
"full_sale_price": 66.0,
"total_price": 66.0,
"full_total_price": 66.0,
"discounts_amount": 0.0,
"vat": -1,
"weight": null,
"barcode": null,
"unit": "pce"
}
]
}
Полное тело запроса также содержит сведения о доставке, оплате, адресе, shipping_address, источнике визита и cookies. В приведённом расчёте скидки эти данные не используются. cookies в расчёт не берите и у себя не сохраняйте.
Если покупатель заполнил дополнительное поле заказа, массив fields_values содержит его значение:
"fields_values": [
{
"field_id": 185,
"name": "Номер карты",
"type": "Текст",
"value": "CARD-77",
"handle": null
}
]
При оформлении заказа объект client содержит имя, почту и телефон, которые ввёл покупатель. У корзины до создания заказа client.id может быть пустым.
Ответ
Обработчик должен вернуть HTTP-статус 200 и JSON-объект со скидкой или ошибками.
Сумма в валюте магазина:
{
"discount": 100,
"discount_type": "MONEY",
"title": "Скидка по карте"
}
На корзине из товаров на 200 ₽ и 66 ₽ эта скидка даёт строку «Скидка по карте» на 100 ₽ и итог 166 ₽.
Процент считается от суммы товаров и показывается в рублях, без копеек. Десять процентов на корзине 200 ₽ и две штуки по 66 ₽ (332 ₽) дают 33 ₽, итог 299 ₽:
{
"discount": 10,
"discount_type": "PERCENT",
"title": "Десять процентов"
}
Если title не передать, на витрине показывается description из настройки скидки. Ответ {"discount": 20, "discount_type": "MONEY"} при описании «Самый дешёвый товар» даёт строку с этим названием и суммой 20 ₽.
Если сумма скидки превышает стоимость товаров, InSales ограничивает её стоимостью товаров. Денежная скидка 10000 ₽ на корзину 332 ₽ оставляет итог 0 ₽.
Если вернуть нулевую скидку с названием, InSales покажет строку скидки, но не изменит итоговую сумму. Покупатель видит «Название» и «0 ₽», сумма товаров не меняется:
{
"discount": 0,
"discount_type": "MONEY",
"title": "Карта принята"
}
Если вернуть нулевую скидку с пустым title, строка скидки не отображается.
Если скидку нельзя применить, верните массив строк errors. InSales не применит скидку и покажет сообщения рядом с полем промокода:
{
"errors": ["Карта не найдена"]
}
В стандартной корзине тот же текст доступен как cart.discount_errors:
{% for error in cart.discount_errors %}
<div class="discount-error">{{ error }}</div>
{% endfor %}
Пример: самый дешёвый товар
При двух и более позициях скидка равна стоимости самой дешёвой строки, sale_price * quantity. На одной позиции скидки нет.
import json
def discount_for(order):
lines = order.get("order_lines") or []
if len(lines) < 2:
return {"discount": 0, "discount_type": "MONEY", "title": ""}
cheapest = min(
float(line["sale_price"]) * float(line["quantity"])
for line in lines
)
return {
"discount": cheapest,
"discount_type": "MONEY",
"title": "Самый дешёвый товар",
}
Для корзины из товаров 200 ₽ и 66 ₽ ответ такой:
{
"discount": 66.0,
"discount_type": "MONEY",
"title": "Самый дешёвый товар"
}
В корзине строка «Самый дешёвый товар» на 66 ₽, итог 200 ₽.
Когда уходит новый запрос
InSales сохраняет результат расчёта. При повторном открытии той же корзины магазин использует сохранённый ответ и не отправляет новый POST-запрос.
Если включена настройка «Зависит от состава корзины», магазин запрашивает расчёт при изменении количества, варианта или набора товаров. Если настройка выключена, изменение количества не вызывает пересчёт: применяется сумма из предыдущего ответа.
Настройка «Зависит от дополнительных полей заказа» вызывает новый запрос при изменении значения выбранного поля. Пустое поле не вызывает запрос.
Для выключенной скидки (active: false) магазин не отправляет запросы, даже если состав корзины изменился.
При оформлении заказа момент запроса зависит от версии чекаута.
Пока обработчик не ответил, корзина не открывается. Если он возвращает статус, отличный от 200, магазин открывает страницу без этой скидки. При пустом теле или невалидном JSON вместо корзины отображается ошибка разбора JSON. Возвращайте описанный выше JSON-объект со статусом 200.
Дополнительное поле
Поле заказа создаётся в бэк-офисе и отмечается в «Зависит от дополнительных полей заказа». На витрине его значение уходит вместе с корзиной. Для сохранения значения отправьте POST на /cart_items с параметром _method=put: HTML-форма с method="put" не отправляет PUT-запрос. У поля «Номер карты» с id 185 тело такое:
_method=put
order[fields_values_attributes][185][field_id]=185
order[fields_values_attributes][185][value]=CARD-42
Нужен и CSRF-токен страницы. После сохранения уходит новый POST на URL скидки, и в fields_values приходит введённое значение. Новое значение того же поля вызывает ещё один запрос и новую сумму.
На оформлении то же поле показывается покупателю. Смена значения и сохранение контактов снова вызывают скрипт. В созданном заказе значение поля остаётся. Его видно в бэк-офисе и в GET /admin/orders/:id.json, в fields_values.
Версия чекаута
Версия оформления заказа не меняет тело запроса, формат ответа и правила выбора между внешней скидкой и купоном. Различаются адрес страницы и момент, когда InSales запрашивает расчёт скидки.
Третья версия — /gocheckout. Открытие и повторная загрузка этой страницы новый POST не отправляют, пока не изменились состав корзины и наблюдаемые поля. На странице остаётся скидка, уже посчитанная в корзине. Новый запрос уходит при смене состава корзины или значения наблюдаемого поля, в том числе при сохранении контактов.
Вторая версия — /new_order. Каждый заход на страницу отправляет POST заново, даже если корзина не менялась. В сводке видна скидка из этого запроса. Смена способа доставки на этой странице новый запрос не вызывает. Кнопка «Подтвердить заказ» отправляет POST ещё раз, уже с контактами и выбранной доставкой.
Заказ хранит скидку одинаково на обеих версиях. Доставка в скидку не входит: товар 200 ₽ со скидкой 40 ₽ и доставкой 300 ₽ даёт товар 160 ₽, доставку 300 ₽ и сумму заказа 460 ₽. В GET /admin/orders/:id.json в discounts лежат description, amount и type_id 2. items_price и total_price уже с учётом скидки.
Корзина, оформление и заказ
Денежная скидка 100 ₽ на товарах 200 ₽ и 66 ₽ даёт название и итог 166 ₽. Десять процентов на товарах 332 ₽ дают строку 33 ₽ и итог 299 ₽. Текст из errors показывается под полем промокода. Нулевая скидка с названием остаётся строкой «0 ₽».
На оформлении видны та же строка и дополнительное поле. В заказе сохраняются название из title и итог с учётом скидки. Для товаров на 398 ₽ и ответа discount: 25, discount_type: "MONEY" итог заказа — 373 ₽. В личном кабинете у заказа есть строка скидки и значение дополнительного поля.
GET /admin/orders/:id.json возвращает скидку в discounts: description — название из title, discount и amount — сумма, type_id 2 — денежная скидка. type_id 1 — процент.
Несколько скидок
Если доступны несколько внешних скидок или внешняя скидка и купон, InSales применяет одну скидку с наибольшей суммой. Скидки не складываются.
Если включены две внешние скидки, запрос уходит на оба URL. В корзине остаётся одна, с большей суммой.
Купон с большей суммой заменяет внешнюю скидку. Купон с меньшей суммой сохраняется в поле промокода, но к заказу применяется внешняя скидка.
Если одна скидка возвращает errors, а другая — сумму, применяется сумма. Текст ошибки при этом виден у промокода.