PluginProbe ʕ •ᴥ•ʔ
Payment Gateway for Authorize.net for WooCommerce / 1.0.3
Payment Gateway for Authorize.net for WooCommerce v1.0.3
1.0.19 1.0.18 1.0.17 1.0.16 1.0.15 1.0.14 1.0.13 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
payment-gateway-for-authorize-net-for-woocommerce / includes / class-easy-payment-authorizenet-gateway.php
payment-gateway-for-authorize-net-for-woocommerce / includes Last commit date
compatibility 8 months ago class-api-handler.php 8 months ago class-easy-payment-authorizenet-gateway.php 8 months ago class-webhook-handler.php 8 months ago
class-easy-payment-authorizenet-gateway.php
1215 lines
1 <?php
2 if (!defined('ABSPATH'))
3 exit;
4
5 class EASYAUTHNET_AuthorizeNet_Gateway extends WC_Payment_Gateway_CC {
6
7 const CARD_META_PREFIX = '_authorizenet_card_';
8 const CLIENT_KEY_CACHE_PREFIX = 'easyauthnet_authorizenet_client_key_';
9 const CLIENT_KEY_CACHE_TTL = HOUR_IN_SECONDS;
10
11 public $enabled;
12 public $title;
13 public $description;
14 public $environment;
15 public $tokenization;
16 public $transaction_type;
17 public $accepted_card_logos;
18 public $debug;
19 public $live_api_login_id;
20 public $live_transaction_key;
21 public $live_signature_key;
22 public $sandbox_api_login_id;
23 public $sandbox_transaction_key;
24 public $sandbox_signature_key;
25 public $api_login_id;
26 public $transaction_key;
27 public $signature_key;
28 public $client_key;
29 public $customer_profile_id;
30
31 protected const CUSTOMER_PROFILE_MIGRATION_FLAG = 'easyauthnet_authorizenet_profile_migration_v1';
32
33 public function __construct() {
34 $this->id = 'easyauthnet_authorizenet';
35 $this->method_title = __('Authorize.Net Credit Card - By Easy Payment', 'payment-gateway-for-authorize-net-for-woocommerce');
36 $this->method_description = __('Securely accept credit cards using Authorize.Net.', 'payment-gateway-for-authorize-net-for-woocommerce');
37 $this->has_fields = true;
38 $this->supports = [
39 'products',
40 'refunds',
41 'subscriptions',
42 'subscription_suspension',
43 'subscription_cancellation',
44 'subscription_reactivation',
45 'subscription_amount_changes',
46 'subscription_date_changes',
47 'multiple_subscriptions',
48 'subscription_payment_method_change_customer',
49 'subscription_payment_method_change_admin',
50 'pre-orders',
51 ];
52 $this->init_form_fields();
53 $this->init_settings();
54 $this->init_properties();
55 $this->init_hooks();
56 $this->maybe_run_customer_profile_migration_once();
57 }
58
59 protected function init_properties() {
60 $this->enabled = $this->get_option('enabled', 'no');
61 $this->title = $this->get_option('title', __('Credit / Debit Card', 'payment-gateway-for-authorize-net-for-woocommerce'));
62 $this->description = $this->get_option('description', __('Pay securely using your credit card.', 'payment-gateway-for-authorize-net-for-woocommerce'));
63 $this->environment = $this->get_option('environment', 'sandbox');
64 if ($this->environment === 'live') {
65 $this->customer_profile_id = EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_live';
66 } else {
67 $this->customer_profile_id = EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_sandbox';
68 }
69 $this->tokenization = $this->get_option('tokenization', 'no');
70 $this->transaction_type = $this->get_option('transaction_type', 'auth_capture');
71 $this->accepted_card_logos = $this->get_option('accepted_card_logos', ['visa', 'mastercard', 'amex', 'discover', 'jcb', 'diners']);
72 $this->debug = $this->get_option('debug', 'no');
73 $this->sandbox_api_login_id = $this->get_option('sandbox_api_login_id', '');
74 $this->sandbox_transaction_key = $this->get_option('sandbox_transaction_key', '');
75 $this->sandbox_signature_key = $this->get_option('sandbox_signature_key', '');
76 $this->live_api_login_id = $this->get_option('live_api_login_id', '');
77 $this->live_transaction_key = $this->get_option('live_transaction_key', '');
78 $this->live_signature_key = $this->get_option('live_signature_key', '');
79 if ($this->environment === 'live') {
80 $this->api_login_id = $this->live_api_login_id;
81 $this->transaction_key = $this->live_transaction_key;
82 $this->signature_key = $this->live_signature_key;
83 } else {
84 $this->api_login_id = $this->sandbox_api_login_id;
85 $this->transaction_key = $this->sandbox_transaction_key;
86 $this->signature_key = $this->sandbox_signature_key;
87 }
88 if (empty($this->client_key)) {
89 $this->init_client_key();
90 }
91 if ($this->tokenization === 'yes') {
92 $this->supports[] = 'tokenization';
93 }
94 }
95
96 protected function init_client_key() {
97 if (is_checkout() || is_checkout_pay_page() || is_account_page()) {
98 if (!empty($this->api_login_id) && !empty($this->transaction_key) && !empty($this->signature_key)) {
99 $merchant_data = EASYAUTHNET_AuthorizeNet_API_Handler::fetch_merchant_details($this->api_login_id, $this->transaction_key, $this->signature_key, $this->environment === 'sandbox');
100 if (!is_wp_error($merchant_data) && isset($merchant_data['publicClientKey'])) {
101 $this->client_key = $merchant_data['publicClientKey'];
102 $this->log("Successfully retrieved client key", [
103 'client_key' => $this->mask_sensitive_data($this->client_key)
104 ]);
105 } else {
106 $this->client_key = '';
107 $error_message = is_wp_error($merchant_data) ? $merchant_data->get_error_message() : 'No publicClientKey in response';
108 $this->log("Failed to retrieve client key", [
109 'error' => $error_message
110 ]);
111 }
112 } else {
113 $this->client_key = '';
114 $this->log("Missing credentials for client key initialization", [
115 'has_api_login_id' => !empty($this->api_login_id),
116 'has_transaction_key' => !empty($this->transaction_key),
117 'has_signature_key' => !empty($this->signature_key)
118 ]);
119 }
120 }
121 }
122
123 private function card_labels(): array {
124 return [
125 'visa' => _x('Visa', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
126 'mastercard' => _x('Mastercard', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
127 'maestro' => _x('Maestro', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
128 'amex' => _x('American Express', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
129 'discover' => _x('Discover', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
130 'jcb' => _x('JCB', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
131 'elo' => _x('Elo', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
132 'hiper' => _x('Hiper', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
133 'diners' => _x('Diners Club', 'Name of credit card', 'payment-gateway-for-authorize-net-for-woocommerce'),
134 ];
135 }
136
137 public function get_icon() {
138 $title_options = $this->card_labels();
139 $images = [];
140 $totalIcons = 0;
141 foreach ($title_options as $icon_key => $icon_value) {
142 if (in_array($icon_key, $this->accepted_card_logos)) {
143 $iconUrl = esc_url(EASYAUTHNET_AUTHORIZENET_PLUGIN_ASSET_URL) . 'assets/css/credit-cards/' . esc_attr($icon_key) . '.svg';
144 $iconTitle = esc_attr($icon_value);
145 // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage -- Gateway icons use static <img> tags for compatibility
146 $images[] = sprintf('<img title="%s" src="%s" class="easyauthnet-card-icon ae-icon-%s" /> ', $iconTitle, $iconUrl, $iconTitle);
147 $totalIcons++;
148 }
149 }
150 return implode('', $images) . '<div class="easyauthnet-wc-payment-gateway-card-icons"></div>';
151 }
152
153 protected function init_hooks() {
154 add_action('admin_enqueue_scripts', [$this, 'admin_scripts']);
155 add_action('wp_enqueue_scripts', [$this, 'public_scripts']);
156 add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
157 add_action('wp_enqueue_scripts', [$this, 'enqueue_scripts']);
158 add_action('woocommerce_scheduled_subscription_payment_easyauthnet_authorizenet', [$this, 'process_scheduled_payment'], 10, 2);
159 add_filter('woocommerce_payment_gateway_get_tokenization_title', [$this, 'override_tokenization_title'], 10, 2);
160 add_action('woocommerce_order_action_easyauthnet_capture_authorized_payment', [$this, 'process_order_action_capture']);
161 add_filter('woocommerce_settings_api_sanitized_fields_' . $this->id, [$this, 'validate_authorizenet_credentials_on_save'], 999, 1);
162 add_action('admin_notices', [$this, 'easyauthnet_authorizenet_show_invalid_credentials_notice']);
163 }
164
165 public function is_credentials_set() {
166 if (!empty($this->api_login_id) && !empty($this->transaction_key) && !empty($this->signature_key)) {
167 return true;
168 } else {
169 return false;
170 }
171 }
172
173 public function is_available() {
174 if ($this->is_credentials_set() && $this->enabled === 'yes') {
175 return true;
176 }
177 return false;
178 }
179
180 public function easyauthnet_authorizenet_show_invalid_credentials_notice() {
181 if ($msg = get_transient('easyauthnet_authorizenet_invalid_credentials_message')) {
182 delete_transient('easyauthnet_authorizenet_invalid_credentials_message');
183 echo '<div class="notice notice-error"><p>' . esc_html($msg) . '</p></div>';
184 }
185 }
186
187 public function validate_authorizenet_credentials_on_save($settings) {
188 $environment = ($settings['environment'] ?? 'sandbox') === 'live' ? 'live' : 'sandbox';
189 $api_login_id = sanitize_text_field($settings["{$environment}_api_login_id"] ?? '');
190 $transaction_key = sanitize_text_field($settings["{$environment}_transaction_key"] ?? '');
191 $signature_key = sanitize_text_field($settings["{$environment}_signature_key"] ?? '');
192 $is_enabled = $settings['enabled'] ?? 'no';
193 if ($is_enabled === 'yes' && (empty($api_login_id) || empty($transaction_key) || empty($signature_key))) {
194 $error_message = __('Please enter the API Login ID, Transaction Key, and Signature Key for Authorize.Net in the selected environment.', 'payment-gateway-for-authorize-net-for-woocommerce');
195 set_transient('easyauthnet_authorizenet_invalid_credentials_message', $error_message, 60);
196 ob_get_clean();
197 wp_safe_redirect(admin_url('admin.php?page=wc-settings&tab=checkout&section=easyauthnet_authorizenet'));
198 exit;
199 }
200 if (!empty($api_login_id) && !empty($transaction_key)) {
201 $is_valid = $this->attempt_authnet_credentials_validation($api_login_id, $transaction_key, $environment === 'sandbox');
202 if (!$is_valid) {
203 $error_message = __('The Authorize.Net API Login ID and Transaction Key you entered are invalid. Ensure you are using the correct credentials for the selected environment (Sandbox or Live).', 'payment-gateway-for-authorize-net-for-woocommerce');
204 set_transient('easyauthnet_authorizenet_invalid_credentials_message', $error_message, 60);
205 $settings["{$environment}_api_login_id"] = '';
206 $settings["{$environment}_transaction_key"] = '';
207 ob_get_clean();
208 wp_safe_redirect(admin_url('admin.php?page=wc-settings&tab=checkout&section=easyauthnet_authorizenet'));
209 exit;
210 }
211 }
212 return $settings;
213 }
214
215 protected function attempt_authnet_credentials_validation($api_login_id, $transaction_key, $is_sandbox = true) {
216 $url = $is_sandbox ? 'https://apitest.authorize.net/xml/v1/request.api' : 'https://api.authorize.net/xml/v1/request.api';
217
218 // Suppress XML warnings (like non-absolute namespace URIs)
219 libxml_use_internal_errors(true);
220
221 $root_element = 'getMerchantDetailsRequest';
222 $xml = new SimpleXMLElement(
223 '<?xml version="1.0" encoding="UTF-8"?>' .
224 "<$root_element xmlns=\"AnetApi/xml/v1/schema/AnetApiSchema.xsd\"></$root_element>"
225 );
226
227 $auth = $xml->addChild('merchantAuthentication');
228 $auth->addChild('name', $api_login_id);
229 $auth->addChild('transactionKey', $transaction_key);
230
231 $request_xml = $xml->asXML();
232
233 $response = wp_remote_post($url, [
234 'headers' => [
235 'Content-Type' => 'text/xml',
236 ],
237 'body' => $request_xml,
238 'timeout' => 30,
239 ]);
240
241 if (is_wp_error($response)) {
242 return false;
243 }
244
245 $body_raw = wp_remote_retrieve_body($response);
246
247 // Again suppress errors for response parsing
248 libxml_use_internal_errors(true);
249 $xml_response = simplexml_load_string($body_raw, 'SimpleXMLElement', LIBXML_NOCDATA);
250 if (!$xml_response) {
251 return false;
252 }
253
254 $json = json_decode(json_encode($xml_response), true);
255
256 return isset($json['messages']['resultCode']) && $json['messages']['resultCode'] === 'Ok';
257 }
258
259 public function admin_options() {
260 $return_url = admin_url('admin.php?page=wc-settings&tab=checkout');
261 if (function_exists('wc_back_header')) {
262 wc_back_header($this->get_method_title(), __('Return to payments', 'payment-gateway-for-authorize-net-for-woocommerce'), $return_url);
263 } else {
264 echo '<h2>' . esc_html($this->method_title) . '</h2>';
265 wc_back_link(
266 __('Return to payments', 'payment-gateway-for-authorize-net-for-woocommerce'),
267 admin_url('admin.php?page=wc-settings&tab=checkout')
268 );
269 }
270 echo wp_kses_post(wpautop($this->get_method_description()));
271 echo '<p>';
272 echo wp_kses(
273 __('Need help? Contact <a href="https://wordpress.org/support/plugin/payment-gateway-for-authorize-net-for-woocommerce/" target="_blank">support</a>.', 'payment-gateway-for-authorize-net-for-woocommerce'),
274 array('a' => array('href' => array(), 'target' => array()))
275 );
276 echo '</p>';
277 echo '<table class="form-table" style="display:none;">' . $this->generate_settings_html($this->get_form_fields(), false) . '</table>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is safe from WooCommerce Settings API.
278 }
279
280 public function process_admin_options() {
281 parent::process_admin_options();
282 $this->easyauthnet_register_anet_webhook_on_save();
283 }
284
285 public function process_scheduled_payment($amount, $order_id) {
286 $order = wc_get_order($order_id);
287 $this->log("Processing scheduled subscription payment", ['order_id' => $order_id, 'amount' => $amount, 'order_status' => $order ? $order->get_status() : 'invalid_order']);
288 if (!$order || !is_a($order, 'WC_Order')) {
289 $this->log("Invalid order for scheduled payment", ['order_id' => $order_id]);
290 return;
291 }
292 $token_id = null;
293 $tokens = $order->get_meta('_payment_tokens', true);
294 if (is_array($tokens) && !empty($tokens)) {
295 $token_id = $tokens[0];
296 $this->log("Found token ID from order meta", ['token_id' => $token_id]);
297 }
298 if (empty($token_id) && function_exists('wcs_get_subscriptions_for_renewal_order')) {
299 $subscriptions = wcs_get_subscriptions_for_renewal_order($order);
300 foreach ($subscriptions as $subscription) {
301 $tokens = $subscription->get_meta('_payment_tokens', true);
302 if (is_array($tokens) && !empty($tokens)) {
303 $token_id = $tokens[0];
304 $this->log("Found token ID from subscription meta", ['token_id' => $token_id, 'subscription_id' => $subscription->get_id()]);
305 break;
306 }
307 }
308 }
309 $token = $token_id ? WC_Payment_Tokens::get($token_id) : false;
310 $this->log("Token details for scheduled payment", ['token_exists' => (bool) $token, 'token_type' => $token ? $token->get_type() : 'none', 'token_last4' => $token ? $token->get_last4() : 'none', 'token_card_type' => $token ? $token->get_card_type() : 'none']);
311 if (!$token || !$token->get_token()) {
312 $this->log("No valid saved token found for scheduled payment", ['order_id' => $order_id]);
313 $order->update_status('failed', __('Subscription renewal failed: no saved payment method available.', 'payment-gateway-for-authorize-net-for-woocommerce'));
314 return;
315 }
316 $response = EASYAUTHNET_AuthorizeNet_API_Handler::charge_saved_token($order, $token);
317 if (is_wp_error($response)) {
318 $this->log("Failed to charge saved token for scheduled payment", ['error_code' => $response->get_error_code(), 'error_message' => $response->get_error_message()]);
319 // translators: %1$s is the error message returned during subscription renewal failure.
320 $order->update_status('failed', sprintf(__('Subscription renewal payment failed: %1$s', 'payment-gateway-for-authorize-net-for-woocommerce'), $response->get_error_message()));
321 return;
322 }
323 $transaction_id = $response['transaction_id'] ?? 'N/A';
324 $this->log("Successfully processed scheduled payment", ['transaction_id' => $transaction_id, 'response_code' => $response['response_code'] ?? null, 'auth_code' => $response['auth_code'] ?? null]);
325 EASYAUTHNET_AuthorizeNet_API_Handler::complete_payment($order, $transaction_id);
326 }
327
328 public function override_tokenization_title($title, $token) {
329 if ($token->get_gateway_id() === 'easyauthnet_authorizenet') {
330 // translators: 1: Card type (e.g., VISA), 2: Last 4 digits of the card.
331 $title = sprintf(__('Saved Card (%1$s ending in %2$s)', 'payment-gateway-for-authorize-net-for-woocommerce'), strtoupper($token->get_card_type()), $token->get_last4());
332 }
333 return $title;
334 }
335
336 public function enqueue_scripts() {
337 if (is_checkout() || is_account_page()) {
338 $url = $this->environment === 'live' ? 'https://js.authorize.net/v1/Accept.js' : 'https://jstest.authorize.net/v1/Accept.js';
339 $this->log("Enqueueing Accept.js script", ['environment' => $this->environment, 'url' => $url]);
340 wp_enqueue_script('wc-credit-card-form');
341 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Third-party script, version controlled by Authorize.Net
342 wp_enqueue_script('easyauthnet_authorizenet_acceptjs', $url, [], null, true);
343 wp_enqueue_script('easyauthnet-acceptjs-handler', plugin_dir_url(__FILE__) . '../assets/js/acceptjs-handler.js', ['jquery', 'easyauthnet_authorizenet_acceptjs', 'wp-i18n', 'wc-credit-card-form'], EASYAUTHNET_AUTHORIZENET_VERSION, true);
344 wp_localize_script('easyauthnet-acceptjs-handler', 'easyauthnet_authorizenet_params', [
345 'client_key' => $this->client_key,
346 'login_id' => $this->api_login_id,
347 'environment' => $this->environment,
348 'debug' => ($this->debug === 'yes') ? true : false
349 ]);
350 }
351 }
352
353 public function public_scripts() {
354 wp_enqueue_style('easyauthnet-authorizenet-public', plugin_dir_url(__FILE__) . '../assets/css/public.css', [], EASYAUTHNET_AUTHORIZENET_VERSION);
355 }
356
357 public function admin_scripts() {
358 if (!$this->is_admin_settings_page()) {
359 return;
360 }
361 wp_enqueue_style('easyauthnet-authorizenet-admin', plugin_dir_url(__FILE__) . '../assets/css/admin.css', [], EASYAUTHNET_AUTHORIZENET_VERSION);
362 wp_enqueue_script('easyauthnet-authorizenet-admin-js', plugin_dir_url(__FILE__) . '../assets/js/easyauthnet-authorizenet-admin.js', ['jquery'], EASYAUTHNET_AUTHORIZENET_VERSION, true);
363 }
364
365 protected function is_admin_settings_page() {
366 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
367 return isset($_GET['page'], $_GET['section']) && 'wc-settings' === $_GET['page'] && $this->id === $_GET['section'];
368 }
369
370 public function generate_authorizenet_signup_banner_html($field_key, $data) {
371 if (isset($data['type']) && $data['type'] === 'authorizenet_signup_banner') {
372 $field_key = $this->get_field_key($field_key);
373 ob_start();
374 ?>
375 <tr valign="top">
376 <th scope="row" class="titledesc">
377 <label for="<?php echo esc_attr($field_key); ?>">
378 <?php
379 echo esc_html($data['title'] ?? __('Business Registration (Merchant Sign Up)', 'payment-gateway-for-authorize-net-for-woocommerce'));
380 ?>
381 </label>
382 </th>
383 <td class="forminp" id="<?php echo esc_attr($field_key); ?>">
384 <?php
385 $signup_url = 'https://account.authorize.net/signUpNow?resellerId=27457';
386 $direct_url = 'https://account.authorize.net/signUpNow?resellerId=27457';
387 ?>
388
389 <div class="easyauthnet-signup-box" role="region" aria-labelledby="<?php echo esc_attr($field_key); ?>-heading">
390 <!-- For Business Owners -->
391 <h4 id="<?php echo esc_attr($field_key); ?>-heading">
392 <span class="dashicons dashicons-businessperson" aria-hidden="true"></span>
393 <?php esc_html_e('For Business Owners', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
394 </h4>
395 <p class="description">
396 <?php esc_html_e('Sign up your business to start accepting payments with Authorize.Net.', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
397 </p>
398 <p>
399 <a href="<?php echo esc_url($signup_url); ?>"
400 class="button button-primary easyauthnet-register-btn"
401 target="_blank"
402 aria-label="<?php esc_attr_e('Sign up with Authorize.Net', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>">
403 <?php esc_html_e('Sign Up with Authorize.Net', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
404 </a>
405 </p>
406
407 <hr />
408
409 <!-- For Developers -->
410 <h4>
411 <span class="dashicons dashicons-admin-links" aria-hidden="true"></span>
412 <?php esc_html_e('For Developers', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
413 </h4>
414 <p class="description">
415 <?php esc_html_e('Share this referral link with your client for easy sign-up.', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
416 </p>
417
418 <div class="easyauthnet-copy-wrap">
419 <input type="text"
420 id="<?php echo esc_attr($field_key); ?>-ref-link"
421 class="easyauthnet-copy-input"
422 value="<?php echo esc_attr($direct_url); ?>"
423 readonly />
424 <button type="button"
425 class="button easyauthnet-copy-link"
426 data-link="<?php echo esc_attr($direct_url); ?>">
427 <?php esc_html_e('Copy Link', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
428 </button>
429 </div>
430
431 <p class="description">
432 <?php esc_html_e('This referral link ensures your client\'s Authorize.Net account is automatically linked to this plugin.', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
433 </p>
434 </div>
435 </td>
436 </tr>
437 <?php
438 return ob_get_clean();
439 }
440 }
441
442 public function init_form_fields() {
443 $this->form_fields = [
444 'section_one' => [
445 'title' => __('Step 1: Registration', 'payment-gateway-for-authorize-net-for-woocommerce'),
446 'type' => 'title',
447 'class' => 'easyauthnet-authorizenet-collapsible-section'
448 ],
449 'signup_account' => array(
450 'type' => 'authorizenet_signup_banner',
451 ),
452 'section_sandbox' => [
453 'title' => __('Step 2: Authorize.Net Gateway Settings', 'payment-gateway-for-authorize-net-for-woocommerce'),
454 'type' => 'title',
455 'class' => 'easyauthnet-authorizenet-collapsible-section'
456 ],
457 'environment' => [
458 'title' => __('Environment', 'payment-gateway-for-authorize-net-for-woocommerce'),
459 'type' => 'select',
460 'description' => __('Select whether to use Sandbox (test mode) or Live (production).', 'payment-gateway-for-authorize-net-for-woocommerce'),
461 'desc_tip' => true,
462 'options' => [
463 'sandbox' => __('Sandbox (Test Mode)', 'payment-gateway-for-authorize-net-for-woocommerce'),
464 'live' => __('Live (Production)', 'payment-gateway-for-authorize-net-for-woocommerce'),
465 ],
466 'default' => 'sandbox',
467 ],
468 'enabled' => [
469 'title' => __('Enable / Disable', 'payment-gateway-for-authorize-net-for-woocommerce'),
470 'type' => 'checkbox',
471 'label' => __('Enable Authorize.net', 'payment-gateway-for-authorize-net-for-woocommerce'),
472 'default' => 'no',
473 ],
474 'title' => [
475 'title' => __('Title', 'payment-gateway-for-authorize-net-for-woocommerce'),
476 'type' => 'text',
477 'description' => __('This controls the payment method title displayed during checkout.', 'payment-gateway-for-authorize-net-for-woocommerce'),
478 'desc_tip' => true,
479 'default' => __('Credit / Debit Card', 'payment-gateway-for-authorize-net-for-woocommerce'),
480 ],
481 'description' => [
482 'title' => __('Description', 'payment-gateway-for-authorize-net-for-woocommerce'),
483 'type' => 'textarea',
484 'css' => 'width: 400px;',
485 'description' => __('This controls the text shown below the title during checkout.', 'payment-gateway-for-authorize-net-for-woocommerce'),
486 'desc_tip' => true,
487 'default' => __('Pay securely using your credit card.', 'payment-gateway-for-authorize-net-for-woocommerce'),
488 ],
489 'sandbox_api_login_id' => [
490 'title' => __('Sandbox API Login ID', 'payment-gateway-for-authorize-net-for-woocommerce'),
491 'type' => 'text',
492 'description' => __(
493 'Get from <a href="https://sandbox.authorize.net/" target="_blank">sandbox.authorize.net</a> → <code>Account → Settings → Security Settings → API Credentials & Keys</code>.',
494 'payment-gateway-for-authorize-net-for-woocommerce'
495 ),
496 'desc_tip' => false,
497 'custom_attributes' => ['autocomplete' => 'off'],
498 ],
499 'sandbox_transaction_key' => [
500 'title' => __('Sandbox Transaction Key', 'payment-gateway-for-authorize-net-for-woocommerce'),
501 'type' => 'password',
502 'custom_attributes' => ['autocomplete' => 'off'],
503 ],
504 'sandbox_signature_key' => [
505 'title' => __('Sandbox Signature Key', 'payment-gateway-for-authorize-net-for-woocommerce'),
506 'type' => 'password',
507 'custom_attributes' => ['autocomplete' => 'off'],
508 ],
509 'live_api_login_id' => [
510 'title' => __('Live API Login ID', 'payment-gateway-for-authorize-net-for-woocommerce'),
511 'type' => 'text',
512 'description' => __(
513 'Get from <a href="https://account.authorize.net/" target="_blank">account.authorize.net</a> → <code>Account → Settings → Security Settings → API Credentials & Keys</code>.',
514 'payment-gateway-for-authorize-net-for-woocommerce'
515 ),
516 'desc_tip' => false,
517 'custom_attributes' => ['autocomplete' => 'off'],
518 ],
519 'live_transaction_key' => [
520 'title' => __('Live Transaction Key', 'payment-gateway-for-authorize-net-for-woocommerce'),
521 'type' => 'password',
522 'custom_attributes' => ['autocomplete' => 'off'],
523 ],
524 'live_signature_key' => [
525 'title' => __('Live Signature Key', 'payment-gateway-for-authorize-net-for-woocommerce'),
526 'type' => 'password',
527 'custom_attributes' => ['autocomplete' => 'off'],
528 ],
529 'section_checkout' => [
530 'title' => __('Step 3: Additional Settings', 'payment-gateway-for-authorize-net-for-woocommerce'),
531 'type' => 'title',
532 'class' => 'easyauthnet-authorizenet-collapsible-section',
533 ],
534 'tokenization' => [
535 'title' => __('Tokenization', 'payment-gateway-for-authorize-net-for-woocommerce'),
536 'type' => 'checkbox',
537 'label' => __('Enable customers to save cards for future purchases.', 'payment-gateway-for-authorize-net-for-woocommerce'),
538 'default' => 'no',
539 ],
540 'transaction_type' => [
541 'title' => __('Transaction Type', 'payment-gateway-for-authorize-net-for-woocommerce'),
542 'type' => 'select',
543 'description' => __('Choose whether to authorize only or to authorize and capture the payment immediately.', 'payment-gateway-for-authorize-net-for-woocommerce'),
544 'desc_tip' => true,
545 'default' => 'auth_capture',
546 'options' => [
547 'auth_only' => __('Authorize Only — capture manually later', 'payment-gateway-for-authorize-net-for-woocommerce'),
548 'auth_capture' => __('Authorize & Capture — charge immediately', 'payment-gateway-for-authorize-net-for-woocommerce'),
549 ],
550 ],
551 'accepted_card_logos' => [
552 'title' => __('Accepted Card Logos', 'payment-gateway-for-authorize-net-for-woocommerce'),
553 'type' => 'multiselect',
554 'class' => 'wc-enhanced-select',
555 'css' => 'width: 350px;',
556 'description' => __('Controls which card logos appear on the checkout form (visual only).', 'payment-gateway-for-authorize-net-for-woocommerce'),
557 'desc_tip' => true,
558 'default' => ['visa', 'mastercard', 'amex', 'discover', 'jcb', 'diners'],
559 'options' => [
560 'visa' => 'Visa',
561 'mastercard' => 'MasterCard',
562 'amex' => 'American Express',
563 'discover' => 'Discover',
564 'jcb' => 'JCB',
565 'diners' => 'Diners Club',
566 ],
567 ],
568 'debug' => [
569 'title' => __('Debug Mode', 'payment-gateway-for-authorize-net-for-woocommerce'),
570 'type' => 'checkbox',
571 'label' => __('Enable logging for troubleshooting in WooCommerce → Status → Logs.', 'payment-gateway-for-authorize-net-for-woocommerce'),
572 'default' => 'no',
573 ],
574 ];
575 }
576
577 public function payment_fields() {
578 if ($this->supports('tokenization') && is_checkout()) {
579 $this->tokenization_script();
580 $this->saved_payment_methods();
581 $this->form();
582 $this->save_payment_method_checkbox();
583 } else {
584 $this->form();
585 }
586 }
587
588 public function form() {
589 wp_enqueue_script('wc-credit-card-form');
590 $id = esc_attr($this->id);
591 ?>
592 <div id="wc-<?php echo $id; ?>-form" class="wc-credit-card-form wc-payment-form">
593 <div class="easyauthnet_authorizenet-field full-width">
594 <label for="<?php echo $id; ?>-card-number">
595 <?php echo esc_html_x('Card number', 'Important', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
596 </label>
597 <div id="<?php echo $id; ?>-card-number-wrapper">
598 <input
599 id="<?php echo $id; ?>-card-number"
600 class="input-text wc-credit-card-form-card-number"
601 type="tel"
602 inputmode="numeric"
603 autocomplete="cc-number"
604 autocorrect="no"
605 autocapitalize="no"
606 spellcheck="no"
607 placeholder="&bull;&bull;&bull;&bull; &bull;&bull;&bull;&bull; &bull;&bull;&bull;&bull; &bull;&bull;&bull;&bull;"
608 <?php echo $this->field_name('card-number'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
609 />
610 </div>
611 </div>
612 <div class="easyauthnet_authorizenet-field half-width">
613 <label for="<?php echo $id; ?>-card-expiry">
614 <?php echo esc_html_x('Expiration date', 'Important', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
615 </label>
616 <div id="<?php echo $id; ?>-card-expiry-wrapper">
617 <input
618 id="<?php echo $id; ?>-card-expiry"
619 class="input-text wc-credit-card-form-card-expiry"
620 type="tel"
621 inputmode="numeric"
622 autocomplete="cc-exp"
623 autocorrect="no"
624 autocapitalize="no"
625 spellcheck="no"
626 placeholder="<?php echo esc_attr__('MM / YY', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>"
627 <?php echo $this->field_name('card-expiry'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
628 />
629 </div>
630 </div>
631 <div class="easyauthnet_authorizenet-field half-width">
632 <label for="<?php echo $id; ?>-card-cvc">
633 <?php echo esc_html_x('Security code', 'Important', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>
634 </label>
635 <div class="easyauthnet_authorizenet-cvc-wrapper">
636 <input
637 id="<?php echo $id; ?>-card-cvc"
638 class="input-text wc-credit-card-form-card-cvc"
639 type="tel"
640 inputmode="numeric"
641 autocomplete="off"
642 autocorrect="no"
643 autocapitalize="no"
644 spellcheck="no"
645 maxlength="4"
646 placeholder="<?php echo esc_attr__('CVV', 'payment-gateway-for-authorize-net-for-woocommerce'); ?>"
647 <?php echo $this->field_name('card-cvc'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
648 />
649 <div class="easyauthnet_authorizenet-parent-card-cvv-icon">
650 <svg class="easyauthnet_authorizenet-card-cvc-icon" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="var(--colorIconCardCvc)" role="img" aria-labelledby="cvcDesc">
651 <path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M15.337 4A5.493 5.493 0 0013 8.5c0 1.33.472 2.55 1.257 3.5H4a1 1 0 00-1 1v1a1 1 0 001 1h16a1 1 0 001-1v-.6a5.526 5.526 0 002-1.737V18a2 2 0 01-2 2H3a2 2 0 01-2-2V6a2 2 0 012-2h12.337zm6.707.293c.239.202.46.424.662.663a2.01 2.01 0 00-.662-.663z"></path>
652 <path opacity=".4" fill-rule="evenodd" clip-rule="evenodd" d="M13.6 6a5.477 5.477 0 00-.578 3H1V6h12.6z"></path>
653 <path fill-rule="evenodd" clip-rule="evenodd" d="M18.5 14a5.5 5.5 0 110-11 5.5 5.5 0 010 11zm-2.184-7.779h-.621l-1.516.77v.786l1.202-.628v3.63h.943V6.22h-.008zm1.807.629c.448 0 .762.251.762.613 0 .393-.37.668-.904.668h-.235v.668h.283c.565 0 .95.282.95.691 0 .393-.377.66-.911.66-.393 0-.786-.126-1.194-.37v.786c.44.189.88.291 1.312.291 1.029 0 1.736-.526 1.736-1.288 0-.535-.33-.967-.88-1.14.472-.157.778-.573.778-1.045 0-.738-.652-1.241-1.595-1.241a3.143 3.143 0 00-1.234.267v.77c.378-.212.763-.33 1.132-.33zm3.394 1.713c.574 0 .974.338.974.778 0 .463-.4.785-.974.785-.346 0-.707-.11-1.076-.337v.809c.385.173.778.26 1.163.26.204 0 .392-.032.573-.08a4.313 4.313 0 00.644-2.262l-.015-.33a1.807 1.807 0 00-.967-.252 3 3 0 00-.448.032V6.944h1.132a4.423 4.423 0 00-.362-.723h-1.587v2.475a3.9 3.9 0 01.943-.133z"></path>
654 </svg>
655
656 </div>
657 </div>
658 </div>
659 </div>
660 <?php
661 }
662
663 protected function is_using_saved_payment_method() {
664 // phpcs:ignore WordPress.Security.NonceVerification.Missing
665 return isset($_POST["wc-{$this->id}-payment-token"]) && 'new' !== $_POST["wc-{$this->id}-payment-token"];
666 }
667
668 public function process_payment($order_id) {
669 $order = wc_get_order($order_id);
670 try {
671 $this->log_start_payment($order);
672 if ($this->is_subscription_checkout_missing_token($order)) {
673 return $this->fail_due_to_missing_token();
674 }
675 if ($this->is_using_saved_card()) {
676 $order_total = floatval($order->get_total());
677 if ($order_total <= 0 && $this->is_subscription_order($order)) {
678 self::log("Subscription order with free trial - skipping charge");
679 $order->payment_complete();
680 return $this->success_redirect($order);
681 } else {
682 return $this->process_with_saved_card($order);
683 }
684 }
685 return $this->process_with_new_card($order);
686 } catch (Exception $ex) {
687 return $this->handle_payment_exception($ex);
688 }
689 }
690
691 protected function log_start_payment($order) {
692 $this->log("Starting payment processing", [
693 'order_id' => $order->get_id(),
694 'amount' => $order->get_total(),
695 'currency' => $order->get_currency(),
696 'customer_id' => $order->get_customer_id(),
697 'is_subscription' => $this->is_subscription_order($order),
698 'is_renewal' => $this->is_renewal_order($order),
699 'payment_method' => $order->get_payment_method()
700 ]);
701 }
702
703 protected function is_subscription_checkout_missing_token($order) {
704 // phpcs:ignore WordPress.Security.NonceVerification.Missing
705 $missing = $this->is_subscription_order($order) && !$this->is_using_saved_card() && empty($_POST['easyauthnet_authorizenet_token']);
706 if ($missing) {
707 // phpcs:ignore WordPress.Security.NonceVerification.Missing
708 $this->log("Subscription checkout missing token", ['is_using_saved_card' => $this->is_using_saved_card(), 'has_token' => !empty($_POST['easyauthnet_authorizenet_token'])]);
709 }
710 return $missing;
711 }
712
713 protected function fail_due_to_missing_token() {
714 $message = __('Saving a payment method is required for subscription purchases.', 'payment-gateway-for-authorize-net-for-woocommerce');
715 $this->log("Subscription checkout failed - missing token", ['error' => $message]);
716 wc_add_notice($message, 'error');
717 return ['result' => 'fail'];
718 }
719
720 protected function is_using_saved_card() {
721 $token_key = 'wc-' . $this->id . '-payment-token';
722
723 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified by WooCommerce during checkout processing.
724 $token_raw = isset($_POST[$token_key]) ? sanitize_text_field(wp_unslash($_POST[$token_key])) : '';
725
726 // Validate token: must not be 'new', must be numeric, and > 0
727 $is_using_saved = $token_raw !== 'new' && is_numeric($token_raw) && (int) $token_raw > 0;
728
729 // Log result
730 $this->log('Checking if using saved card', [
731 'is_using_saved' => $is_using_saved,
732 'token_value' => $token_raw,
733 ]);
734
735 return $is_using_saved;
736 }
737
738 protected function validate_card_type($card_type) {
739 $allowed_types = $this->accepted_card_logos;
740 $this->log("Validating card type", ['card_type' => $card_type, 'allowed_types' => $allowed_types]);
741 if (!in_array($card_type, $allowed_types)) {
742 // translators: %s is the credit card type (e.g., AMEX, VISA).
743 $error = sprintf(__('%s cards are not accepted. Please use a different payment method.', 'payment-gateway-for-authorize-net-for-woocommerce'), strtoupper($card_type));
744 $this->log("Invalid card type rejected", ['error' => $error]);
745 throw new Exception(esc_html($error));
746 }
747 return true;
748 }
749
750 protected function process_with_saved_card($order) {
751 $token_key = 'wc-' . $this->id . '-payment-token';
752 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce is validated earlier
753 if (isset($_POST[$token_key])) {
754 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified earlier in the request
755 $token_id = absint(wp_unslash($_POST[$token_key]));
756 } else {
757 $token_id = 0; // or handle as needed
758 }
759 $token = WC_Payment_Tokens::get($token_id);
760 $this->log("Processing with saved card", ['token_id' => $token_id, 'token_valid' => $this->is_token_valid($token), 'token_type' => $token ? $token->get_type() : 'none', 'token_last4' => $token ? $token->get_last4() : 'none']);
761 if (!$this->is_token_valid($token)) {
762 $error = __('Invalid saved payment method selected.', 'payment-gateway-for-authorize-net-for-woocommerce');
763 $this->log("Invalid token encountered", ['error' => $error]);
764 wc_add_notice($error, 'error');
765 return ['result' => 'fail'];
766 }
767 $response = EASYAUTHNET_AuthorizeNet_API_Handler::charge_saved_token($order, $token);
768 if (is_wp_error($response)) {
769 $this->log("Failed to charge saved token", ['error_code' => $response->get_error_code(), 'error_message' => $response->get_error_message()]);
770 return $this->fail_with_error($response);
771 }
772 $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]);
773 EASYAUTHNET_AuthorizeNet_API_Handler::complete_payment($order, $response['transaction_id']);
774 if ($this->is_subscription_order($order)) {
775 EASYAUTHNET_Subscription_Helper::assign_token_to_order_and_subscriptions($order, $token);
776 }
777 return $this->success_redirect($order);
778 }
779
780 protected function is_token_valid($token) {
781 return $token && $token->get_user_id() === get_current_user_id();
782 }
783
784 protected function process_with_new_card($order) {
785 $opaque_data = $this->get_opaque_card_data();
786 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified earlier in the request
787 $card_type = isset($_POST['easyauthnet_authorizenet_card_type']) ? sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_card_type'])) : '';
788 $this->log("Processing with new card", ['has_opaque_data' => !empty($opaque_data['gateway_token']['dataValue']), 'card_type' => $card_type]);
789 $this->validate_card_type($card_type);
790 if (empty($opaque_data['gateway_token']['dataValue'])) {
791 $error = __('Missing or invalid card token. Please try again.', 'payment-gateway-for-authorize-net-for-woocommerce');
792 $this->log("Missing card token", ['error' => $error]);
793 wc_add_notice($error, 'error');
794 return ['result' => 'fail'];
795 }
796 if ($this->should_save_card($order)) {
797 return $this->save_card_and_charge($order, $opaque_data);
798 }
799 return $this->charge_without_saving($order, $opaque_data['gateway_token']);
800 }
801
802 /**
803 * phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce is verified earlier in the request flow.
804 * phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Input is explicitly sanitized before use.
805 */
806 protected function get_opaque_card_data() {
807 $token_value = '';
808 if (isset($_POST['easyauthnet_authorizenet_token'])) {
809 $token_value = wc_clean(wp_unslash($_POST['easyauthnet_authorizenet_token']));
810 }
811 $last4 = '';
812 if (isset($_POST['easyauthnet_authorizenet_card_last4'])) {
813 $last4 = sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_card_last4']));
814 }
815 $card_type = '';
816 if (isset($_POST['easyauthnet_authorizenet_card_type'])) {
817 $card_type = sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_card_type']));
818 }
819 $expiry = '';
820 if (isset($_POST['easyauthnet_authorizenet_card_expiry'])) {
821 $expiry = sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_card_expiry']));
822 }
823 $data = [
824 'gateway_token' => [
825 'dataDescriptor' => 'COMMON.ACCEPT.INAPP.PAYMENT',
826 'dataValue' => $token_value,
827 ],
828 'last4' => $last4,
829 'card_type' => $card_type,
830 'expiry' => $expiry,
831 ];
832 $this->log(
833 'Retrieved opaque card data',
834 [
835 'has_token' => !empty($data['gateway_token']['dataValue']),
836 'last4' => !empty($data['last4']) ? '****' . $data['last4'] : 'none',
837 'card_type' => $data['card_type'] ?? 'none',
838 'expiry' => $data['expiry'] ?? 'none',
839 ]
840 );
841 return $data;
842 }
843
844 protected function should_save_card($order) {
845 $field_key = 'wc-' . $this->id . '-new-payment-method';
846
847 // Sanitize input early
848 $field_value = isset($_POST[$field_key]) ? sanitize_text_field(wp_unslash($_POST[$field_key])) : '';
849
850 // Validate against allowed values
851 $user_opted_in = in_array($field_value, ['1', 'true'], true);
852
853 // Determine if it's a subscription order
854 $is_subscription = $this->is_subscription_order($order);
855
856 // Log debug values safely
857 $this->log('Checking if should save card', [
858 'field_value' => $field_value,
859 'user_opted_in' => $user_opted_in,
860 'is_subscription' => $is_subscription,
861 ]);
862
863 return $user_opted_in || $is_subscription;
864 }
865
866 protected function save_card_and_charge($order, $opaque_data) {
867 $this->log("Attempting to save card and charge");
868 $token = $this->maybe_save_payment_token($order, $opaque_data);
869 if (is_wp_error($token)) {
870 $this->log("Failed to save payment token", ['error_code' => $token->get_error_code(), 'error_message' => $token->get_error_message()]);
871 return $this->fail_with_error($token);
872 }
873 if ($this->is_subscription_order($order)) {
874 EASYAUTHNET_Subscription_Helper::assign_token_to_order_and_subscriptions($order, $token);
875 }
876 $order_total = floatval($order->get_total());
877 if ($order_total <= 0 && $this->is_subscription_order($order)) {
878 $this->log("Subscription order with free trial - skipping charge");
879 $order->payment_complete();
880 return $this->success_redirect($order);
881 }
882 $response = EASYAUTHNET_AuthorizeNet_API_Handler::charge_saved_token($order, $token);
883 if (is_wp_error($response)) {
884 $this->log("Failed to charge after saving token", ['error_code' => $response->get_error_code(), 'error_message' => $response->get_error_message()]);
885 return $this->fail_with_error($response);
886 }
887 $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]);
888 EASYAUTHNET_AuthorizeNet_API_Handler::complete_payment($order, $response['transaction_id']);
889 return $this->success_redirect($order);
890 }
891
892 protected function charge_without_saving($order, $gateway_token) {
893 $this->log("Processing one-time payment without saving card");
894 $response = EASYAUTHNET_AuthorizeNet_API_Handler::charge_transaction($order, $gateway_token);
895 if (is_wp_error($response)) {
896 $this->log("Failed to process one-time payment", ['error_code' => $response->get_error_code(), 'error_message' => $response->get_error_message()]);
897 return $this->fail_with_error($response);
898 }
899 $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]);
900 EASYAUTHNET_AuthorizeNet_API_Handler::complete_payment($order, $response['transaction_id']);
901 return $this->success_redirect($order);
902 }
903
904 protected function fail_with_error($error) {
905 $this->log("Payment processing failed", ['error_code' => $error->get_error_code(), 'error_message' => $error->get_error_message()]);
906 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $error->get_error_message(), 400);
907 }
908
909 protected function is_subscription_order($order) {
910 $is_subscription = class_exists('EASYAUTHNET_Subscription_Helper') && EASYAUTHNET_Subscription_Helper::order_contains_subscription($order);
911 $this->log("Checking if order contains subscription", ['order_id' => $order->get_id(), 'is_subscription' => $is_subscription]);
912 return $is_subscription;
913 }
914
915 protected function is_renewal_order($order) {
916 $is_renewal = class_exists('EASYAUTHNET_Subscription_Helper') && EASYAUTHNET_Subscription_Helper::is_renewal_order($order);
917 $this->log("Checking if order is renewal", ['order_id' => $order->get_id(), 'is_renewal' => $is_renewal]);
918 return $is_renewal;
919 }
920
921 protected function success_redirect($order) {
922 $this->log("Payment processing successful - redirecting to thank you page", ['order_id' => $order->get_id(), 'status' => $order->get_status()]);
923 return [
924 'result' => 'success',
925 'redirect' => $this->get_return_url($order),
926 ];
927 }
928
929 protected function handle_payment_exception($e) {
930 $this->log("Payment processing exception", ['exception' => get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
931 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException('AuthorizeNet_API', $e->getMessage(), 400);
932 }
933
934 protected function store_card_details($order) {
935 $meta_updates = [];
936 if (isset($_POST['easyauthnet_authorizenet_card_last4'])) {
937 $last4 = wc_clean(wp_unslash($_POST['easyauthnet_authorizenet_card_last4']));
938 $order->update_meta_data(self::CARD_META_PREFIX . 'last4', $last4);
939 $meta_updates['last4'] = '****' . $last4;
940 }
941 if (isset($_POST['easyauthnet_authorizenet_card_expiry'])) {
942 $expiry = wc_clean(wp_unslash($_POST['easyauthnet_authorizenet_card_expiry']));
943 $order->update_meta_data(self::CARD_META_PREFIX . 'expiry', $expiry);
944 $meta_updates['expiry'] = $expiry;
945 }
946 if (isset($_POST['easyauthnet_authorizenet_card_type'])) {
947 $type = wc_clean(wp_unslash($_POST['easyauthnet_authorizenet_card_type']));
948 $order->update_meta_data(self::CARD_META_PREFIX . 'type', $type);
949 $meta_updates['type'] = $type;
950 }
951 $order->save();
952 $this->log('Stored card details in order meta', $meta_updates);
953 }
954
955 public function maybe_save_payment_token($order, $payment_data) {
956 $user_id = $order->get_user_id();
957 $this->log("Attempting to save payment token", ['user_id' => $user_id, 'has_token_data' => !empty($payment_data['gateway_token']['dataValue']), 'card_type' => $payment_data['card_type'] ?? 'none', 'last4' => !empty($payment_data['last4']) ? '****' . $payment_data['last4'] : 'none']);
958 if (!$user_id || empty($payment_data['gateway_token']['dataValue'])) {
959 $error = __('Missing token or user information for saving payment method.', 'payment-gateway-for-authorize-net-for-woocommerce');
960 $this->log("Cannot save token - missing requirements", ['error' => $error]);
961 return throw new Exception($error);
962 }
963 $customer_profile_id = get_user_meta($user_id, $this->customer_profile_id, true);
964 if ($customer_profile_id && !EASYAUTHNET_AuthorizeNet_API_Handler::validate_customer_profile($customer_profile_id)) {
965 $this->log("Existing customer profile not valid - recreating", ['customer_profile_id' => $this->mask_sensitive_data($customer_profile_id)]);
966 delete_user_meta($user_id, $this->customer_profile_id);
967 $customer_profile_id = '';
968 }
969 if (!$customer_profile_id) {
970 $create_result = EASYAUTHNET_AuthorizeNet_API_Handler::create_customer_profile($order, $payment_data['gateway_token']);
971 if (is_wp_error($create_result)) {
972 $this->log("Failed to create customer profile", ['error_code' => $create_result->get_error_code(), 'error_message' => $create_result->get_error_message()]);
973 if ($create_result->get_error_code() === 'duplicate_profile') {
974 $customer_profile_id = $create_result->get_error_data();
975 $this->log("Using duplicate profile ID", ['customer_profile_id' => $this->mask_sensitive_data($customer_profile_id)]);
976 } else {
977 return $create_result;
978 }
979 } else {
980 $customer_profile_id = $create_result['customerProfileId'];
981 $this->log("Successfully created new customer profile", ['customer_profile_id' => $this->mask_sensitive_data($customer_profile_id)]);
982 }
983 update_user_meta($user_id, $this->customer_profile_id, $customer_profile_id);
984 }
985 $payment_profile_id = EASYAUTHNET_AuthorizeNet_API_Handler::get_latest_payment_profile_id($customer_profile_id);
986 if (!$payment_profile_id) {
987 $error = __('Failed to retrieve saved payment method.', 'payment-gateway-for-authorize-net-for-woocommerce');
988 $this->log("No payment profile ID found", ['error' => $error]);
989 return throw new Exception($error);
990 }
991 $token = new WC_Payment_Token_CC();
992 $token->set_token($payment_profile_id);
993 $token->set_gateway_id($this->id);
994 $token->set_card_type(strtolower($payment_data['card_type']));
995 $token->set_last4($payment_data['last4']);
996 $token->set_expiry_month(substr($payment_data['expiry'], 0, 2));
997 $token->set_expiry_year('20' . substr($payment_data['expiry'], -2));
998 $token->set_user_id($user_id);
999 $token->set_default(true);
1000 $token->add_meta_data('customer_profile_id', $customer_profile_id, true);
1001 $token->add_meta_data('payment_profile_id', $payment_profile_id, true);
1002 $token->save();
1003 $this->log("Successfully saved payment token", ['token_id' => $token->get_id(), 'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id), 'card_type' => $token->get_card_type(), 'last4' => $token->get_last4(), 'expiry' => $token->get_expiry_month() . '/' . substr($token->get_expiry_year(), -2)]);
1004 return $token;
1005 }
1006
1007 public function process_refund($order_id, $amount = null, $reason = '') {
1008 try {
1009 $order = wc_get_order($order_id);
1010 $this->log("Processing refund", ['order_id' => $order_id, 'amount' => $amount, 'reason' => $reason]);
1011 return EASYAUTHNET_AuthorizeNet_API_Handler::process_payment_refund($order, $amount, $reason);
1012 } catch (Exception $e) {
1013 $this->log("Refund processing failed", ['exception' => get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
1014 return new WP_Error('error', $e->getMessage());
1015 }
1016 }
1017
1018 public function easyauthnet_register_anet_webhook_on_save() {
1019 try {
1020 EASYAUTHNET_AuthorizeNet_API_Handler::register_webhook();
1021 } catch (Exception $ex) {
1022
1023 }
1024 }
1025
1026 public function add_payment_method() {
1027 try {
1028 $this->log("Attempting to add payment method");
1029 if (!is_user_logged_in()) {
1030 throw new Exception(__('You must be logged in to add a payment method.', 'payment-gateway-for-authorize-net-for-woocommerce'));
1031 }
1032 $user_id = get_current_user_id();
1033 $required_fields = [
1034 'easyauthnet_authorizenet_token' => __('Missing payment token. Please try again.', 'payment-gateway-for-authorize-net-for-woocommerce'),
1035 'easyauthnet_authorizenet_card_last4' => __('Missing card last 4 digits.', 'payment-gateway-for-authorize-net-for-woocommerce'),
1036 'easyauthnet_authorizenet_card_type' => __('Missing card type.', 'payment-gateway-for-authorize-net-for-woocommerce'),
1037 'easyauthnet_authorizenet_card_expiry' => __('Missing card expiration date.', 'payment-gateway-for-authorize-net-for-woocommerce')
1038 ];
1039 foreach ($required_fields as $field => $error_message) {
1040 if (empty($_POST[$field])) {
1041 throw new Exception($error_message);
1042 }
1043 }
1044 $token_data = [
1045 'dataDescriptor' => 'COMMON.ACCEPT.INAPP.PAYMENT',
1046 'dataValue' => isset($_POST['easyauthnet_authorizenet_token']) ? wc_clean(wp_unslash($_POST['easyauthnet_authorizenet_token'])) : '',
1047 ];
1048 $card_last4 = isset($_POST['easyauthnet_authorizenet_card_last4']) ? sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_card_last4'])) : '';
1049 $card_type = isset($_POST['easyauthnet_authorizenet_card_type']) ? sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_card_type'])) : '';
1050 $card_expiry = isset($_POST['easyauthnet_authorizenet_card_expiry']) ? sanitize_text_field(wp_unslash($_POST['easyauthnet_authorizenet_card_expiry'])) : '';
1051 $expiry_parts = array_map('trim', explode('/', $card_expiry));
1052 if (count($expiry_parts) !== 2) {
1053 throw new Exception(__('Invalid card expiration date format. Use MM/YY.', 'payment-gateway-for-authorize-net-for-woocommerce'));
1054 }
1055 $expiry_month = str_pad($expiry_parts[0], 2, '0', STR_PAD_LEFT);
1056 $expiry_year = '20' . $expiry_parts[1];
1057 $this->log("Creating customer profile from token", [
1058 'user_id' => $user_id,
1059 'card_type' => $card_type,
1060 'last4' => '****' . $card_last4,
1061 'expiry' => $expiry_month . '/' . substr($expiry_year, -2)
1062 ]);
1063 $token = EASYAUTHNET_AuthorizeNet_API_Handler::create_customer_profile_from_token(
1064 $user_id,
1065 $token_data,
1066 [
1067 'last4' => $card_last4,
1068 'type' => $card_type,
1069 'expiry_month' => $expiry_month,
1070 'expiry_year' => $expiry_year
1071 ]
1072 );
1073 if (is_wp_error($token)) {
1074 throw new Exception($token->get_error_message());
1075 }
1076 if (!empty($_POST['wc-' . $this->id . '-new-payment-method-default'])) {
1077 WC_Payment_Tokens::set_users_default($user_id, $token->get_id());
1078 $this->log("Set new payment method as default", ['token_id' => $token->get_id()]);
1079 }
1080 $this->log("Successfully added payment method", ['token_id' => $token->get_id(), 'card_type' => $card_type, 'last4' => '****' . $card_last4, 'expiry' => $expiry_month . '/' . substr($expiry_year, -2)]);
1081 return ['result' => 'success', 'redirect' => wc_get_endpoint_url('payment-methods')];
1082 } catch (Exception $e) {
1083 $this->log("Failed to add payment method", ['exception' => get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
1084 wc_add_notice($e->getMessage(), 'error');
1085 return ['result' => 'failure', 'redirect' => wc_get_endpoint_url('add-payment-method')];
1086 }
1087 }
1088
1089 protected function mask_sensitive_data($data) {
1090 if (is_string($data) && strlen($data) > 4) {
1091 return substr($data, 0, 2) . '***' . substr($data, -2);
1092 }
1093 return '***MASKED***';
1094 }
1095
1096 protected function format_log_message($message, $context = []) {
1097 $formatted = $message;
1098 if (!empty($context)) {
1099 $formatted .= "\n" . json_encode($this->mask_sensitive_data_in_context($context), JSON_PRETTY_PRINT);
1100 }
1101 return $formatted;
1102 }
1103
1104 protected function mask_sensitive_data_in_context($context) {
1105 if (!is_array($context)) {
1106 return $context;
1107 }
1108 $masked = [];
1109 $sensitive_keys = ['key', 'token', 'password', 'login', 'id', 'secret', 'signature'];
1110 foreach ($context as $key => $value) {
1111 $should_mask = false;
1112 foreach ($sensitive_keys as $sensitive) {
1113 if (stripos($key, $sensitive) !== false) {
1114 $should_mask = true;
1115 break;
1116 }
1117 }
1118 $masked[$key] = $should_mask ? $this->mask_sensitive_data($value) : $value;
1119 }
1120 return $masked;
1121 }
1122
1123 protected function log($message, $context = []) {
1124 if ($this->debug !== 'yes') {
1125 return;
1126 }
1127 $formatted_message = $this->format_log_message($message, $context);
1128 wc_get_logger()->info($formatted_message, ['source' => 'easyauthnet_authorizenet']);
1129 }
1130
1131 public function process_capture($order_id) {
1132 $order = wc_get_order($order_id);
1133 $transaction_id = $order->get_meta('_easyauthnet_authorize_net_auth_transaction_id');
1134 $this->log("Attempting to capture authorized payment", [
1135 'order_id' => $order_id,
1136 'transaction_id' => $transaction_id ?: 'none'
1137 ]);
1138 if (empty($transaction_id)) {
1139 $error = __('No authorized transaction found to capture.', 'payment-gateway-for-authorize-net-for-woocommerce');
1140 $this->log("Capture failed - no transaction ID", ['error' => $error]);
1141 return throw new Exception($error);
1142 }
1143 $response = EASYAUTHNET_AuthorizeNet_API_Handler::capture_authorized_transaction($order, $transaction_id);
1144 if (is_wp_error($response)) {
1145 $this->log("Capture API request failed", ['error_code' => $response->get_error_code(), 'error_message' => $response->get_error_message()]);
1146 return $response;
1147 }
1148 $this->log("Successfully captured authorized payment", ['transaction_id' => $transaction_id, 'response_code' => $response['response_code'] ?? null, 'auth_code' => $response['auth_code'] ?? null]);
1149 $order->payment_complete($transaction_id);
1150 // translators: 1: Transaction ID, 2: Authorization code.
1151 $order->add_order_note(sprintf(__('Authorized payment captured successfully. Transaction ID: %1$s, Auth Code: %2$s.', 'payment-gateway-for-authorize-net-for-woocommerce'), $transaction_id, $response['auth_code'] ?? null));
1152 return true;
1153 }
1154
1155 public function process_order_action_capture($order) {
1156 $this->log("Processing order capture action", ['order_id' => $order->get_id()]);
1157 $result = $this->process_capture($order->get_id());
1158 if (is_wp_error($result)) {
1159 $this->log("Order capture action failed", ['error_code' => $result->get_error_code(), 'error_message' => $result->get_error_message()]);
1160 $order->add_order_note(__('Capture failed: ', 'payment-gateway-for-authorize-net-for-woocommerce') . $result->get_error_message(), false, true);
1161 } else {
1162 $this->log("Order capture action completed successfully", ['order_id' => $order->get_id()]);
1163 }
1164 }
1165
1166 protected function maybe_run_customer_profile_migration_once() {
1167 $flag = get_option(self::CUSTOMER_PROFILE_MIGRATION_FLAG, '');
1168 if (!empty($flag)) {
1169 return;
1170 }
1171 if (class_exists('EASYAUTHNET_AuthorizeNet_API_Handler') && method_exists('EASYAUTHNET_AuthorizeNet_API_Handler', 'prime_runtime_credentials')) {
1172 EASYAUTHNET_AuthorizeNet_API_Handler::prime_runtime_credentials(
1173 $this->environment ?? 'sandbox',
1174 $this->api_login_id ?? '',
1175 $this->transaction_key ?? ''
1176 );
1177 }
1178 try {
1179 $result = ( class_exists('EASYAUTHNET_AuthorizeNet_API_Handler') && method_exists('EASYAUTHNET_AuthorizeNet_API_Handler', 'migrate_customer_profile_ids') ) ? EASYAUTHNET_AuthorizeNet_API_Handler::migrate_customer_profile_ids() : ['migrated' => 0, 'failed' => 0, 'total' => 0];
1180 update_option(
1181 self::CUSTOMER_PROFILE_MIGRATION_FLAG,
1182 wp_json_encode([
1183 'done' => true,
1184 'time' => time(),
1185 'result' => [
1186 'migrated' => (int) ( $result['migrated'] ?? 0 ),
1187 'failed' => (int) ( $result['failed'] ?? 0 ),
1188 'total' => (int) ( $result['total'] ?? 0 ),
1189 ],
1190 ]),
1191 false
1192 );
1193 if (method_exists($this, 'log')) {
1194 $this->log(__METHOD__, 'Customer profile migration finished', $result);
1195 }
1196 } catch (\Throwable $e) {
1197 if (method_exists($this, 'log')) {
1198 $this->log(__METHOD__, 'Customer profile migration threw', [
1199 'exception' => get_class($e),
1200 'message' => $e->getMessage(),
1201 ]);
1202 }
1203 update_option(
1204 self::CUSTOMER_PROFILE_MIGRATION_FLAG,
1205 wp_json_encode([
1206 'done' => false,
1207 'time' => time(),
1208 'error' => $e->getMessage(),
1209 ]),
1210 false
1211 );
1212 }
1213 }
1214 }
1215