🏥 Комплексная интеграция с Ютекой с нуля

🏥 Комплексная интеграция с Ютекой с нуля #


Данный документ описывает полную интеграцию аптечной сети (Партнёра) с Ютекой, включая все поддерживаемые типы заказов:

Тип заказа Описание Источник остатков
pickup Самовывоз из аптеки. Товар уже в аптеке, покупатель бронирует и забирает Остатки аптек
extendedPickup Самовывоз на следующий день. Товар на складе, доставляется в аптеку, покупатель забирает по прибытии Остатки складов
delivery Доставка на дом. Товар в аптеке, Партнёр организует доставку курьером Остатки аптек
courier Доставка курьером Ютеки. Товар в аптеке, курьер Ютеки забирает заказ из аптеки и доставляет покупателю Остатки аптек

Какие типы заказов необходимо поддержать Партнёру #

Не каждый Партнёр обязан поддерживать все типы заказов. Минимально необходимый тип — pickup. Остальные типы подключаются по мере готовности инфраструктуры Партнёра.

Тип заказа Требования к инфраструктуре Партнёра
pickup Аптеки с остатками
extendedPickup Склады + аптеки с привязкой к складам + остатки на складах + даты доставки
delivery Аптеки с остатками + собственная курьерская служба + эндпоинт расчёта доставки
courier Аптеки с остатками (курьерскую службу предоставляет Ютека)

Из чего состоит интеграция #

Интеграция с Ютекой состоит из двух глобальных процессов:

  • синхронизация каталога (выгрузки)
  • обмен заказами
graph TD
A[Интеграция с Ютекой] --> B[Выгрузки]
A --> C[Обмен заказами]
B --> B1[Склады]
B --> B2[Аптеки]
B --> B3[Товары]
B --> B4[Остатки аптек]
B --> B5[Остатки складов]
C --> C1[Создание заказа]
C --> C2[Получение статусов]
C --> C3[Отмена заказа]
C --> C4[Расчёт доставки]

Выгрузки #

Это процесс получения Ютекой информации от Партнёра о:

  • складах
  • аптеках
  • товарах
  • остатках (аптек и/или складов)

Обмен заказами #

Это процесс, при котором Ютека:

  • отправляет новые заказы в инфраструктуру Партнёра
  • получает статусы из инфраструктуры Партнёра
  • отправляет статусы в инфраструктуру Партнёра
  • запрашивает расчёт доставки (для типа delivery)

Зависимость выгрузок от типов заказов #

Не все выгрузки необходимы для каждого типа заказов:

Выгрузка pickup extendedPickup delivery courier
Склады ✔️
Аптеки (базовые поля) ✔️ ✔️ ✔️ ✔️
Аптеки (warehouseId) ✔️
Аптеки (deliveryDates) ✔️
Товары ✔️ ✔️ ✔️ ✔️
Остатки аптек ✔️ ✔️ ✔️
Остатки складов ✔️
Расчёт доставки (эндпоинт) ✔️

Примеры реализации комплексной интеграции с нуля #

Выгрузки через протокол FTP #

graph TD
Partner[Партнёр] --> Warehouses[Склады]
Partner[Партнёр] --> Pharmacies[Аптеки]
Partner[Партнёр] --> Products[Товары]
Partner[Партнёр] --> Stocks[Остатки]
Warehouses[Склады] --> FTP
Pharmacies[Аптеки] --> FTP
Products[Товары] --> FTP
Stocks[Остатки] --> FTP
FTP --> Uteka[Ютека]
Uteka[Ютека] --> FTP

На данный момент мы поддерживаем следующие форматы файлов: JSON (приоритетнее), CSV, XML. Файлы должны быть в кодировке UTF-8 или Windows-1251 без BOM. Для CSV формата рекомендуемый разделитель должен быть: ;, но можно ,, если технически можете сформировать корректный csv. Формат файлов должен быть одинаковый для всех файлов в выгрузках. Т.е. не должно быть такого, что аптеки находятся в json, товары - в xml, а остатки - в csv. Для всех процессов выбирается только 1 формат.

В случае, если у Партнера нет своего FTP сервера, Ютека может предоставить доступ на свой FTP сервер.

Аптеки FTP #

Необходимо для: всех типов заказов

Частота выгрузки аптек должна быть от 1 раза в 3 часа до 1 раза в сутки. Нестандартная частота выгрузок обсуждается в индивидуальном порядке.

Для выгрузки аптек в корень FTP выгружается файл pharmacies.json (или csv/xml в зависимости от выбранного формата).

Файл должен состоять из следующих полей:

Поле Тип Обязательное Описание Комментарий
id или pharmacyId string ✔️ ID аптеки
title string ✔️ Название аптеки
warehouseId string extendedPickup ✔️ ID склада из выгрузок складов Обязательно для extendedPickup. Одна аптека может принимать остатки только с одного склада.
region string Регион местонахождения аптеки Не обязателен, если указан в поле address
city string Город местонахождения аптеки Не обязателен, если указан в поле address
address string ✔️ Адрес местонахождения аптеки полностью (Область, населённый пункт, улица, дом). Без почтового индекса. С точностью до дома. Если возможно, без дополнительных комментариев, вида пом.75843б, за магазином канцтоваров. Регион и город не обязательно указывать тут, если они передаются в полях region и city
phone string ✔️ Телефон в международном формате (+71234567890) Если это городской номер телефона, то с кодом города (например для Новосибирска это +7(383)000‒00‒00)
workingHours Object или string (см. пример ниже) ✔️ Часы работы в формате
deliveryDates Object (см. пример ниже) extendedPickup ✔️ Даты доставки остатков со склада warehouseId Обязательно для extendedPickup. Ежедневное обновление на ближайшие 2-3 дня
location string ✔️ GPS координаты Latitude,Longitude Например, 59.939032,30.315826. При подстановке в гугл/яндекс/другие карты должна показываться точка на аптеку. По координатам проверяется адрес
email string Email аптеки для связи Мы можем присылать email оповещения в аптеку при поступлении новых или отмене заказов

Возможно добавление дополнительных полей для более специфичной логики, например, ограничение доставки некоторых остатков в некоторые аптеки (например, по признаку наличия холодильника в аптеке для вакцин) или по другим условиям.

Обсуждается в индивидуальном порядке.

График работы #

График работы ожидается в виде JSON объекта, разбитый по дням недели, либо в строковом типе.

Для JSON-объекта:

  1. Дни недели пронумерованы от 1 до 7, где 1 - понедельник, 7 - воскресенье.
  2. open - время открытия аптеки, close - время закрытия. Относительно этих значений присылаются уведомления пользователям, и считаются регламенты (например, “будет собран до”).
  3. Если аптека не работает в какой-то день (например, в воскресенье), то день недели пропускается, либо значения open и close заполняются пустыми строками: "open":"","close":"".
  4. Если аптека круглосуточная, то open и close присылается со значением 00:00: "open":"00:00","close":"00:00"
  5. Разработчикам: нужно учитывать, что JSON формат относится только к одному полю из АПИ, а поле (колонка в выгрузке) должна быть строкой в соответствующем формате (JSON, CSV или XML, как договорились). Поэтому нужно не забывать про экранирование данных при подстановке в выгрузку.
Пример JSON объекта workingHours
"workingHours": {
  "1": { "open": "08:00", "close": "20:00" },
  "2": { "open": "08:00", "close": "20:00" },
  "3": { "open": "08:00", "close": "20:00" },
  "4": { "open": "08:00", "close": "20:00" },
  "5": { "open": "08:00", "close": "20:00" },
  "6": { "open": "10:00", "close": "17:30" },
  "7": { "open": "", "close": "" }
}

Для строкового типа:

Это может быть тот же самый json-объект, но завёрнутый в строку.

Пример строкового workingHours с завёрнутым JSON-объектом
"workingHours": "{\"1\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"2\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"3\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"4\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"5\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"6\":{\"open\":\"10:00\",\"close\":\"17:30\"},\"7\":{\"open\":\"\",\"close\":\"\"}}"
  • Либо это может быть текст, содержащий сокращённые названия дней недели и их время работы.
  • Дни недели могут указываться интервалами от пн до вс.
  • Интервалы разделяются: ,
  • Если аптека не работает в какой-то день или интервал, то для него нужно указать ключевое слово выходной.
  • Если аптека работает круглосуточно, то для этого дня или интервала нужно указать ключевое слово круглосуточно
Пример строкового workingHours с текстовыми днями недели
"пн-пт 08:00-20:00, сб 10:00-17:30, вс выходной"

Для круглосуточной аптеки:

"пн-вс круглосуточно"

Даты доставки (deliveryDates) #

Необходимо для: extendedPickup

Даты доставки описывают, когда остатки со склада warehouseId будут доставлены в аптеку. Объект со следующими полями:

Поле Тип Обязательное Описание
deliveryDate string ✔️ Ближайшая дата доставки остатков в аптеку
deliveryDateExpires string Крайняя дата и время оформления заказа для попадания в ближайшую поставку. Опционально — на случай, если deliveryDate неактуальна в течение всего дня
nextDeliveryDate string Следующая дата доставки после ближайшей

Формат дат: YYYY-MM-DDTHH:mm:ss

Рекомендуется обновлять даты доставки ежедневно.

Пример deliveryDates — все поля
"deliveryDates": {
  "deliveryDate": "2024-04-27T00:00:00",
  "deliveryDateExpires": "2024-04-26T20:30:00",
  "nextDeliveryDate": "2024-04-28T00:00:00"
}

Здесь: ближайшая доставка — 27 апреля. Чтобы попасть в эту доставку, заказ нужно оформить до 26 апреля 20:30. Следующая доставка — 28 апреля.

Пример deliveryDates — только обязательное поле
"deliveryDates": {
  "deliveryDate": "2024-04-27T00:00:00"
}

Если deliveryDateExpires не указан, подразумевается, что deliveryDate актуальна в течение всего дня.

Примеры файлов аптек целиком:

