惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
Y
Y Combinator Blog
美团技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
S
SegmentFault 最新的问题
IT之家
IT之家
Recent Announcements
Recent Announcements
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
T
The Blog of Author Tim Ferriss
Martin Fowler
Martin Fowler
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
U
Unit 42
WordPress大学
WordPress大学
博客园 - Franky
L
LangChain Blog
人人都是产品经理
人人都是产品经理
小众软件
小众软件
博客园 - 叶小钗
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
雷峰网
雷峰网
腾讯CDC
Google DeepMind News
Google DeepMind News
博客园 - 三生石上(FineUI控件)
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Help Net Security
Help Net Security
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Schneier on Security
Schneier on Security
博客园 - 聂微东
A
Arctic Wolf
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
The Exploit Database - CXSecurity.com
C
Cyber Attacks, Cyber Crime and Cyber Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Google DeepMind News
Google DeepMind News

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как я написал плагин для WooCommerce под Yandex YCP или как купить в 1 клик из Алисы
nlaik · 2026-05-21 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение11 мин

Охват и читатели0

Туториал

Разбираю архитектуру интеграции магазина на WordPress + WooCommerce с YCP — протоколом Яндекса для покупок через Алису AI, Поиск и Ритм

В конце мая 2026 Яндекс открыл Yandex Commerce Protocol для всех — теперь любой онлайн-магазин может подключить продажи через Алису AI, Поиск и рекомендательную ленту Ритм. Из коробки готовые решения есть для Яндекс KIT, Яндекс Маркета и 1С-Битрикс. Для всего остального — API.

У меня магазин на WooCommerce, и решения «нажать кнопку и заработало» для меня не было. Я мог либо ждать, пока Яндекс или кто-то ещё сделает интеграцию для WordPress (с непредсказуемым ETA), либо разобраться с API и написать плагин сам. Выбрал второе. На разработку ушло около двух недель работы по вечерам, плюс ещё неделя на отладку с тестовым кабинетом Яндекса. По итогу получился open-source плагин на GPL-2.0, в котором закрыты все 10 эндпоинтов спецификации YCP v1: github.com/perfinn/YCP-Yandex-Commerce-Woocommerce.

В статье разберу не «как поставить» (это в README), а как технически устроена интеграция: какие эндпоинты протокол требует реализовать, как маппить заказы из Яндекса в WC, где можно наступить на грабли (статусы заказов, письма, HPOS-совместимость), и почему я добавил идемпотентность по session_id с самого начала. Может, кому-то поможет либо взять плагин под свой проект, либо написать аналог под другую платформу.


Что вообще делает YCP с точки зрения архитектуры

Чтобы было понятно, что мы реализуем — короткий обзор протокола.

YCP — это HTTPS REST API от Яндекса к вашему сайту. Не наоборот. То есть когда покупатель в Алисе или Поиске нажимает «Купить в 1 клик», запросы летят от инфраструктуры Яндекса к серверу магазина. Магазин должен предоставить набор эндпоинтов, через которые Яндекс получает информацию о товарах, рассчитывает доставку, оформляет заказ и потом синхронизирует его статус.

В отличие от классических маркетплейс-интеграций (где магазин выгружает фид и забывает), тут двусторонний live-протокол. Каждый раз, когда пользователь видит товар в результатах Алисы — Яндекс делает запрос к магазину «уточни актуальную цену и наличие». Каждый раз, когда что-то меняется со статусом заказа — синхронизация через API.

Это значит, что плагин должен держать REST API всегда доступным, быстрым и идемпотентным. И тут начинается интересное.


Структура плагина

Прежде чем разбирать конкретные эндпоинты, покажу общий скелет. WordPress-плагин строится по стандартной схеме: главный PHP-файл с заголовком плагина, классы под отдельные ответственности, action/filter хуки для интеграции с WP и WooCommerce.

yandex-ycp-woo/
├── yandex-ycp-woo.php       # Главный файл, хуки активации, регистрация
├── includes/
│   ├── class-ycp-rest.php       # Регистрация REST API эндпоинтов
│   ├── class-ycp-auth.php       # Bearer-токен авторизация
│   ├── class-ycp-checkout.php   # Логика создания заказа из Яндекса
│   ├── class-ycp-delivery.php   # Варианты доставки (СДЭК, ПВЗ)
│   ├── class-ycp-orders.php     # Маппинг товаров, обновление статусов
│   ├── class-ycp-logger.php     # Лог в WP option (для отладки)
│   └── class-ycp-admin.php      # Страница настроек в админке
├── assets/
│   └── admin.css
└── readme.txt                   # Стандарт WordPress.org

