title: 'Жизненный цикл заказа, FSM статусов и асинхронный Action Scheduler' description: >- Проектирование надёжного конвейера обработки заказов в WooCommerce 9.x (WordPress 7.0+, PHP 8.4+): конечный автомат статусов (FSM), защита от Race Conditions между IPN-вебхуком и клиентом, предотвращение дублирования транзакций (Idempotency Locks) и асинхронные очереди Action Scheduler. module: woocommerce order: 3 difficulty: advanced estimatedTime: 40 мин topics:
В e-commerce момент нажатия кнопки «Подтвердить заказ» покупателем — это начало сложнейшего распределённого процесса. Заказ должен списать складские остатки, зафиксировать авторизацию платёжного шлюза, сформировать фискальный чек для налоговой, отправить данные в CRM, сгенерировать накладную в службе доставки и уведомить склад через ERP.
Если реализовать эти интеграции синхронно внутри стандартного HTTP-запроса чекаута, интернет-магазин обречён на провал:
Thank You. Оба параллельных PHP-воркера пытаются перевести статус заказа в processing и отправить подтверждающее письмо, приводя к отправке двух писем клиенту и двукратной выгрузке заказа в складскую систему.Современный архитектурный стандарт WooCommerce 9.x+ решает эти задачи через разделение процесса на Строгий Конечный Автомат (Order Finite State Machine — FSM) и Асинхронную Очередь Фоновых Задач (Action Scheduler). Любые интеграции с внешним миром ставятся в очередь через as_enqueue_async_action() и исполняются в фоновом воркере с гарантией идемпотентности и автоматическими повторами при сбоях (Retry with Exponential Backoff).
Представьте конвейер обработки заказов в WooCommerce как авиационный перелёт бизнес-классом:
pending) перейти в статус «Посадка завершена» (completed), минуя «Разрешение на взлёт» (processing). Каждый переход фиксируется главным диспетчером атомарно.[Покупатель жмёт «Оплатить»] ──► Заказ создан (pending) ──► Редирект в шлюз
│
┌─────────────────────────────────────────────────┴────────────────────────┐
▼ ▼
[Браузер возвращается на /thank-you] [Шлюз шлёт вебхук POST /wc-api/...]
│ │
└─────────────────────────┬────────────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ АТОМАРНАЯ СВЕРКА СТАТУСА: $order->payment_complete()│
│ • Idempotency Lock: _paid_lock │
│ • Проверка перехода FSM: pending -> processing │
└─────────────────────────┬────────────────────────┘
│ Успешный переход
▼
┌──────────────────────────────────────────────────┐
│ ACTION SCHEDULER: as_enqueue_async_action() │
│ • В фоне: Отправка чека в онлайн-кассу │
│ • В фоне: Экспорт лида в AmoCRM / Bitrix24 │
│ • В фоне: Регистрация трек-номера в службе CDEK │
└──────────────────────────────────────────────────┘Заказ сохраняется в HPOS в начальном статусе 'pending' (ожидает оплаты). Выделяются складские резервы.
При поступлении оплаты метод $order->payment_complete() атомарно проверяет текущий статус в БД через транзакцию InnoDB, отсекая параллельный вебхук.
Срабатывает конечный автомат: вызываются динамический хук woocommerce_order_status_processing и общий хук смены статуса.
Обработчики не делают синхронных внешних HTTP-запросов, а планируют асинхронные задачи в таблицах wp_actionscheduler_actions.
Фоновые воркеры Action Scheduler обрабатывают очередь пачками, пишут системные Order Notes и повторяют попытки при сбоях сети (Exponential Backoff).
Определите бизнес-событие: онлайн-оплата (woocommerce_payment_complete), готовность к сборке (status_processing) или выдача клиенту (status_completed).
Проверьте метаполе заказа: если действие уже выполняется или завершено, немедленно прервите выполнение, предотвратив дублирование.
Вызовите as_enqueue_async_action() с ID заказа, сохранив быстрый отклик (менее 150ms) интерфейса для покупателя на странице чекаута.
Напишите обработчик фоновой задачи, который при ошибках сети повторно планирует себя через 1, 5 и 15 минут и ведёт аудит в Order Notes.
В чём ключевое отличие хука woocommerce_payment_complete от хука woocommerce_order_status_processing?
В проектах Fullstack Agentic Lab архитектура FSM и Action Scheduler используется для:
payment_complete() с защитой от повторного пробития чека.processing и подтверждение отгрузки при переводе в completed.hash_equals() подписи и строгой блокировкой статуса.Сравним наивную синхронную обработку событий заказа и отказоустойчивый асинхронный стандарт WooCommerce 9.x:
Ниже представлен эталонный производственный артефакт для WooCommerce 9.x (PHP 8.4+). Он реализует Resilient Order Event Subscriber:
_agentic_crm_lock).Order Notes.<?php
/**
* Plugin Name: Fullstack Agentic Lab — Resilient Order Event Pipeline & Action Scheduler
* Description: Идемпотентный конвейер событий заказа и фоновые очереди на базе Action Scheduler.
* Version: 3.2.0
* Requires at least: 6.8
* Requires PHP: 8.4
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Commerce\Orders;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
use WC_Order;
use WP_Error;
defined('ABSPATH') || exit;
final readonly class OrderLifecycleOrchestrator
{
private const string ACTION_SYNC_CRM = 'agentic_lab_async_sync_order_to_crm';
private const string META_LOCK_KEY = '_agentic_crm_sync_status';
public function registerHooks(): void
{
// 1. Декларируем совместимость с HPOS
add_action('before_woocommerce_init', static function (): void {
if (class_exists(FeaturesUtil::class)) {
FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
}
});
// 2. Слушаем переход заказа в статус "В обработке" (оплачен)
add_action('woocommerce_order_status_processing', $this->onOrderProcessing(...), 10, 2);
// 3. Регистрируем обработчик фоновой задачи Action Scheduler
add_action(self::ACTION_SYNC_CRM, $this->executeCrmSyncWorker(...), 10, 2);
}
/**
* Срабатывает в момент перевода заказа в статус processing.
* НЕ ДЕЛАЕТ ВНЕШНИХ HTTP-ЗАПРОСОВ! Только планирует асинхронную задачу.
*/
public function onOrderProcessing(int $orderId, WC_Order $order): void
{
// Идемпотентная проверка: если задача уже была поставлена или выполнена, выходим
$currentLock = (string) $order->get_meta(self::META_LOCK_KEY, true);
if ($currentLock === 'queued' || $currentLock === 'completed') {
return;
}
// Атомарно помечаем заказ как находящийся в очереди
$order->update_meta_data(self::META_LOCK_KEY, 'queued');
$order->add_order_note('[Pipeline] Заказ поставлен в асинхронную очередь Action Scheduler для синхронизации с ERP.');
$order->save();
// Ставим задачу в Action Scheduler (выполнится в фоне в ближайшие секунды)
if (function_exists('as_enqueue_async_action')) {
as_enqueue_async_action(self::ACTION_SYNC_CRM, [
'orderId' => $orderId,
'attemptCount'=> 1,
], 'agentic-commerce-integrations');
}
}
/**
* Фоновый воркер Action Scheduler. Исполняется асинхронно без блокировки пользователя.
*/
public function executeCrmSyncWorker(int $orderId, int $attemptCount = 1): void
{
$order = wc_get_order($orderId);
if (!$order instanceof WC_Order) {
return;
}
// Проверяем, не был ли заказ уже успешно синхронизирован другим параллельным процессом
if ($order->get_meta(self::META_LOCK_KEY, true) === 'completed') {
return;
}
// Подготавливаем нормализованный пакет данных для внешней CRM
$payload = [
'orderNumber' => $order->get_order_number(),
'totalAmount' => (float) $order->get_total(),
'currency' => $order->get_currency(),
'customerEmail' => $order->get_billing_email(),
'customerPhone' => $order->get_billing_phone(),
'items' => [],
];
foreach ($order->get_items() as $item) {
$product = $item->get_product();
$payload['items'][] = [
'sku' => $product?->get_sku() ?? 'N/A',
'name' => $item->get_name(),
'quantity' => $item->get_quantity(),
'total' => (float) $item->get_total(),
];
}
// Выполняем защищённый сетевой запрос с контролем таймаута
$result = $this->dispatchToExternalCrm($payload);
if ($result instanceof WP_Error) {
$errorMessage = $result->get_error_message();
$order->add_order_note("[CRM Pipeline] Сбой синхронизации (Попытка #{$attemptCount}): {$errorMessage}");
$order->save();
// Если лимит попыток (3) не исчерпан, планируем повтор с экспоненциальной задержкой
if ($attemptCount менее 3 && function_exists('as_schedule_single_action')) {
$retryDelaySeconds = match ($attemptCount) {
1 => 60, // Повтор через 1 минуту
2 => 300, // Повтор через 5 минут
default => 900,
};
as_schedule_single_action(
time() + $retryDelaySeconds,
self::ACTION_SYNC_CRM,
['orderId' => $orderId, 'attemptCount' => $attemptCount + 1],
'agentic-commerce-integrations'
);
} else {
$order->update_meta_data(self::META_LOCK_KEY, 'failed');
$order->add_order_note('[CRM Pipeline] КРИТИЧНО: Исчерпаны все 3 попытки фоновой синхронизации. Требуется ручной разбор.');
$order->save();
}
return;
}
// Фиксация успешного завершения
$order->update_meta_data(self::META_LOCK_KEY, 'completed');
$order->update_meta_data('_agentic_external_lead_id', sanitize_text_field((string) $result['leadId']));
$order->add_order_note("[CRM Pipeline] Заказ успешно доставлен в ERP. Внешний ID: {$result['leadId']}");
$order->save();
}
/**
* Эмуляция отправки данных во внешний REST шлюз.
*
* @param array<string, mixed> $payload
* @return array<string, mixed>|WP_Error
*/
private function dispatchToExternalCrm(array $payload): array|WP_Error
{
// В реальном продакшене: wp_remote_post('https://api.crm.internal/leads', ...)
// Для демонстрации отказоустойчивости возвращаем успех
return [
'success' => true,
'leadId' => 'LEAD-' . strtoupper(bin2hex(random_bytes(4))),
];
}
}
(new OrderLifecycleOrchestrator())->registerHooks();
Интернет-магазин отправляет фискальные чеки в онлайн-кассу при подтверждении оплаты. Какая архитектура гарантирует отсутствие дублирования чеков при одновременном поступлении IPN-вебхука от банка и редиректа клиента на страницу Thank You?