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() │
│ • У фоні: Відправка чека в онлайн-касу │
│ • У фоні: Експорт ліда в CRM / ERP │
│ • У фоні: Реєстрація трек-номера в службі доставки│
└──────────────────────────────────────────────────┘Замовлення зберігається в HPOS у початковому статусі 'pending' (очікує оплати). Виділяються складські резерви.
При надходженні оплати метод $order->payment_complete() атомарно перевіряє поточний статус у БД через транзакцію InnoDB, відсікаючи паралельний вебхук.
Спрацьовує скінченний автомат: викликаються динамічний хук woocommerce_order_status_processing та загальний хук зміни статусу.
Обробники не роблять синхронних зовнішніх HTTP-запитів, а планують асинхронні завдання в таблицях wp_actionscheduler_actions.
Фонові воркери Action Scheduler обробляють чергу пачками, пишуть системні Order Notes та повторюють спроби при збоях мережі (Exponential Backoff).
У проєктах Fullstack Agentic Lab архітектура FSM та Action Scheduler використовується для:
< 150ms час створення замовлення) з гарантованою фоновою доставкою ліда навіть при тимчасовій недоступності серверів CRM.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();
Визначте бізнес-подію: онлайн-оплата (woocommerce_payment_complete), готовність до комплектування (status_processing) або видача клієнту (status_completed).
Перевірте метаполе замовлення: якщо дія вже виконується або завершена, негайно припиніть виконання, запобігши дублюванню.
Викличте as_enqueue_async_action() з ID замовлення, зберігши швидкий відгук (< 150ms) інтерфейсу для покупця на сторінці чекауту.
Напишіть обробник фонового завдання, який при помилках мережі повторно планує себе через 1, 5 і 15 хвилин та веде аудит в Order Notes.