Changeset 3462451
- Timestamp:
- 02/16/2026 11:43:16 AM (7 weeks ago)
- Location:
- payment-gateway-for-authorize-net-for-woocommerce
- Files:
-
- 61 added
- 6 edited
-
tags/1.0.8 (added)
-
tags/1.0.8/LICENSE.txt (added)
-
tags/1.0.8/assets (added)
-
tags/1.0.8/assets/css (added)
-
tags/1.0.8/assets/css/admin.css (added)
-
tags/1.0.8/assets/css/credit-cards (added)
-
tags/1.0.8/assets/css/credit-cards/amex.svg (added)
-
tags/1.0.8/assets/css/credit-cards/diners.svg (added)
-
tags/1.0.8/assets/css/credit-cards/discover.svg (added)
-
tags/1.0.8/assets/css/credit-cards/elo.svg (added)
-
tags/1.0.8/assets/css/credit-cards/hiper.svg (added)
-
tags/1.0.8/assets/css/credit-cards/jcb.svg (added)
-
tags/1.0.8/assets/css/credit-cards/maestro.svg (added)
-
tags/1.0.8/assets/css/credit-cards/mastercard.svg (added)
-
tags/1.0.8/assets/css/credit-cards/visa.svg (added)
-
tags/1.0.8/assets/css/public.css (added)
-
tags/1.0.8/assets/images (added)
-
tags/1.0.8/assets/images/brands (added)
-
tags/1.0.8/assets/images/brands/google-pay.svg (added)
-
tags/1.0.8/assets/images/check-routing-account.png (added)
-
tags/1.0.8/assets/js (added)
-
tags/1.0.8/assets/js/acceptjs-echeck-handler.js (added)
-
tags/1.0.8/assets/js/acceptjs-handler.js (added)
-
tags/1.0.8/assets/js/blocks (added)
-
tags/1.0.8/assets/js/blocks-authorizenet.js (added)
-
tags/1.0.8/assets/js/blocks/authorizenet-card.js (added)
-
tags/1.0.8/assets/js/blocks/authorizenet-echeck.js (added)
-
tags/1.0.8/assets/js/blocks/authorizenet-googlepay.js (added)
-
tags/1.0.8/assets/js/blocks/blocks-common.js (added)
-
tags/1.0.8/assets/js/easyauthnet-authorizenet-admin.js (added)
-
tags/1.0.8/assets/js/easyauthnet-review-ajax.js (added)
-
tags/1.0.8/assets/js/googlepay-express.js (added)
-
tags/1.0.8/assets/js/googlepay-handler.js (added)
-
tags/1.0.8/feedback (added)
-
tags/1.0.8/feedback/css (added)
-
tags/1.0.8/feedback/css/deactivation-feedback-modal.css (added)
-
tags/1.0.8/feedback/deactivation-feedback-form.php (added)
-
tags/1.0.8/feedback/fonts (added)
-
tags/1.0.8/feedback/fonts/icomoon.eot (added)
-
tags/1.0.8/feedback/fonts/icomoon.svg (added)
-
tags/1.0.8/feedback/fonts/icomoon.ttf (added)
-
tags/1.0.8/feedback/fonts/icomoon.woff (added)
-
tags/1.0.8/feedback/js (added)
-
tags/1.0.8/feedback/js/deactivation-feedback-modal.js (added)
-
tags/1.0.8/includes (added)
-
tags/1.0.8/includes/class-api-handler.php (added)
-
tags/1.0.8/includes/class-easy-payment-authorizenet-echeck-gateway.php (added)
-
tags/1.0.8/includes/class-easy-payment-authorizenet-gateway.php (added)
-
tags/1.0.8/includes/class-easy-payment-authorizenet-googlepay-gateway.php (added)
-
tags/1.0.8/includes/class-webhook-handler.php (added)
-
tags/1.0.8/includes/compatibility (added)
-
tags/1.0.8/includes/compatibility/class-block-support.php (added)
-
tags/1.0.8/includes/compatibility/class-easyauthnet-subscription-helper.php (added)
-
tags/1.0.8/includes/compatibility/class-preorders-compat.php (added)
-
tags/1.0.8/languages (added)
-
tags/1.0.8/languages/easyauthnet-payment-authorizenet.pot (added)
-
tags/1.0.8/languages/payment-gateway-for-authorize-net-for-woocommerce.pot (added)
-
tags/1.0.8/payment-gateway-for-authorizenet-for-woocommerce-admin.php (added)
-
tags/1.0.8/payment-gateway-for-authorizenet-for-woocommerce.php (added)
-
tags/1.0.8/readme.txt (added)
-
tags/1.0.8/uninstall.php (added)
-
trunk/includes/class-api-handler.php (modified) (11 diffs)
-
trunk/includes/class-easy-payment-authorizenet-echeck-gateway.php (modified) (3 diffs)
-
trunk/includes/class-easy-payment-authorizenet-gateway.php (modified) (19 diffs)
-
trunk/includes/class-easy-payment-authorizenet-googlepay-gateway.php (modified) (4 diffs)
-
trunk/payment-gateway-for-authorizenet-for-woocommerce.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-api-handler.php
r3440293 r3462451 258 258 ]); 259 259 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 260 returnthrow new Exception($message);260 throw new Exception($message); 261 261 } 262 262 if (isset($response['transactionResponse']['responseCode']) && (string) $response['transactionResponse']['responseCode'] !== '1') { … … 313 313 self::log(__METHOD__, 'Transaction failed', [ 314 314 'response_code' => $response['transactionResponse']['responseCode'], 315 'response_code' => (string) $response['transactionResponse']['responseCode'],316 315 'error_code' => $error_code, 317 316 'error_message' => $message, … … 432 431 self::log(__METHOD__, $error_msg, ['order_id' => $order->get_id()]); 433 432 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 434 returnthrow new Exception($error_msg);433 throw new Exception($error_msg); 435 434 } 436 435 $endpoint = self::get_api_endpoint(); … … 518 517 if (!$trans_id) { 519 518 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 520 returnthrow new Exception('easyauthnet_no_trans_id', __('Missing transaction ID.', 'payment-gateway-for-authorize-net-for-woocommerce'));519 throw new Exception('easyauthnet_no_trans_id', __('Missing transaction ID.', 'payment-gateway-for-authorize-net-for-woocommerce')); 521 520 } 522 521 self::log(__METHOD__, 'Checking whether to refund or void', ['order_id' => $order->get_id(), 'transaction_id' => $trans_id, 'amount' => $amount, 'reason' => $reason]); … … 537 536 if (!$order) { 538 537 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 539 returnthrow new Exception(__('Invalid order.', 'payment-gateway-for-authorize-net-for-woocommerce'));538 throw new Exception(__('Invalid order.', 'payment-gateway-for-authorize-net-for-woocommerce')); 540 539 } 541 540 self::log(__METHOD__, 'Processing payment refund/void', ['order_id' => $order->get_id(), 'amount' => $amount, 'reason' => $reason]); … … 557 556 if (!$trans_id) { 558 557 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 559 returnthrow new Exception(__('Missing transaction ID.', 'payment-gateway-for-authorize-net-for-woocommerce'));558 throw new Exception(__('Missing transaction ID.', 'payment-gateway-for-authorize-net-for-woocommerce')); 560 559 } 561 560 $endpoint = self::get_api_endpoint(); … … 951 950 self::log(__METHOD__, $error, ['has_customer_profile' => (bool) $customer_profile_id, 'has_payment_profile' => (bool) $payment_profile_id]); 952 951 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 953 returnthrow new Exception($error);952 throw new Exception($error); 954 953 } 955 954 $request = [ … … 1133 1132 self::log(__METHOD__, $error); 1134 1133 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 1135 returnthrow new Exception($error);1134 throw new Exception($error); 1136 1135 } 1137 1136 $customer_profile_id = get_user_meta($user_id, self::$customer_profile_id, true); … … 1156 1155 self::log(__METHOD__, $error, ['response' => $result]); 1157 1156 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 1158 returnthrow new Exception($error);1157 throw new Exception($error); 1159 1158 } 1160 1159 … … 1207 1206 self::log(__METHOD__, $error, ['user_id' => $user_id]); 1208 1207 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 1209 returnthrow new Exception($error);1208 throw new Exception($error); 1210 1209 } 1211 1210 $customer_profile_id = get_user_meta($user_id, self::$customer_profile_id, true); … … 1365 1364 self::log(__METHOD__, 'Capture failed', ['response_code' => $response['transactionResponse']['responseCode'] ?? 'none', 'error' => $error]); 1366 1365 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 1367 returnthrow new Exception($error);1366 throw new Exception($error); 1368 1367 } 1369 1368 self::log(__METHOD__, 'Successfully captured transaction', ['transaction_id' => $response['transactionResponse']['transId'] ?? $transaction_id, 'auth_code' => $response['transactionResponse']['authCode'] ?? '']); -
payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-easy-payment-authorizenet-echeck-gateway.php
r3440293 r3462451 306 306 if (is_checkout() || is_checkout_pay_page() || is_account_page()) { 307 307 $merchant_data = EASYAUTHNET_AuthorizeNet_API_Handler::fetch_merchant_details( 308 $this->api_login_id,309 $this->transaction_key,310 $this->signature_key,311 $this->environment !== 'live'308 $this->api_login_id, 309 $this->transaction_key, 310 $this->signature_key, 311 $this->environment !== 'live' 312 312 ); 313 313 if (!is_wp_error($merchant_data) && isset($merchant_data['publicClientKey'])) { … … 448 448 } 449 449 450 protected function is_store_api_request(): bool { 451 if (defined('REST_REQUEST') && REST_REQUEST) { 452 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 453 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : ''; 454 return ($uri && strpos($uri, '/wc/store/') !== false); 455 } 456 return false; 457 } 458 459 protected function maybe_mark_order_failed(WC_Order $order, string $message): void { 460 if ($order->has_status(['processing', 'completed', 'cancelled', 'refunded'])) { 461 return; 462 } 463 $note = sprintf( 464 __('Authorize.Net payment failed: %s', 'payment-gateway-for-authorize-net-for-woocommerce'), 465 wp_strip_all_tags($message) 466 ); 467 if (!$order->has_status('failed')) { 468 $order->update_status('failed', $note); 469 } else { 470 $order->add_order_note($note); 471 } 472 $order->save(); 473 } 474 475 protected function fail_with_error(WC_Order $order, $error) { 476 $message = is_wp_error($error) ? (string) $error->get_error_message() : (string) $error; 477 $message = wp_strip_all_tags($message); 478 $this->maybe_mark_order_failed($order, $message); 479 if ($this->is_store_api_request()) { 480 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 481 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $message, 400); 482 } 483 wc_add_notice($message, 'error'); 484 return ['result' => 'failure']; 485 } 486 487 protected function handle_payment_exception(WC_Order $order, Exception $e) { 488 $message = wp_strip_all_tags((string) $e->getMessage()); 489 $this->maybe_mark_order_failed($order, $message); 490 if ($this->is_store_api_request()) { 491 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 492 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $message, 400); 493 } 494 wc_add_notice($message, 'error'); 495 return ['result' => 'failure']; 496 } 497 450 498 public function process_payment($order_id) { 451 499 $order = wc_get_order($order_id); … … 454 502 } 455 503 456 // eCheck (ACH) is available for USD only. 457 $allowed_currency = strtoupper((string) apply_filters('easyauthnet_authorizenet_echeck_allowed_currency', 'USD')); 458 if (strtoupper((string) $order->get_currency()) !== $allowed_currency) { 459 wc_add_notice( 504 try { 505 // eCheck (ACH) is available for USD only. 506 $allowed_currency = strtoupper((string) apply_filters('easyauthnet_authorizenet_echeck_allowed_currency', 'USD')); 507 if (strtoupper((string) $order->get_currency()) !== $allowed_currency) { 508 return $this->fail_with_error( 509 $order, 460 510 sprintf( 461 // translators: %s is a currency code like USD. 462 __('eCheck (ACH) is available for %s only.', 'payment-gateway-for-authorize-net-for-woocommerce'), 463 esc_html($allowed_currency) 464 ), 465 'error' 466 ); 467 return ['result' => 'failure']; 468 } 469 470 // phpcs:ignore WordPress.Security.NonceVerification.Missing 471 $token = isset($_POST['easyauthnet_authorizenet_echeck_token']) ? sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_echeck_token'])) : ''; 472 if (preg_match('/^"(.+)"$/', $token, $m)) { 473 $token = $m[1]; 474 } 475 476 // phpcs:ignore WordPress.Security.NonceVerification.Missing 477 $posted_descriptor = isset($_POST['easyauthnet_authorizenet_echeck_descriptor']) ? sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_echeck_descriptor'])) : ''; 478 if (preg_match('/^"(.+)"$/', $posted_descriptor, $m2)) { 479 $posted_descriptor = $m2[1]; 480 } 481 482 if (empty($token)) { 483 wc_add_notice(__('Payment error: unable to tokenize bank details. Please try again.', 'payment-gateway-for-authorize-net-for-woocommerce'), 'error'); 484 return ['result' => 'failure']; 485 } 486 487 EASYAUTHNET_AuthorizeNet_API_Handler::init(); 488 489 // Accept.js should return COMMON.ACCEPT.INAPP.CHECK for eCheck tokens. 490 // However, some merchant setups can return COMMON.ACCEPT.INAPP.PAYMENT even for bank tokenization. 491 // To avoid Authorize.Net error 153 (Unable to decrypt data), we forward the descriptor returned by Accept.js 492 // after validating it against a strict allowlist. 493 $allowed_descriptors = [ 494 'COMMON.ACCEPT.INAPP.CHECK', 495 'COMMON.ACCEPT.INAPP.PAYMENT', 496 ]; 497 $descriptor = 'COMMON.ACCEPT.INAPP.CHECK'; 498 if ($posted_descriptor && in_array($posted_descriptor, $allowed_descriptors, true)) { 499 $descriptor = $posted_descriptor; 500 } 501 502 $opaque_token = [ 503 'dataDescriptor' => $descriptor, 504 'dataValue' => $token, 505 ]; 506 507 $this->log('Processing eCheck payment', [ 508 'order_id' => $order->get_id(), 509 'total' => $order->get_total(), 510 'environment' => $this->environment, 511 ]); 512 513 $response = EASYAUTHNET_AuthorizeNet_API_Handler::charge_transaction($order, $opaque_token, [ 514 'transaction_type' => $this->echeck_transaction_type, 515 'statement_descriptor' => $this->statement_descriptor, 516 ]); 517 if (is_wp_error($response)) { 518 $this->log('eCheck charge failed', [ 519 'error_code' => $response->get_error_code(), 520 'error_message' => $response->get_error_message(), 511 /* translators: %s is a currency code like USD. */ 512 __('eCheck (ACH) is available for %s only.', 'payment-gateway-for-authorize-net-for-woocommerce'), 513 esc_html($allowed_currency) 514 ) 515 ); 516 517 } 518 519 // phpcs:ignore WordPress.Security.NonceVerification.Missing 520 $token = isset($_POST['easyauthnet_authorizenet_echeck_token']) ? sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_echeck_token'])) : ''; 521 if (preg_match('/^"(.+)"$/', $token, $m)) { 522 $token = $m[1]; 523 } 524 525 // phpcs:ignore WordPress.Security.NonceVerification.Missing 526 $posted_descriptor = isset($_POST['easyauthnet_authorizenet_echeck_descriptor']) ? sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_echeck_descriptor'])) : ''; 527 if (preg_match('/^"(.+)"$/', $posted_descriptor, $m2)) { 528 $posted_descriptor = $m2[1]; 529 } 530 531 if (empty($token)) { 532 return $this->fail_with_error($order, __('Payment error: unable to tokenize bank details. Please try again.', 'payment-gateway-for-authorize-net-for-woocommerce')); 533 } 534 535 EASYAUTHNET_AuthorizeNet_API_Handler::init(); 536 537 // Accept.js should return COMMON.ACCEPT.INAPP.CHECK for eCheck tokens. 538 // However, some merchant setups can return COMMON.ACCEPT.INAPP.PAYMENT even for bank tokenization. 539 // To avoid Authorize.Net error 153 (Unable to decrypt data), we forward the descriptor returned by Accept.js 540 // after validating it against a strict allowlist. 541 $allowed_descriptors = [ 542 'COMMON.ACCEPT.INAPP.CHECK', 543 'COMMON.ACCEPT.INAPP.PAYMENT', 544 ]; 545 $descriptor = 'COMMON.ACCEPT.INAPP.CHECK'; 546 if ($posted_descriptor && in_array($posted_descriptor, $allowed_descriptors, true)) { 547 $descriptor = $posted_descriptor; 548 } 549 550 $opaque_token = [ 551 'dataDescriptor' => $descriptor, 552 'dataValue' => $token, 553 ]; 554 555 $this->log('Processing eCheck payment', [ 556 'order_id' => $order->get_id(), 557 'total' => $order->get_total(), 558 'environment' => $this->environment, 521 559 ]); 522 wc_add_notice($response->get_error_message(), 'error'); 523 return ['result' => 'failure']; 524 } 525 526 $transaction_id = $response['transaction_id'] ?? ''; 527 if (!empty($transaction_id)) { 528 $order->update_meta_data('_easyauthnet_authorizenet_echeck_transaction_id', $transaction_id); 529 $order->save(); 530 } 531 532 // For eCheck, settlement can be delayed. Keep it on the configured status (default: on-hold). 533 if ($order->has_status(['pending', 'failed'])) { 534 $status = str_replace('wc-', '', (string) $this->echeck_order_status); 535 if ($status === '') { 536 $status = 'on-hold'; 537 } 538 $order->update_status($status, __('eCheck payment initiated. Awaiting settlement/confirmation.', 'payment-gateway-for-authorize-net-for-woocommerce')); 539 } 540 541 WC()->cart->empty_cart(); 542 return [ 543 'result' => 'success', 544 'redirect' => $this->get_return_url($order), 545 ]; 560 561 $response = EASYAUTHNET_AuthorizeNet_API_Handler::charge_transaction($order, $opaque_token, [ 562 'transaction_type' => $this->echeck_transaction_type, 563 'statement_descriptor' => $this->statement_descriptor, 564 ]); 565 if (is_wp_error($response)) { 566 $this->log('eCheck charge failed', [ 567 'error_code' => $response->get_error_code(), 568 'error_message' => $response->get_error_message(), 569 ]); 570 return $this->fail_with_error($order, $response); 571 } 572 if (!is_array($response)) { 573 return $this->fail_with_error($order, __('Transaction failed.', 'payment-gateway-for-authorize-net-for-woocommerce')); 574 } 575 $transaction_id = $response['transaction_id'] ?? ''; 576 if (!empty($transaction_id)) { 577 $order->update_meta_data('_easyauthnet_authorizenet_echeck_transaction_id', $transaction_id); 578 $order->save(); 579 } 580 // For eCheck, settlement can be delayed. Keep it on the configured status (default: on-hold). 581 if ($order->has_status(['pending', 'failed'])) { 582 $status = str_replace('wc-', '', (string) $this->echeck_order_status); 583 if ($status === '') { 584 $status = 'on-hold'; 585 } 586 $order->update_status($status, __('eCheck payment initiated. Awaiting settlement/confirmation.', 'payment-gateway-for-authorize-net-for-woocommerce')); 587 } 588 589 if (function_exists('WC') && WC()->cart) { 590 WC()->cart->empty_cart(); 591 } 592 return [ 593 'result' => 'success', 594 'redirect' => $this->get_return_url($order), 595 ]; 596 } catch (Exception $ex) { 597 if ($ex instanceof \Automattic\WooCommerce\StoreApi\Exceptions\RouteException) { 598 throw $ex; 599 } 600 return $this->handle_payment_exception($order, $ex); 601 } 546 602 } 547 603 -
payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-easy-payment-authorizenet-gateway.php
r3440293 r3462451 881 881 public function process_payment($order_id) { 882 882 $order = wc_get_order($order_id); 883 884 if (! $order) { 885 if ($this->is_store_api_request()) { 886 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', __('Invalid order.', 'payment-gateway-for-authorize-net-for-woocommerce'), 400); 887 } 888 wc_add_notice(__('Invalid order.', 'payment-gateway-for-authorize-net-for-woocommerce'), 'error'); 889 return ['result' => 'failure']; 890 } 891 883 892 try { 884 893 $this->log_start_payment($order); … … 898 907 return $this->process_with_new_card($order); 899 908 } catch (Exception $ex) { 900 return $this->handle_payment_exception($ex); 909 if ($ex instanceof \Automattic\WooCommerce\StoreApi\Exceptions\RouteException) { 910 throw $ex; 911 } 912 return $this->handle_payment_exception($order, $ex); 901 913 } 902 914 } … … 980 992 if (is_wp_error($response)) { 981 993 $this->log("Failed to charge saved token", ['error_code' => $response->get_error_code(), 'error_message' => $response->get_error_message()]); 982 return $this->fail_with_error($ response);994 return $this->fail_with_error($order, $response); 983 995 } 984 996 $this->log("Successfully charged saved token", ['transaction_id' => $response['transaction_id'] ?? 'N/A', 'response_code' => $response['response_code'] ?? null, 'auth_code' => $response['auth_code'] ?? null]); … … 1067 1079 $is_subscription = $this->is_subscription_order($order); 1068 1080 1069 // Default logic: user opted in OR it's a subscription 1081 // Default logic: save only when explicitly required. 1082 // - User opted in via the "Save payment method" checkbox, OR 1083 // - Subscription order (requires a reusable token for renewals). 1070 1084 $should_save = $user_opted_in || $is_subscription; 1071 1085 … … 1077 1091 * @param EASYAUTHNET_AuthorizeNet_Gateway $gateway Gateway instance. 1078 1092 */ 1079 $ should_save = (bool) apply_filters(1093 $filtered_should_save = (bool) apply_filters( 1080 1094 'easyauthnet_authorizenet_should_save_card', 1081 1095 $should_save, … … 1084 1098 ); 1085 1099 1100 // Enterprise-safe guardrail: 1101 // Never save a card for a non-subscription order unless the customer opted in. 1102 // This avoids consuming the one-time opaque token on CIM operations for typical one-time checkouts. 1103 if (!$user_opted_in && !$is_subscription) { 1104 $should_save = false; 1105 } else { 1106 $should_save = $filtered_should_save; 1107 } 1108 1086 1109 // Log debug values safely 1087 1110 $this->log('Checking if should save card', [ … … 1089 1112 'user_opted_in' => $user_opted_in, 1090 1113 'is_subscription' => $is_subscription, 1114 'filtered_should_save' => $filtered_should_save, 1091 1115 'final_should_save' => $should_save, 1092 1116 ]); … … 1103 1127 if (is_wp_error($token)) { 1104 1128 $error_code = $token->get_error_code(); 1129 $error_data = $token->get_error_data(); 1130 $anet_code = ''; 1131 if (is_array($error_data) && !empty($error_data['error_code'])) { 1132 $anet_code = (string) $error_data['error_code']; 1133 } 1105 1134 $this->log("Failed to save payment token", ['error_code' => $error_code, 'error_message' => $token->get_error_message()]); 1135 1136 // Graceful fallback: intermittent Authorize.Net E00114 (Invalid OTS Token). 1137 // In this scenario, the CIM save fails but we can still charge the order using opaque data. 1138 // NOTE: For subscriptions, this will successfully charge the initial order but the card will NOT be saved 1139 // for renewals; we add an order note so the merchant can follow up. 1140 if ($anet_code === 'E00114' || stripos((string) $token->get_error_message(), 'Invalid OTS Token') !== false) { 1141 $this->log('CIM save failed with Invalid OTS Token - falling back to one-time charge using opaque data', [ 1142 'order_id' => $order->get_id(), 1143 'wp_error_code' => $error_code, 1144 'anet_error_code' => $anet_code ?: 'E00114', 1145 'is_subscription' => $this->is_subscription_order($order), 1146 ]); 1147 1148 if ($order instanceof WC_Order) { 1149 $order->add_order_note( 1150 __('Authorize.Net: Card could not be saved to CIM (Invalid OTS Token). Order was charged as a one-time payment. Customer may need to re-add a saved payment method for future renewals.', 'payment-gateway-for-authorize-net-for-woocommerce') 1151 ); 1152 } 1153 1154 return $this->charge_without_saving($order, $opaque_data['gateway_token']); 1155 } 1106 1156 1107 1157 if ($error_code === 'easyauthnet_duplicate_payment_profile') { … … 1112 1162 'card_type' => $opaque_data['card_type'] ?? 'none', 1113 1163 ]); 1164 1165 // If Authorize.Net returned the duplicate payment profile ID, charge via CIM profile. 1166 // IMPORTANT: do NOT attempt to charge with opaque data here because the token may 1167 // have already been consumed by the CIM validation step (leading to E00114). 1168 $dup_customer_profile_id = is_array($error_data) ? ($error_data['customer_profile_id'] ?? '') : ''; 1169 $dup_payment_profile_id = is_array($error_data) ? ($error_data['customer_payment_profile_id'] ?? '') : ''; 1170 1171 if (!empty($dup_payment_profile_id)) { 1172 $user_id = (int) $order->get_user_id(); 1173 1174 // Try to locate an existing Woo token that matches this CIM payment profile. 1175 $existing_by_profile = null; 1176 if ($user_id > 0) { 1177 $tokens = WC_Payment_Tokens::get_customer_tokens($user_id, $this->id); 1178 foreach ($tokens as $t) { 1179 if ($t instanceof WC_Payment_Token && (string) $t->get_token() === (string) $dup_payment_profile_id) { 1180 $existing_by_profile = $t; 1181 break; 1182 } 1183 } 1184 } 1185 1186 $token_to_use = $existing_by_profile; 1187 1188 // If not found, create a Woo token so renewals/manual reuse can work. 1189 if (!$token_to_use && $user_id > 0) { 1190 $token_to_use = new WC_Payment_Token_CC(); 1191 $token_to_use->set_token($dup_payment_profile_id); 1192 $token_to_use->set_gateway_id($this->id); 1193 $token_to_use->set_card_type(strtolower($opaque_data['card_type'] ?? '')); 1194 $token_to_use->set_last4($opaque_data['last4'] ?? ''); 1195 1196 $expiry = $opaque_data['expiry'] ?? ''; 1197 if (preg_match('/^(\d{2})\/(\d{2})$/', $expiry, $m)) { 1198 $token_to_use->set_expiry_month($m[1]); 1199 $token_to_use->set_expiry_year('20' . $m[2]); 1200 } 1201 1202 $token_to_use->set_user_id($user_id); 1203 $token_to_use->set_default(true); 1204 if (!empty($dup_customer_profile_id)) { 1205 $token_to_use->add_meta_data('customer_profile_id', $dup_customer_profile_id, true); 1206 } 1207 $token_to_use->add_meta_data('payment_profile_id', $dup_payment_profile_id, true); 1208 $token_to_use->add_meta_data('created_via_order_id', $order->get_id(), true); 1209 $token_to_use->save(); 1210 1211 $this->log('Created Woo token for duplicate CIM payment profile', [ 1212 'token_id' => $token_to_use->get_id(), 1213 'payment_profile_id' => $this->mask_sensitive_data($dup_payment_profile_id), 1214 ]); 1215 } 1216 1217 if ($token_to_use) { 1218 if ($this->is_subscription_order($order)) { 1219 EASYAUTHNET_Subscription_Helper::assign_token_to_order_and_subscriptions($order, $token_to_use); 1220 } 1221 1222 $response = EASYAUTHNET_AuthorizeNet_API_Handler::charge_saved_token($order, $token_to_use); 1223 if (!is_wp_error($response)) { 1224 $this->log('Successfully charged using duplicate CIM payment profile', [ 1225 'transaction_id' => $response['transaction_id'] ?? 'N/A', 1226 ]); 1227 EASYAUTHNET_AuthorizeNet_API_Handler::complete_payment($order, $response['transaction_id']); 1228 return $this->success_redirect($order); 1229 } 1230 1231 $this->log('Charge using duplicate CIM payment profile failed', [ 1232 'error_code' => $response->get_error_code(), 1233 'error_message' => $response->get_error_message(), 1234 ]); 1235 // Fall through to existing-token lookup below, then final fallback. 1236 } 1237 } 1114 1238 1115 1239 // 1) Try to reuse an existing Woo token for the same card (best for subscriptions). … … 1148 1272 } 1149 1273 1150 // 2) Fallback: charge as a one-time payment (do not save). 1274 // 2) Final fallback: as a last resort, attempt a one-time charge. 1275 // NOTE: If CIM validation already consumed the token, this may still fail with E00114. 1151 1276 return $this->charge_without_saving($order, $opaque_data['gateway_token']); 1152 1277 } 1153 1278 1154 return $this->fail_with_error($ token);1279 return $this->fail_with_error($order, $token); 1155 1280 } 1156 1281 … … 1167 1292 if (is_wp_error($response)) { 1168 1293 $this->log("Failed to charge after saving token", ['error_code' => $response->get_error_code(), 'error_message' => $response->get_error_message()]); 1169 return $this->fail_with_error($ response);1294 return $this->fail_with_error($order, $response); 1170 1295 } 1171 1296 $this->log("Successfully charged after saving token", ['transaction_id' => $response['transaction_id'] ?? 'N/A', 'response_code' => $response['response_code'] ?? null, 'auth_code' => $response['auth_code'] ?? null]); … … 1231 1356 if (is_wp_error($response)) { 1232 1357 $this->log("Failed to process one-time payment", ['error_code' => $response->get_error_code(), 'error_message' => $response->get_error_message()]); 1233 return $this->fail_with_error($ response);1358 return $this->fail_with_error($order, $response); 1234 1359 } 1235 1360 $this->log("Successfully processed one-time payment", ['transaction_id' => $response['transaction_id'] ?? 'N/A', 'response_code' => $response['response_code'] ?? null, 'auth_code' => $response['auth_code'] ?? null]); … … 1238 1363 } 1239 1364 1240 protected function fail_with_error($error) { 1241 $this->log("Payment processing failed", ['error_code' => $error->get_error_code(), 'error_message' => $error->get_error_message()]); 1242 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at REST output time. 1243 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $error->get_error_message(), 400); 1365 protected function fail_with_error($order, $error) { 1366 $message = is_wp_error($error) ? (string) $error->get_error_message() : (string) $error; 1367 $code = is_wp_error($error) ? (string) $error->get_error_code() : 'unknown'; 1368 $message = wp_strip_all_tags($message); 1369 $this->log('Payment processing failed', [ 1370 'order_id' => $order instanceof WC_Order ? $order->get_id() : 0, 1371 'error_code' => $code, 1372 'error_message' => $message, 1373 'is_store_api' => $this->is_store_api_request(), 1374 ]); 1375 if ($order instanceof WC_Order) { 1376 $this->maybe_mark_order_failed($order, $message); 1377 } 1378 if ($this->is_store_api_request()) { 1379 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 1380 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $message, 400); 1381 } 1382 wc_add_notice($message, 'error'); 1383 return ['result' => 'failure']; 1384 } 1385 1386 protected function maybe_mark_order_failed(WC_Order $order, $message) { 1387 if ($order->has_status(['processing', 'completed', 'cancelled', 'refunded'])) { 1388 return; 1389 } 1390 $note = sprintf( 1391 __('Authorize.Net payment failed: %s', 'payment-gateway-for-authorize-net-for-woocommerce'), 1392 wp_strip_all_tags((string) $message) 1393 ); 1394 if (!$order->has_status('failed')) { 1395 $order->update_status('failed', $note); 1396 } else { 1397 $order->add_order_note($note); 1398 } 1399 $order->save(); 1244 1400 } 1245 1401 … … 1263 1419 ]; 1264 1420 } 1265 1266 protected function handle_payment_exception($e) { 1267 $this->log("Payment processing exception", ['exception' => get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]); 1268 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at REST output time. 1269 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $e->getMessage(), 400); 1421 1422 protected function is_store_api_request(): bool { 1423 if (defined('REST_REQUEST') && REST_REQUEST) { 1424 // Woo Store API endpoints usually include /wc/store/ 1425 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 1426 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : ''; 1427 if ($uri && strpos($uri, '/wc/store/') !== false) { 1428 return true; 1429 } 1430 } 1431 return false; 1432 } 1433 1434 protected function handle_payment_exception($order, $e) { 1435 $message = $e instanceof Exception ? (string) $e->getMessage() : (string) $e; 1436 $message = wp_strip_all_tags($message); 1437 $this->log('Payment processing exception', [ 1438 'order_id' => $order instanceof WC_Order ? $order->get_id() : 0, 1439 'exception' => is_object($e) ? get_class($e) : 'unknown', 1440 'message' => $message, 1441 'is_store_api' => $this->is_store_api_request(), 1442 ]); 1443 if ($order instanceof WC_Order) { 1444 $this->maybe_mark_order_failed($order, $message); 1445 } 1446 if ($this->is_store_api_request()) { 1447 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 1448 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $message, 400); 1449 } 1450 wc_add_notice($message, 'error'); 1451 return ['result' => 'failure']; 1270 1452 } 1271 1453 … … 1303 1485 $this->log("Cannot save token - missing requirements", ['error' => $error]); 1304 1486 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 1305 returnthrow new Exception($error);1487 throw new Exception($error); 1306 1488 } 1307 1489 $customer_profile_id = get_user_meta($user_id, $this->customer_profile_id, true); … … 1313 1495 $customer_profile_id = ''; 1314 1496 } 1497 $payment_profile_id_from_create = ''; 1498 1315 1499 if (!$customer_profile_id) { 1316 1500 $create_result = EASYAUTHNET_AuthorizeNet_API_Handler::create_customer_profile($order, $payment_data['gateway_token']); … … 1333 1517 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1334 1518 ]); 1519 1520 // IMPORTANT: createCustomerProfileRequest already includes a payment profile built from opaqueData. 1521 // If Authorize.Net returns the created paymentProfileId(s), reuse the first one and DO NOT call 1522 // createCustomerPaymentProfileRequest again (opaqueData tokens are one-time-use). 1523 if (!empty($create_result['customerPaymentProfileIdList'])) { 1524 $list = $create_result['customerPaymentProfileIdList']; 1525 $candidate = ''; 1526 if (is_array($list)) { 1527 // Authorize.Net may return { numericString: ["123"] } or a flat array. 1528 if (isset($list['numericString'])) { 1529 $ns = $list['numericString']; 1530 if (is_array($ns)) { 1531 $candidate = (string) reset($ns); 1532 } else { 1533 $candidate = (string) $ns; 1534 } 1535 } else { 1536 $candidate = (string) reset($list); 1537 } 1538 } else { 1539 $candidate = (string) $list; 1540 } 1541 if ($candidate !== '') { 1542 $payment_profile_id_from_create = $candidate; 1543 $this->log('Reusing payment profile id returned by createCustomerProfileRequest', [ 1544 'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id_from_create), 1545 ]); 1546 } 1547 } 1335 1548 } 1336 1549 update_user_meta($user_id, $this->customer_profile_id, $customer_profile_id); 1337 1550 } 1338 // 2) ✅ Create a NEW payment profile (card) under the EXISTING customer profile 1339 $this->log("Creating new CIM payment profile for saved card", [ 1340 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1341 ]); 1342 $payment_profile_id = EASYAUTHNET_AuthorizeNet_API_Handler::create_customer_payment_profile( 1343 $customer_profile_id, 1344 $payment_data['gateway_token'], 1345 $order 1346 ); 1551 // 2) Create a NEW payment profile (card) under the EXISTING customer profile. 1552 // If we JUST created the customer profile and Authorize.Net returned the paymentProfileId, reuse it. 1553 if (!empty($payment_profile_id_from_create)) { 1554 $payment_profile_id = $payment_profile_id_from_create; 1555 } else { 1556 $this->log("Creating new CIM payment profile for saved card", [ 1557 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1558 ]); 1559 $payment_profile_id = EASYAUTHNET_AuthorizeNet_API_Handler::create_customer_payment_profile( 1560 $customer_profile_id, 1561 $payment_data['gateway_token'], 1562 $order 1563 ); 1564 } 1347 1565 if (is_wp_error($payment_profile_id)) { 1348 1566 $this->log("Failed to create CIM payment profile", [ … … 1356 1574 $this->log("No payment profile ID returned from CIM create", ['error' => $error]); 1357 1575 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 1358 returnthrow new Exception($error);1576 throw new Exception($error); 1359 1577 } 1360 1578 $token = new WC_Payment_Token_CC(); … … 1520 1738 $this->log("Capture failed - no transaction ID", ['error' => $error]); 1521 1739 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized and escaped at output time. 1522 returnthrow new Exception($error);1740 throw new Exception($error); 1523 1741 } 1524 1742 $response = EASYAUTHNET_AuthorizeNet_API_Handler::capture_authorized_transaction($order, $transaction_id); -
payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-easy-payment-authorizenet-googlepay-gateway.php
r3440293 r3462451 489 489 return true; 490 490 } 491 492 protected function is_store_api_request(): bool { 493 if (defined('REST_REQUEST') && REST_REQUEST) { 494 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 495 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : ''; 496 return ($uri && strpos($uri, '/wc/store/') !== false); 497 } 498 return false; 499 } 500 501 protected function maybe_mark_order_failed(WC_Order $order, string $message): void { 502 if ($order->has_status(['processing', 'completed', 'cancelled', 'refunded'])) { 503 return; 504 } 505 $note = sprintf( 506 __('Authorize.Net payment failed: %s', 'payment-gateway-for-authorize-net-for-woocommerce'), 507 wp_strip_all_tags($message) 508 ); 509 if (!$order->has_status('failed')) { 510 $order->update_status('failed', $note); 511 } else { 512 $order->add_order_note($note); 513 } 514 $order->save(); 515 } 516 517 protected function fail_with_error(WC_Order $order, $error) { 518 $message = is_wp_error($error) ? (string) $error->get_error_message() : (string) $error; 519 $message = wp_strip_all_tags($message); 520 $this->maybe_mark_order_failed($order, $message); 521 if ($this->is_store_api_request()) { 522 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 523 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $message, 400); 524 } 525 wc_add_notice($message, 'error'); 526 return ['result' => 'failure']; 527 } 528 529 protected function handle_payment_exception(WC_Order $order, Exception $e) { 530 $message = wp_strip_all_tags((string) $e->getMessage()); 531 $this->maybe_mark_order_failed($order, $message); 532 if ($this->is_store_api_request()) { 533 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 534 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $message, 400); 535 } 536 wc_add_notice($message, 'error'); 537 return ['result' => 'failure']; 538 } 539 491 540 492 541 public function process_payment($order_id) { … … 534 583 535 584 if (is_wp_error($response)) { 536 throw new Exception($response->get_error_message() ?: __('Google Pay payment failed.', 'payment-gateway-for-authorize-net-for-woocommerce'));585 return $this->fail_with_error($order, $response); 537 586 } 538 587 … … 563 612 if (!$ok) { 564 613 $message = !empty($response['message']) ? $response['message'] : __('Transaction failed.', 'payment-gateway-for-authorize-net-for-woocommerce'); 565 throw new Exception($message); 614 // Use the unified handler so order becomes Failed + note, 615 // and Blocks gets RouteException while Classic gets wc notice. 616 return $this->fail_with_error($order, $message); 566 617 } 567 618 … … 578 629 ]; 579 630 } catch (Exception $e) { 580 wc_add_notice($e->getMessage(), 'error'); 581 return ['result' => 'failure']; 631 if ($e instanceof \Automattic\WooCommerce\StoreApi\Exceptions\RouteException) { 632 throw $e; 633 } 634 return $this->handle_payment_exception($order, $e); 582 635 } 583 636 } -
payment-gateway-for-authorize-net-for-woocommerce/trunk/payment-gateway-for-authorizenet-for-woocommerce.php
r3440293 r3462451 7 7 * Author: easypayment 8 8 * Author URI: https://profiles.wordpress.org/easypayment/ 9 * Version: 1.0. 79 * Version: 1.0.8 10 10 * Requires at least: 5.6 11 * Tested up to: 6.9 11 * Tested up to: 6.9.1 12 12 * Requires PHP: 7.4 13 13 * Text Domain: payment-gateway-for-authorize-net-for-woocommerce 14 14 * Domain Path: /languages/ 15 15 * WC requires at least: 6.0 16 * WC tested up to: 10. 4.316 * WC tested up to: 10.5.1 17 17 * Requires Plugins: woocommerce 18 18 * License: GPLv2 or later … … 23 23 24 24 if (!defined('EASYAUTHNET_AUTHORIZENET_VERSION')) { 25 define('EASYAUTHNET_AUTHORIZENET_VERSION', '1.0. 7');25 define('EASYAUTHNET_AUTHORIZENET_VERSION', '1.0.8'); 26 26 } 27 27 define('EASYAUTHNET_AUTHORIZENET_PLUGIN_FILE', __FILE__); -
payment-gateway-for-authorize-net-for-woocommerce/trunk/readme.txt
r3440293 r3462451 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1.0. 77 Stable tag: 1.0.8 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 118 118 == Changelog == 119 119 120 = 1.0.8 = 121 * Fixed – Intermittent checkout failures (E00114 Invalid OTS Token) and improved CIM card-saving flow stability. 122 120 123 = 1.0.7 = 121 124 * Added – Support for eCheck (ACH) payments.
Note: See TracChangeset
for help on using the changeset viewer.