Plugin Directory

Changeset 3491681


Ignore:
Timestamp:
03/26/2026 10:36:52 AM (8 days ago)
Author:
easypayment
Message:

tags/1.0.10

Location:
payment-gateway-for-authorize-net-for-woocommerce
Files:
63 added
5 edited

Legend:

Unmodified
Added
Removed
  • payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-api-handler.php

    r3473557 r3491681  
    903903                $meta_key = EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_' . $env;
    904904                update_user_meta($user_id, $meta_key, $dup_id);
     905
     906                $validation_list = $response['validationDirectResponseList'] ?? [];
     907                $token_consumed  = !empty($validation_list) && $validation_list !== [];
     908
    905909                self::log(__METHOD__, 'Duplicate profile detected; reusing existing profile id', [
    906910                    'user_id' => $user_id,
    907911                    'profile_id' => $dup_id,
    908912                    'env' => $env,
     913                    'token_consumed' => $token_consumed,
    909914                ]);
    910915                return [
     
    921926                        ],
    922927                    ],
    923                     'is_duplicate' => true,
     928                    'is_duplicate'    => true,
     929                    'token_consumed'  => $token_consumed,
    924930                ];
    925931            }
     
    14361442    }
    14371443
     1444    /**
     1445     * Full list of Authorize.Net event types this plugin handles.
     1446     * Any change here is automatically applied to existing webhook registrations
     1447     * via update_webhook_event_types_if_needed().
     1448     */
     1449    protected static function get_required_webhook_event_types(): array {
     1450        return [
     1451            'net.authorize.payment.authcapture.created',
     1452            'net.authorize.payment.authorization.created',
     1453            'net.authorize.payment.priorAuthCapture.created',
     1454            'net.authorize.payment.void.created',
     1455            'net.authorize.payment.refund.created',
     1456            'net.authorize.customer.deleted',
     1457            'net.authorize.customer.paymentProfile.deleted',
     1458        ];
     1459    }
     1460
    14381461    public static function register_webhook() {
    14391462        self::init_settings();
     
    14521475        self::log(__METHOD__, 'Checking existing webhook', ['option_key' => $option_key, 'existing_webhook_id' => $existing_webhook_id]);
    14531476        if ($existing_webhook_id) {
    1454             self::log(__METHOD__, 'Webhook already registered — skipping registration', ['webhook_id' => $existing_webhook_id]);
     1477            // Webhook already registered — ensure event types are up to date.
     1478            self::update_webhook_event_types_if_needed($existing_webhook_id, $is_sandbox);
    14551479            return;
    14561480        }
     
    14591483        $auth_header = 'Basic ' . base64_encode("{$api_login_id}:{$transaction_key}");
    14601484        $body = [
    1461             'url' => $webhook_url,
    1462             'eventTypes' => [
    1463                 'net.authorize.payment.authcapture.created',
    1464                 'net.authorize.payment.authorization.created',
    1465                 'net.authorize.payment.priorAuthCapture.created',
    1466                 'net.authorize.payment.void.created',
    1467                 'net.authorize.payment.refund.created',
    1468             ],
    1469             'status' => 'active',
     1485            'url'        => $webhook_url,
     1486            'eventTypes' => self::get_required_webhook_event_types(),
     1487            'status'     => 'active',
    14701488        ];
    14711489        self::log(__METHOD__, 'Prepared REST request', ['endpoint' => $endpoint, 'webhook_url' => $webhook_url, 'event_types' => $body['eventTypes']]);
     
    14951513        } catch (Exception $e) {
    14961514            self::log(__METHOD__, 'Webhook registration failed', ['message' => $e->getMessage()]);
     1515        }
     1516    }
     1517
     1518    /**
     1519     * Update an existing Authorize.Net webhook subscription if its event-type
     1520     * list differs from get_required_webhook_event_types(). This ensures that
     1521     * sites which registered the webhook with an older version of the plugin
     1522     * automatically receive any newly required events on the next settings save.
     1523     */
     1524    protected static function update_webhook_event_types_if_needed(string $webhook_id, bool $is_sandbox): void {
     1525        $api_login_id  = self::$api_login_id;
     1526        $transaction_key = self::$transaction_key;
     1527        $base_endpoint = $is_sandbox ? 'https://apitest.authorize.net/rest/v1/webhooks' : 'https://api.authorize.net/rest/v1/webhooks';
     1528        $auth_header   = 'Basic ' . base64_encode("{$api_login_id}:{$transaction_key}");
     1529
     1530        // Fetch the current webhook definition.
     1531        try {
     1532            $get_response = wp_remote_get("{$base_endpoint}/{$webhook_id}", [
     1533                'headers' => [
     1534                    'Content-Type' => 'application/json',
     1535                    'Authorization' => $auth_header,
     1536                    'User-Agent' => 'WooCommerce-Easy-Payment-AuthorizeNet/' . EASYAUTHNET_AUTHORIZENET_VERSION,
     1537                ],
     1538                'timeout' => 15,
     1539            ]);
     1540
     1541            if (is_wp_error($get_response)) {
     1542                throw new Exception($get_response->get_error_message());
     1543            }
     1544
     1545            $get_http_code = (int) wp_remote_retrieve_response_code($get_response);
     1546            $current = json_decode(wp_remote_retrieve_body($get_response), true);
     1547
     1548            // If the webhook no longer exists on Authorize.Net's side (404 or missing webhookId),
     1549            // clear the stored option so register_webhook() creates a fresh one next time.
     1550            if ($get_http_code !== 200 || empty($current['webhookId'])) {
     1551                $option_key = $is_sandbox ? 'easyauthnet_anet_webhook_id_sandbox' : 'easyauthnet_anet_webhook_id_live';
     1552                delete_option($option_key);
     1553                self::log(__METHOD__, 'Stored webhook no longer exists in Authorize.Net — cleared option for re-registration', [
     1554                    'webhook_id'  => $webhook_id,
     1555                    'http_status' => $get_http_code,
     1556                    'option_key'  => $option_key,
     1557                ]);
     1558                return;
     1559            }
     1560
     1561            $current_types = isset($current['eventTypes']) && is_array($current['eventTypes'])
     1562                ? $current['eventTypes']
     1563                : [];
     1564
     1565            $required_types = self::get_required_webhook_event_types();
     1566
     1567            $missing = array_diff($required_types, $current_types);
     1568
     1569            if (empty($missing)) {
     1570                self::log(__METHOD__, 'Webhook event types are already up to date', ['webhook_id' => $webhook_id]);
     1571                return;
     1572            }
     1573
     1574            self::log(__METHOD__, 'Updating webhook with missing event types', [
     1575                'webhook_id'    => $webhook_id,
     1576                'missing_types' => array_values($missing),
     1577            ]);
     1578
     1579            // PATCH the existing webhook with the full required list.
     1580            $patch_response = wp_remote_request("{$base_endpoint}/{$webhook_id}", [
     1581                'method'  => 'PUT',
     1582                'headers' => [
     1583                    'Content-Type' => 'application/json',
     1584                    'Authorization' => $auth_header,
     1585                    'User-Agent' => 'WooCommerce-Easy-Payment-AuthorizeNet/' . EASYAUTHNET_AUTHORIZENET_VERSION,
     1586                ],
     1587                'body'    => wp_json_encode([
     1588                    'url'        => isset($current['url']) ? $current['url'] : home_url('/wp-json/easyauthnet-authorizenet/v1/webhook'),
     1589                    'eventTypes' => $required_types,
     1590                    'status'     => 'active',
     1591                ]),
     1592                'timeout' => 20,
     1593            ]);
     1594
     1595            if (is_wp_error($patch_response)) {
     1596                throw new Exception($patch_response->get_error_message());
     1597            }
     1598
     1599            $http_code = wp_remote_retrieve_response_code($patch_response);
     1600            self::log(__METHOD__, 'Webhook update response', [
     1601                'webhook_id'  => $webhook_id,
     1602                'http_status' => $http_code,
     1603            ]);
     1604        } catch (Exception $e) {
     1605            self::log(__METHOD__, 'Failed to update webhook event types', [
     1606                'webhook_id' => $webhook_id,
     1607                'message'    => $e->getMessage(),
     1608            ]);
    14971609        }
    14981610    }
     
    15481660            self::log(__METHOD__, 'Exception during profile validation', ['exception' => get_class($e), 'message' => $e->getMessage(), 'profile_id' => $customer_profile_id]);
    15491661            return false;
     1662        }
     1663    }
     1664
     1665    public static function delete_customer_profile_from_cim($customer_profile_id) {
     1666        self::init_settings();
     1667
     1668        $endpoint = self::get_api_endpoint();
     1669        self::log(__METHOD__, 'Deleting customer profile from CIM', [
     1670            'customer_profile_id' => $customer_profile_id,
     1671        ]);
     1672
     1673        try {
     1674            $request = [
     1675                'root_element' => 'deleteCustomerProfileRequest',
     1676                'merchantAuthentication' => [
     1677                    'name' => self::$api_login_id,
     1678                    'transactionKey' => self::$transaction_key,
     1679                ],
     1680                'customerProfileId' => $customer_profile_id,
     1681            ];
     1682
     1683            $response = self::send_request($endpoint, $request);
     1684            $result_code = $response['messages']['resultCode'] ?? '';
     1685            $message_code = $response['messages']['message']['code'] ?? '';
     1686            $message_text = $response['messages']['message']['text'] ?? '';
     1687
     1688            // E00040 = record not found — treat as success (already deleted on their side).
     1689            if ($result_code === 'Ok' || $message_code === 'E00040') {
     1690                self::log(__METHOD__, 'Customer profile delete treated as success', [
     1691                    'customer_profile_id' => $customer_profile_id,
     1692                    'message_code' => $message_code ?: 'I00001',
     1693                    'message_text' => $message_text,
     1694                ]);
     1695                return true;
     1696            }
     1697
     1698            self::log(__METHOD__, 'Customer profile delete failed', [
     1699                'customer_profile_id' => $customer_profile_id,
     1700                'result_code' => $result_code ?: 'none',
     1701                'message_code' => $message_code ?: 'none',
     1702                'message_text' => $message_text ?: __('Unknown error.', 'payment-gateway-for-authorize-net-for-woocommerce'),
     1703            ]);
     1704
     1705            return new WP_Error(
     1706                'easyauthnet_delete_customer_profile_failed',
     1707                $message_text ?: __('Failed to delete customer profile from Authorize.Net CIM.', 'payment-gateway-for-authorize-net-for-woocommerce'),
     1708                [
     1709                    'customer_profile_id' => $customer_profile_id,
     1710                    'response' => $response,
     1711                ]
     1712            );
     1713        } catch (Exception $e) {
     1714            self::log(__METHOD__, 'Exception deleting customer profile', [
     1715                'customer_profile_id' => $customer_profile_id,
     1716                'exception' => get_class($e),
     1717                'message' => $e->getMessage(),
     1718            ]);
     1719
     1720            return new WP_Error('easyauthnet_delete_customer_profile_exception', $e->getMessage());
     1721        }
     1722    }
     1723
     1724    public static function delete_customer_payment_profile_from_cim($customer_profile_id, $payment_profile_id) {
     1725        self::init_settings();
     1726
     1727        $endpoint = self::get_api_endpoint();
     1728        self::log(__METHOD__, 'Deleting customer payment profile from CIM', [
     1729            'customer_profile_id' => $customer_profile_id,
     1730            'payment_profile_id' => $payment_profile_id,
     1731        ]);
     1732
     1733        try {
     1734            $request = [
     1735                'root_element' => 'deleteCustomerPaymentProfileRequest',
     1736                'merchantAuthentication' => [
     1737                    'name' => self::$api_login_id,
     1738                    'transactionKey' => self::$transaction_key,
     1739                ],
     1740                'customerProfileId' => $customer_profile_id,
     1741                'customerPaymentProfileId' => $payment_profile_id,
     1742            ];
     1743
     1744            $response = self::send_request($endpoint, $request);
     1745            $result_code = $response['messages']['resultCode'] ?? '';
     1746            $message_code = $response['messages']['message']['code'] ?? '';
     1747            $message_text = $response['messages']['message']['text'] ?? '';
     1748
     1749            if ($result_code === 'Ok' || $message_code === 'E00040') {
     1750                self::log(__METHOD__, 'Customer payment profile delete treated as success', [
     1751                    'customer_profile_id' => $customer_profile_id,
     1752                    'payment_profile_id' => $payment_profile_id,
     1753                    'message_code' => $message_code ?: 'I00001',
     1754                    'message_text' => $message_text,
     1755                ]);
     1756                return true;
     1757            }
     1758
     1759            self::log(__METHOD__, 'Customer payment profile delete failed', [
     1760                'customer_profile_id' => $customer_profile_id,
     1761                'payment_profile_id' => $payment_profile_id,
     1762                'result_code' => $result_code ?: 'none',
     1763                'message_code' => $message_code ?: 'none',
     1764                'message_text' => $message_text ?: __('Unknown error.', 'payment-gateway-for-authorize-net-for-woocommerce'),
     1765            ]);
     1766
     1767            return new WP_Error(
     1768                'easyauthnet_delete_payment_profile_failed',
     1769                $message_text ?: __('Failed to delete payment profile from Authorize.Net CIM.', 'payment-gateway-for-authorize-net-for-woocommerce'),
     1770                [
     1771                    'customer_profile_id' => $customer_profile_id,
     1772                    'payment_profile_id' => $payment_profile_id,
     1773                    'response' => $response,
     1774                ]
     1775            );
     1776        } catch (Exception $e) {
     1777            self::log(__METHOD__, 'Exception deleting customer payment profile', [
     1778                'customer_profile_id' => $customer_profile_id,
     1779                'payment_profile_id' => $payment_profile_id,
     1780                'exception' => get_class($e),
     1781                'message' => $e->getMessage(),
     1782            ]);
     1783
     1784            return new WP_Error('easyauthnet_delete_payment_profile_exception', $e->getMessage());
     1785        }
     1786    }
     1787
     1788    public static function validate_customer_payment_profile($customer_profile_id, $payment_profile_id) {
     1789        self::init_settings();
     1790
     1791        $endpoint = self::get_api_endpoint();
     1792        self::log(__METHOD__, 'Validating customer payment profile', [
     1793            'customer_profile_id' => $customer_profile_id,
     1794            'payment_profile_id' => $payment_profile_id,
     1795        ]);
     1796
     1797        try {
     1798            $request = [
     1799                'root_element' => 'getCustomerPaymentProfileRequest',
     1800                'merchantAuthentication' => [
     1801                    'name' => self::$api_login_id,
     1802                    'transactionKey' => self::$transaction_key,
     1803                ],
     1804                'customerProfileId' => $customer_profile_id,
     1805                'customerPaymentProfileId' => $payment_profile_id,
     1806            ];
     1807
     1808            $response = self::send_request($endpoint, $request);
     1809            $result_code = $response['messages']['resultCode'] ?? '';
     1810            $message_code = $response['messages']['message']['code'] ?? '';
     1811
     1812            if ($result_code === 'Ok') {
     1813                self::log(__METHOD__, 'Customer payment profile exists', [
     1814                    'customer_profile_id' => $customer_profile_id,
     1815                    'payment_profile_id' => $payment_profile_id,
     1816                ]);
     1817                return true;
     1818            }
     1819
     1820            if ($message_code === 'E00040') {
     1821                self::log(__METHOD__, 'Customer payment profile not found', [
     1822                    'customer_profile_id' => $customer_profile_id,
     1823                    'payment_profile_id' => $payment_profile_id,
     1824                ]);
     1825                return false;
     1826            }
     1827
     1828            self::log(__METHOD__, 'Customer payment profile validation failed open after API response', [
     1829                'customer_profile_id' => $customer_profile_id,
     1830                'payment_profile_id' => $payment_profile_id,
     1831                'result_code' => $result_code ?: 'none',
     1832                'message_code' => $message_code ?: 'none',
     1833                'message_text' => $response['messages']['message']['text'] ?? '',
     1834            ]);
     1835
     1836            return true;
     1837        } catch (Exception $e) {
     1838            self::log(__METHOD__, 'Exception validating customer payment profile - failing open', [
     1839                'customer_profile_id' => $customer_profile_id,
     1840                'payment_profile_id' => $payment_profile_id,
     1841                'exception' => get_class($e),
     1842                'message' => $e->getMessage(),
     1843            ]);
     1844
     1845            return true;
    15501846        }
    15511847    }
  • payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-easy-payment-authorizenet-gateway.php

    r3473557 r3491681  
    2929    public $customer_profile_id;
    3030    private static $easyauthnet_notice_rendered = false;
     31    private static $cim_skip_token_ids = [];
     32
     33    /**
     34     * Mark a token ID so that the woocommerce_payment_token_deleted hook will
     35     * skip the CIM deletion for it. Used by the webhook handler when a profile
     36     * has already been removed on the Authorize.Net side.
     37     */
     38    public static function skip_cim_sync_for_token(int $token_id): void {
     39        self::$cim_skip_token_ids[$token_id] = true;
     40    }
     41
     42    public static function unskip_cim_sync_for_token(int $token_id): void {
     43        unset(self::$cim_skip_token_ids[$token_id]);
     44    }
    3145
    3246    protected const CUSTOMER_PROFILE_MIGRATION_FLAG = 'easyauthnet_authorizenet_profile_migration_v1';
     
    173187        add_filter('safe_style_css', array($this, 'easyauthnet_allowed_css_properties'));
    174188        add_action('admin_notices', [$this, 'easyauthnet_cc_missing_creds_notice']);
     189        add_action('woocommerce_payment_token_deleted', [$this, 'easyauthnet_sync_delete_cim_on_token_removed'], 10, 2);
     190        add_action('woocommerce_account_payment_methods_endpoint', [$this, 'easyauthnet_prune_stale_tokens_on_account_page']);
     191        add_action('before_delete_user', [$this, 'easyauthnet_delete_cim_profile_on_user_deleted']);
    175192    }
    176193
     
    11741191            $this->log("Failed to save payment token", ['error_code' => $error_code, 'error_message' => $token->get_error_message()]);
    11751192
     1193            if ($error_code === 'easyauthnet_ots_token_consumed') {
     1194                $this->log('OTS token was consumed by duplicate-profile check; direct charge not possible — asking customer to retry', [
     1195                    'order_id' => $order->get_id(),
     1196                ]);
     1197                wc_add_notice($token->get_error_message(), 'error');
     1198                return ['result' => 'failure'];
     1199            }
     1200
    11761201            // Graceful fallback: intermittent Authorize.Net E00114 (Invalid OTS Token).
    11771202            // In this scenario, the CIM save fails but we can still charge the order using opaque data.
     
    15701595                ]);
    15711596
     1597                if (!empty($create_result['is_duplicate']) && !empty($create_result['token_consumed'])) {
     1598                    if ($user_id > 0) {
     1599                        update_user_meta($user_id, $this->customer_profile_id, $customer_profile_id);
     1600                    }
     1601                    if ($order instanceof WC_Order) {
     1602                        $order->update_meta_data('_easyauthnet_authorizenet_customer_profile_id', $customer_profile_id);
     1603                        $order->save();
     1604                    }
     1605               
     1606               
     1607                    if (!empty($create_result['customerPaymentProfileIdList'])) {
     1608                        $list      = $create_result['customerPaymentProfileIdList'];
     1609                        $candidate = '';
     1610                        if (is_array($list)) {
     1611                            if (isset($list['numericString'])) {
     1612                                $ns        = $list['numericString'];
     1613                                $candidate = is_array($ns) ? (string) reset($ns) : (string) $ns;
     1614                            } else {
     1615                                $candidate = (string) reset($list);
     1616                            }
     1617                        } else {
     1618                            $candidate = (string) $list;
     1619                        }
     1620                        if ($candidate !== '') {
     1621                            $payment_profile_id_from_create = $candidate;
     1622                            $this->log('OTS token consumed by duplicate-profile; reusing payment profile ID returned in response', [
     1623                                'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id),
     1624                                'payment_profile_id'  => $this->mask_sensitive_data($payment_profile_id_from_create),
     1625                            ]);
     1626                            // Fall through — $payment_profile_id_from_create will be used below.
     1627                        }
     1628                    }
     1629 
     1630                    if (empty($payment_profile_id_from_create)) {
     1631                        $this->log('OTS token consumed by duplicate-profile validation; cannot reuse for payment profile creation', [
     1632                            'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id),
     1633                        ]);
     1634                        return new WP_Error(
     1635                            'easyauthnet_ots_token_consumed',
     1636                            __('Your payment session has expired. Please try again.', 'payment-gateway-for-authorize-net-for-woocommerce'),
     1637                            ['customer_profile_id' => $customer_profile_id]
     1638                        );
     1639                    }
     1640               
     1641                }
    15721642                if (!empty($create_result['customerPaymentProfileIdList'])) {
    15731643                    $list = $create_result['customerPaymentProfileIdList'];
     
    16641734    }
    16651735
     1736    public function easyauthnet_sync_delete_cim_on_token_removed($token_id, $token) {
     1737        if (!$token instanceof WC_Payment_Token) {
     1738            $this->log('Skipping CIM delete for invalid token instance', ['token_id' => $token_id]);
     1739            return;
     1740        }
     1741
     1742        if ($token->get_gateway_id() !== $this->id) {
     1743            return;
     1744        }
     1745
     1746        if (!empty(self::$cim_skip_token_ids[$token_id])) {
     1747            $this->log('Skipping CIM delete for internally pruned token', ['token_id' => $token_id]);
     1748            return;
     1749        }
     1750
     1751        $customer_profile_id = (string) $token->get_meta('customer_profile_id');
     1752        $payment_profile_id = (string) $token->get_meta('payment_profile_id');
     1753        if ($payment_profile_id === '') {
     1754            $payment_profile_id = (string) $token->get_token();
     1755        }
     1756
     1757        if ($customer_profile_id === '' || $payment_profile_id === '') {
     1758            $this->log('Skipping CIM delete for legacy or incomplete token', [
     1759                'token_id' => $token_id,
     1760                'has_customer_profile_id' => $customer_profile_id !== '',
     1761                'has_payment_profile_id' => $payment_profile_id !== '',
     1762            ]);
     1763            return;
     1764        }
     1765
     1766        $result = EASYAUTHNET_AuthorizeNet_API_Handler::delete_customer_payment_profile_from_cim($customer_profile_id, $payment_profile_id);
     1767        if (is_wp_error($result)) {
     1768            $this->log('Failed deleting CIM payment profile after Woo token removal', [
     1769                'token_id' => $token_id,
     1770                'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id),
     1771                'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id),
     1772                'error_code' => $result->get_error_code(),
     1773                'error_message' => $result->get_error_message(),
     1774            ]);
     1775            return;
     1776        }
     1777
     1778        $this->log('Deleted CIM payment profile after Woo token removal', [
     1779            'token_id' => $token_id,
     1780            'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id),
     1781            'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id),
     1782        ]);
     1783
     1784        $this->maybe_delete_customer_profile_meta_if_unused($token->get_user_id(), $customer_profile_id);
     1785    }
     1786
     1787    public function easyauthnet_delete_cim_profile_on_user_deleted($user_id) {
     1788        $user_id = (int) $user_id;
     1789        if ($user_id <= 0) {
     1790            return;
     1791        }
     1792
     1793        $customer_profile_id = (string) get_user_meta($user_id, $this->customer_profile_id, true);
     1794        if ($customer_profile_id === '') {
     1795            return;
     1796        }
     1797
     1798        $result = EASYAUTHNET_AuthorizeNet_API_Handler::delete_customer_profile_from_cim($customer_profile_id);
     1799        if (is_wp_error($result)) {
     1800            $this->log('Failed deleting CIM customer profile after WP user deletion', [
     1801                'user_id' => $user_id,
     1802                'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id),
     1803                'error_code' => $result->get_error_code(),
     1804                'error_message' => $result->get_error_message(),
     1805            ]);
     1806            return;
     1807        }
     1808
     1809        $this->log('Deleted CIM customer profile after WP user deletion', [
     1810            'user_id' => $user_id,
     1811            'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id),
     1812        ]);
     1813    }
     1814
     1815    public function easyauthnet_prune_stale_tokens_on_account_page() {
     1816        if (!is_user_logged_in()) {
     1817            return;
     1818        }
     1819
     1820        $user_id = get_current_user_id();
     1821        $tokens = WC_Payment_Tokens::get_customer_tokens($user_id, $this->id);
     1822        if (empty($tokens)) {
     1823            return;
     1824        }
     1825
     1826        foreach ($tokens as $token) {
     1827            if (!$token instanceof WC_Payment_Token) {
     1828                continue;
     1829            }
     1830
     1831            $token_id = $token->get_id();
     1832            $customer_profile_id = (string) $token->get_meta('customer_profile_id');
     1833            $payment_profile_id = (string) $token->get_meta('payment_profile_id');
     1834            if ($payment_profile_id === '') {
     1835                $payment_profile_id = (string) $token->get_token();
     1836            }
     1837
     1838            if ($customer_profile_id === '' || $payment_profile_id === '') {
     1839                $this->log('Skipping stale token prune for legacy or incomplete token', [
     1840                    'token_id' => $token_id,
     1841                    'has_customer_profile_id' => $customer_profile_id !== '',
     1842                    'has_payment_profile_id' => $payment_profile_id !== '',
     1843                ]);
     1844                continue;
     1845            }
     1846
     1847            $exists = EASYAUTHNET_AuthorizeNet_API_Handler::validate_customer_payment_profile($customer_profile_id, $payment_profile_id);
     1848            if ($exists) {
     1849                continue;
     1850            }
     1851
     1852            $this->log('Pruning stale Woo token missing in CIM', [
     1853                'token_id' => $token_id,
     1854                'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id),
     1855                'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id),
     1856            ]);
     1857
     1858            self::$cim_skip_token_ids[$token_id] = true;
     1859            WC_Payment_Tokens::delete($token_id);
     1860            unset(self::$cim_skip_token_ids[$token_id]);
     1861
     1862            $this->maybe_delete_customer_profile_meta_if_unused($user_id, $customer_profile_id);
     1863        }
     1864    }
     1865
     1866    protected function maybe_delete_customer_profile_meta_if_unused($user_id, $customer_profile_id) {
     1867        $user_id = (int) $user_id;
     1868        if ($user_id <= 0 || $customer_profile_id === '') {
     1869            return;
     1870        }
     1871
     1872        $tokens = WC_Payment_Tokens::get_customer_tokens($user_id, $this->id);
     1873        foreach ($tokens as $token) {
     1874            if (!$token instanceof WC_Payment_Token) {
     1875                continue;
     1876            }
     1877
     1878            if ((string) $token->get_meta('customer_profile_id') === $customer_profile_id) {
     1879                return;
     1880            }
     1881        }
     1882
     1883        delete_user_meta($user_id, $this->customer_profile_id);
     1884        $this->log('Deleted stored CIM customer profile meta after last token removal', [
     1885            'user_id' => $user_id,
     1886            'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id),
     1887        ]);
     1888    }
     1889
    16661890    public function process_refund($order_id, $amount = null, $reason = '') {
    16671891        try {
  • payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-webhook-handler.php

    r3462451 r3491681  
    1919        $status = str_replace('wc-', '', $status);
    2020        return $status !== '' ? $status : 'on-hold';
     21    }
     22
     23    protected static function force_delete_token_rows(int $token_id): void {
     24        if ($token_id <= 0) {
     25            return;
     26        }
     27
     28        global $wpdb;
     29
     30        $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
     31            "{$wpdb->prefix}woocommerce_payment_tokenmeta",
     32            ['payment_token_id' => $token_id],
     33            ['%d']
     34        );
     35
     36        $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
     37            "{$wpdb->prefix}woocommerce_payment_tokens",
     38            ['token_id' => $token_id],
     39            ['%d']
     40        );
    2141    }
    2242
     
    2747        self::$transaction_type = isset($settings['transaction_type']) ? sanitize_text_field($settings['transaction_type']) : 'auth_capture';
    2848
    29         // Note: Raw body must be read unmodified for signature verification.
    3049        $raw_body = file_get_contents('php://input');
    3150
     
    5069        );
    5170
    52         // Sanitize signature header
    53         $signature_header = isset($_SERVER['HTTP_X_ANET_SIGNATURE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_X_ANET_SIGNATURE'])) : '';
    54 
    55         // Validate structure
     71        $signature_header = isset($_SERVER['HTTP_X_ANET_SIGNATURE']) ? wp_unslash($_SERVER['HTTP_X_ANET_SIGNATURE']) : '';
     72
    5673        if (empty($data['eventType']) || empty($data['payload']) || empty($signature_header)) {
    5774            return new WP_REST_Response(['status' => 'invalid_payload_or_signature'], 400);
    5875        }
    5976
    60         // Verify signature
    6177        if (!self::verify_signature($raw_body, $signature_header)) {
    6278            self::log('Signature mismatch — webhook rejected', [], 'warning');
     
    8096                self::handle_refund($payload);
    8197                break;
     98            case 'net.authorize.customer.deleted':
     99                self::handle_customer_deleted($payload);
     100                break;
     101            case 'net.authorize.customer.paymentProfile.deleted':
     102                self::handle_payment_profile_deleted($payload);
     103                break;
    82104            default:
    83105                do_action('easyauthnet_authorizenet_webhook_' . $event_type, $payload);
     
    138160            if (in_array($current_status, ['pending', 'on-hold', 'processing'], true)) {
    139161                if (abs($capture_amount - $order_total) < 0.01) {
    140                     // For eCheck (ACH), merchants often want to keep the order on-hold until they confirm settlement.
    141162                    $new_status = $is_echeck ? $echeck_webhook_status : 'completed';
    142163                    $order->update_status($new_status);
     
    294315    }
    295316
     317    protected static function handle_customer_deleted($payload) {
     318        $customer_profile_id = sanitize_text_field($payload['id'] ?? $payload['customerProfileId'] ?? $payload['profileId'] ?? '');
     319
     320        if (empty($customer_profile_id)) {
     321            self::log('Customer deleted webhook: missing customer profile ID in payload', [], 'warning');
     322            return;
     323        }
     324
     325        self::log('Customer deleted webhook received', ['customer_profile_id' => $customer_profile_id]);
     326
     327        $user_ids_live    = get_users([
     328            'meta_key'   => EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_live',
     329            'meta_value' => $customer_profile_id,
     330            'fields'     => 'ID',
     331            'number'     => -1,
     332        ]);
     333        $user_ids_sandbox = get_users([
     334            'meta_key'   => EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_sandbox',
     335            'meta_value' => $customer_profile_id,
     336            'fields'     => 'ID',
     337            'number'     => -1,
     338        ]);
     339
     340        $user_ids = array_values(array_unique(array_merge(
     341            array_map('intval', (array) $user_ids_live),
     342            array_map('intval', (array) $user_ids_sandbox)
     343        )));
     344
     345        $token_map         = [];
     346        $affected_user_ids = [];
     347
     348        foreach ($user_ids as $user_id) {
     349            if ($user_id <= 0) {
     350                continue;
     351            }
     352            $affected_user_ids[] = $user_id;
     353
     354            $user_tokens = WC_Payment_Tokens::get_customer_tokens($user_id, 'easyauthnet_authorizenet');
     355            foreach ($user_tokens as $token) {
     356                if (!$token instanceof WC_Payment_Token) {
     357                    continue;
     358                }
     359                if ((string) $token->get_meta('customer_profile_id') !== $customer_profile_id) {
     360                    continue;
     361                }
     362                $token_map[(int) $token->get_id()] = $token;
     363            }
     364        }
     365
     366        if (empty($token_map)) {
     367            self::log('Customer deleted webhook: no local tokens found for profile', ['customer_profile_id' => $customer_profile_id]);
     368        }
     369
     370        foreach ($token_map as $token_id => $token) {
     371            $token_id = (int) $token_id;
     372
     373            if (!$token instanceof WC_Payment_Token) {
     374                self::force_delete_token_rows($token_id);
     375                self::log('Force-deleted local token rows for customer-deleted webhook (invalid token object)', [
     376                    'token_id'            => $token_id,
     377                    'customer_profile_id' => $customer_profile_id,
     378                ], 'warning');
     379                continue;
     380            }
     381
     382            $affected_user_ids[] = (int) $token->get_user_id();
     383
     384            if (class_exists('EASYAUTHNET_AuthorizeNet_Gateway')) {
     385                EASYAUTHNET_AuthorizeNet_Gateway::skip_cim_sync_for_token($token_id);
     386            }
     387
     388            WC_Payment_Tokens::delete($token_id);
     389
     390            if (class_exists('EASYAUTHNET_AuthorizeNet_Gateway')) {
     391                EASYAUTHNET_AuthorizeNet_Gateway::unskip_cim_sync_for_token($token_id);
     392            }
     393
     394            self::log('Deleted local token for Authorize.Net customer-deleted webhook', [
     395                'token_id'            => $token_id,
     396                'customer_profile_id' => $customer_profile_id,
     397            ]);
     398        }
     399
     400        foreach (array_unique($affected_user_ids) as $user_id) {
     401            delete_user_meta($user_id, EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_live');
     402            delete_user_meta($user_id, EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_sandbox');
     403            self::log('Deleted customer profile user meta after Authorize.Net customer-deleted webhook', [
     404                'user_id'             => $user_id,
     405                'customer_profile_id' => $customer_profile_id,
     406            ]);
     407        }
     408    }
     409
     410    protected static function handle_payment_profile_deleted($payload) {
     411        $payment_profile_id  = sanitize_text_field($payload['id'] ?? '');
     412        $customer_profile_id = sanitize_text_field($payload['customerProfileId'] ?? $payload['profileId'] ?? '');
     413
     414        if (empty($payment_profile_id)) {
     415            self::log('Payment profile deleted webhook: missing payment profile ID in payload', [], 'warning');
     416            return;
     417        }
     418
     419        self::log('Payment profile deleted webhook received', [
     420            'payment_profile_id'  => $payment_profile_id,
     421            'customer_profile_id' => $customer_profile_id,
     422        ]);
     423
     424        $matching_tokens = [];
     425
     426        if (!empty($customer_profile_id)) {
     427            $user_ids_live    = get_users([
     428                'meta_key'   => EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_live',
     429                'meta_value' => $customer_profile_id,
     430                'fields'     => 'ID',
     431                'number'     => -1,
     432            ]);
     433            $user_ids_sandbox = get_users([
     434                'meta_key'   => EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_sandbox',
     435                'meta_value' => $customer_profile_id,
     436                'fields'     => 'ID',
     437                'number'     => -1,
     438            ]);
     439            $user_ids = array_values(array_unique(array_merge(
     440                array_map('intval', (array) $user_ids_live),
     441                array_map('intval', (array) $user_ids_sandbox)
     442            )));
     443
     444            foreach ($user_ids as $user_id) {
     445                if ($user_id <= 0) {
     446                    continue;
     447                }
     448                foreach (WC_Payment_Tokens::get_customer_tokens($user_id, 'easyauthnet_authorizenet') as $token) {
     449                    if (!$token instanceof WC_Payment_Token) {
     450                        continue;
     451                    }
     452                    if (
     453                        (string) $token->get_meta('payment_profile_id') === $payment_profile_id ||
     454                        (string) $token->get_token() === $payment_profile_id
     455                    ) {
     456                        $matching_tokens[(int) $token->get_id()] = $token;
     457                    }
     458                }
     459            }
     460        } else {
     461            foreach (WC_Payment_Tokens::get_tokens(['gateway_id' => 'easyauthnet_authorizenet']) as $token) {
     462                if (!$token instanceof WC_Payment_Token) {
     463                    continue;
     464                }
     465                if (
     466                    (string) $token->get_meta('payment_profile_id') === $payment_profile_id ||
     467                    (string) $token->get_token() === $payment_profile_id
     468                ) {
     469                    $matching_tokens[(int) $token->get_id()] = $token;
     470                }
     471            }
     472        }
     473
     474        if (empty($matching_tokens)) {
     475            self::log('Payment profile deleted webhook: no local tokens found for payment profile', [
     476                'payment_profile_id' => $payment_profile_id,
     477            ]);
     478            return;
     479        }
     480
     481        foreach ($matching_tokens as $token_id => $token) {
     482            $token_id = (int) $token_id;
     483            $user_id  = (int) $token->get_user_id();
     484
     485            if (class_exists('EASYAUTHNET_AuthorizeNet_Gateway')) {
     486                EASYAUTHNET_AuthorizeNet_Gateway::skip_cim_sync_for_token($token_id);
     487            }
     488
     489            WC_Payment_Tokens::delete($token_id);
     490
     491            if (class_exists('EASYAUTHNET_AuthorizeNet_Gateway')) {
     492                EASYAUTHNET_AuthorizeNet_Gateway::unskip_cim_sync_for_token($token_id);
     493            }
     494
     495            self::log('Deleted local token for Authorize.Net payment-profile-deleted webhook', [
     496                'token_id'            => $token_id,
     497                'payment_profile_id'  => $payment_profile_id,
     498                'customer_profile_id' => $customer_profile_id,
     499            ]);
     500        }
     501    }
     502
    296503    protected static function log($message, $context = [], $level = 'info') {
    297504        if (!class_exists('WC_Logger')) {
  • payment-gateway-for-authorize-net-for-woocommerce/trunk/payment-gateway-for-authorizenet-for-woocommerce.php

    r3473557 r3491681  
    77 * Author: easypayment
    88 * Author URI: https://profiles.wordpress.org/easypayment/
    9  * Version: 1.0.9
     9 * Version: 1.0.10
    1010 * Requires at least: 5.6
    11  * Tested up to: 6.9.1
     11 * Tested up to: 6.9.4
    1212 * Requires PHP: 7.4
    1313 * Text Domain: payment-gateway-for-authorize-net-for-woocommerce
    1414 * Domain Path: /languages/
    1515 * WC requires at least: 6.0
    16  * WC tested up to: 10.5.2
     16 * WC tested up to: 10.6.1
    1717 * Requires Plugins: woocommerce
    1818 * License: GPLv2 or later
     
    2323
    2424if (!defined('EASYAUTHNET_AUTHORIZENET_VERSION')) {
    25     define('EASYAUTHNET_AUTHORIZENET_VERSION', '1.0.9');
     25    define('EASYAUTHNET_AUTHORIZENET_VERSION', '1.0.10');
    2626}
    2727define('EASYAUTHNET_AUTHORIZENET_PLUGIN_FILE', __FILE__);
  • payment-gateway-for-authorize-net-for-woocommerce/trunk/readme.txt

    r3473557 r3491681  
    33Tags: authorize.net, credit card, visa 
    44Requires at least: 5.6 
    5 Tested up to: 6.9.1
     5Tested up to: 6.9.4
    66Requires PHP: 7.4 
    7 Stable tag: 1.0.9
     7Stable tag: 1.0.10
    88License: GPLv2 or later 
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html 
     
    121121== Changelog ==
    122122
     123= 1.0.10 =
     124* Improved - Synchronization between Authorize.net CIM and WooCommerce saved payment methods.
     125
    123126= 1.0.9 =
    124127* Added - FunnelKit compatible for upsell and cross-sell flows.
Note: See TracChangeset for help on using the changeset viewer.