$handler * @return mixed */ public function capture_checkout_request( $response, array $handler, WP_REST_Request $request ) { if ( ! $this->is_store_checkout_request( $request ) ) { return $response; } $trace_id = $this->get_trace_id( $request ); if ( '' === $trace_id ) { return $response; } $payment_data = $request->get_param( 'payment_data' ); $payment_method = (string) $request->get_param( 'payment_method' ); $payment_data_keys = $this->extract_payment_data_keys( $payment_data ); $express_payment_type = $this->get_payment_data_value( $payment_data, 'express_payment_type' ); CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => 'store_api_request_received', 'step' => $this->get_trace_step( $request ), 'status' => 'info', 'origin' => 'backend', 'gatewayId' => $payment_method, 'context' => [ 'route' => $request->get_route(), 'request_method' => $request->get_method(), 'payment_data_keys' => $payment_data_keys, 'payment_data_count' => count( $payment_data_keys ), 'has_card_token' => $this->has_payment_data_key( $payment_data, 'cardToken' ) || $this->has_payment_data_key( $payment_data, 'cardtoken' ), 'has_payment_method' => $this->has_payment_data_key( $payment_data, 'payment_method' ), 'has_selected_upe_payment_type' => $this->has_payment_data_key( $payment_data, 'selected_upe_payment_type' ) || $this->has_payment_data_key( $payment_data, 'wc_stripe_selected_upe_payment_type' ), 'has_deferred_intent' => $this->has_payment_data_key( $payment_data, 'wc-stripe-is-deferred-intent' ), 'has_payment_method_title' => $this->has_payment_data_key( $payment_data, 'payment_method_title' ), 'has_wcpay_payment_method' => $this->has_payment_data_key( $payment_data, 'wcpay-payment-method' ), 'has_express_payment_type' => '' !== $express_payment_type, 'express_payment_type' => $express_payment_type, 'has_wcpay_express_payment_method_types' => $this->has_payment_data_key( $payment_data, 'wcpay-express-payment-method-types' ), 'has_wcpay_fraud_prevention_token' => $this->has_payment_data_key( $payment_data, 'wcpay-fraud-prevention-token' ), 'has_wcpay_fingerprint' => $this->has_payment_data_key( $payment_data, 'wcpay-fingerprint' ), 'request_address_summary' => $this->summarize_request_addresses( $request ), ], ] ); return $response; } public function capture_payment_context( PaymentContext $payment_context, $payment_result ): void { $trace_id = $this->get_trace_id(); if ( '' === $trace_id ) { return; } $payment_data = is_array( $payment_context->payment_data ?? null ) ? $payment_context->payment_data : []; $result_status = ''; $express_payment_type = sanitize_key( (string) ( $payment_data['express_payment_type'] ?? '' ) ); if ( is_object( $payment_result ) && isset( $payment_result->status ) ) { $result_status = (string) $payment_result->status; } CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => 'store_api_payment_context', 'step' => 'review', 'status' => 'start', 'origin' => 'backend', 'gatewayId' => (string) ( $payment_context->payment_method ?? '' ), 'context' => [ 'payment_data_keys' => array_keys( $payment_data ), 'payment_data_count' => count( $payment_data ), 'has_card_token' => isset( $payment_data['cardToken'] ) || isset( $payment_data['cardtoken'] ), 'has_payment_method' => isset( $payment_data['payment_method'] ), 'has_selected_upe_payment_type' => isset( $payment_data['selected_upe_payment_type'] ) || isset( $payment_data['wc_stripe_selected_upe_payment_type'] ), 'has_deferred_intent' => isset( $payment_data['wc-stripe-is-deferred-intent'] ), 'has_payment_method_title' => isset( $payment_data['payment_method_title'] ), 'has_components_ready' => isset( $payment_data['componentsReady'] ) || isset( $payment_data['components_ready'] ), 'has_token_created' => isset( $payment_data['tokenCreated'] ) || isset( $payment_data['token_created'] ), 'has_wcpay_payment_method' => isset( $payment_data['wcpay-payment-method'] ), 'has_express_payment_type' => '' !== $express_payment_type, 'express_payment_type' => $express_payment_type, 'has_wcpay_express_payment_method_types' => isset( $payment_data['wcpay-express-payment-method-types'] ), 'has_wcpay_fraud_prevention_token' => isset( $payment_data['wcpay-fraud-prevention-token'] ), 'has_wcpay_fingerprint' => isset( $payment_data['wcpay-fingerprint'] ), 'result_status_pre_legacy' => $result_status, ], ] ); } /** * @param mixed $response * @param array $handler * @return mixed */ public function capture_checkout_response( $response, array $handler, WP_REST_Request $request ) { if ( ! $this->is_store_checkout_request( $request ) ) { return $response; } $trace_id = $this->get_trace_id( $request ); if ( '' === $trace_id ) { return $response; } $gateway_id = (string) $request->get_param( 'payment_method' ); $summary = $this->summarize_store_api_response( $response, $request ); $status_code = (int) $summary['status_code']; $message = (string) $summary['message']; $response_code = (string) $summary['response_code']; $context = is_array( $summary['context'] ?? null ) ? $summary['context'] : [ 'route' => $request->get_route() ]; if ( $status_code >= 400 ) { $response = $this->maybe_enrich_payment_failure_response( $response, $gateway_id ); $summary = $this->summarize_store_api_response( $response, $request ); $status_code = (int) $summary['status_code']; $message = (string) $summary['message']; $response_code = (string) $summary['response_code']; $context = is_array( $summary['context'] ?? null ) ? $summary['context'] : $context; } CheckoutTraceService::record( [ 'traceId' => $trace_id, 'event' => $status_code >= 400 ? 'store_api_request_failed' : 'store_api_request_completed', 'step' => $this->get_trace_step( $request ), 'status' => $status_code >= 400 ? 'error' : 'success', 'origin' => 'backend', 'gatewayId' => $gateway_id, 'message' => '' !== $message ? $message : ( '' !== $response_code ? $response_code : '' ), 'context' => array_merge( $context, [ 'request_address_summary' => $this->summarize_request_addresses( $request ), 'status_code' => $status_code, 'response_code' => $response_code, ] ), ] ); return $response; } /** * @param mixed $response * @return array */ private function summarize_store_api_response( $response, WP_REST_Request $request ): array { $summary = [ 'status_code' => 200, 'message' => '', 'response_code' => '', 'context' => [ 'route' => $request->get_route(), ], ]; if ( is_wp_error( $response ) ) { $summary['status_code'] = 400; $summary['response_code'] = (string) $response->get_error_code(); $summary['message'] = (string) $response->get_error_message(); $summary['context']['error_data'] = $this->sanitize_error_data( $response->get_error_data() ); return $summary; } if ( $response instanceof WP_REST_Response ) { $summary['status_code'] = $response->get_status(); $data = $response->get_data(); $summary['context'] = array_merge( $summary['context'], $this->summarize_response_data( $data ) ); if ( is_array( $data ) ) { $summary['message'] = (string) ( $data['message'] ?? '' ); $summary['response_code'] = (string) ( $data['code'] ?? '' ); } } return $summary; } /** * @param mixed $response * @return mixed */ private function maybe_enrich_payment_failure_response( $response, string $gateway_id ) { $gateway_id = sanitize_key( $gateway_id ); if ( '' === $gateway_id ) { return $response; } $failure_summary = CheckoutMollieFailureProbe::instance()->get_failure_summary( $gateway_id ); $friendly_message = CheckoutPaymentFailureMessageBuilder::build( $failure_summary ); if ( '' === $friendly_message ) { return $response; } if ( is_wp_error( $response ) ) { $error_code = (string) $response->get_error_code(); $error_data = $response->get_error_data( $error_code ); return new WP_Error( '' !== $error_code ? $error_code : 'ips_checkout_payment_failed', $friendly_message, $error_data ); } if ( $response instanceof WP_REST_Response ) { $data = $response->get_data(); $payload = is_array( $data ) ? $data : []; $payload['message'] = $friendly_message; return new WP_REST_Response( $payload, $response->get_status() ); } return $response; } private function is_store_checkout_request( WP_REST_Request $request ): bool { if ( 'POST' !== strtoupper( $request->get_method() ) ) { return false; } return in_array( $request->get_route(), [ '/wc/store/v1/checkout', '/wc/store/v1/checkout-order', '/wc/store/v1/cart/update-customer', '/wc/store/v1/cart/select-shipping-rate', ], true ); } private function get_trace_step( WP_REST_Request $request ): string { return in_array( $request->get_route(), [ '/wc/store/v1/cart/update-customer', '/wc/store/v1/cart/select-shipping-rate', ], true ) ? 'contact' : 'review'; } private function get_trace_id( ?WP_REST_Request $request = null ): string { $trace_id = ''; if ( $request instanceof WP_REST_Request ) { $trace_id = (string) $request->get_header( 'X-IPS-Checkout-Trace' ); } if ( '' === $trace_id ) { $trace_id = isset( $_SERVER['HTTP_X_IPS_CHECKOUT_TRACE'] ) ? sanitize_key( wp_unslash( $_SERVER['HTTP_X_IPS_CHECKOUT_TRACE'] ) ) : ''; } return sanitize_key( $trace_id ); } /** * @param mixed $payment_data * @return array */ private function extract_payment_data_keys( $payment_data ): array { if ( ! is_array( $payment_data ) ) { return []; } $keys = []; foreach ( array_slice( $payment_data, 0, 20 ) as $entry ) { if ( ! is_array( $entry ) ) { continue; } $key = sanitize_key( (string) ( $entry['key'] ?? '' ) ); if ( '' === $key ) { continue; } $keys[] = $key; } return array_values( array_unique( $keys ) ); } /** * @param mixed $payment_data */ private function has_payment_data_key( $payment_data, string $needle ): bool { return in_array( sanitize_key( $needle ), $this->extract_payment_data_keys( $payment_data ), true ); } /** * @param mixed $payment_data */ private function get_payment_data_value( $payment_data, string $needle ): string { if ( ! is_array( $payment_data ) ) { return ''; } $needle = sanitize_key( $needle ); foreach ( array_slice( $payment_data, 0, 40 ) as $entry ) { if ( ! is_array( $entry ) ) { continue; } if ( sanitize_key( (string) ( $entry['key'] ?? '' ) ) !== $needle ) { continue; } return sanitize_key( (string) ( $entry['value'] ?? '' ) ); } return ''; } /** * @return array> */ private function summarize_request_addresses( WP_REST_Request $request ): array { return [ 'billing_summary' => $this->summarize_store_api_address( $request->get_param( 'billing_address' ) ), 'shipping_summary' => $this->summarize_store_api_address( $request->get_param( 'shipping_address' ) ), ]; } private function is_placeholder_last_name( string $value ): bool { $value = strtolower( trim( $value ) ); $decoded = html_entity_decode( $value, ENT_QUOTES, 'UTF-8' ); return in_array( $value, [ '-', '–', '–', '–', '—', '—', '—' ], true ) || in_array( $decoded, [ '-', html_entity_decode( '–', ENT_QUOTES, 'UTF-8' ), html_entity_decode( '—', ENT_QUOTES, 'UTF-8' ) ], true ); } /** * @param mixed $address * @return array */ private function summarize_store_api_address( $address ): array { if ( ! is_array( $address ) ) { return [ 'available' => false, ]; } $address_1 = trim( (string) ( $address['address_1'] ?? '' ) ); $address_2 = trim( (string) ( $address['address_2'] ?? '' ) ); $first_name = trim( (string) ( $address['first_name'] ?? '' ) ); $last_name = trim( (string) ( $address['last_name'] ?? '' ) ); $city = trim( (string) ( $address['city'] ?? '' ) ); $postcode = trim( (string) ( $address['postcode'] ?? '' ) ); $state = trim( (string) ( $address['state'] ?? '' ) ); $country = strtoupper( substr( sanitize_text_field( (string) ( $address['country'] ?? '' ) ), 0, 2 ) ); $last_name_is_placeholder = $this->is_placeholder_last_name( $last_name ); $has_last_name = '' !== $last_name && ! $last_name_is_placeholder; $missing_required_fields = array_values( array_filter( [ '' === $first_name ? 'first_name' : '', ! $has_last_name ? 'last_name' : '', '' === $address_1 ? 'address_1' : '', '' === $city ? 'city' : '', '' === $postcode ? 'postcode' : '', '' === $country ? 'country' : '', ] ) ); return [ 'available' => true, 'country_code' => $country, 'has_first_name' => '' !== $first_name, 'has_last_name' => $has_last_name, 'has_placeholder_last_name' => $last_name_is_placeholder, 'last_name_kind' => $last_name_is_placeholder ? 'placeholder' : ( $has_last_name ? 'real' : 'empty' ), 'has_address_1' => '' !== $address_1, 'has_address_2' => '' !== $address_2, 'has_city' => '' !== $city, 'has_postcode' => '' !== $postcode, 'has_state' => '' !== $state, 'address_1_missing_with_address_2' => '' === $address_1 && '' !== $address_2, 'required_street_missing' => '' === $address_1, 'missing_required_fields' => $missing_required_fields, ]; } /** * @param mixed $data * @return array */ private function summarize_response_data( $data ): array { if ( ! is_array( $data ) ) { return []; } $payment_result = is_array( $data['payment_result'] ?? null ) ? $data['payment_result'] : []; $payment_details = []; if ( isset( $payment_result['payment_details'] ) && is_array( $payment_result['payment_details'] ) ) { foreach ( array_slice( $payment_result['payment_details'], 0, 20 ) as $entry ) { if ( ! is_array( $entry ) ) { continue; } $detail_key = sanitize_key( (string) ( $entry['key'] ?? '' ) ); if ( '' === $detail_key ) { continue; } $payment_details[ $detail_key ] = $entry['value'] ?? ''; } } return [ 'response_keys' => array_slice( array_map( 'sanitize_key', array_keys( $data ) ), 0, 10 ), 'response_error_data_keys' => isset( $data['data'] ) && is_array( $data['data'] ) ? array_slice( array_map( 'sanitize_key', array_keys( $data['data'] ) ), 0, 12 ) : [], 'response_error_status' => isset( $data['data'] ) && is_array( $data['data'] ) ? absint( (int) ( $data['data']['status'] ?? 0 ) ) : 0, 'has_payment_result' => ! empty( $payment_result ), 'payment_result_keys' => array_slice( array_map( 'sanitize_key', array_keys( $payment_result ) ), 0, 10 ), 'payment_result_status' => sanitize_key( (string) ( $payment_result['payment_status'] ?? '' ) ), 'has_redirect' => ! empty( $payment_result['redirect_url'] ), 'redirect_url' => $this->summarize_redirect_value( (string) ( $payment_result['redirect_url'] ?? '' ) ), 'redirect_is_hash_only' => '#' === trim( (string) ( $payment_result['redirect_url'] ?? '' ) ), 'payment_result_detail_keys' => array_slice( array_keys( $payment_details ), 0, 20 ), 'has_amazon_create_checkout_params' => isset( $payment_details['amazoncreatecheckoutparams'] ) && '' !== (string) $payment_details['amazoncreatecheckoutparams'], 'has_amazon_estimated_order_amount' => isset( $payment_details['amazonestimatedorderamount'] ) && '' !== (string) $payment_details['amazonestimatedorderamount'], 'has_verification_endpoint' => isset( $payment_details['verification_endpoint'] ) && '' !== (string) $payment_details['verification_endpoint'], 'has_payment_intent_secret' => isset( $payment_details['payment_intent_secret'] ) && '' !== (string) $payment_details['payment_intent_secret'], 'has_setup_intent_secret' => isset( $payment_details['setup_intent_secret'] ) && '' !== (string) $payment_details['setup_intent_secret'], ]; } private function summarize_redirect_value( string $redirect_url ): string { $redirect_url = trim( $redirect_url ); if ( '' === $redirect_url ) { return ''; } if ( '#' === $redirect_url ) { return '#'; } $parts = wp_parse_url( $redirect_url ); if ( ! is_array( $parts ) ) { return substr( sanitize_text_field( $redirect_url ), 0, 200 ); } $host = sanitize_text_field( (string) ( $parts['host'] ?? '' ) ); $path = sanitize_text_field( (string) ( $parts['path'] ?? '' ) ); $query = isset( $parts['query'] ) && '' !== (string) $parts['query'] ? '?…' : ''; $fragment = isset( $parts['fragment'] ) && '' !== (string) $parts['fragment'] ? '#…' : ''; return substr( $host . $path . $query . $fragment, 0, 200 ); } /** * @param mixed $data * @return array */ private function sanitize_error_data( $data ): array { if ( ! is_array( $data ) ) { return []; } $sanitized = []; foreach ( array_slice( $data, 0, 8, true ) as $key => $value ) { $normalized_key = sanitize_key( (string) $key ); if ( '' === $normalized_key ) { continue; } if ( is_scalar( $value ) || null === $value ) { $sanitized[ $normalized_key ] = is_string( $value ) ? sanitize_text_field( $value ) : $value; continue; } if ( is_array( $value ) ) { $sanitized[ $normalized_key ] = array_map( static function ( $item ) { if ( is_string( $item ) ) { return sanitize_text_field( $item ); } if ( is_scalar( $item ) || null === $item ) { return $item; } return ''; }, array_slice( $value, 0, 8 ) ); } } return $sanitized; } }