Пример JSON с часами работы в виде объекта
[
  {
    "pharmacyId": "228",
    "title": "Аптечный пункт №1",
    "address": "Краснодарский край, г Тихорецк, ул Октябрьская, д 101",
    "phone": "8-800-228-22-88",
    "warehouseId": "20247701-bf4b-11ed-812f-00e0ed9e2e92",
    "deliveryDates": {
      "deliveryDate": "2024-04-27T00:00:00",
      "deliveryDateExpires": "2024-04-26T20:30:00",
      "nextDeliveryDate": "2024-04-29T00:00:00"
    },
    "workingHours": {
      "1": { "open": "08:00", "close": "20:00" },
      "2": { "open": "08:00", "close": "20:00" },
      "3": { "open": "08:00", "close": "20:00" },
      "4": { "open": "08:00", "close": "20:00" },
      "5": { "open": "08:00", "close": "20:00" },
      "6": { "open": "10:00", "close": "17:30" },
      "7": { "open": "", "close": "" }
    },
    "location": "45.868670,40.144246",
    "email": "test@test.com"
  },
  {
    "pharmacyId": "100",
    "title": "Аптека на Ленина",
    "address": "г Москва, ул Ленина, д 15",
    "phone": "+74951234567",
    "workingHours": {
      "1": { "open": "08:00", "close": "21:00" },
      "2": { "open": "08:00", "close": "21:00" },
      "3": { "open": "08:00", "close": "21:00" },
      "4": { "open": "08:00", "close": "21:00" },
      "5": { "open": "08:00", "close": "21:00" },
      "6": { "open": "09:00", "close": "18:00" },
      "7": { "open": "10:00", "close": "16:00" }
    },
    "location": "55.753215,37.622504",
    "email": "apteka100@partner.ru"
  }
]

Аптека 228 — с привязкой к складу и датами доставки (для extendedPickup). Аптека 100 — без привязки к складу (для pickup, delivery, courier).

Пример JSON с часами работы в виде строки
[
  {
    "pharmacyId": "228",
    "title": "Аптечный пункт №1",
    "address": "Краснодарский край, г Тихорецк, ул Октябрьская, д 101",
    "phone": "8-800-228-22-88",
    "warehouseId": "20247701-bf4b-11ed-812f-00e0ed9e2e92",
    "deliveryDates": {
      "deliveryDate": "2024-04-27T00:00:00",
      "deliveryDateExpires": "2024-04-26T20:30:00",
      "nextDeliveryDate": "2024-04-29T00:00:00"
    },
    "workingHours": "{\"1\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"2\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"3\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"4\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"5\":{\"open\":\"08:00\",\"close\":\"20:00\"},\"6\":{\"open\":\"10:00\",\"close\":\"17:30\"},\"7\":{\"open\":\"\",\"close\":\"\"}}",
    "location": "45.868670,40.144246",
    "email": "test@test.com"
  },
  {
    "pharmacyId": "100",
    "title": "Аптека на Ленина",
    "address": "г Москва, ул Ленина, д 15",
    "phone": "+74951234567",
    "workingHours": "пн-пт 08:00-21:00, сб 09:00-18:00, вс 10:00-16:00",
    "location": "55.753215,37.622504",
    "email": "apteka100@partner.ru"
  }
]
Пример CSV
pharmacyId;title;address;phone;warehouseId;deliveryDates;workingHours;location;email
228;Аптечный пункт №1;Краснодарский край, г Тихорецк, ул Октябрьская, д 101;8-800-228-22-88;20247701-bf4b-11ed-812f-00e0ed9e2e92;"{""deliveryDate"": ""2024-04-27T00:00:00"", ""deliveryDateExpires"": ""2024-04-26T20:30:00"", ""nextDeliveryDate"": ""2024-04-29T00:00:00""}";"{""1"":{""open"":""08:00"",""close"":""20:00""},""2"":{""open"":""08:00"",""close"":""20:00""},""3"":{""open"":""08:00"",""close"":""20:00""},""4"":{""open"":""08:00"",""close"":""20:00""},""5"":{""open"":""08:00"",""close"":""20:00""},""6"":{""open"":""10:00"",""close"":""17:30""},""7"":{""open"":"""",""close"":""""}}";45.868670,40.144246;test@test.com
100;Аптека на Ленина;г Москва, ул Ленина, д 15;+74951234567;;;"пн-пт 08:00-21:00, сб 09:00-18:00, вс 10:00-16:00";55.753215,37.622504;apteka100@partner.ru
Пример XML
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <pharmacyId>228</pharmacyId>
        <title>Аптечный пункт №1</title>
        <address>Краснодарский край, г Тихорецк, ул Октябрьская, д 101</address>
        <phone>8-800-228-22-88</phone>
        <warehouseId>20247701-bf4b-11ed-812f-00e0ed9e2e92</warehouseId>
        <deliveryDates>
            <deliveryDate>2024-04-27T00:00:00</deliveryDate>
            <deliveryDateExpires>2024-04-26T20:30:00</deliveryDateExpires>
            <nextDeliveryDate>2024-04-29T00:00:00</nextDeliveryDate>
        </deliveryDates>
        <workingHours>
            {"1":{"open":"08:00","close":"20:00"},"2":{"open":"08:00","close":"20:00"},"3":{"open":"08:00","close":"20:00"},"4":{"open":"08:00","close":"20:00"},"5":{"open":"08:00","close":"20:00"},"6":{"open":"10:00","close":"17:30"},"7":{"open":"","close":""}}
        </workingHours>
        <location>45.868670,40.144246</location>
        <email>test@test.com</email>
    </item>
    <item>
        <pharmacyId>100</pharmacyId>
        <title>Аптека на Ленина</title>
        <address>г Москва, ул Ленина, д 15</address>
        <phone>+74951234567</phone>
        <workingHours>
            {"1":{"open":"08:00","close":"21:00"},"2":{"open":"08:00","close":"21:00"},"3":{"open":"08:00","close":"21:00"},"4":{"open":"08:00","close":"21:00"},"5":{"open":"08:00","close":"21:00"},"6":{"open":"09:00","close":"18:00"},"7":{"open":"10:00","close":"16:00"}}
        </workingHours>
        <location>55.753215,37.622504</location>
        <email>apteka100@partner.ru</email>
    </item>
</items>

Склады FTP #

Необходимо для: extendedPickup

Частота выгрузки складов должна быть до 1 раза в сутки. Нестандартная частота выгрузок обсуждается в индивидуальном порядке.

Для выгрузки складов в корень FTP выгружается файл region.json (или csv/xml в зависимости от выбранного формата).

Файл должен состоять из следующих полей:

Поле Тип Обязательное Описание Комментарий
id string ✔️ ID склада Нужен для привязки к аптекам, куда будут доставляться остатки. Может содержать название региона.
title string ✔️ Название склада Краткое описание. Может содержать название региона.

Важно! На один регион может быть несколько складов, один склад может быть на несколько регионов.

Пример JSON
[
  {
    "id": "msc",
    "title": "Москва"
  },
  {
    "id": "20247701-bf4b-11ed-812f-00e0ed9e2e92",
    "title": "Краснодарский край \"Парк А\""
  }
]
Пример CSV
id;title
msc;Москва
20247701-bf4b-11ed-812f-00e0ed9e2e92;Краснодарский край "Парк А"
Пример XML
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <id>msc</id>
        <title>Москва</title>
    </item>
    <item>
        <id>20247701-bf4b-11ed-812f-00e0ed9e2e92</id>
        <title>Краснодарский край "Парк А"</title>
    </item>
</items>

Товары FTP #

Необходимо для: всех типов заказов

Частота выгрузки товаров должна быть от 1 раза в 3 часа до 1 раза в сутки. Нестандартная частота выгрузок обсуждается в индивидуальном порядке.

Для выгрузки товаров в корень FTP выгружается файл products.json (или csv/xml в зависимости от выбранного формата).

Файл должен состоять из следующих полей:

Поле Тип Обязательное Описание
id или productId string ✔️ ID товара
barcode или barcodes string ✔️ Штрихкод. Если несколько, перечислять через ,, например: 4250369505166,2250369505188
title string ✔️ Название
vendor string ✔️ Производитель
country string ✔️ Страна
egk string код egk значительно ускорит привязку товаров к нашему каталогу. Если несколько, то перечислять через ,, например: 161421,161369
rls string код rls значительно ускорит привязку товаров к нашему каталогу. Если несколько, то перечислять через ,, например: 82531,82592
katren string код katren значительно ускорит привязку товаров к нашему каталогу. Если несколько, то перечислять через ,, например: 55079077,55079091
protek string код protek значительно ускорит привязку товаров к нашему каталогу. Если несколько, то перечислять через ,, например: 1-218661,218661,70218661
Пример JSON
[
  {
    "productId": "228",
    "barcode": "4250369505166",
    "title": "Лекарственные средства АСПИРИН ЭКСПРЕСС табл. шип. 500мг N12",
    "vendor": "БАЙЕР",
    "country": "ГЕРМАНИЯ",
    "katren": "55079077",
    "protek": "1-218661,218661,70218661",
    "egk": "100986"
  },
  {
    "productId": "229",
    "barcode": "4630000891184",
    "title": "Травы, чаи прочие ГАЛЕГИ ТРАВА (Козлятник) 50г N1",
    "vendor": "Камелия-ЛТ",
    "country": "РФ",
    "katren": "400707176"
  }
]
Пример CSV
productId;barcode;title;vendor;country;rls;katren;protek;egk
228;4250369505166;Лекарственные средства АСПИРИН ЭКСПРЕСС табл. шип. 500мг N12;БАЙЕР;ГЕРМАНИЯ;;55079077;1-218661,218661,70218661;100986
229;4630000891184;Травы, чаи прочие ГАЛЕГИ ТРАВА (Козлятник) 50г N1;Камелия-ЛТ;РФ;;400707176;;
Пример XML
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <productId>228</productId>
        <barcode>4250369505166</barcode>
        <title>Лекарственные средства АСПИРИН ЭКСПРЕСС табл. шип. 500мг N12</title>
        <vendor>БАЙЕР</vendor>
        <country>ГЕРМАНИЯ</country>
        <katren>55079077</katren>
        <protek>1-218661,218661,70218661</protek>
        <egk>100986</egk>
    </item>
    <item>
        <productId>229</productId>
        <barcode>4630000891184</barcode>
        <title>Травы, чаи прочие ГАЛЕГИ ТРАВА (Козлятник) 50г N1</title>
        <vendor>Камелия-ЛТ</vendor>
        <country>РФ</country>
        <katren>400707176</katren>
    </item>
</items>

Остатки FTP #

Частота выгрузки остатков должна быть не чаще 1 раза в 15 минут, не реже 1 раза в час. Нестандартная частота выгрузок обсуждается в индивидуальном порядке.

Для выгрузки остатков в директорию /stocks FTP выгружается файл stocks_<pharmacyId>.json (или csv/xml в зависимости от выбранного формата), где суффикс названия соответствует ID аптеки.