Регистрация эндпоинтов идёт через стандартный для WP механизм register_rest_route в action rest_api_init:

add_action( 'rest_api_init', function() {
    $namespace = 'ycp/v1';
 
    // GET /ycp/v1/warehouses
    register_rest_route( $namespace, '/warehouses', [
        'methods'             => 'GET',
        'callback'            => [ 'YCP_Rest', 'get_warehouses' ],
        'permission_callback' => [ 'YCP_Auth', 'check_bearer' ],
    ]);
 
    // POST /ycp/v1/checkout
    register_rest_route( $namespace, '/checkout', [
        'methods'             => 'POST',
        'callback'            => [ 'YCP_Rest', 'create_checkout' ],
        'permission_callback' => [ 'YCP_Auth', 'check_bearer' ],
    ]);
 
    // ... ещё 8 эндпоинтов
});

Здесь важный момент с permission_callback. Это не опциональная штука, это обязательный механизм безопасности WP REST API. Если поставить туда __return_true, WordPress будет ругаться, и эндпоинт станет публично доступным без авторизации. Что для платёжного API недопустимо.

Авторизация в YCP идёт по Bearer-токену, который Яндекс генерирует в кабинете и присылает в каждом запросе:

class YCP_Auth {
    public static function check_bearer( WP_REST_Request $request ) {
        $stored_token = get_option( 'ycpy_bearer_token' );
        if ( empty( $stored_token ) ) {
            return new WP_Error( 'no_token_configured', 'Token not set', [ 'status' => 503 ] );
        }
 
        // Основной заголовок — Authorization: Bearer ...
        $auth = $request->get_header( 'authorization' );
        if ( $auth && stripos( $auth, 'Bearer ' ) === 0 ) {
            $token = trim( substr( $auth, 7 ) );
            if ( hash_equals( $stored_token, $token ) ) {
                return true;
            }
        }
 
        // Fallback на X-API-Key — иногда так удобнее тестировать через curl
        $api_key = $request->get_header( 'x_api_key' );
        if ( $api_key && hash_equals( $stored_token, $api_key ) ) {
            return true;
        }
 
        return new WP_Error( 'unauthorized', 'Invalid token', [ 'status' => 401 ] );
    }
}

Использование hash_equals вместо обычного === — это защита от timing attacks. Для платёжного API такая мелочь критична, и WordPress-разработчики часто её забывают.


Эндпоинт 1: warehouses

Самый простой. Яндекс при подключении магазина первым делом запрашивает список ваших складов. Из админки настройки нашего плагина я даю заполнить только один основной склад — для большинства мелких магазинов на WooCommerce этого достаточно.

public static function get_warehouses( WP_REST_Request $request ) {
    $warehouse = [
        'id'      => 'main',
        'name'    => get_option( 'ycpy_warehouse_name', 'Основной склад' ),
        'address' => [
            'country'  => 'RU',
            'city'     => get_option( 'ycpy_warehouse_city' ),
            'street'   => get_option( 'ycpy_warehouse_street' ),
            'building' => get_option( 'ycpy_warehouse_building' ),
        ],
        'phone'   => get_option( 'ycpy_warehouse_phone' ),
    ];
 
    return rest_ensure_response([
        'warehouses' => [ $warehouse ],
    ]);
}

Тут ничего сложного, но обращу внимание на rest_ensure_response() — это WP-обёртка, которая гарантирует правильный формат ответа с заголовками. Без неё может улететь голый array, и Яндекс не распарсит.


Эндпоинт 2: basket/check — проверка корзины

Вот это уже интересно. Когда пользователь смотрит товар в Алисе и нажимает «Купить», Яндекс перед показом универсального чекаута спрашивает магазин: «у нас в корзине вот эти товары — они актуальные, цены те же, наличие есть, габариты для расчёта доставки какие?».

public static function basket_check( WP_REST_Request $request ) {
    $body  = $request->get_json_params();
    $items = $body['items'] ?? [];
 
    $checked_items = [];
    $unavailable   = [];
 
    foreach ( $items as $item ) {
        $product = self::find_product( $item['id'] );
 
        if ( ! $product || ! $product->is_in_stock() ) {
            $unavailable[] = [ 'id' => $item['id'], 'reason' => 'out_of_stock' ];
            continue;
        }
 
        $checked_items[] = [
            'id'         => $item['id'],
            'price'      => self::format_money( $product->get_price() ),
            'currency'   => 'RUB',
            'quantity'   => $item['quantity'],
            'available'  => $product->get_stock_quantity() ?? 999,
            'dimensions' => self::get_product_dimensions( $product ),
        ];
    }
 
    return rest_ensure_response([
        'items'       => $checked_items,
        'unavailable' => $unavailable,
    ]);
}

