yoone-wc-moneris-payments/docs/woocommerce-gateway-moneris...

19 KiB
Raw Blame History

WooCommerce Gateway Moneris 令牌化与自动续费实现参考

背景意义

为什么需要令牌化

  • 安全性:避免存储敏感的信用卡信息
  • 合规性:符合 PCI DSS 标准要求
  • 用户体验:支持一键支付和自动续费
  • 业务价值:提高订阅业务的续费成功率

为什么需要自动续费

  • 订阅业务WooCommerce Subscriptions 的核心功能
  • 减少流失:避免手动续费导致的客户流失
  • 现金流:提供可预测的收入流
  • 运营效率:减少人工干预和客服成本

核心概念定义

令牌化Tokenization

将敏感的支付信息如信用卡号替换为安全的令牌Token用于后续的支付操作。

自动续费Auto Renewal

基于已保存的支付令牌,在订阅到期时自动处理续费支付。

实现路径对比

实现方式 适用场景 安全级别 实现复杂度 用户体验
Checkout 令牌化 标准电商支付 优秀
直接 API 令牌化 自定义支付流程 良好
临时转永久令牌 特殊业务需求 一般

使用流程

1. Checkout 令牌化流程

步骤说明

  1. 用户在结账页面输入支付信息
  2. 前端调用 Moneris Checkout API
  3. 获取临时令牌
  4. 后端将临时令牌转换为永久令牌
  5. 保存令牌到用户账户

核心代码示例

/**
 * Checkout 令牌化处理类
 * 负责处理 Moneris Checkout 的令牌化流程
 */
class WC_Gateway_Moneris_Checkout_Credit_Card extends WC_Gateway_Moneris_Credit_Card {
    
    /**
     * 处理支付并创建令牌
     * @param int $order_id 订单ID
     * @return array 支付结果
     */
    public function process_payment($order_id) {
        $order = wc_get_order($order_id);
        
        // 获取 Checkout 响应中的临时令牌
        $temp_token = $this->get_checkout_temp_token();
        
        if ($temp_token && $this->should_tokenize_payment_method()) {
            // 将临时令牌转换为永久令牌
            $permanent_token = $this->create_permanent_token($temp_token, $order);
            
            if ($permanent_token) {
                // 保存令牌到用户账户
                $this->get_payment_tokens_handler()->add_token($order->get_user_id(), $permanent_token);
            }
        }
        
        return $this->process_standard_payment($order);
    }
    
    /**
     * 创建永久令牌
     * @param string $temp_token 临时令牌
     * @param WC_Order $order 订单对象
     * @return WC_Gateway_Moneris_Payment_Token|null
     */
    protected function create_permanent_token($temp_token, $order) {
        try {
            // 调用 Moneris API 创建永久令牌
            $request = $this->get_api()->get_add_token_request($order);
            $request->set_temp_token($temp_token);
            
            $response = $this->get_api()->add_token($request);
            
            if ($response && $response->transaction_approved()) {
                // 创建令牌对象
                return new WC_Gateway_Moneris_Payment_Token(
                    $response->get_payment_token_id(),
                    array(
                        'type'    => 'credit_card',
                        'last4'   => $response->get_masked_pan(),
                        'exp_month' => $response->get_exp_month(),
                        'exp_year'  => $response->get_exp_year(),
                        'card_type' => $response->get_card_type(),
                    )
                );
            }
        } catch (Exception $e) {
            $this->add_debug_message('令牌创建失败: ' . $e->getMessage());
        }
        
        return null;
    }
}

2. 直接 API 令牌化流程

/**
 * 支付令牌处理器
 * 负责令牌的创建、验证和管理
 */
class WC_Gateway_Moneris_Payment_Tokens_Handler {
    
    /**
     * 创建支付令牌
     * @param WC_Order $order 订单对象
     * @param array $payment_data 支付数据
     * @return WC_Gateway_Moneris_Payment_Token|null
     */
    public function create_token($order, $payment_data) {
        // 检查是否应该进行令牌化
        if (!$this->should_tokenize($order, $payment_data)) {
            return null;
        }
        
        try {
            // 构建令牌化请求
            $request = $this->build_tokenize_request($order, $payment_data);
            
            // 调用 Moneris API
            $response = $this->get_api()->tokenize_payment_method($request);
            
            if ($response && $response->transaction_approved()) {
                // 创建并保存令牌
                $token = $this->build_token_from_response($response);
                $this->add_token($order->get_user_id(), $token);
                
                return $token;
            }
        } catch (Exception $e) {
            $this->log_error('令牌创建失败', $e);
        }
        
        return null;
    }
    