Для остатков складов (extendedPickup) — аналогично, файл stocks_<warehouseId>.json, где суффикс — ID склада.

Файл должен состоять из следующих полей:

Поле Тип Обязательное Описание
productId string ✔️ ID товара из выгрузок товаров
warehouseId string ID склада из выгрузок складов. Обязателен для остатков склада (extendedPickup). Не обязательный, т.к. мы можем достать его из названия файла
price float64 или string или int (если стоимость в копейках) ✔️ Цена
quantity int или string ✔️ Количество
partNumber или consignment string Номер партии
expirationDate string Срок годности в формате: 2006-01-02T15:04:05 или 2006-01-02
maxQuantity int или string Максимально доступное количество этой позиции на единицу заказа
isDelivery bool Доступен ли данный остаток для доставки (только для типа delivery). По умолчанию true

Остатки с quantity = 0 должны передаваться — Ютека убирает их на своей стороне.

Возможно добавление дополнительных полей для более специфичной логики, например, ограничение доставки некоторых остатков в некоторые аптеки (например, по признаку наличия холодильника в аптеке для вакцин) или по другим условиям.

Пример JSON
[
  {
    "product_id": "1234",
    "price": 51.0,
    "quantity": 914,
    "partNumber": "20250201228228",
    "expirationDate": "2040-01-02T00:00:00",
    "maxQuantity": 10
  },
  {
    "product_id": "1235",
    "price": 78.0,
    "quantity": 1324,
    "partNumber": "20250201228114",
    "expirationDate": "2045-06-02T00:00:00"
  },
  {
    "product_id": "1236",
    "price": 58.0,
    "quantity": 730,
    "partNumber": "20250201228553",
    "expirationDate": "2042-09-01T00:00:00"
  }
]
Пример CSV
product_id;price;quantity;partNumber;expirationDate;maxQuantity
1234;51.0;914;20250201228228;2040-01-02T00:00:00;10
1235;78.0;1324;20250201228114;2045-06-02T00:00:00;
1236;58.0;730;20250201228553;2042-09-01T00:00:00;
Пример XML
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <product_id>1234</product_id>
        <price>51.0</price>
        <quantity>914</quantity>
        <partNumber>20250201228228</partNumber>
        <expirationDate>2040-01-02T00:00:00</expirationDate>
        <maxQuantity>10</maxQuantity>
    </item>
    <item>
        <product_id>1235</product_id>
        <price>78.0</price>
        <quantity>1324</quantity>
        <partNumber>20250201228114</partNumber>
        <expirationDate>2045-06-02T00:00:00</expirationDate>
    </item>
    <item>
        <product_id>1236</product_id>
        <price>58.0</price>
        <quantity>730</quantity>
        <partNumber>20250201228553</partNumber>
        <expirationDate>2042-09-01T00:00:00</expirationDate>
    </item>
</items>

Обмен заказами через протокол FTP (менее предпочтительный ❌) #

graph TD
Partner[Партнёр] -->|Статус заказа| FTP
Uteka[Ютека] -->|Новый заказ| FTP
Uteka -->|Отмена заказа| FTP

Этот способ обмена заказами устарел и поддерживается минимально. Для малейших отклонений от схемы и различной кастомизации понадобятся отдельные доработки и тестирование кастомизированной схемы обмена заказами. Мы не рекомендуем его использовать в обмене заказами.

Важно! В данном способе обмена заказами невозможна реализация тайм-слотов доставки, поэтому тип заказа delivery не поддерживается через FTP. Для заказов с доставкой курьером Партнёра необходимо использовать обмен заказами через HTTP REST, где реализован эндпоинт расчёта доставки.

Вместо этого способа, лучше использовать процесс обмена заказами через HTTP REST интерфейса для обработки заказов.

Это примечание не относится к процессу выгрузок через протокол FTP

Раскрыть

Ютека будет загружать свои запросы в директорию /orders/outgoing. А аптечная сеть должна выгружать статусы по заказам в директорию /orders/incoming. Передача запроса считается успешной, когда файл будет удалён с ftp-сервера получателем.

ID заказов — внутренние ID Ютеки.

Создание заказа FTP #

После того, как пользователь создал заказ, Ютека передаёт его в интеграцию через FTP в виде файла. На данный момент поддерживается обмен файлами в формате .json, и .xml. Формат файлов должен быть одинаковый для всех процессов. Имя файла соответствует номеру заказа + расширение (.json, .xml) в кодировке UTF-8 без BOM.

Например, файл с запросом на создание заказа с ID=1234 будет лежать в папке /orders/outgoing и называться 1234.json, полный путь: /orders/outgoing/1234.json

Файл будет состоять из следующих полей:

Поле Тип Обязательное Описание
orderId int ✔️ ID заказа Ютеки
warehouse string extendedPickup ✔️ ID склада из выгрузок складов, из которого нужно отправить препараты в аптеку
pharmacy string ✔️ ID аптеки из выгрузок аптек, куда нужно отправить препараты из склада
amount float64 ✔️ Полная стоимость корзины в рублях (сумма всех item.price), округлённая до копеек
name string ✔️ Имя покупателя
phone string ✔️ Телефон покупателя в международном формате без 7, например, 9181231234
items []Object ✔️ Корзина
items.productId string ✔️ ID товара из выгрузок товаров. Если есть номера партий, в это поле можем подставлять его
items.quantity int ✔️ Количество текущего ID товара в корзине
items.price float64 ✔️ Цена за единицу текущего ID товара

Кастомизация полей, их содержания и типов обговаривается с Партнёром в индивидуальном порядке.

Пример JSON
{
  "orderId": 1234,
  "pharmacy": "228",
  "warehouse": "msc",
  "items": [
    {
      "productId": "1234",
      "quantity": 2,
      "price": 51
    },
    {
      "productId": "1235",
      "quantity": 1,
      "price": 78
    }
  ],
  "amount": 180,
  "name": "Иванов Иван Иванович",
  "phone": "9181231234"
}
Пример XML
<order>
    <OrderID>1234</OrderID>
    <Pharmacy>228</Pharmacy>
    <Warehouse>msc</Warehouse>
    <Items>
        <Item>
            <ProductID>1234</ProductID>
            <Quantity>2</Quantity>
            <Price>51</Price>
        </Item>
        <Item>
            <ProductID>1235</ProductID>
            <Quantity>1</Quantity>
            <Price>78</Price>
        </Item>
    </Items>
    <Amount>180</Amount>
    <Name>Иванов Иван Иванович</Name>
    <Phone>9181231234</Phone>
</order>

Получение статусов по заказам от Партнёра FTP #

Для получения статусов, Партнёр должен выгрузить файлы со статусами по каждому заказу в директорию /orders/incoming. Имя файла соответствует номеру заказа + расширение (.json, .xml) в кодировке UTF-8 без BOM. Например, файл со статусом заказа 1234 будет лежать в папке /orders/incoming и называться status_1234.json, полный путь: orders/incoming/status_1234.json

Файл должен состоять из следующих полей:

Поле Тип Обязательное Описание
orderId string ✔️ ID заказа Ютеки
status string ✔️ Статус заказа
comment string Причина отмены (например, “нет на складе” или “отказ покупателя”)

Возможные статусы заказов:

Статус Описание
approved Подтверждён. Интеграция взяла заказ в обработку. Фармацевт в аптеке увидел заказ или заказ дошёл до аптеки и сохранился со стороны аптечной сети.
ready Готов к выдаче. Интеграция доставила корзину в аптеку, собрала заказ и готова выдать покупателю. Покупатель может идти в аптеку
completed Продан. Покупатель пришёл в аптеку и выкупил корзину
cancelled Отменён аптекой. Аптека не смогла на каком-то из этапов собрать заказ и отменила его
Заказ подтверждён JSON
{
  "orderId": "1234",
  "status": "approved"
}
Заказ подтверждён XML
<Status>
    <OrderID>1234</OrderID>
    <Status>approved</Status>
</Status>
Заказ готов к выдаче JSON
{
  "orderId": "1234",
  "status": "ready"
}
Заказ готов к выдаче XML
<Status>
    <OrderID>1234</OrderID>
    <Status>ready</Status>
</Status>
Заказ продан JSON
{
  "orderId": "1234",
  "status": "completed"
}
Заказ продан XML
<Status>
    <OrderID>1234</OrderID>
    <Status>completed</Status>
</Status>
Заказ отменён JSON
{
  "orderId": "1234",
  "status": "cancelled",
  "comment": "Нет на складе"
}
Заказ отменён XML
<Status>
    <OrderID>1234</OrderID>
    <Status>cancelled</Status>
    <Comment>Нет на складе</Comment>
</Status>

Ограничения обмена заказами через FTP #

  • Частичная сборка корзины — не предусмотрена в данном способе обмена заказами.
  • Тип заказа delivery — не поддерживается, т.к. невозможна реализация тайм-слотов доставки. Для delivery необходим эндпоинт расчёта доставки, который доступен только в REST-варианте.

Для поддержки этих возможностей лучше рассмотреть использование процесса обмена заказами через HTTP REST интерфейса для обработки заказов.

Отмена заказа FTP #

Ютека будет выгружать отмены заказов так же в директорию /orders/outgoing. ID заказов — внутренние ID Ютеки.

Имя файла соответствует status_<id>.json (или .xml).

Например, файл с запросом на отмену заказа с номером 1234 будет лежать в папке /orders/outgoing и называться status_1234.json, полный путь: /orders/outgoing/status_1234.json

Файл будет состоять из следующих полей:

Поле Тип Обязательное Описание
orderId string ✔️ ID заказа Ютеки
status string ✔️ Статус заказа
Заказ отменён со стороны Ютеки JSON
{
  "orderId": "1234",
  "status": "cancelled"
}
Заказ отменён со стороны Ютеки XML
<Status>
    <OrderID>1234</OrderID>
    <Status>cancelled</Status>
</Status>

Подтверждение выполнения заказа FTP (courier) #

Необходимо для: courier (опционально)

Если Партнёру нужно от Ютеки явное подтверждение о том, что заказ доставлен покупателю, для таких случаев Ютека может отправить Партнёру запрос с подтверждением выполнения заказа после того, как курьер вручит заказ.

Механизм аналогичен отмене заказа FTP — Ютека выгружает файл status_<id>.json (или .xml) в директорию /orders/outgoing, но вместо статуса отмены передаётся статус выполнения.

Партнёру необходимо предоставить Ютеке код статуса, который означает «заказ выполнен» в его системе.