Маппинг товаров — отдельная история. Яндекс присылает id товара, и непонятно, что это: SKU в WC или числовой post_id WooCommerce-продукта. Я сделал двойной поиск:

private static function find_product( $id ) {
    // 1. Пробуем как SKU (артикул)
    $product_id = wc_get_product_id_by_sku( $id );
    if ( $product_id ) {
        return wc_get_product( $product_id );
    }
 
    // 2. Если ID числовой — пробуем как post_id
    if ( is_numeric( $id ) ) {
        $product = wc_get_product( (int) $id );
        if ( $product && $product->exists() ) {
            return $product;
        }
    }
 
    return null;
}

Это гибкое поведение — мерчант может в фиде Яндекса указывать что удобнее: SKU или числовой ID WC-товара. Главное, чтобы при маппинге в обратную сторону всё нашлось.

С габаритами тоже есть нюанс. WooCommerce из коробки даёт поля weight, length, width, height — но многие магазины их не заполняют. А без габаритов Яндекс не сможет посчитать стоимость доставки СДЭК, и заказ не оформится. Я добавил дефолтные значения для товаров без указанных размеров:

private static function get_product_dimensions( $product ) {
    $weight = $product->get_weight() ?: get_option( 'ycpy_default_weight', 0.5 );
    $length = $product->get_length() ?: get_option( 'ycpy_default_length', 15 );
    $width  = $product->get_width()  ?: get_option( 'ycpy_default_width', 10 );
    $height = $product->get_height() ?: get_option( 'ycpy_default_height', 5 );
 
    return [
        'weight' => (float) $weight,
        'length' => (float) $length,
        'width'  => (float) $width,
        'height' => (float) $height,
    ];
}

Дефолты задаются в админке плагина — это компромисс между «магазин обязан заполнить размеры всех товаров» и «давайте просто что-то отдадим, чтобы заказ хотя бы прошёл». Я выбираю «лучше отдать дефолт и предупредить мерчанта», чем «отказать в покупке».


Эндпоинт 3: checkout — создание заказа

Самый ответственный эндпоинт. Это когда Яндекс реально хочет создать у нас в WC новый заказ. И тут самая большая ловушка, на которую я наступил во время разработки.

Ловушка 1: письма «Новый заказ на 0 ₽».

Если просто создавать заказ в WC через wc_create_order() со статусом pending, WooCommerce немедленно срабатывает на хуки order-created и шлёт письмо администратору магазина «Новый заказ #1234 на сумму 0 ₽». Почему 0? Потому что Яндекс ещё не прислал финальную сумму — она будет известна позже, после расчёта доставки и применения промокодов.

Решение — создавать заказ в нестандартном статусе checkout-draft, который WC использует для «висящих» корзин:

public static function create_checkout( WP_REST_Request $request ) {
    $body       = $request->get_json_params();
    $session_id = $body['session_id'] ?? '';
 
    // Идемпотентность: проверяем, не создан ли уже заказ по этой сессии
    $existing = self::find_order_by_session( $session_id );
    if ( $existing ) {
        return rest_ensure_response([
            'order_id' => $existing->get_id(),
            'status'   => 'exists',
        ]);
    }
 
    // Создаём заказ в специальном статусе draft
    $order = wc_create_order( [
        'status' => 'checkout-draft',
    ] );
 
    // Сохраняем session_id для последующего поиска
    $order->update_meta_data( '_ycp_session_id', $session_id );
    $order->update_meta_data( '_ycp_origin', 'yandex' );
 
    // Добавляем товары из запроса
    foreach ( $body['items'] as $item_data ) {
        $product = self::find_product( $item_data['id'] );
        if ( $product ) {
            $order->add_product( $product, $item_data['quantity'] );
        }
    }
 
    // Адрес доставки
    if ( ! empty( $body['delivery_address'] ) ) {
        $order->set_address( self::map_address( $body['delivery_address'] ), 'shipping' );
    }
 
    $order->calculate_totals();
    $order->save();
 
    return rest_ensure_response([
        'order_id'   => $order->get_id(),
        'status'     => 'created',
        'session_id' => $session_id,
    ]);
}

