*/ private static array $recorded = []; /** * @var array */ private static array $recorded_cleanup = []; public static function record_order_received( \WC_Order $order, string $order_key = '' ): void { $trace_id = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_ID, true ) ); if ( '' === $trace_id ) { return; } $order_id = (int) $order->get_id(); $recording_key = $trace_id . ':' . $order_id; if ( isset( self::$recorded[ $recording_key ] ) ) { return; } self::$recorded[ $recording_key ] = true; $gateway_id = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_GATEWAY_ID, true ) ); if ( '' === $gateway_id ) { $gateway_id = sanitize_key( (string) $order->get_payment_method() ); } $provider_key = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_PROVIDER_KEY, true ) ); $verified_gateway_ids = self::normalize_gateway_id_list( $order->get_meta( self::ORDER_META_VERIFIED_GATEWAY_IDS, true ) ); $verified_integration_ids = self::normalize_gateway_id_list( $order->get_meta( self::ORDER_META_VERIFIED_INTEGRATION_IDS, true ) ); $verified_wallet_ids = self::normalize_gateway_id_list( $order->get_meta( self::ORDER_META_VERIFIED_WALLET_IDS, true ) ); $resolved_order_key = '' !== $order_key ? $order_key : (string) $order->get_order_key(); CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => 'order_received', 'step' => 'thankyou', 'status' => 'success', 'origin' => 'backend', 'gatewayId' => $gateway_id, 'providerKey' => $provider_key, 'context' => [ 'orderId' => $order_id, 'orderKey' => sanitize_text_field( $resolved_order_key ), 'orderStatus' => sanitize_key( (string) $order->get_status() ), 'paymentMethod' => sanitize_key( (string) $order->get_payment_method() ), 'paymentMethodTitle' => sanitize_text_field( (string) $order->get_payment_method_title() ), 'verifiedGatewayIds' => $verified_gateway_ids, 'verifiedIntegrationIds' => $verified_integration_ids, 'verifiedWalletIds' => $verified_wallet_ids, 'currency' => sanitize_text_field( (string) $order->get_currency() ), 'cartTotal' => (float) $order->get_total(), 'thankyouSource' => 'portal', ], ] ); } /** * @param array $cleanup_result */ public static function record_cart_cleanup( \WC_Order $order, string $order_key = '', array $cleanup_result = [] ): void { $trace_id = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_ID, true ) ); if ( '' === $trace_id ) { return; } $order_id = (int) $order->get_id(); $reason = sanitize_key( (string) ( $cleanup_result['reason'] ?? '' ) ); $recording_key = implode( ':', [ $trace_id, $order_id, 'cleanup', $reason, ! empty( $cleanup_result['performed'] ) ? '1' : '0', absint( (int) ( $cleanup_result['cartItemCountBefore'] ?? 0 ) ), absint( (int) ( $cleanup_result['cartItemCountAfter'] ?? 0 ) ), ] ); if ( isset( self::$recorded_cleanup[ $recording_key ] ) ) { return; } self::$recorded_cleanup[ $recording_key ] = true; $gateway_id = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_GATEWAY_ID, true ) ); if ( '' === $gateway_id ) { $gateway_id = sanitize_key( (string) $order->get_payment_method() ); } $provider_key = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_PROVIDER_KEY, true ) ); $verified_gateway_ids = self::normalize_gateway_id_list( $order->get_meta( self::ORDER_META_VERIFIED_GATEWAY_IDS, true ) ); $verified_integration_ids = self::normalize_gateway_id_list( $order->get_meta( self::ORDER_META_VERIFIED_INTEGRATION_IDS, true ) ); $verified_wallet_ids = self::normalize_gateway_id_list( $order->get_meta( self::ORDER_META_VERIFIED_WALLET_IDS, true ) ); $resolved_order_key = '' !== $order_key ? $order_key : (string) $order->get_order_key(); CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => 'thankyou_cart_cleanup', 'step' => 'thankyou', 'status' => self::trace_status_for_cleanup( (string) ( $cleanup_result['status'] ?? '' ) ), 'origin' => 'backend', 'gatewayId' => $gateway_id, 'providerKey' => $provider_key, 'message' => (string) ( $cleanup_result['message'] ?? '' ), 'context' => [ 'orderId' => $order_id, 'orderKey' => sanitize_text_field( $resolved_order_key ), 'orderStatus' => sanitize_key( (string) $order->get_status() ), 'paymentMethod' => sanitize_key( (string) $order->get_payment_method() ), 'verifiedGatewayIds' => $verified_gateway_ids, 'verifiedIntegrationIds' => $verified_integration_ids, 'verifiedWalletIds' => $verified_wallet_ids, 'thankyouSource' => 'portal', 'cleanup' => [ 'reason' => $reason, 'status' => sanitize_key( (string) ( $cleanup_result['status'] ?? '' ) ), 'eligible' => ! empty( $cleanup_result['eligible'] ), 'attempted' => ! empty( $cleanup_result['attempted'] ), 'performed' => ! empty( $cleanup_result['performed'] ), 'matchMethod' => sanitize_key( (string) ( $cleanup_result['matchMethod'] ?? '' ) ), ], 'cart' => [ 'itemCountBefore' => absint( (int) ( $cleanup_result['cartItemCountBefore'] ?? 0 ) ), 'itemCountAfter' => absint( (int) ( $cleanup_result['cartItemCountAfter'] ?? 0 ) ), 'quantityBefore' => absint( (int) ( $cleanup_result['cartQuantityBefore'] ?? 0 ) ), 'quantityAfter' => absint( (int) ( $cleanup_result['cartQuantityAfter'] ?? 0 ) ), 'matchesOrder' => ! empty( $cleanup_result['cartMatchesOrder'] ), 'signature' => self::signature_to_string( $cleanup_result['cartSignature'] ?? [] ), ], 'session' => [ 'orderId' => absint( (int) ( $cleanup_result['sessionOrderId'] ?? 0 ) ), 'orderIds' => self::normalize_int_list( $cleanup_result['sessionOrderIds'] ?? [] ), 'matchesOrder' => ! empty( $cleanup_result['sessionOrderMatch'] ), ], 'order' => [ 'itemCount' => absint( (int) ( $cleanup_result['orderItemCount'] ?? 0 ) ), 'signature' => self::signature_to_string( $cleanup_result['orderSignature'] ?? [] ), ], ], ] ); } /** * @param array $context */ public static function record_canonical_redirect( \WC_Order $order, string $status, string $reason, array $context = [] ): void { $trace_id = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_ID, true ) ); if ( '' === $trace_id ) { return; } $gateway_id = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_GATEWAY_ID, true ) ); if ( '' === $gateway_id ) { $gateway_id = sanitize_key( (string) $order->get_payment_method() ); } $provider_key = sanitize_key( (string) $order->get_meta( self::ORDER_META_TRACE_PROVIDER_KEY, true ) ); $trace_status = 'success' === sanitize_key( $status ) ? 'success' : 'warning'; $event = 'success' === $trace_status ? 'thankyou_canonical_redirect' : 'thankyou_canonical_redirect_blocked'; CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => $event, 'step' => 'thankyou', 'status' => $trace_status, 'origin' => 'backend', 'gatewayId' => $gateway_id, 'providerKey' => $provider_key, 'context' => array_merge( [ 'orderId' => (int) $order->get_id(), 'orderStatus' => sanitize_key( (string) $order->get_status() ), 'paymentMethod' => sanitize_key( (string) $order->get_payment_method() ), 'reason' => sanitize_key( $reason ), 'thankyouSource' => 'portal', ], self::sanitize_context( $context ) ), ] ); } private static function trace_status_for_cleanup( string $status ): string { switch ( sanitize_key( $status ) ) { case 'cleared': return 'success'; case 'attention': return 'warning'; default: return 'info'; } } /** * @param mixed $value * @return array */ private static function normalize_gateway_id_list( $value ): array { if ( is_string( $value ) ) { $decoded = json_decode( $value, true ); $value = is_array( $decoded ) ? $decoded : preg_split( '/[,\\s]+/', $value ); } if ( ! is_array( $value ) ) { return []; } $ids = []; foreach ( $value as $gateway_id ) { $gateway_id = sanitize_key( (string) $gateway_id ); if ( '' !== $gateway_id ) { $ids[ $gateway_id ] = true; } } return array_keys( $ids ); } /** * @param array $context * @return array */ private static function sanitize_context( array $context ): array { $sanitized = []; foreach ( $context as $key => $value ) { $context_key = preg_replace( '/[^a-zA-Z0-9_]/', '', (string) $key ); if ( ! is_string( $context_key ) || '' === $context_key || 'orderkey' === strtolower( $context_key ) ) { continue; } if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) ) { $sanitized[ $context_key ] = $value; } elseif ( is_scalar( $value ) ) { $sanitized[ $context_key ] = sanitize_text_field( (string) $value ); } } return $sanitized; } /** * @param mixed $value * @return array */ private static function normalize_int_list( $value ): array { if ( ! is_array( $value ) ) { return []; } $ids = []; foreach ( $value as $item ) { if ( is_numeric( $item ) && absint( $item ) > 0 ) { $ids[ absint( $item ) ] = true; } } return array_keys( $ids ); } /** * @param mixed $value */ private static function signature_to_string( $value ): string { if ( ! is_array( $value ) ) { return ''; } ksort( $value ); $parts = []; foreach ( $value as $line_key => $quantity ) { $parts[] = sanitize_text_field( (string) $line_key ) . '=' . absint( $quantity ); } return implode( ',', $parts ); } }