Заказ выполнен (подтверждение) JSON
{
  "orderId": "1234",
  "status": "<код статуса выполнения, предоставленный Партнёром>"
}
Заказ выполнен (подтверждение) XML
<Status>
    <OrderID>1234</OrderID>
    <Status><код статуса выполнения, предоставленный Партнёром></Status>
</Status>

Выгрузки через HTTP REST интерфейс #

graph TD
Uteka[Ютека] -->|GET /warehouses| Partner[REST API Партнёра]
Uteka -->|GET /pharmacies| Partner
Uteka -->|GET /products| Partner
Uteka -->|GET /stocks| Partner

Этот раздел описывает процесс интеграции Ютеки и Партнера через реализацию на стороне Партнера HTTP REST интерфейса для процесса выгрузок. Партнёру нужно реализовать на своей стороне HTTP REST интерфейс для получения Ютекой информации о:

Общие требования к REST-эндпоинтам #

Для всех эндпоинтов действуют следующие правила:

  • X-Request-ID — специальный заголовок для трассировки запросов. При его наличии, в ответе на запрос должен быть точно такой же заголовок с таким же значением. Если со стороны Ютеки этот заголовок не пришёл, Партнёру нужно сгенерировать и отправить его со своей стороны самостоятельно.
  • Допускается возможность авторизации разными способами, например, Basic, Bearer и др.
  • Допускается наличие у партнёра отдельного авторизационного эндпоинта, который будет возвращать временный токен доступа, а Ютека будет обновлять его по истечению срока его жизни.
  • Допускается использование комбинированных подходов, дополнительных заголовков и др.
  • Допускается реализация пагинации, но обсуждается в индивидуальном порядке с Партнёром.
  • Допускается использование названий полей, типов их содержания и не по шаблону (обговаривается с Партнёром в индивидуальном порядке).

Аптеки REST #

Необходимо для: всех типов заказов

Частота обновления аптек в системе Партнёра должна быть от 1 раза в 3 часа до 1 раза в сутки.

Для выгрузки аптек со стороны Партнёра должен быть реализован эндпоинт, принимающий GET-запросы, и возвращающий список аптек с content-type: application/json (или xml, на усмотрение Партнёра).

Ютека будет отправлять запросы, например:

GET https://partner-host.ru/pharmacies
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

Ответ должен состоять из следующих полей:

Поле Тип Обязательное Описание Комментарий
id или pharmacyId string ✔️ ID аптеки
title string ✔️ Название аптеки
warehouseId string extendedPickup ✔️ ID склада из выгрузок складов Обязательно для extendedPickup
region string Регион местонахождения аптеки Не обязателен, если указан в поле address
city string Город местонахождения аптеки Не обязателен, если указан в поле address
address string ✔️ Адрес местонахождения аптеки полностью (Область, населённый пункт, улица, дом). Без почтового индекса. С точностью до дома. Если возможно, без дополнительных комментариев
phone string ✔️ Телефон в международном формате (+71234567890) Если это городской номер телефона, то с кодом города
workingHours Object или string (см. пример ниже) ✔️ Часы работы в формате
deliveryDates Object (см. пример ниже) extendedPickup ✔️ Даты доставки остатков со склада warehouseId Обязательно для extendedPickup
location string ✔️ GPS координаты Latitude,Longitude Например, 59.939032,30.315826. При подстановке в карты должна показываться точка на аптеку
email string Email аптеки для связи Мы можем присылать email оповещения в аптеку при поступлении новых или отмене заказов

Возможно добавление дополнительных полей для более специфичной логики, например, ограничение доставки некоторых остатков в некоторые аптеки (например, по признаку наличия холодильника в аптеке для вакцин) или по другим условиям.

Обсуждается в индивидуальном порядке.

Пример ответа JSON с часами работы в виде объекта
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
[
  {
    "pharmacyId": "228",
    "title": "Аптечный пункт №1",
    "address": "Краснодарский край, г Тихорецк, ул Октябрьская, д 101",
    "phone": "8-800-228-22-88",
    "warehouseId": "20247701-bf4b-11ed-812f-00e0ed9e2e92",
    "deliveryDates": {
      "deliveryDate": "2024-04-27T00:00:00",
      "deliveryDateExpires": "2024-04-26T20:30:00",
      "nextDeliveryDate": "2024-04-29T00:00:00"
    },
    "workingHours": {
      "1": { "open": "08:00", "close": "20:00" },
      "2": { "open": "08:00", "close": "20:00" },
      "3": { "open": "08:00", "close": "20:00" },
      "4": { "open": "08:00", "close": "20:00" },
      "5": { "open": "08:00", "close": "20:00" },
      "6": { "open": "10:00", "close": "17:30" },
      "7": { "open": "", "close": "" }
    },
    "location": "45.868670,40.144246",
    "email": "test@test.com"
  },
  {
    "pharmacyId": "100",
    "title": "Аптека на Ленина",
    "address": "г Москва, ул Ленина, д 15",
    "phone": "+74951234567",
    "workingHours": {
      "1": { "open": "08:00", "close": "21:00" },
      "2": { "open": "08:00", "close": "21:00" },
      "3": { "open": "08:00", "close": "21:00" },
      "4": { "open": "08:00", "close": "21:00" },
      "5": { "open": "08:00", "close": "21:00" },
      "6": { "open": "09:00", "close": "18:00" },
      "7": { "open": "10:00", "close": "16:00" }
    },
    "location": "55.753215,37.622504",
    "email": "apteka100@partner.ru"
  }
]

Аптека 228 — с привязкой к складу и датами доставки (для extendedPickup). Аптека 100 — без привязки к складу (для pickup, delivery, courier).

Пример ответа JSON с часами работы в виде строки
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
[
  {
    "pharmacyId": "228",
    "title": "Аптечный пункт №1",
    "address": "Краснодарский край, г Тихорецк, ул Октябрьская, д 101",
    "phone": "8-800-228-22-88",
    "warehouseId": "20247701-bf4b-11ed-812f-00e0ed9e2e92",
    "deliveryDates": {
      "deliveryDate": "2024-04-27T00:00:00",
      "deliveryDateExpires": "2024-04-26T20:30:00",
      "nextDeliveryDate": "2024-04-29T00:00:00"
    },
    "workingHours": "пн-пт 08:00-20:00, сб 10:00-17:30, вс выходной",
    "location": "45.868670,40.144246",
    "email": "test@test.com"
  },
  {
    "pharmacyId": "100",
    "title": "Аптека на Ленина",
    "address": "г Москва, ул Ленина, д 15",
    "phone": "+74951234567",
    "workingHours": "пн-пт 08:00-21:00, сб 09:00-18:00, вс 10:00-16:00",
    "location": "55.753215,37.622504",
    "email": "apteka100@partner.ru"
  }
]
Пример ответа XML
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <pharmacyId>228</pharmacyId>
        <title>Аптечный пункт №1</title>
        <address>Краснодарский край, г Тихорецк, ул Октябрьская, д 101</address>
        <phone>8-800-228-22-88</phone>
        <warehouseId>20247701-bf4b-11ed-812f-00e0ed9e2e92</warehouseId>
        <deliveryDates>
            <deliveryDate>2024-04-27T00:00:00</deliveryDate>
            <deliveryDateExpires>2024-04-26T20:30:00</deliveryDateExpires>
            <nextDeliveryDate>2024-04-29T00:00:00</nextDeliveryDate>
        </deliveryDates>
        <workingHours>
            {"1":{"open":"08:00","close":"20:00"},"2":{"open":"08:00","close":"20:00"},"3":{"open":"08:00","close":"20:00"},"4":{"open":"08:00","close":"20:00"},"5":{"open":"08:00","close":"20:00"},"6":{"open":"10:00","close":"17:30"},"7":{"open":"","close":""}}
        </workingHours>
        <location>45.868670,40.144246</location>
        <email>test@test.com</email>
    </item>
    <item>
        <pharmacyId>100</pharmacyId>
        <title>Аптека на Ленина</title>
        <address>г Москва, ул Ленина, д 15</address>
        <phone>+74951234567</phone>
        <workingHours>
            {"1":{"open":"08:00","close":"21:00"},"2":{"open":"08:00","close":"21:00"},"3":{"open":"08:00","close":"21:00"},"4":{"open":"08:00","close":"21:00"},"5":{"open":"08:00","close":"21:00"},"6":{"open":"09:00","close":"18:00"},"7":{"open":"10:00","close":"16:00"}}
        </workingHours>
        <location>55.753215,37.622504</location>
        <email>apteka100@partner.ru</email>
    </item>
</items>

Склады REST #

Необходимо для: extendedPickup

Частота обновления складов в системе Партнёра должна быть до 1 раза в сутки.

Для выгрузки складов со стороны Партнёра должен быть реализован эндпоинт, принимающий GET-запросы, и возвращающий список складов с content-type: application/json (или xml, на усмотрение Партнёра).

Ютека будет отправлять запросы, например:

GET https://partner-host.ru/warehouses
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

Ответ должен состоять из следующих полей:

Поле Тип Обязательное Описание Комментарий
id string ✔️ ID склада Нужен для привязки к аптекам, куда будут доставляться остатки. Может содержать название региона.
title string ✔️ Название склада Краткое описание. Может содержать название региона.
Пример ответа JSON
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
[
  {
    "id": "msc",
    "title": "Москва"
  },
  {
    "id": "20247701-bf4b-11ed-812f-00e0ed9e2e92",
    "title": "Краснодарский край \"Парк А\""
  }
]
Пример ответа XML
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <id>msc</id>
        <title>Москва</title>
    </item>
    <item>
        <id>20247701-bf4b-11ed-812f-00e0ed9e2e92</id>
        <title>Краснодарский край "Парк А"</title>
    </item>
</items>

Товары REST #

Необходимо для: всех типов заказов

Частота обновления товаров в системе Партнёра должна быть от 1 раза в 3 часа до 1 раза в сутки.

Для выгрузки товаров со стороны Партнёра должен быть реализован эндпоинт, принимающий GET-запросы, и возвращающий список товаров с content-type: application/json (или xml, на усмотрение Партнёра).

Ютека будет отправлять запросы, например:

GET https://partner-host.ru/products
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

Ответ должен состоять из следующих полей:

Поле Тип Обязательное Описание
id или productId string ✔️ ID товара
barcode или barcodes string ✔️ Штрихкод. Если несколько, перечислять через ,, например: 4250369505166,2250369505188
title string ✔️ Название
vendor string ✔️ Производитель
country string ✔️ Страна
egk string код egk значительно ускорит привязку товаров к нашему каталогу
rls string код rls значительно ускорит привязку товаров к нашему каталогу
katren string код katren значительно ускорит привязку товаров к нашему каталогу
protek string код protek значительно ускорит привязку товаров к нашему каталогу
Пример ответа JSON
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
[
  {
    "productId": "228",
    "barcode": "4250369505166",
    "title": "Лекарственные средства АСПИРИН ЭКСПРЕСС табл. шип. 500мг N12",
    "vendor": "БАЙЕР",
    "country": "ГЕРМАНИЯ",
    "katren": "55079077",
    "protek": "1-218661,218661,70218661",
    "egk": "100986"
  },
  {
    "productId": "229",
    "barcode": "4630000891184",
    "title": "Травы, чаи прочие ГАЛЕГИ ТРАВА (Козлятник) 50г N1",
    "vendor": "Камелия-ЛТ",
    "country": "РФ",
    "katren": "400707176"
  }
]
Пример ответа XML
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <productId>228</productId>
        <barcode>4250369505166</barcode>
        <title>Лекарственные средства АСПИРИН ЭКСПРЕСС табл. шип. 500мг N12</title>
        <vendor>БАЙЕР</vendor>
        <country>ГЕРМАНИЯ</country>
        <katren>55079077</katren>
        <protek>1-218661,218661,70218661</protek>
        <egk>100986</egk>
    </item>
    <item>
        <productId>229</productId>
        <barcode>4630000891184</barcode>
        <title>Травы, чаи прочие ГАЛЕГИ ТРАВА (Козлятник) 50г N1</title>
        <vendor>Камелия-ЛТ</vendor>
        <country>РФ</country>
        <katren>400707176</katren>
    </item>
</items>

Остатки REST #

Частота обновления остатков в системе Партнёра должна быть не реже, чем 1 раза в 15 минут.

Для выгрузки остатков со стороны Партнёра должен быть реализован эндпоинт, принимающий GET-запросы, и возвращающий список остатков с content-type: application/json (или xml, на усмотрение Партнёра).

Ютека будет отправлять запросы, например:

GET https://partner-host.ru/stocks?warehouseId={warehouseId}
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

, где:

  • partner-host.ru - хост партнёра
  • stocks - адрес ресурса с информацией по остаткам
  • {warehouseId} - ID склада, по которому Ютека получит остатки. Для остатков аптек — pharmacyId={pharmacyId}
  • X-Request-ID - специальный заголовок для трассировки запросов. При его наличии, в ответе на запрос должен быть точно такой же заголовок с таким же значением. Если со стороны Ютеки этот заголовок не пришёл, Партнёру нужно сгенерировать и отправить его со своей стороны самостоятельно.

Ютека будет получать остатки по каждому складу/аптеке, т.е. кол-во запросов на остатки = кол-ву складов/аптек Партнёра.

Ютека будет отправлять запросы на получение остатков одновременно по нескольким складам/аптекам, но с лимитом на кол-во одновременных запросов (по умолчанию 10 одновременных запросов, увеличение/уменьшение обсуждается индивидуально).

Ответ должен состоять из следующих полей:

Поле Тип Обязательное Описание
productId string ✔️ ID товара из выгрузок товаров
warehouseId string ID склада из выгрузок складов. Обязателен для остатков склада (extendedPickup). Не обязательный, т.к. мы можем достать его из запроса
price float64 или string или int (если стоимость в копейках) ✔️ Цена
quantity int или string ✔️ Количество
partNumber или consignment string Номер партии
expirationDate string Срок годности в формате: 2006-01-02T15:04:05 или 2006-01-02
maxQuantity int или string Максимально доступное количество этой позиции на единицу заказа
isDelivery bool Доступен ли данный остаток для доставки (только для типа delivery). По умолчанию true

Допускается использование названий полей, типов их содержания и не по шаблону (обговаривается с Партнёром в индивидуальном порядке).

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

Пример ответа JSON
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
[
  {
    "product_id": "1234",
    "price": 51.0,
    "quantity": 914,
    "partNumber": "20250201228228",
    "expirationDate": "2040-01-02T00:00:00",
    "maxQuantity": 10
  },
  {
    "product_id": "1235",
    "price": 78.0,
    "quantity": 1324,
    "partNumber": "20250201228114",
    "expirationDate": "2045-06-02T00:00:00"
  },
  {
    "product_id": "1236",
    "price": 58.0,
    "quantity": 730,
    "partNumber": "20250201228553",
    "expirationDate": "2042-09-01T00:00:00"
  }
]
Пример ответа XML
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<items>
    <item>
        <product_id>1234</product_id>
        <price>51.0</price>
        <quantity>914</quantity>
        <partNumber>20250201228228</partNumber>
        <expirationDate>2040-01-02T00:00:00</expirationDate>
        <maxQuantity>10</maxQuantity>
    </item>
    <item>
        <product_id>1235</product_id>
        <price>78.0</price>
        <quantity>1324</quantity>
        <partNumber>20250201228114</partNumber>
        <expirationDate>2045-06-02T00:00:00</expirationDate>
    </item>
    <item>
        <product_id>1236</product_id>
        <price>58.0</price>
        <quantity>730</quantity>
        <partNumber>20250201228553</partNumber>
        <expirationDate>2042-09-01T00:00:00</expirationDate>
    </item>
</items>

Обмен заказами через протокол REST (рекомендуемый ✅) #

graph TD
Uteka[Ютека] -->|POST /orders/create| Partner[REST API Партнёра]
Uteka -->|GET или POST /orders/status| Partner
Uteka -->|POST или DELETE /orders/cancel| Partner
Uteka -->|POST /delivery/calculate| Partner

Ютека будет отправлять запросы с новыми заказами в инфраструктуру Партнёра используя HTTP REST протокол. Далее Ютека будет опрашивать API партнёра по каждому незавершённому заказу для получения обновлений их статусов. Также, Ютека может отправлять отмены по этим заказам, когда отмена инициирована, например, пользователем, либо истекло время сборки заказа. Для заказов типа delivery Ютека дополнительно запрашивает расчёт вариантов доставки.

Партнёру нужно реализовать на своей стороне HTTP REST интерфейс для отправки Ютекой:

Типы заказов и их флоу #

Прежде чем описать каждый эндпоинт, важно понять жизненный цикл заказа. Все четыре типа заказов проходят одинаковую цепочку статусов — отличаются только смысл каждого шага и дополнительные поля.

Общая диаграмма статусов #

graph LR
A[approved] --> B[ready]
B --> C[completed]
A -.->|отмена| X[cancelled]
B -.->|отмена| X
Статус Описание
approved Заказ подтверждён, взят в обработку
ready Готов к выдаче / передан курьеру
completed Заказ завершён (выкуплен / доставлен)
cancelled Отменён Партнёром

Могут быть дополнительные промежуточные статусы Партнёра, текст статусов может быть разным, главное — наличие 4-х разных статусов, соответствующих по смыслу описаниям в таблице выше.

Что означают статусы для каждого типа заказа #

Статус pickup extendedPickup delivery courier
approved Аптека увидела заказ Товар готовится к отправке со склада Формируется отправление Аптека начинает сборку
ready Заказ собран, ожидает покупателя Товар доставлен в аптеку, ожидает покупателя Заказ передан курьеру / в пути Заказ собран, ожидает курьера Ютеки
completed Покупатель забрал заказ Покупатель забрал заказ Покупатель получил заказ Курьер забрал заказ из аптеки

Отличия по типам заказов #

pickup — самовывоз из аптеки. Покупатель бронирует товар в аптеке и приходит забрать его лично. Самый простой тип, не требует дополнительных полей или эндпоинтов.

extendedPickup — самовывоз на следующий день. Товар на складе, отправляется в аптеку. В ответах на статусы Партнёр может передавать дополнительные поля:

  • deliveryDate — актуальная дата доставки в аптеку (если изменилась)
  • shippingStartedAt — дата/время отправки товара со склада

delivery — доставка курьером Партнёра. Перед созданием заказа Ютека запрашивает у Партнёра расчёт доставки, выбранный покупателем тайм-слот передаётся в запросе на создание заказа. После передачи заказа курьеру отмена может быть невозможна (на усмотрение Партнёра).

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

Дополнительные поля в ответе на статус по типам заказов #

Поле Описание pickup extendedPickup delivery courier
deliveryDate Дата доставки товара в аптеку ✔️
shippingStartedAt Дата/время отправки со склада ✔️ (опционально)
reservationEndsAt Дата окончания резервирования ✔️ (опционально) ✔️ (опционально)
items Актуальная корзина (для частичной сборки) ✔️ (опционально) ✔️ (опционально) ✔️ (опционально) ✔️ (опционально)

Создание заказа REST #

После того, как пользователь создал заказ, Ютека передаёт его в интеграцию используя HTTP REST интерфейс.

Для отправки заказов Ютекой со стороны Партнёра должен быть реализован эндпоинт, принимающий POST-запросы, и возвращающий ID заказа в инфраструктуре партнёра с content-type: application/json (или xml, на усмотрение Партнёра).

Ютека будет отправлять запросы, которые будут состоять из следующих полей:

Поле Тип Обязательное Описание
orderId или utekaOrderId string ✔️ ID заказа Ютеки
orderType string ✔️ Тип заказа: pickup, extendedPickup, delivery, courier
warehouseId string extendedPickup ✔️ ID склада из выгрузок складов, из которого нужно отправить препараты в аптеку
pharmacyId string ✔️ ID аптеки из выгрузок аптек
amount float64 ✔️ Полная стоимость корзины в рублях (сумма всех item.price * item.quantity), округлённая до копеек
name string ✔️ Имя покупателя
phone string ✔️ Телефон покупателя в международном формате без 7, например, 9181231234
items []Object ✔️ Корзина
items.productId string ✔️ ID товара из выгрузок товаров
items.consignment или items.partNumber string Номер партии товара при наличии из выгрузки остатков
items.quantity int ✔️ Количество текущего ID товара в корзине
items.price float64 ✔️ Цена за единицу текущего ID товара из выгрузки остатков
deliveryAddress string delivery ✔️ Адрес доставки (только для delivery)
deliverySlotId string delivery ✔️ ID выбранного тайм-слота из расчёта доставки (только для delivery)
deliveryParams string Параметры доставки, возвращённые в расчёте доставки (только для delivery)
courierCode string Код курьера для получения заказа из аптеки (только для courier). Формируется Ютекой при взаимодействии с курьерской службой

