$data */ public function capture_create_order_request( array $data ): void { $this->record_ppcp_event( 'ppcp_native_create_order_request_started', 'create_order', $data ); } /** * @param mixed $order */ public function capture_paypal_order_created( $order ): void { $trace_id = $this->get_trace_id(); if ( '' === $trace_id ) { return; } $paypal_order_id = is_object( $order ) && method_exists( $order, 'id' ) ? (string) $order->id() : ''; CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => 'ppcp_native_paypal_order_created', 'step' => 'payment', 'status' => 'success', 'origin' => 'backend', 'gatewayId' => 'ppcp-gateway', 'providerKey' => 'ppcp', 'context' => [ 'stage' => 'create_order', 'has_paypal_order_id' => '' !== trim( $paypal_order_id ), 'paypal_order_id_length' => strlen( trim( $paypal_order_id ) ), 'cart_summary' => $this->summarize_cart(), 'ppcp_session_summary' => $this->summarize_ppcp_session(), ], ] ); } /** * @param array $data */ public function capture_approve_order_request( array $data ): void { $this->record_ppcp_event( 'ppcp_native_approve_order_request_started', 'approve_order', $data ); } /** * @param array $data * @param mixed $payment_method * @param mixed $request_data * @return array */ public function capture_create_order_body( array $data, $payment_method = '', $request_data = [] ): array { $trace_id = $this->get_trace_id(); if ( '' === $trace_id ) { return $data; } try { $request_data = is_array( $request_data ) ? $request_data : []; $payment_method = sanitize_key( (string) $payment_method ); $funding_source = sanitize_key( (string) ( $request_data['funding_source'] ?? '' ) ); $callback_url = $this->extract_order_update_callback_url( $data ); $callback_summary = CheckoutPpcpShippingCallbackTraceProbe::remember_trace_for_callback_url( $callback_url, $trace_id, [ 'payment_method' => $payment_method, 'funding_source' => $funding_source, ] ); CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => 'ppcp_native_create_order_body_prepared', 'step' => 'payment', 'status' => 'info', 'origin' => 'backend', 'gatewayId' => 'ppcp-gateway', 'providerKey' => 'ppcp', 'context' => array_merge( $this->summarize_create_order_body( $data, $payment_method, $request_data ), $callback_summary ), ] ); } catch ( \Throwable $error ) { unset( $error ); } return $data; } /** * @param array $data */ private function record_ppcp_event( string $event, string $stage, array $data ): void { $trace_id = $this->get_trace_id(); if ( '' === $trace_id ) { return; } CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => $event, 'step' => 'payment', 'status' => 'start', 'origin' => 'backend', 'gatewayId' => 'ppcp-gateway', 'providerKey' => 'ppcp', 'context' => $this->build_trace_context( $stage, $data ), ] ); } /** * @param array $data * @return array */ private function build_trace_context( string $stage, array $data ): array { $form = $this->resolve_form_data( $data ); $paypal_order_id = (string) ( $data['order_id'] ?? $data['orderID'] ?? $data['orderId'] ?? '' ); return [ 'stage' => $stage, 'request_keys' => $this->sanitize_keys( array_keys( $data ), 40 ), 'request_context' => sanitize_key( (string) ( $data['context'] ?? '' ) ), 'payment_method' => sanitize_key( (string) ( $data['payment_method'] ?? 'ppcp-gateway' ) ), 'funding_source' => sanitize_key( (string) ( $data['funding_source'] ?? '' ) ), 'has_paypal_order_id' => '' !== trim( $paypal_order_id ), 'paypal_order_id_length' => strlen( trim( $paypal_order_id ) ), 'form_summary' => $this->summarize_form_data( $form ), 'cart_summary' => $this->summarize_cart(), 'customer_summary' => $this->summarize_customer(), 'ppcp_session_summary' => $this->summarize_ppcp_session(), ]; } /** * @param array $data * @param array $request_data * @return array */ private function summarize_create_order_body( array $data, string $payment_method, array $request_data ): array { $purchase_units = $this->trace_list( $this->read_trace_path( $data, [ 'purchase_units' ] ) ); $payment_source = $this->trace_map( $this->read_trace_path( $data, [ 'payment_source' ] ) ); $paypal_source = $this->trace_map( $this->read_trace_path( $data, [ 'payment_source', 'paypal' ] ) ); $experience_context = $this->trace_map( $this->read_trace_path( $data, [ 'payment_source', 'paypal', 'experience_context' ] ) ); $callback_config = $this->trace_map( $this->read_trace_path( $data, [ 'payment_source', 'paypal', 'experience_context', 'order_update_callback_config' ] ) ); $callback_events = $this->trace_list( $this->read_trace_path( $callback_config, [ 'callback_events' ] ) ); return [ 'stage' => 'create_order_body', 'payment_method' => $payment_method, 'funding_source' => sanitize_key( (string) ( $request_data['funding_source'] ?? '' ) ), 'request_data_keys' => $this->sanitize_keys( array_keys( $request_data ), 20 ), 'intent' => sanitize_key( (string) ( $data['intent'] ?? '' ) ), 'purchase_unit_count' => count( $purchase_units ), 'purchase_unit_summary' => $this->summarize_purchase_units( $purchase_units ), 'has_payment_source' => [] !== $payment_source, 'payment_source_keys' => $this->sanitize_keys( array_keys( $payment_source ), 8 ), 'has_paypal_source' => [] !== $paypal_source, 'paypal_source_keys' => $this->sanitize_keys( array_keys( $paypal_source ), 12 ), 'experience_context_keys' => $this->sanitize_keys( array_keys( $experience_context ), 16 ), 'shipping_preference' => sanitize_key( (string) ( $experience_context['shipping_preference'] ?? '' ) ), 'has_order_update_callback_config' => [] !== $callback_config, 'callback_event_count' => count( $callback_events ), 'callback_events' => $this->sanitize_keys( $callback_events, 8 ), ]; } /** * @param array $purchase_units * @return array> */ private function summarize_purchase_units( array $purchase_units ): array { $summaries = []; foreach ( array_slice( $purchase_units, 0, 4 ) as $purchase_unit ) { $purchase_unit = $this->trace_map( $purchase_unit ); if ( [] === $purchase_unit ) { continue; } $shipping = $this->trace_map( $this->read_trace_path( $purchase_unit, [ 'shipping' ] ) ); $shipping_options = $this->trace_list( $this->read_trace_path( $shipping, [ 'options' ] ) ); $amount = $this->trace_map( $this->read_trace_path( $purchase_unit, [ 'amount' ] ) ); $summaries[] = [ 'reference_id_length' => strlen( trim( (string) ( $purchase_unit['reference_id'] ?? '' ) ) ), 'has_amount' => [] !== $amount, 'currency_code' => sanitize_key( (string) ( $amount['currency_code'] ?? '' ) ), 'has_shipping' => [] !== $shipping, 'shipping_keys' => $this->sanitize_keys( array_keys( $shipping ), 12 ), 'shipping_option_count' => count( $shipping_options ), ]; } return $summaries; } /** * @param array $data */ private function extract_order_update_callback_url( array $data ): string { $callback_url = $this->read_trace_path( $data, [ 'payment_source', 'paypal', 'experience_context', 'order_update_callback_config', 'callback_url' ] ); if ( ! is_scalar( $callback_url ) ) { return ''; } return trim( (string) $callback_url ); } /** * @param mixed $value * @param array $path * @return mixed|null */ private function read_trace_path( $value, array $path ) { foreach ( $path as $key ) { if ( is_array( $value ) && array_key_exists( $key, $value ) ) { $value = $value[ $key ]; continue; } if ( is_object( $value ) && property_exists( $value, $key ) ) { $value = $value->{$key}; continue; } return null; } return $value; } /** * @param mixed $value * @return array */ private function trace_map( $value ): array { if ( is_array( $value ) ) { return $value; } if ( is_object( $value ) ) { return get_object_vars( $value ); } return []; } /** * @param mixed $value * @return array */ private function trace_list( $value ): array { if ( is_array( $value ) ) { return array_values( $value ); } if ( is_object( $value ) ) { return array_values( get_object_vars( $value ) ); } return []; } private function get_trace_id(): string { $trace_id = isset( $_SERVER['HTTP_X_IPS_CHECKOUT_TRACE'] ) ? sanitize_key( wp_unslash( $_SERVER['HTTP_X_IPS_CHECKOUT_TRACE'] ) ) : ''; if ( '' === $trace_id && isset( $_SERVER['HTTP_X_IPS_CHECKOUT_TRACE_ID'] ) ) { $trace_id = sanitize_key( wp_unslash( $_SERVER['HTTP_X_IPS_CHECKOUT_TRACE_ID'] ) ); } return sanitize_key( $trace_id ); } /** * @param array $data * @return array */ private function resolve_form_data( array $data ): array { if ( isset( $data['form'] ) && is_array( $data['form'] ) ) { return $data['form']; } $form_encoded = (string) ( $data['form_encoded'] ?? '' ); if ( '' === $form_encoded ) { return []; } $form = []; parse_str( $form_encoded, $form ); return is_array( $form ) ? $form : []; } /** * @param array $form * @return array */ private function summarize_form_data( array $form ): array { $keys = $this->sanitize_keys( array_keys( $form ), 40 ); $billing_keys = $this->filter_prefixed_keys( $keys, 'billing_' ); $shipping_keys = $this->filter_prefixed_keys( $keys, 'shipping_' ); return [ 'total_key_count' => count( $keys ), 'billing_key_count' => count( $billing_keys ), 'shipping_key_count' => count( $shipping_keys ), 'has_payment_method' => isset( $form['payment_method'] ), 'has_billing_email' => isset( $form['billing_email'] ), 'has_billing_phone' => isset( $form['billing_phone'] ), 'has_billing_country' => isset( $form['billing_country'] ), 'has_shipping_country' => isset( $form['shipping_country'] ), 'has_shipping_postcode' => isset( $form['shipping_postcode'] ), 'has_shipping_city' => isset( $form['shipping_city'] ), 'has_shipping_address_1' => isset( $form['shipping_address_1'] ), 'billing_address' => $this->summarize_form_address( $form, 'billing_' ), 'shipping_address' => $this->summarize_form_address( $form, 'shipping_' ), 'address_relation' => $this->summarize_form_address_relation( $form ), 'billing_keys' => array_slice( $billing_keys, 0, 10 ), 'shipping_keys' => array_slice( $shipping_keys, 0, 10 ), ]; } /** * @return array */ private function summarize_cart(): array { if ( ! function_exists( 'WC' ) || ! WC() || ! WC()->cart ) { return [ 'available' => false, ]; } $chosen_shipping_methods = []; if ( WC()->session && method_exists( WC()->session, 'get' ) ) { $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); } if ( ! is_array( $chosen_shipping_methods ) ) { $chosen_shipping_methods = []; } $cart_hash = method_exists( WC()->cart, 'get_cart_hash' ) ? (string) WC()->cart->get_cart_hash() : ''; return [ 'available' => true, 'needs_shipping' => (bool) WC()->cart->needs_shipping(), 'needs_payment' => (bool) WC()->cart->needs_payment(), 'item_count' => (int) WC()->cart->get_cart_contents_count(), 'shipping_total' => (float) WC()->cart->get_shipping_total(), 'cart_total' => (float) WC()->cart->get_total( 'edit' ), 'currency' => function_exists( 'get_woocommerce_currency' ) ? sanitize_key( (string) get_woocommerce_currency() ) : '', 'cart_hash_length' => strlen( $cart_hash ), 'chosen_shipping_methods_count' => count( $chosen_shipping_methods ), 'has_chosen_shipping_methods' => [] !== $chosen_shipping_methods, 'chosen_shipping_method_keys' => $this->summarize_shipping_method_keys( $chosen_shipping_methods ), 'shipping_packages' => $this->summarize_wc_shipping_packages(), ]; } /** * @return array */ private function summarize_customer(): array { if ( ! function_exists( 'WC' ) || ! WC() || ! WC()->customer ) { return [ 'available' => false, ]; } $customer = WC()->customer; return [ 'available' => true, 'is_logged_in' => is_user_logged_in(), 'has_billing_country' => '' !== trim( (string) $customer->get_billing_country() ), 'has_billing_postcode' => '' !== trim( (string) $customer->get_billing_postcode() ), 'has_billing_city' => '' !== trim( (string) $customer->get_billing_city() ), 'has_billing_state' => '' !== trim( (string) $customer->get_billing_state() ), 'has_billing_address_1' => '' !== trim( (string) $customer->get_billing_address_1() ), 'has_billing_phone' => '' !== trim( (string) $customer->get_billing_phone() ), 'has_billing_email' => '' !== trim( (string) $customer->get_billing_email() ), 'has_shipping_country' => '' !== trim( (string) $customer->get_shipping_country() ), 'has_shipping_postcode' => '' !== trim( (string) $customer->get_shipping_postcode() ), 'has_shipping_city' => '' !== trim( (string) $customer->get_shipping_city() ), 'has_shipping_state' => '' !== trim( (string) $customer->get_shipping_state() ), 'has_shipping_address_1' => '' !== trim( (string) $customer->get_shipping_address_1() ), 'has_calculated_shipping' => method_exists( $customer, 'has_calculated_shipping' ) ? (bool) $customer->has_calculated_shipping() : false, ]; } /** * @return array */ private function summarize_ppcp_session(): array { if ( ! function_exists( 'WC' ) || ! WC() || ! WC()->session || ! method_exists( WC()->session, 'get' ) ) { return [ 'available' => false, ]; } $session = WC()->session->get( 'ppcp' ); if ( ! is_array( $session ) ) { return [ 'available' => true, 'present' => null !== $session, 'type' => is_object( $session ) ? 'object' : gettype( $session ), ]; } $checkout_form = isset( $session['checkout_form'] ) && is_array( $session['checkout_form'] ) ? $session['checkout_form'] : []; return [ 'available' => true, 'present' => [] !== $session, 'has_order' => isset( $session['order'] ), 'has_funding_source' => isset( $session['funding_source'] ) && '' !== trim( (string) $session['funding_source'] ), 'has_checkout_form' => [] !== $checkout_form, 'checkout_form_key_count' => count( $checkout_form ), 'checkout_form_summary' => $this->summarize_form_data( $checkout_form ), ]; } /** * @param array $form * @return array */ private function summarize_form_address( array $form, string $prefix ): array { $country = $this->normalize_country_code( (string) ( $form[ $prefix . 'country' ] ?? '' ) ); $state = $this->normalize_state_code( (string) ( $form[ $prefix . 'state' ] ?? '' ) ); $postcode = trim( (string) ( $form[ $prefix . 'postcode' ] ?? '' ) ); $city = trim( (string) ( $form[ $prefix . 'city' ] ?? '' ) ); $address_1 = trim( (string) ( $form[ $prefix . 'address_1' ] ?? '' ) ); $address_2 = trim( (string) ( $form[ $prefix . 'address_2' ] ?? '' ) ); $first_name = trim( (string) ( $form[ $prefix . 'first_name' ] ?? '' ) ); $last_name = trim( (string) ( $form[ $prefix . 'last_name' ] ?? '' ) ); $email = trim( (string) ( $form[ $prefix . 'email' ] ?? '' ) ); $phone = trim( (string) ( $form[ $prefix . 'phone' ] ?? '' ) ); return [ 'present' => '' !== $country || '' !== $postcode || '' !== $city || '' !== $address_1 || '' !== $address_2 || '' !== $first_name || '' !== $last_name || '' !== $email || '' !== $phone, 'country' => $country, 'has_postcode' => '' !== $postcode, 'postcode_length' => strlen( $postcode ), 'has_city' => '' !== $city, 'city_length' => strlen( $city ), 'has_state' => '' !== $state, 'state_length' => strlen( $state ), 'has_address_1' => '' !== $address_1, 'has_address_2' => '' !== $address_2, 'has_name' => '' !== $first_name || '' !== $last_name, 'has_email' => '' !== $email, 'has_phone' => '' !== $phone, ]; } /** * @param array $form * @return array */ private function summarize_form_address_relation( array $form ): array { $billing_country = $this->normalize_country_code( (string) ( $form['billing_country'] ?? '' ) ); $shipping_country = $this->normalize_country_code( (string) ( $form['shipping_country'] ?? '' ) ); $billing_state = $this->normalize_state_code( (string) ( $form['billing_state'] ?? '' ) ); $shipping_state = $this->normalize_state_code( (string) ( $form['shipping_state'] ?? '' ) ); $billing_postcode = trim( (string) ( $form['billing_postcode'] ?? '' ) ); $shipping_postcode = trim( (string) ( $form['shipping_postcode'] ?? '' ) ); return [ 'country_matches' => '' !== $billing_country && '' !== $shipping_country && $billing_country === $shipping_country, 'state_matches' => '' !== $billing_state && '' !== $shipping_state && $billing_state === $shipping_state, 'postcode_length_matches' => '' !== $billing_postcode && '' !== $shipping_postcode && strlen( $billing_postcode ) === strlen( $shipping_postcode ), 'billing_has_state' => '' !== $billing_state, 'shipping_has_state' => '' !== $shipping_state, ]; } /** * @param array $shipping_methods * @return array */ private function summarize_shipping_method_keys( array $shipping_methods ): array { $keys = []; foreach ( $shipping_methods as $method ) { if ( ! is_scalar( $method ) ) { continue; } $method_key = explode( ':', (string) $method )[0] ?? ''; $method_key = sanitize_key( $method_key ); if ( '' !== $method_key ) { $keys[] = $method_key; } } return array_slice( array_values( array_unique( $keys ) ), 0, 4 ); } /** * @return array> */ private function summarize_wc_shipping_packages(): array { if ( ! function_exists( 'WC' ) || ! WC() || ! WC()->shipping() || ! method_exists( WC()->shipping(), 'get_packages' ) ) { return []; } $packages = WC()->shipping()->get_packages(); if ( ! is_array( $packages ) ) { return []; } $summaries = []; foreach ( array_slice( $packages, 0, 4 ) as $index => $package ) { if ( ! is_array( $package ) ) { continue; } $rates = isset( $package['rates'] ) && is_array( $package['rates'] ) ? $package['rates'] : []; $destination = isset( $package['destination'] ) && is_array( $package['destination'] ) ? $package['destination'] : []; $rate_method_keys = []; foreach ( array_slice( $rates, 0, 8 ) as $rate ) { $method_id = is_object( $rate ) && method_exists( $rate, 'get_method_id' ) ? (string) $rate->get_method_id() : ''; $method_id = sanitize_key( $method_id ); if ( '' !== $method_id ) { $rate_method_keys[] = $method_id; } } $summaries[] = [ 'package_index' => (int) $index, 'rate_count' => count( $rates ), 'rate_method_keys' => array_values( array_unique( $rate_method_keys ) ), 'destination' => [ 'country' => $this->normalize_country_code( (string) ( $destination['country'] ?? '' ) ), 'has_state' => '' !== $this->normalize_state_code( (string) ( $destination['state'] ?? '' ) ), 'has_postcode' => '' !== trim( (string) ( $destination['postcode'] ?? '' ) ), 'has_city' => '' !== trim( (string) ( $destination['city'] ?? '' ) ), 'has_address_1' => '' !== trim( (string) ( $destination['address_1'] ?? '' ) ), ], ]; } return $summaries; } private function normalize_country_code( string $value ): string { $normalized = strtoupper( preg_replace( '/[^A-Z]/', '', $value ) ?: '' ); return substr( $normalized, 0, 2 ); } private function normalize_state_code( string $value ): string { $normalized = strtoupper( preg_replace( '/[^A-Z0-9_-]/', '', $value ) ?: '' ); return substr( $normalized, 0, 16 ); } /** * @param array $keys * @return array */ private function sanitize_keys( array $keys, int $limit ): array { $sanitized = []; foreach ( $keys as $key ) { $normalized = sanitize_key( (string) $key ); if ( '' === $normalized ) { continue; } $sanitized[] = $normalized; } return array_slice( array_values( array_unique( $sanitized ) ), 0, max( 1, $limit ) ); } /** * @param array $keys * @return array */ private function filter_prefixed_keys( array $keys, string $prefix ): array { return array_values( array_filter( $keys, static fn( string $key ): bool => 0 === strpos( $key, $prefix ) ) ); } }