$payload['currency'], 'total_minor' => $payload['total_minor'], 'payment_method' => $payload['payment_method'], 'line_items' => $payload['line_items'], 'shipping' => $payload['shipping'], 'phone' => $payload['billing']['phone'], ); return hash( 'sha256', (string) wp_json_encode( $covered ) ); } /** * Whether the whole order is cancelled or refunded away. * * @param WC_Order $order Order. * @return bool */ function orderping_order_is_cancelled( WC_Order $order ) { if ( in_array( $order->get_status(), ORDERPING_CANCELLED_STATUSES, true ) ) { return true; } return (float) $order->get_total() > 0 && (float) $order->get_remaining_refund_amount() <= 0; } /** * Whether this order's changes are reportable: placed at checkout while capture * was on, and its placed event was recorded. * * @param WC_Order $order Order. * @return bool */ function orderping_order_reportable( WC_Order $order ) { if ( ! orderping_order_in_scope( $order ) ) { return false; // Pilot filter: out-of-scope orders are never reported. } if ( '1' !== (string) $order->get_meta( ORDERPING_CAPTURE_ELIGIBLE_META ) ) { return false; } return orderping_order_was_captured( (int) $order->get_id() ); } /** * Save / status / refund hook. Cheap: compares and schedules, never writes. * * @param int $order_id Order ID. */ function orderping_on_order_saved( $order_id ) { try { $order = wc_get_order( $order_id ); if ( ! $order instanceof WC_Order || null === orderping_connection() || ! orderping_order_reportable( $order ) ) { return; } $changed = orderping_order_fingerprint( $order ) !== (string) $order->get_meta( ORDERPING_FINGERPRINT_META ) || orderping_order_is_cancelled( $order ); if ( ! $changed ) { return; // Cosmetic save: notes, meta, status among placed statuses. } // One pending action per order, so a burst of saves is one event. Action Scheduler's unique // flag ignores args (one order's action would suppress every other order's), so check per order. $args = array( (int) $order_id ); if ( function_exists( 'as_schedule_single_action' ) ) { if ( ! function_exists( 'as_has_scheduled_action' ) || ! as_has_scheduled_action( ORDERPING_UPDATE_HOOK, $args, ORDERPING_AS_GROUP ) ) { as_schedule_single_action( time() + ORDERPING_UPDATE_DEBOUNCE, ORDERPING_UPDATE_HOOK, $args, ORDERPING_AS_GROUP, false ); } } elseif ( ! wp_next_scheduled( ORDERPING_UPDATE_HOOK, $args ) ) { wp_schedule_single_event( time() + ORDERPING_UPDATE_DEBOUNCE, ORDERPING_UPDATE_HOOK, $args ); } } catch ( \Throwable $e ) { return; // Saving an order must never fail because of this plugin. } } /** * Records the fingerprint the backend already knows about (called at capture). * * @param WC_Order $order Order. */ function orderping_remember_fingerprint( WC_Order $order ) { $order->update_meta_data( ORDERPING_FINGERPRINT_META, orderping_order_fingerprint( $order ) ); $order->save(); } /** * The order's modified time as a UTC ISO instant, or null when WooCommerce has none. * * @param WC_Order $order Order. * @return string|null */ function orderping_order_modified_at( WC_Order $order ) { $ts = orderping_order_modified( $order ); return $ts > 0 ? gmdate( 'Y-m-d\TH:i:s\Z', $ts ) : null; } /** * Action Scheduler callback: emits the current state once per distinct change. * The event key carries the fingerprint, so a retried action cannot enqueue the * same change twice; cancellation has a single key per order. * * @param int $order_id Order ID. */ function orderping_emit_order_updated( $order_id ) { try { $order = orderping_fresh_order( (int) $order_id ); if ( null === $order || null === orderping_connection() || ! orderping_order_reportable( $order ) ) { return; } $fingerprint = orderping_order_fingerprint( $order ); $cancelled = orderping_order_is_cancelled( $order ); if ( ! $cancelled && (string) $order->get_meta( ORDERPING_FINGERPRINT_META ) === $fingerprint ) { return; } $key = $cancelled ? 'cancelled-' . (int) $order_id : 'updated-' . (int) $order_id . '-' . substr( $fingerprint, 0, 16 ); $event = array( 'type' => 'order_updated', 'event_id' => $key, 'contract_version' => ORDERPING_CONTRACT_VERSION, 'plugin_version' => ORDERPING_VERSION, 'cancelled' => $cancelled, 'order' => orderping_order_payload( $order ), ); // Optional source time (read before the save below bumps it): lets the backend ignore a reordered, older edit. $modified_at = orderping_order_modified_at( $order ); if ( null !== $modified_at ) { $event['modified_at'] = $modified_at; } $enqueued = orderping_outbox_enqueue( $key, (int) $order_id, $event ); // Advance only when a durable event exists (new or already stored), never on a failed write. if ( false === $enqueued ) { return; } // Remember what was reported so the same state does not schedule again. $order->update_meta_data( ORDERPING_FINGERPRINT_META, $fingerprint ); $order->save(); } catch ( \Throwable $e ) { return; } }