Допускается использование названий полей, типов их содержания и не по шаблону (обговаривается с Партнёром в индивидуальном порядке).

Пример JSON — pickup
POST https://partner-host.ru/orders/create
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "utekaOrderId": "1234",
  "orderType": "pickup",
  "pharmacyId": "100",
  "items": [
    {
      "productId": "1234",
      "quantity": 2,
      "price": 51
    },
    {
      "productId": "1235",
      "quantity": 1,
      "price": 78
    }
  ],
  "amount": 180,
  "name": "Иванов Иван Иванович",
  "phone": "9181231234"
}
Пример JSON — extendedPickup
POST https://partner-host.ru/orders/create
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "utekaOrderId": "1235",
  "orderType": "extendedPickup",
  "pharmacyId": "228",
  "warehouseId": "msc",
  "items": [
    {
      "productId": "1234",
      "quantity": 1,
      "price": 51
    }
  ],
  "amount": 51,
  "name": "Петров Пётр Петрович",
  "phone": "9261234567"
}
Пример JSON — delivery
POST https://partner-host.ru/orders/create
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "utekaOrderId": "1236",
  "orderType": "delivery",
  "pharmacyId": "100",
  "items": [
    {
      "productId": "1234",
      "quantity": 2,
      "price": 51
    },
    {
      "productId": "1235",
      "quantity": 1,
      "price": 78
    }
  ],
  "amount": 180,
  "name": "Сидоров Сидор Сидорович",
  "phone": "9031234567",
  "deliveryAddress": "г Москва, ул Пушкина, д 10, кв 5",
  "deliverySlotId": "slot-100-1",
  "deliveryParams": "{\"deliveryTimeSlotId\":42}"
}
Пример JSON — courier
POST https://partner-host.ru/orders/create
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "utekaOrderId": "1237",
  "orderType": "courier",
  "pharmacyId": "100",
  "items": [
    {
      "productId": "1235",
      "quantity": 3,
      "price": 78
    }
  ],
  "amount": 234,
  "name": "Козлова Анна Сергеевна",
  "phone": "9161234567",
  "courierCode": "A7B3"
}
Пример XML — extendedPickup
POST https://partner-host.ru/orders/create
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<order>
  <utekaOrderId>1235</utekaOrderId>
  <orderType>extendedPickup</orderType>
  <pharmacyId>228</pharmacyId>
  <warehouseId>msc</warehouseId>
  <items>
    <item>
      <productId>1234</productId>
      <quantity>1</quantity>
      <price>51</price>
    </item>
  </items>
  <amount>51</amount>
  <name>Петров Пётр Петрович</name>
  <phone>9261234567</phone>
</order>

Ответ должен состоять из следующих полей:

Поле Тип Обязательное Описание
partnerOrderId string ✔️ ID заказа в инфраструктуре Партнёра. Можно использовать ID Ютеки (обсуждается перед реализацией)
utekaOrderId string ID заказа Ютеки
status string Статус заказа. Если не указан, считаем, что заказ взят в обработку
deliveryDate string Дата доставки корзины в аптеку (для extendedPickup), если она не совпадает с датой из выгрузки аптек
Пример ответа JSON
HTTP/2.0 201 Created
Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "0001-4321",
  "utekaOrderId": "1234",
  "status": "approved"
}
Пример ответа XML
HTTP/2.0 201 Created
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<?xml version="1.0" encoding="UTF-8"?>
<orderStatus>
    <partnerOrderId>0001-4321</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
    <status>approved</status>
</orderStatus>

Получение статусов по заказам от Партнёра REST #

После того, как Ютека передала заказ в инфраструктуру партнёра, Ютека будет отправлять запросы на получение статусов по заказам.

Для отправки статусов по заказам со стороны Партнёра должен быть реализован эндпоинт, принимающий GET или POST-запросы, и возвращающий объект заказа в инфраструктуре партнёра с content-type: application/json (или xml, на усмотрение Партнёра).

Ютека будет отправлять запросы, которые будут состоять из следующих полей:

Поле Тип Обязательное Описание
partnerOrderId string ✔️ ID заказа в инфраструктуре Партнёра. Можно использовать ID Ютеки (обсуждается перед реализацией)
utekaOrderId string ID заказа Ютеки
Пример запроса JSON

POST-запрос:

POST https://partner-host.ru/orders/status
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "partnerOrderId": "228",
  "utekaOrderId": "1234"
}

Или GET-запрос:

GET https://partner-host.ru/orders/status?partnerOrderId=228
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
Пример запроса XML
POST https://partner-host.ru/orders/status
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<orderStatus>
  <partnerOrderId>228</partnerOrderId>
  <utekaOrderId>1234</utekaOrderId>
</orderStatus>

Ответ должен состоять из следующих полей:

Поле Тип Обязательное Описание
partnerOrderId string ✔️ ID заказа в инфраструктуре Партнёра
utekaOrderId string ID заказа Ютеки
status string ✔️ Статус заказа
deliveryDate string Дата доставки корзины в аптеку (для extendedPickup)
shippingStartedAt string Дата/время отправки товара со склада (для extendedPickup)
reservationEndsAt string Дата/время окончания резервирования заказа
items []Object Актуальная корзина при частичной сборке заказа
items.productId string ✔️(если заполнен items) ID товара из выгрузок товаров
items.consignment или items.partNumber string Номер партии товара при наличии
items.quantity int ✔️(если заполнен items) Количество текущего ID товара в корзине
items.price float64 ✔️(если заполнен items) Цена за единицу текущего ID товара

Возможные статусы заказов:

Статус Описание
approved Подтверждён. Интеграция взяла заказ в обработку. Фармацевт в аптеке увидел заказ или заказ дошёл до аптеки и сохранился со стороны аптечной сети.
ready Готов к выдаче. Интеграция доставила корзину в аптеку, собрала заказ и готова выдать покупателю. Покупатель может идти в аптеку
completed Продан. Покупатель пришёл в аптеку и выкупил корзину
cancelled Отменён аптекой. Аптека не смогла на каком-то из этапов собрать заказ и отменила его

Могут быть дополнительные статусы, текст статусов может быть разным, главное, наличие 4-х разных статусов, соответствующих по смыслу к описаниям в таблице выше.

Заказ подтверждён JSON
Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "228",
  "utekaOrderId": "1234",
  "status": "approved"
}
Заказ подтверждён XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<status>
    <partnerOrderId>228</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
    <status>approved</status>
</status>
extendedPickup — approved с датой доставки JSON
Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "0001-4322",
  "utekaOrderId": "1235",
  "status": "approved",
  "deliveryDate": "2024-04-27T00:00:00",
  "shippingStartedAt": "2024-04-26T14:30:00"
}
Заказ отменён JSON
Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "228",
  "utekaOrderId": "1234",
  "status": "cancelled",
  "comment": "Нет на складе"
}
Заказ отменён XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<status>
    <partnerOrderId>228</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
    <status>cancelled</status>
    <comment>Нет на складе</comment>
</status>

Batch получение статусов по заказам от Партнёра REST #

Это необязательный вариант реализации обмена статусами по заказам, когда Ютека в одном запросе передаёт сразу несколько IDs заказов, по которым ожидает получить статусы.

По сути, отличие от реализации обмена статусами по заказам состоит только в том, что Ютека будет передавать массив объектов заказов вместо одного заказа, по которым нужно получить статусы, а инфраструктура Партнёра будет возвращать в ответе массив объектов заказов, вместо одного заказа, с актуальными статусами.

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

Пример запроса JSON

POST-запрос:

POST https://partner-host.ru/orders/status
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "orderIds": [
    {
      "partnerOrderId": "0001-4321",
      "utekaOrderId": "1234"
    },
    {
      "partnerOrderId": "0001-4322",
      "utekaOrderId": "1235"
    },
    {
      "partnerOrderId": "0001-4323",
      "utekaOrderId": "1236"
    }
  ]
}

Или GET-запрос:

GET https://partner-host.ru/orders/status?partnerOrderId=0001-4321&partnerOrderId=0001-4322&partnerOrderId=0001-4323
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
Пример запроса XML
POST https://partner-host.ru/orders/status
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<orderIds>
  <orderId>
    <partnerOrderId>0001-4321</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
  </orderId>
  <orderId>
    <partnerOrderId>0001-4322</partnerOrderId>
    <utekaOrderId>1235</utekaOrderId>
  </orderId>
  <orderId>
    <partnerOrderId>0001-4323</partnerOrderId>
    <utekaOrderId>1236</utekaOrderId>
  </orderId>
</orderIds>
Пример ответа JSON — разные типы заказов
Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "orderIds": [
    {
      "partnerOrderId": "0001-4321",
      "utekaOrderId": "1234",
      "status": "approved"
    },
    {
      "partnerOrderId": "0001-4322",
      "utekaOrderId": "1235",
      "status": "approved",
      "deliveryDate": "2024-04-27T00:00:00"
    },
    {
      "partnerOrderId": "0001-4323",
      "utekaOrderId": "1236",
      "status": "completed"
    }
  ]
}
Пример ответа XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<?xml version="1.0" encoding="UTF-8"?>
<orderStatuses>
    <order>
        <partnerOrderId>0001-4321</partnerOrderId>
        <utekaOrderId>1234</utekaOrderId>
        <status>approved</status>
    </order>
    <order>
        <partnerOrderId>0001-4322</partnerOrderId>
        <utekaOrderId>1235</utekaOrderId>
        <status>approved</status>
        <deliveryDate>2024-04-27T00:00:00</deliveryDate>
    </order>
    <order>
        <partnerOrderId>0001-4323</partnerOrderId>
        <utekaOrderId>1236</utekaOrderId>
        <status>completed</status>
    </order>
</orderStatuses>

Частичная сборка корзины заказа REST #

Это необязательный механизм, при котором после подтверждения заказа его состав может быть изменён. Поддерживается для всех типов заказов.

Например, инфраструктура Партнёра взяла заказ в обработку, но при сборке на складе или уже в аптеке собрать заказ целиком не получилось. В таком случае вместо мгновенной отмены, со стороны Ютеки предусмотрен механизм, при котором Ютека отправляет покупателю уведомление об изменении состава заказа, заказ переходит в состояние ожидание действия от пользователя. Далее может быть 2 ситуации:

  • пользователь согласился с изменениями. В этом случае, состав корзины меняется на новый и заказ идёт дальше по флоу обработки и выдачи. Можем отправлять подтверждение в инфраструктуру Партнёра (обговаривается индивидуально), но это необязательное действие.
  • пользователь отклонил изменения. В этом случае, со стороны Ютеки будет отправлена отмена по этому заказу в инфраструктуру Партнёра.