    /**
     * 判断是否应该进行令牌化
     * @param WC_Order $order 订单对象
     * @param array $payment_data 支付数据
     * @return bool
     */
    protected function should_tokenize($order, $payment_data) {
        // 用户选择保存支付方式
        if (!empty($payment_data['save_payment_method'])) {
            return true;
        }
        
        // 订阅订单自动令牌化
        if (function_exists('wcs_order_contains_subscription') && 
            wcs_order_contains_subscription($order)) {
            return true;
        }
        
        // 预授权订单
        if ($this->is_pre_order($order)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * 从 API 响应构建令牌对象
     * @param WC_Moneris_API_Response $response API 响应
     * @return WC_Gateway_Moneris_Payment_Token
     */
    protected function build_token_from_response($response) {
        return new WC_Gateway_Moneris_Payment_Token(
            $response->get_payment_token_id(),
            array(
                'type'      => 'credit_card',
                'last4'     => $response->get_masked_pan(),
                'exp_month' => $response->get_exp_month(),
                'exp_year'  => $response->get_exp_year(),
                'card_type' => $response->get_card_type(),
                'gateway_id' => $this->get_gateway()->get_id(),
                'user_id'   => get_current_user_id(),
            )
        );
    }
}

3. WooCommerce Subscriptions 集成

/**
 * Moneris 订阅集成类
 * 处理与 WooCommerce Subscriptions 的集成
 */
class WC_Moneris_Payment_Gateway_Integration_Subscriptions {
    
    /**
     * 处理订阅续费
     * @param float $amount 续费金额
     * @param WC_Order $renewal_order 续费订单
     * @return void
     */
    public function process_subscription_payment($amount, $renewal_order) {
        try {
            // 获取父订单的支付令牌
            $subscription = wcs_get_subscription($renewal_order->get_meta('_subscription_renewal'));
            $parent_order = $subscription->get_parent();
            $token = $this->get_order_token($parent_order);
            
            if (!$token) {
                throw new Exception('未找到有效的支付令牌');
            }
            
            // 使用令牌处理续费支付
            $result = $this->process_token_payment($renewal_order, $token, $amount);
            
            if ($result['success']) {
                // 续费成功
                $renewal_order->payment_complete($result['transaction_id']);
                $renewal_order->add_order_note(
                    sprintf('Moneris 自动续费成功 - 交易ID: %s', $result['transaction_id'])
                );
            } else {
                // 续费失败
                $renewal_order->update_status('failed', $result['message']);
            }
            
        } catch (Exception $e) {
            $renewal_order->update_status('failed', '自动续费处理失败: ' . $e->getMessage());
        }
    }
    
    /**
     * 使用令牌处理支付
     * @param WC_Order $order 订单对象
     * @param WC_Gateway_Moneris_Payment_Token $token 支付令牌
     * @param float $amount 支付金额
     * @return array 支付结果
     */
    protected function process_token_payment($order, $token, $amount) {
        // 构建支付请求
        $request = $this->build_payment_request($order, $token, $amount);
        
        // 设置 Credential on File (CoF) 参数
        $request->set_cof_info(array(
            'payment_indicator' => 'R', // R = Recurring (订阅续费)
            'payment_information' => '2', // 2 = Subsequent payment
            'issuer_id' => $token->get_issuer_id(), // 从初始交易获取
        ));
        
        // 调用支付 API
        $response = $this->get_api()->credit_card_purchase($request);
        
        return array(
            'success' => $response && $response->transaction_approved(),
            'transaction_id' => $response ? $response->get_transaction_id() : null,
            'message' => $response ? $response->get_message() : '支付处理失败',
        );
    }
    
    /**
     * 获取订单关联的支付令牌
     * @param WC_Order $order 订单对象
     * @return WC_Gateway_Moneris_Payment_Token|null
     */
    protected function get_order_token($order) {
        $token_id = $order->get_meta('_payment_token_id');
        
        if ($token_id) {
            $tokens = WC_Payment_Tokens::get_customer_tokens($order->get_user_id());
            
            foreach ($tokens as $token) {
                if ($token->get_token() === $token_id && 
                    $token instanceof WC_Gateway_Moneris_Payment_Token) {
                    return $token;
                }
            }
        }
        
        return null;
    }
}

4. 令牌对象结构

/**
 * Moneris 支付令牌类
 * 继承自 WooCommerce 的支付令牌基类
 */
class WC_Gateway_Moneris_Payment_Token extends WC_Payment_Token_CC {
    
    /** @var string 令牌类型 */
    protected $type = 'moneris_cc';
    
    /**
     * 获取令牌的额外数据
     * @return array
     */
    protected function get_hook_prefix() {
        return 'woocommerce_payment_token_moneris_cc_get_';
    }
    
    /**
     * 验证令牌数据
     * @return bool
     */
    public function validate() {
        if (false === parent::validate()) {
            return false;
        }
        
        // 验证 Moneris 特定字段
        if (!$this->get_token()) {
            return false;
        }
        
        return true;
    }
    
    /**
     * 获取 Moneris 令牌ID
     * @return string
     */
    public function get_moneris_token_id() {
        return $this->get_meta('moneris_token_id');
    }
    
    /**
     * 设置 Moneris 令牌ID
     * @param string $token_id
     */
    public function set_moneris_token_id($token_id) {
        $this->add_meta_data('moneris_token_id', $token_id, true);
    }
    
    /**
     * 获取发卡行ID用于 CoF
     * @return string
     */
    public function get_issuer_id() {
        return $this->get_meta('issuer_id');
    }
    
    /**
     * 设置发卡行ID
     * @param string $issuer_id
     */
    public function set_issuer_id($issuer_id) {
        $this->add_meta_data('issuer_id', $issuer_id, true);
    }
}

端到端流程示例

订阅创建到自动续费完整流程

/**
 * 完整的订阅支付流程示例
 */
class Moneris_Subscription_Flow_Example {
    
    /**
     * 步骤1: 用户创建订阅订单
     */
    public function create_subscription_order() {
        // 1. 用户在前端选择订阅产品并结账
        // 2. 系统检测到订阅产品,自动启用令牌化
        // 3. 调用 process_payment 处理初始支付
        
        $order_id = 12345;
        $order = wc_get_order($order_id);
        
        // 检查是否为订阅订单
        if (wcs_order_contains_subscription($order)) {
            // 强制启用令牌化
            add_filter('wc_moneris_tokenize_payment_method', '__return_true');
        }
        
        return $this->gateway->process_payment($order_id);
    }
    
    /**
     * 步骤2: 处理初始支付并保存令牌
     */
    public function process_initial_payment($order) {
        // 1. 处理支付
        $payment_result = $this->process_credit_card_payment($order);
        
        if ($payment_result['success']) {
            // 2. 创建并保存令牌
            $token = $this->create_payment_token($order, $payment_result['response']);
            
            // 3. 将令牌关联到订阅
            $subscriptions = wcs_get_subscriptions_for_order($order);
            foreach ($subscriptions as $subscription) {
                $subscription->update_meta_data('_payment_token_id', $token->get_id());
                $subscription->save();
            }
        }
        
        return $payment_result;
    }
    
    /**
     * 步骤3: 自动续费处理(由 WooCommerce Subscriptions 触发)
     */
    public function process_renewal_payment($amount, $renewal_order) {
        // 1. 获取原始订阅和令牌
        $subscription = wcs_get_subscription($renewal_order->get_meta('_subscription_renewal'));
        $token_id = $subscription->get_meta('_payment_token_id');
        $token = WC_Payment_Tokens::get($token_id);
        
        if (!$token) {
            $renewal_order->update_status('failed', '未找到有效的支付令牌');
            return;
        }
        
        // 2. 使用令牌处理续费
        try {
            $request = $this->build_renewal_request($renewal_order, $token, $amount);
            $response = $this->get_api()->credit_card_purchase($request);
            
            if ($response && $response->transaction_approved()) {
                // 续费成功
                $renewal_order->payment_complete($response->get_transaction_id());
                $renewal_order->add_order_note(
                    sprintf('自动续费成功 - 令牌: %s, 交易ID: %s', 
                        $token->get_token(), 
                        $response->get_transaction_id()
                    )
                );
            } else {
                // 续费失败
                $error_msg = $response ? $response->get_message() : '支付处理失败';
                $renewal_order->update_status('failed', $error_msg);
            }
            
        } catch (Exception $e) {
            $renewal_order->update_status('failed', '续费异常: ' . $e->getMessage());
        }
    }
    
    /**
     * 构建续费支付请求
     */
    protected function build_renewal_request($order, $token, $amount) {
        $request = new WC_Moneris_API_Credit_Card_Purchase_Request();
        
        // 基本支付信息
        $request->set_order_id($order->get_order_number());
        $request->set_amount($amount);
        $request->set_payment_token($token->get_token());
        
        // CoF (Credential on File) 信息 - 用于订阅续费
        $request->set_cof_info(array(
            'payment_indicator' => 'R',  // R = Recurring
            'payment_information' => '2', // 2 = Subsequent payment
            'issuer_id' => $token->get_issuer_id(),
        ));
        
        // 客户信息
        $request->set_customer_info(array(
            'email' => $order->get_billing_email(),
            'phone' => $order->get_billing_phone(),
        ));
        
        return $request;
    }
}

关键配置与注意事项

1. 网关配置要求

配置项 说明 必需性
Store ID Moneris 商户标识 必需
API Token API 访问令牌 必需
令牌化支持 启用令牌化功能 订阅必需
CoF 支持 Credential on File 支持 续费推荐
CSC 验证 安全码验证设置 可选

2. 订阅集成配置

/**
 * 订阅功能集成配置
 */
public function configure_subscription_support() {
    // 声明支持的功能
    $this->supports = array(
        'products',
        'subscriptions',
        'subscription_cancellation',
        'subscription_suspension',
        'subscription_reactivation',
        'subscription_amount_changes',
        'subscription_date_changes',
        'multiple_subscriptions',
        'tokenization',
    );
    
    // 注册订阅相关的钩子
    add_action('woocommerce_scheduled_subscription_payment_' . $this->id, 
               array($this, 'process_subscription_payment'), 10, 2);
    
    add_action('wcs_resubscribe_order_created', 
               array($this, 'delete_resubscribe_meta'), 10);
    
    add_filter('wcs_renewal_order_meta_query', 
               array($this, 'remove_renewal_order_meta'), 10);
}

3. 错误处理与重试机制

/**
 * 续费失败处理
 */
public function handle_renewal_failure($renewal_order, $error_message) {
    // 记录失败原因
    $renewal_order->add_order_note('自动续费失败: ' . $error_message);
    
    // 根据错误类型决定处理策略
    if ($this->is_temporary_error($error_message)) {
        // 临时错误:标记为待重试
        $renewal_order->update_meta_data('_renewal_retry_count', 
            $renewal_order->get_meta('_renewal_retry_count') + 1);
        
        // 安排重试(如果未超过最大重试次数)
        if ($renewal_order->get_meta('_renewal_retry_count') < 3) {
            wp_schedule_single_event(
                time() + (24 * 60 * 60), // 24小时后重试
                'moneris_retry_renewal_payment',
                array($renewal_order->get_id())
            );
        }
    } else {
        // 永久错误:通知客户更新支付方式
        $this->send_payment_method_update_notice($renewal_order);
    }
}

限制与注意事项

技术限制

  • CSC 要求:如果启用 CSC 验证,可能影响令牌化支付
  • 令牌有效期:需要处理令牌过期和更新
  • 网络依赖:依赖 Moneris API 的可用性

合规要求

  • PCI DSS:必须遵循 PCI 合规标准
  • 数据保护:不得存储敏感的卡片信息
  • 用户同意:令牌化需要用户明确同意

最佳实践

  • 错误处理:实现完善的错误处理和重试机制
  • 日志记录:记录关键操作用于调试和审计
  • 测试环境:充分测试令牌化和续费流程
  • 监控告警:监控续费成功率和失败原因

参考文档