repository = $repository ?: new Akkedli_V2_Outbox_Repository(); $this->client = $client ?: new Akkedli_V2_Signed_Client(); } public static function order_provenance($order_id) { try { $found = (new Akkedli_V2_Outbox_Repository())->order_provenance( self::order_event_id($order_id) ); return true === $found ? 'present' : (false === $found ? 'absent' : 'ambiguous'); } catch (Throwable $error) { unset($error); return 'ambiguous'; } } public function signal_new_order($order_id, $order = null, $selection = null) { $this->signal_order($order_id, $order, false, $selection); } public function signal_store_api_order($order, $selection = null) { if (!is_object($order) || !is_a($order, 'WC_Order')) { return; } try { $order_id = $order->get_id(); } catch (Throwable $error) { unset($error); return; } $this->signal_order($order_id, $order, true, $selection); } public function signal_status($order_id, $old_status, $new_status, $order, $selection = null) { if (null === $selection) { $selection = Akkedli_Outbound_Selector::status($order_id, $old_status, $new_status); } if (!Akkedli_Outbound_Selector::is_v2($selection) || !self::native_id($order_id) || !is_string($old_status) || !is_string($new_status) || $old_status === $new_status || !is_object($order) || !is_a($order, 'WC_Order') || self::is_suppressed($order_id)) { return; } $guard = self::context('status', $order_id . ':' . $old_status . ':' . $new_status); if (isset(self::$status_signals[$guard])) { return; } self::$status_signals[$guard] = true; try { if ((string) $order->get_id() !== (string) $order_id) { return; } $order_event = self::order_event_id($order_id); $order_state = $this->repository->order_state($order_event); if (!in_array($order_state, array('capture_pending', 'capture_processing', 'pending', 'processing', 'retry', 'paused', 'confirmed'), true)) { return; } $body = $this->client->build_status_body($order, $old_status, $new_status); if (!is_string($body)) { return; } $event_id = bin2hex(random_bytes(32)); $idempotency = 'status.' . Akkedli_V2_Crypto::base64url_encode(random_bytes(32)); $envelope = Akkedli_V2_Outbox_Crypto::seal(array( 'body' => $body, 'idempotency_key' => $idempotency, ), $event_id, 'status'); unset($body, $idempotency); $row_id = $this->repository->insert_status( $event_id, self::order_key($order_id), $envelope, time() ); unset($envelope); if (false !== $row_id) { Akkedli_V2_Outbox_Worker::schedule_delivery($row_id); } } catch (Throwable $error) { unset($error, $body, $event_id, $idempotency, $envelope, $row_id); } } public static function suppress_order($order_id) { if (!self::native_id($order_id)) { return false; } $key = self::context('suppress', (string) $order_id); self::$suppressed_orders[$key] = isset(self::$suppressed_orders[$key]) ? self::$suppressed_orders[$key] + 1 : 1; return $key; } public static function release_suppression($key) { if (!is_string($key) || !isset(self::$suppressed_orders[$key])) { return false; } self::$suppressed_orders[$key]--; if (self::$suppressed_orders[$key] < 1) { unset(self::$suppressed_orders[$key]); } return true; } public static function order_event_id($order_id) { return self::opaque('event:protocol:1:operation:order-created', $order_id); } public static function order_key($order_id) { return self::opaque('fifo:protocol:1:order', $order_id); } private function signal_order($order_id, $order, $accelerate, $selection) { if (null === $selection) { $selection = Akkedli_Outbound_Selector::order($order_id); } if (!Akkedli_Outbound_Selector::is_v2($selection) || !self::native_id($order_id)) { return; } try { if (null !== $order && (!is_object($order) || !is_a($order, 'WC_Order') || (string) $order->get_id() !== (string) $order_id)) { return; } $row_id = $this->repository->signal_order((string) $order_id, self::order_event_id($order_id), self::order_key($order_id), $accelerate, time()); if (false !== $row_id) { Akkedli_V2_Outbox_Worker::schedule_capture($row_id); } } catch (Throwable $error) { unset($error, $row_id); } } private static function is_suppressed($order_id) { return isset(self::$suppressed_orders[self::context('suppress', (string) $order_id)]); } private static function native_id($value) { $value = is_int($value) ? (string) $value : $value; return is_string($value) && 1 === preg_match('/^[1-9]\d{0,15}$/D', $value) && (strlen($value) < 16 || strcmp($value, '9007199254740991') <= 0); } private static function opaque($purpose, $order_id) { return hash_hmac('sha256', self::context($purpose, (string) $order_id), wp_salt('auth')); } private static function context($purpose, $value) { return implode(':', array('akkedli', 'v2', 'blog', (string) get_current_blog_id(), $purpose, (string) $value)); } }