graph TD

status[Мониторинг статуса на Ютеке] -->|POST /orders/status| Partner[REST API Партнёра]
validateCart -->|было изменение| userFlow[Ожидание ответа пользователя]
validateCart -->|не было изменений| status
Partner -->|HTTP 200 + items| validateCart[Проверка состава на Ютеке]
userFlow -->|Не принял изменения POST /orders/cancel| Partner
userFlow -->|Принял изменения| status

Для поддержки этого механизма при опросе Ютекой статусов по заказам в ответе должен приходить массив объектов каждого элемента корзины.

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

  • уменьшение количества каких-нибудь позиций в корзине
  • увеличение стоимости каких-нибудь позиций в корзине
  • если корзина целиком недоступна, отмена заказа будет сразу

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

Вместе (либо вместо) с productId дополнительно (но не обязательно) можно использовать номера партий partNumber для более точного сравнения корзин. Допускается использование названий полей, типов их содержания и не по шаблону ( обговаривается с Партнёром в индивидуальном порядке).

Примеры ответов ниже.

Корзина доступна целиком JSON

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

Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "228",
  "utekaOrderId": "1234",
  "items": [
    {
      "productId": "1234",
      "quantity": 2,
      "price": 51
    },
    {
      "productId": "1235",
      "quantity": 1,
      "price": 78
    }
  ],
  "status": "approved"
}
Корзина доступна целиком XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<?xml version="1.0" encoding="UTF-8"?>
<status>
    <partnerOrderId>228</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
    <items>
        <item>
            <productId>1234</productId>
            <quantity>2</quantity>
            <price>51</price>
        </item>
        <item>
            <productId>1235</productId>
            <quantity>1</quantity>
            <price>78</price>
        </item>
    </items>
    <status>approved</status>
</status>
В корзине не хватает одной из позиций JSON

В этом случае корзина будет отличаться от исходной количеством товара 1234, 2 -> 1. Ютека отправит пользователю уведомление и будет ждать его ответа. В зависимости от ответа пришлёт либо отмену, либо продолжит опрашивать статусы. При продолжении опроса корзины из ответов будут сравниваться уже с новой корзиной.

Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "228",
  "utekaOrderId": "1234",
  "items": [
    {
      "productId": "1234",
      "quantity": 1,
      "price": 51
    },
    {
      "productId": "1235",
      "quantity": 1,
      "price": 78
    }
  ],
  "status": "approved"
}
В корзине не хватает одной из позиций XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<?xml version="1.0" encoding="UTF-8"?>
<status>
    <partnerOrderId>228</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
    <items>
        <item>
            <productId>1234</productId>
            <quantity>1</quantity>
            <price>51</price>
        </item>
        <item>
            <productId>1235</productId>
            <quantity>1</quantity>
            <price>78</price>
        </item>
    </items>
    <status>approved</status>
</status>
В корзине изменилась цена на одну из позиций JSON

В этом случае корзина будет отличаться от исходной ценой товара 1235, 78 -> 105. Ютека отправит пользователю уведомление и будет ждать его ответа.

Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "228",
  "utekaOrderId": "1234",
  "items": [
    {
      "productId": "1234",
      "quantity": 2,
      "price": 51
    },
    {
      "productId": "1235",
      "quantity": 1,
      "price": 105
    }
  ],
  "status": "approved"
}
В корзине изменилась цена на одну из позиций XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<?xml version="1.0" encoding="UTF-8"?>
<status>
    <partnerOrderId>228</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
    <items>
        <item>
            <productId>1234</productId>
            <quantity>2</quantity>
            <price>51</price>
        </item>
        <item>
            <productId>1235</productId>
            <quantity>1</quantity>
            <price>105</price>
        </item>
    </items>
    <status>approved</status>
</status>
Изменилась цена и не хватает позиции одновременно JSON

В этом случае корзина будет отличаться от исходной количеством товара 1234, 2 -> 1 и ценой товара 1235, 78 -> 105.

Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "228",
  "utekaOrderId": "1234",
  "items": [
    {
      "productId": "1234",
      "quantity": 1,
      "price": 51
    },
    {
      "productId": "1235",
      "quantity": 1,
      "price": 105
    }
  ],
  "status": "approved"
}
Изменилась цена и не хватает позиции одновременно XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<?xml version="1.0" encoding="UTF-8"?>
<status>
    <partnerOrderId>228</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
    <items>
        <item>
            <productId>1234</productId>
            <quantity>1</quantity>
            <price>51</price>
        </item>
        <item>
            <productId>1235</productId>
            <quantity>1</quantity>
            <price>105</price>
        </item>
    </items>
    <status>approved</status>
</status>

Отмена заказа REST #

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

Для отправки отмен по заказам со стороны Партнёра должен быть реализован эндпоинт, принимающий DELETE или POST-запросы, и возвращающий результат отмены в инфраструктуре партнёра с content-type: application/json (или xml, на усмотрение Партнёра).

Ютека будет отправлять запросы, которые будут состоять из следующих полей:

Поле Тип Обязательное Описание
partnerOrderId string ✔️ ID заказа в инфраструктуре Партнёра. Можно использовать ID Ютеки (обсуждается перед реализацией)
utekaOrderId string ID заказа Ютеки
status string Передача статуса отмены явно, если это требуется
Пример JSON

POST-запрос:

POST https://partner-host.ru/orders/cancel
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "partnerOrderId": "228",
  "utekaOrderId": "1234"
}

Или DELETE-запрос:

DELETE https://partner-host.ru/orders/cancel?partnerOrderId=228
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
Пример XML
POST https://partner-host.ru/orders/cancel
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<orderStatus>
  <partnerOrderId>228</partnerOrderId>
  <utekaOrderId>1234</utekaOrderId>
</orderStatus>

Ответ должен состоять из следующих полей:

Поле Тип Обязательное Описание
partnerOrderId string ID заказа в инфраструктуре Партнёра. Можно использовать ID Ютеки (обсуждается перед реализацией)
utekaOrderId string ID заказа Ютеки
status string Передача статуса отмены явно, если это требуется

Тело ответа при этом не обязательно. Будет достаточно 2xx статуса в ответе на запрос отмены. Но можно заполнить поля с IDs и статусом, чтобы на стороне Ютеки сделать дополнительные проверки.

Заказ отменён со стороны Ютеки JSON
Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "partnerOrderId": "228",
  "utekaOrderId": "1234",
  "status": "cancelled"
}
Заказ отменён со стороны Ютеки XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<status>
    <partnerOrderId>228</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
    <status>cancelled</status>
</status>

Ограничения отмены по типам заказов #

Тип заказа Ограничения отмены
pickup Отмена возможна в любом нетерминальном статусе
extendedPickup Отмена возможна в любом нетерминальном статусе
delivery После передачи курьеру отмена может быть невозможна (на усмотрение Партнёра)
courier Отмена возможна в любом нетерминальном статусе

Batch отмена заказов REST #

Это необязательный вариант реализации отмен заказов, когда Ютека в одном запросе передаёт сразу несколько IDs заказов, которые нужно отменить в инфраструктуре Партнёра.

По сути, отличие от реализации отмен заказов состоит только в том, что Ютека будет передавать массив объектов заказов вместо одного заказа, которые нужно отменить, а инфраструктура Партнёра сможет обрабатывать такие запросы.

Пример запроса JSON

POST-запрос:

POST https://partner-host.ru/orders/cancel
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "orderIds": [
    {
      "partnerOrderId": "228",
      "utekaOrderId": "1234"
    },
    {
      "partnerOrderId": "339",
      "utekaOrderId": "1235"
    },
    {
      "partnerOrderId": "440",
      "utekaOrderId": "1236"
    }
  ]
}

Или DELETE-запрос:

DELETE https://partner-host.ru/orders/cancel?partnerOrderId=228,339,440
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json
Пример запроса XML
POST https://partner-host.ru/orders/cancel
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<orderIds>
  <orderId>
    <partnerOrderId>228</partnerOrderId>
    <utekaOrderId>1234</utekaOrderId>
  </orderId>
  <orderId>
    <partnerOrderId>339</partnerOrderId>
    <utekaOrderId>1235</utekaOrderId>
  </orderId>
  <orderId>
    <partnerOrderId>440</partnerOrderId>
    <utekaOrderId>1236</utekaOrderId>
  </orderId>
</orderIds>
Пример ответа JSON
Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
{
  "orderIds": [
    {
      "partnerOrderId": "228",
      "utekaOrderId": "1234",
      "status": "cancelled"
    },
    {
      "partnerOrderId": "339",
      "utekaOrderId": "1235",
      "status": "cancelled"
    },
    {
      "partnerOrderId": "440",
      "utekaOrderId": "1236",
      "status": "cancelled"
    }
  ]
}
Пример ответа XML
Content-Type: application/xml; charset=utf-8
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
<?xml version="1.0" encoding="UTF-8"?>
<orderStatuses>
    <order>
        <partnerOrderId>228</partnerOrderId>
        <utekaOrderId>1234</utekaOrderId>
        <status>cancelled</status>
    </order>
    <order>
        <partnerOrderId>339</partnerOrderId>
        <utekaOrderId>1235</utekaOrderId>
        <status>cancelled</status>
    </order>
    <order>
        <partnerOrderId>440</partnerOrderId>
        <utekaOrderId>1236</utekaOrderId>
        <status>cancelled</status>
    </order>
</orderStatuses>

Подтверждение выполнения заказа REST (courier) #

Необходимо для: courier (опционально)

Если Партнёру нужно от Ютеки явное подтверждение о том, что заказ доставлен покупателю, для таких случаев Ютека может отправить Партнёру запрос с подтверждением выполнения заказа после того, как курьер вручит заказ.

Конкретный способ передачи подтверждения обсуждается с Партнёром в индивидуальном порядке. Как правило, используется тот же эндпоинт, что и для отмены заказа (например, changeStatus), но вместо статуса отмены передаётся статус выполнения.

Партнёру необходимо предоставить Ютеке числовой или строковый код статуса, который означает «заказ выполнен» в его системе.