Статус checkout-draft WC не показывает на странице «Заказы» по умолчанию (это технический статус), не шлёт писем, и не считает заказ «реально оформленным». Только когда придёт запрос на placed, мы переведём заказ в pending, и вот тогда WC отработает все стандартные хуки.

Ловушка 2: идемпотентность.

API Яндекса гарантирует at-least-once delivery — то есть запрос может прийти дважды, например если у магазина был timeout на ответ. Если просто создавать заказ каждый раз, получим два дубля одного и того же заказа.

Решение — поиск по session_id перед созданием:

private static function find_order_by_session( $session_id ) {
    if ( empty( $session_id ) ) {
        return null;
    }
 
    // HPOS-совместимый запрос
    $orders = wc_get_orders([
        'limit'      => 1,
        'meta_key'   => '_ycp_session_id',
        'meta_value' => $session_id,
        'status'     => [ 'checkout-draft', 'pending', 'processing', 'on-hold' ],
    ]);
 
    return ! empty( $orders ) ? $orders[0] : null;
}

Тут второй важный момент — HPOS (High-Performance Order Storage). WooCommerce с версии 8.0 поддерживает новое хранилище заказов на основе кастомных таблиц вместо wp_posts. На новых сайтах оно включено по умолчанию. Если писать запросы через прямой SQL к wp_postmeta (как многие старые плагины), на HPOS-сайтах ничего не найдётся.

Поэтому я использую везде высокоуровневые wc_get_orders(), $order->update_meta_data(), $order->get_meta() — они работают в обоих хранилищах. Это требует чуть больше дисциплины, но плагин становится совместимым «навсегда».


Эндпоинт 4: checkout/placed — заказ подтверждён

Когда пользователь нажал «Оплатить» в виджете Яндекса, прилетает placed. Финальная сумма, выбранный способ доставки, статус оплаты — всё уже сформировано на стороне Яндекса. Наша задача — перевести WC-заказ из checkout-draft в нормальный статус.

public static function checkout_placed( WP_REST_Request $request ) {
    $body       = $request->get_json_params();
    $session_id = $body['session_id'] ?? '';
 
    $order = self::find_order_by_session( $session_id );
    if ( ! $order ) {
        return new WP_Error( 'order_not_found', 'No draft order for session', [ 'status' => 404 ] );
    }
 
    // Записываем финальные данные
    $order->update_meta_data( '_ycp_yandex_order_id', $body['yandex_order_id'] );
    $order->update_meta_data( '_ycp_payment_method', $body['payment_method'] );
    $order->set_total( $body['total_amount'] );
 
    // Способ оплаты определяет статус
    if ( $body['payment_status'] === 'paid' ) {
        // Оплата прошла на стороне Яндекса (Яндекс Пэй, Сплит) → processing
        $order->update_status( 'processing', 'Оплачено через Яндекс Чекаут' );
    } else {
        // Наложенный платёж → pending (магазин обработает сам)
        $order->update_status( 'pending', 'Заказ оформлен, оплата при получении' );
    }
 
    return rest_ensure_response([
        'status'   => 'ok',
        'order_id' => $order->get_id(),
    ]);
}

Когда статус заказа переходит из checkout-draft в pending или processing, WooCommerce автоматически отправляет администратору письмо с правильной финальной суммой. Это и есть тот «флоу checkout-draft → pending», который решает проблему пустых писем.


Эндпоинты 5–10: остальная синхронизация

Чтобы не превращать статью в reference manual, остальные эндпоинты опишу кратко:

POST /checkout/cancel — пользователь закрыл виджет, не оформив. Удаляем draft-заказ или помечаем его как cancelled.

POST /checkout/delivery/options — возвращаем варианты доставки. Тут я подтягиваю активные методы из WC Shipping, плюс отдельно поддерживаю «самовывоз из ПВЗ».

GET /checkout/delivery/pickup_points — для магазинов с собственными ПВЗ. Я добавил в админке простой UI «список ПВЗ магазина» с CRUD-операциями.

GET /order — Яндекс синхронизирует статусы. Возвращаем историю изменений статуса заказа.

POST /order/cancel — отмена оформленного заказа. Переводим WC-заказ в cancelled, восстанавливаем сток.

POST /order/delivered — заказ доставлен. Переводим заказ в completed.

