AgenticController.php
4 months ago
AgenticWebhookManager.php
7 months ago
AgenticWebhookPayloadBuilder.php
7 months ago
AgenticWebhookPayloadBuilder.php
180 lines
| 1 | <?php |
| 2 | declare(strict_types=1); |
| 3 | |
| 4 | namespace Automattic\WooCommerce\Internal\Admin\Agentic; |
| 5 | |
| 6 | use Automattic\WooCommerce\Enums\OrderStatus; |
| 7 | use Automattic\WooCommerce\StoreApi\Formatters\MoneyFormatter; |
| 8 | use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey; |
| 9 | use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\OrderStatus as ACPOrderStatus; |
| 10 | use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\RefundType; |
| 11 | use WC_Logger_Interface; |
| 12 | use WC_Order; |
| 13 | use WC_Order_Refund; |
| 14 | |
| 15 | /** |
| 16 | * AgenticWebhookPayloadBuilder class |
| 17 | * |
| 18 | * Builds webhook payloads for the Agentic Commerce Protocol following |
| 19 | * the specification for order lifecycle events. |
| 20 | * |
| 21 | * @since 10.3.0 |
| 22 | */ |
| 23 | class AgenticWebhookPayloadBuilder { |
| 24 | /** |
| 25 | * Money formatter instance. |
| 26 | * |
| 27 | * @var MoneyFormatter |
| 28 | */ |
| 29 | private $money_formatter; |
| 30 | |
| 31 | /** |
| 32 | * Dependency initialization. |
| 33 | * |
| 34 | * @internal |
| 35 | */ |
| 36 | final public function init() { |
| 37 | $this->money_formatter = new MoneyFormatter(); |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * Build the webhook payload for an order event. |
| 42 | * |
| 43 | * @param string $event Event type ('order_create' or 'order_update'). |
| 44 | * @param WC_Order $order Order object. |
| 45 | * @return array Webhook payload. |
| 46 | */ |
| 47 | public function build_payload( string $event, WC_Order $order ): array { |
| 48 | return array( |
| 49 | 'type' => $event, |
| 50 | 'data' => $this->build_order_data( $order ), |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Build the order data for the webhook payload. |
| 56 | * |
| 57 | * @param WC_Order $order Order object. |
| 58 | * @return array Order data. |
| 59 | */ |
| 60 | private function build_order_data( WC_Order $order ): array { |
| 61 | return array( |
| 62 | 'type' => 'order', |
| 63 | 'checkout_session_id' => $order->get_meta( OrderMetaKey::AGENTIC_CHECKOUT_SESSION_ID ), |
| 64 | 'permalink_url' => $order->get_checkout_order_received_url(), |
| 65 | 'status' => $this->map_order_status( $order->get_status() ), |
| 66 | 'refunds' => $this->build_refunds_data( $order ), |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Map WooCommerce order status to ACP status. |
| 72 | * |
| 73 | * ACP statuses: created, manual_review, confirmed, canceled, shipped, fulfilled |
| 74 | * |
| 75 | * @param string $wc_status WooCommerce order status. |
| 76 | * @return string ACP status. |
| 77 | */ |
| 78 | private function map_order_status( string $wc_status ): string { |
| 79 | $status_map = array( |
| 80 | // WooCommerce status => ACP status. |
| 81 | OrderStatus::PENDING => ACPOrderStatus::CREATED, |
| 82 | OrderStatus::PROCESSING => ACPOrderStatus::CONFIRMED, |
| 83 | OrderStatus::ON_HOLD => ACPOrderStatus::MANUAL_REVIEW, |
| 84 | OrderStatus::COMPLETED => ACPOrderStatus::FULFILLED, |
| 85 | OrderStatus::CANCELLED => ACPOrderStatus::CANCELED, |
| 86 | OrderStatus::REFUNDED => ACPOrderStatus::FULFILLED, // Refunded orders are still fulfilled. |
| 87 | OrderStatus::FAILED => ACPOrderStatus::CANCELED, |
| 88 | ); |
| 89 | |
| 90 | /** |
| 91 | * Filter the WooCommerce to ACP order status mapping. |
| 92 | * |
| 93 | * Allows extensions to map custom WooCommerce order statuses to ACP order statuses. |
| 94 | * The mapped status must be one of: created, manual_review, confirmed, canceled, shipped, fulfilled. |
| 95 | * |
| 96 | * @see Automattic\WooCommerce\Internal\Agentic\Enums\Specs\OrderStatus |
| 97 | * |
| 98 | * @since 10.3.0 |
| 99 | * |
| 100 | * @param array $status_map Associative array of WooCommerce status => ACP status. |
| 101 | * @param string $wc_status The WooCommerce order status being mapped. |
| 102 | */ |
| 103 | $status_map = apply_filters( 'woocommerce_agentic_webhook_order_status_map', $status_map, $wc_status ); |
| 104 | |
| 105 | // Get mapped status or default to 'created'. |
| 106 | $mapped_status = isset( $status_map[ $wc_status ] ) ? $status_map[ $wc_status ] : ACPOrderStatus::CREATED; |
| 107 | |
| 108 | // Validate the mapped status is a valid ACP status. |
| 109 | if ( ! ACPOrderStatus::is_valid( $mapped_status ) ) { |
| 110 | // Log a warning for invalid status but continue with fallback. |
| 111 | wc_get_logger()->warning( |
| 112 | sprintf( |
| 113 | 'Invalid ACP order status "%s" returned by woocommerce_agentic_webhook_order_status_map filter for WooCommerce status "%s". Using "created" as fallback.', |
| 114 | $mapped_status, |
| 115 | $wc_status |
| 116 | ), |
| 117 | array( 'source' => 'agentic-webhooks' ) |
| 118 | ); |
| 119 | return ACPOrderStatus::CREATED; |
| 120 | } |
| 121 | |
| 122 | return $mapped_status; |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Build refunds data for the order. |
| 127 | * |
| 128 | * @param WC_Order $order Order object. |
| 129 | * @return array Array of refunds. |
| 130 | */ |
| 131 | private function build_refunds_data( WC_Order $order ): array { |
| 132 | return array_map( |
| 133 | array( $this, 'build_single_refund_data' ), |
| 134 | $order->get_refunds() |
| 135 | ); |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Build data for a single refund. |
| 140 | * |
| 141 | * @param WC_Order_Refund $refund Refund object. |
| 142 | * @return array Refund data. |
| 143 | */ |
| 144 | private function build_single_refund_data( WC_Order_Refund $refund ): array { |
| 145 | $refund_type = $this->determine_refund_type( $refund ); |
| 146 | $amount = abs( (float) $refund->get_total() ); // Get absolute value as refunds are negative. |
| 147 | |
| 148 | // Convert amount to minor units using MoneyFormatter (respects store currency decimals). |
| 149 | $amount_in_minor_units = (int) $this->money_formatter->format( $amount ); |
| 150 | |
| 151 | return array( |
| 152 | 'type' => $refund_type, |
| 153 | 'amount' => $amount_in_minor_units, |
| 154 | ); |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * Determine the refund type. |
| 159 | * |
| 160 | * @param WC_Order_Refund $refund Refund object. |
| 161 | * @return string Refund type ('store_credit' or 'original_payment'). |
| 162 | */ |
| 163 | private function determine_refund_type( WC_Order_Refund $refund ): string { |
| 164 | // Default to original payment method. |
| 165 | $refund_type = RefundType::ORIGINAL_PAYMENT; |
| 166 | |
| 167 | /** |
| 168 | * Filter the refund type for Agentic webhooks. |
| 169 | * |
| 170 | * This allows extensions to specify when a refund is store credit. |
| 171 | * By default, all refunds are assumed to be original payment method. |
| 172 | * |
| 173 | * @since 10.4.0 |
| 174 | * @param string $refund_type The refund type ('store_credit' or 'original_payment'). |
| 175 | * @param WC_Order_Refund $refund The refund object. |
| 176 | */ |
| 177 | return apply_filters( 'woocommerce_agentic_webhook_refund_type', $refund_type, $refund ); |
| 178 | } |
| 179 | } |
| 180 |