Пример: подтверждение через changeStatus JSON
POST https://partner-host.ru/orders/changeStatus
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "partnerOrderId": "228",
  "utekaOrderId": "1234",
  "status": "<код статуса выполнения, предоставленный Партнёром>"
}
Пример: подтверждение через changeStatus XML
POST https://partner-host.ru/orders/changeStatus
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<orderStatus>
  <partnerOrderId>228</partnerOrderId>
  <utekaOrderId>1234</utekaOrderId>
  <status><код статуса выполнения, предоставленный Партнёром></status>
</orderStatus>

Ответ аналогичен ответу на отмену заказа — достаточно 2xx статуса.


Расчёт доставки REST #

Необходимо для: delivery

Перед созданием заказа типа delivery Ютека запрашивает у Партнёра варианты доставки. Покупатель выбирает подходящий вариант (тайм-слот), и выбранный вариант передаётся в запросе на создание заказа.

Для расчёта доставки со стороны Партнёра должен быть реализован эндпоинт, принимающий POST-запросы, и возвращающий массив вариантов доставки с content-type: application/json.

Ютека будет отправлять запросы, которые будут состоять из следующих полей:

Поле Тип Обязательное Описание
lat float64 Широта адреса доставки
lon float64 Долгота адреса доставки
city string Город доставки
address string Адрес доставки
cart []Object ✔️ Корзина
cart.productId string ✔️ ID товара
cart.quantity int ✔️ Количество
pharmacyIDs []string ✔️ Список ID аптек, из которых возможна доставка
Пример запроса JSON
POST https://partner-host.ru/delivery/calculate
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
Content-Type: application/json

{
  "lat": 55.753215,
  "lon": 37.622504,
  "city": "Москва",
  "address": "ул Пушкина, д 10",
  "cart": [
    {
      "productId": "1234",
      "quantity": 2
    },
    {
      "productId": "1235",
      "quantity": 1
    }
  ],
  "pharmacyIDs": ["100", "101", "102"]
}

Ответ должен содержать массив вариантов доставки. Каждый вариант — это тайм-слот, привязанный к конкретной аптеке:

Поле Тип Обязательное Описание
id string ✔️ ID тайм-слота (передаётся обратно при создании заказа)
title string ✔️ Название метода доставки (отображается покупателю)
courierTitle string Название службы доставки
date string ✔️ Дата доставки в формате YYYY-MM-DD
timeFrom string ✔️ Начало временного окна, например "10:00"
timeTo string ✔️ Конец временного окна, например "14:00"
price string ✔️ Стоимость доставки в рублях
pharmacyID string ✔️ ID аптеки, из которой будет осуществляться доставка
isPaymentRequired bool Требуется ли оплата при получении
isPaymentAvailable bool Доступна ли оплата при получении
params string Произвольные параметры (JSON-строка, пробрасывается обратно при создании заказа)
Пример ответа JSON
Content-Type: application/json
X-Request-ID: TQaWgDfqCyWufZPvilhiyznyGfoLTDKP
[
  {
    "id": "slot-100-1",
    "title": "Доставка сегодня 14:00–18:00",
    "courierTitle": "Экспресс-доставка",
    "date": "2024-04-26",
    "timeFrom": "14:00",
    "timeTo": "18:00",
    "price": "199.00",
    "pharmacyID": "100",
    "isPaymentRequired": false,
    "isPaymentAvailable": true,
    "params": "{\"deliveryTimeSlotId\":42}"
  },
  {
    "id": "slot-100-2",
    "title": "Доставка завтра 10:00–14:00",
    "courierTitle": "Стандартная доставка",
    "date": "2024-04-27",
    "timeFrom": "10:00",
    "timeTo": "14:00",
    "price": "149.00",
    "pharmacyID": "100",
    "isPaymentRequired": false,
    "isPaymentAvailable": true,
    "params": "{\"deliveryTimeSlotId\":43}"
  },
  {
    "id": "slot-101-1",
    "title": "Доставка завтра 14:00–18:00",
    "courierTitle": "Стандартная доставка",
    "date": "2024-04-27",
    "timeFrom": "14:00",
    "timeTo": "18:00",
    "price": "149.00",
    "pharmacyID": "101",
    "isPaymentRequired": false,
    "isPaymentAvailable": true,
    "params": "{\"deliveryTimeSlotId\":44}"
  },
  {
    "id": "slot-100-3",
    "title": "Доставка послезавтра 10:00–14:00",
    "courierTitle": "Эконом-доставка",
    "date": "2024-04-28",
    "timeFrom": "10:00",
    "timeTo": "14:00",
    "price": "99.00",
    "pharmacyID": "100",
    "isPaymentRequired": false,
    "isPaymentAvailable": true,
    "params": "{\"deliveryTimeSlotId\":45}"
  }
]

Ютека покажет покупателю эти варианты. Покупатель выберет один из них. Выбранные id и params будут переданы в запросе на создание заказа в полях deliverySlotId и deliveryParams.


Сводная схема взаимодействия по типам заказов #

pickup — самовывоз из аптеки #

sequenceDiagram
    participant U as Покупатель
    participant Y as Ютека
    participant P as Партнёр

    Note over Y,P: Выгрузки (периодически)
    Y->>P: GET /pharmacies
    Y->>P: GET /products
    Y->>P: GET /stocks?pharmacyId=100

    Note over U,P: Оформление заказа
    U->>Y: Выбирает товар в аптеке
    Y->>P: POST /orders/create (orderType: pickup)
    P-->>Y: partnerOrderId + status

    Note over Y,P: Мониторинг статуса
    loop Пока заказ не завершён
        Y->>P: POST /orders/status
        P-->>Y: status (approved → ready → completed)
    end

    U->>Y: Приходит в аптеку, забирает заказ

extendedPickup — самовывоз на следующий день #

sequenceDiagram
    participant U as Покупатель
    participant Y as Ютека
    participant P as Партнёр

    Note over Y,P: Выгрузки (периодически)
    Y->>P: GET /warehouses
    Y->>P: GET /pharmacies (с warehouseId, deliveryDates)
    Y->>P: GET /products
    Y->>P: GET /stocks?warehouseId=msc

    Note over U,P: Оформление заказа
    U->>Y: Выбирает товар со склада, аптеку для самовывоза
    Y->>P: POST /orders/create (orderType: extendedPickup, warehouseId)
    P-->>Y: partnerOrderId + status + deliveryDate

    Note over Y,P: Мониторинг статуса
    loop Пока заказ не завершён
        Y->>P: POST /orders/status
        P-->>Y: status + deliveryDate (approved → ready → completed)
    end

    U->>Y: Получает уведомление "Заказ в аптеке"
    U->>Y: Приходит в аптеку, забирает заказ

delivery — доставка курьером Партнёра #

sequenceDiagram
    participant U as Покупатель
    participant Y as Ютека
    participant P as Партнёр

    Note over Y,P: Выгрузки (периодически)
    Y->>P: GET /pharmacies
    Y->>P: GET /products
    Y->>P: GET /stocks?pharmacyId=100

    Note over U,P: Оформление заказа
    U->>Y: Выбирает товар, указывает адрес доставки
    Y->>P: POST /delivery/calculate (pharmacyIDs, cart, address)
    P-->>Y: Массив тайм-слотов доставки
    U->>Y: Выбирает тайм-слот
    Y->>P: POST /orders/create (orderType: delivery, deliverySlotId, deliveryAddress)
    P-->>Y: partnerOrderId + status

    Note over Y,P: Мониторинг статуса
    loop Пока заказ не завершён
        Y->>P: POST /orders/status
        P-->>Y: status (approved → ready → completed)
    end

    Note over P,U: Партнёр доставляет заказ
    P->>U: Курьер Партнёра доставляет заказ

courier — доставка курьером Ютеки #

sequenceDiagram
    participant U as Покупатель
    participant Y as Ютека
    participant K as Курьер Ютеки
    participant P as Партнёр

    Note over Y,P: Выгрузки (периодически)
    Y->>P: GET /pharmacies
    Y->>P: GET /products
    Y->>P: GET /stocks?pharmacyId=100

    Note over U,P: Оформление заказа
    U->>Y: Выбирает товар, указывает адрес доставки
    Y->>P: POST /orders/create (orderType: courier, courierCode)
    P-->>Y: partnerOrderId + status

    Note over Y,P: Мониторинг статуса
    loop Пока заказ не собран
        Y->>P: POST /orders/status
        P-->>Y: status (approved → ready)
    end

    Note over Y,K: Ютека назначает курьера
    Y->>K: Назначение доставки + courierCode
    K->>P: Приезжает в аптеку, называет courierCode
    P-->>K: Проверяет courierCode, выдаёт заказ
    K->>U: Доставляет заказ покупателю

Коды ответов HTTP #

Код Описание
200 Успех (GET-запросы, получение статусов)
201 Создано (POST на создание заказа)
400 Ошибка валидации запроса
403 Ошибка авторизации
500 Внутренняя ошибка сервера

Формат ошибок:

{
  "error": "описание ошибки"
}

Чек-лист для Партнёра #

Минимальная интеграция (pickup) #

  • Реализовать выгрузку аптек (FTP или REST)
  • Реализовать выгрузку товаров (FTP или REST)
  • Реализовать выгрузку остатков (FTP или REST)
  • Реализовать эндпоинт создания заказа
  • Реализовать эндпоинт получения статуса заказа
  • Реализовать эндпоинт отмены заказа
  • Поддержать X-Request-ID
  • Настроить авторизацию

Дополнительно для extendedPickup #

  • Реализовать выгрузку складов
  • Добавить warehouseId и deliveryDates в выгрузку аптек
  • Реализовать выгрузку остатков складов (аналогично остаткам аптек, с warehouseId)
  • Поддержать поле deliveryDate в ответах на статус заказа
  • Поддержать warehouseId в запросе на создание заказа

Дополнительно для delivery #

  • Реализовать эндпоинт расчёта доставки (/delivery/calculate)
  • Настроить тайм-слоты доставки для аптек
  • Поддержать поля deliveryAddress, deliverySlotId, deliveryParams в запросе на создание заказа

Дополнительно для courier #

  • Поддержать поле courierCode в запросе на создание заказа (передаётся Ютекой опционально)
  • Опционально: реализовать подтверждение выполнения заказа, если аптека не списывает позиции без явного подтверждения от Ютеки

Опционально #

  • Поддержать частичную сборку корзины (поле items в ответе на статус)
  • Поддержать batch-запросы статусов
  • Поддержать batch-отмены заказов
  • Поддержать поле reservationEndsAt в ответах на статус
  • Поддержать поле shippingStartedAt в ответах на статус (для extendedPickup)