Все эти эндпоинты идемпотентны по тому же session_id или yandex_order_id, чтобы повторные запросы не делали ничего вредного.


Логирование: главный инструмент отладки

Когда работаешь с внешним API, без логов жить нельзя. На моей стороне нет способа повторить запрос Яндекса вручную — я могу только реагировать на то, что прилетело. Поэтому логирование критично.

Я не стал тащить файловую запись или БД — для маленьких магазинов это лишний overhead. Сделал лог в WP option с ротацией по последним 100 записям:

class YCP_Logger {
    const OPTION_NAME = 'ycpy_log';
    const MAX_ENTRIES = 100;
 
    public static function log( $type, $endpoint, $request_body, $response, $error = null ) {
        $log = get_option( self::OPTION_NAME, [] );
 
        array_unshift( $log, [
            'timestamp'    => current_time( 'mysql' ),
            'type'         => $type, // REQUEST, RESPONSE, EXCEPTION
            'endpoint'     => $endpoint,
            'request_body' => is_string( $request_body ) 
                              ? $request_body 
                              : wp_json_encode( $request_body ),
            'response'     => is_string( $response ) 
                              ? $response 
                              : wp_json_encode( $response ),
            'error'        => $error,
        ]);
 
        // Оставляем только последние N записей
        if ( count( $log ) > self::MAX_ENTRIES ) {
            $log = array_slice( $log, 0, self::MAX_ENTRIES );
        }
 
        update_option( self::OPTION_NAME, $log, false ); // autoload = false
    }
}

Вызывается перед каждым ответом эндпоинта, плюс из глобального try-catch на случай исключений. В админке плагина — таблица с фильтрацией по типу. Когда мерчант приходит с проблемой «заказ не оформляется», первое, что я прошу — открыть лог и найти запись с типом EXCEPTION. Чаще всего там сразу видно, что прислал Яндекс и где упало.

autoload = false — важный нюанс. По умолчанию WP-опции загружаются вместе с каждым запросом к сайту. Если лог огромный, это замедлит каждую страницу. Отключаем autoload — опция читается только когда явно запрошена.


Чему я научился за это время

Несколько уроков из разработки.

WC HPOS — это будущее, и игнорировать его нельзя. Если бы я писал плагин «по-старому» через $wpdb->postmeta, он бы не работал у половины современных магазинов. Все обращения к данным заказов — только через WC API.

Идемпотентность с первого дня. Я мог бы добавить проверку на дубли «когда-нибудь потом». В реальности это бы означало пачку дублированных заказов на боевых магазинах в первый же день. Делай идемпотентность с первого коммита.

Локальные дефолты для нестабильных данных. Магазины — это всегда грязные данные. Половина товаров без габаритов, четверть — без артикулов, в каждом десятом — пустые описания. Плагин, который требует «все товары должны быть идеально заполнены», не взлетит. Плагин с разумными дефолтами и понятными warning-ами — взлетит.

Лог в админке важнее любой документации. Когда у мерчанта что-то не работает, никто не будет лезть в FTP смотреть /wp-content/uploads/ycp-logs/. Кнопка «Лог последних запросов» прямо в админке решает 80% запросов в поддержку.

Статус checkout-draft — недооценённая фишка WooCommerce. До этой работы я о нём не знал. Это очень удобно для любых сценариев «отложенного оформления заказа» — корзины с saved later, multi-step checkout, и теперь — внешние чекауты вроде Яндекса.


Что дальше

Плагин сейчас закрывает все 10 эндпоинтов YCP v1 и работает на нескольких боевых магазинах, включая мой. Из планов:

  • Поддержка multi-warehouse (Яндекс это поддерживает, но я пока вынес наружу только один склад)

  • Интеграция с WC Multilingual для магазинов на нескольких языках

  • Bulk-операции по массовому экспорту товаров в Яндекс Товары (сейчас это делается через стандартный YML-фид)

  • Поддержка партиал-оплат и подписочных моделей, если Яндекс их откроет в YCP v2 Если у вас тоже WooCommerce, и вы хотите подключить продажи через Алису AI — попробуйте плагин. GPL-2.0, бесплатно, поддержка по issue в репозитории: github.com/perfinn/YCP-Yandex-Commerce-Woocommerce.

PR и баг-репорты приветствую. Особенно полезно было бы получить фидбек от магазинов с большим ассортиментом (10k+ товаров) — на моих тестовых стендах таких объёмов не было, и где-то могут вылезти узкие места по производительности.


Полезные ссылки: