subscription/includes/payment/class-yoone-moneris-gateway...

842 lines
29 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Moneris 支付网关类
*
* 处理 Moneris 支付集成和订阅支付
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Moneris 支付网关类
*/
class Yoone_Moneris_Gateway extends WC_Payment_Gateway implements Interface_Yoone_Payment_Gateway {
/**
* 网关ID
*/
public $id = 'yoone_moneris';
/**
* 支持的功能
*/
public $supports = array(
'products',
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'multiple_subscriptions',
'refunds',
'pre-orders'
);
/**
* 测试模式
*/
protected $testmode;
/**
* API 凭据
*/
protected $store_id;
protected $api_token;
protected $processing_country;
/**
* 构造函数
*/
public function __construct() {
$this->method_title = __('Yoone Moneris', 'yoone-subscriptions');
$this->method_description = __('通过 Moneris 处理信用卡支付和订阅', 'yoone-subscriptions');
$this->has_fields = true;
// 加载设置
$this->init_form_fields();
$this->init_settings();
// 获取设置值
$this->title = $this->get_option('title');
$this->description = $this->get_option('description');
$this->testmode = 'yes' === $this->get_option('testmode');
$this->store_id = $this->get_option('store_id');
$this->api_token = $this->get_option('api_token');
$this->processing_country = $this->get_option('processing_country', 'CA');
// 钩子
add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
add_action('wp_enqueue_scripts', array($this, 'payment_scripts'));
// 订阅钩子
add_action('woocommerce_scheduled_subscription_payment_' . $this->id, array($this, 'scheduled_subscription_payment'), 10, 2);
add_action('wcs_resubscribe_order_created', array($this, 'delete_resubscribe_meta'), 10);
// 支持令牌化
$this->supports[] = 'tokenization';
$this->supports[] = 'add_payment_method';
}
/**
* 初始化表单字段
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __('启用/禁用', 'yoone-subscriptions'),
'type' => 'checkbox',
'label' => __('启用 Moneris 支付', 'yoone-subscriptions'),
'default' => 'no'
),
'title' => array(
'title' => __('标题', 'yoone-subscriptions'),
'type' => 'text',
'description' => __('用户在结账时看到的支付方式标题', 'yoone-subscriptions'),
'default' => __('信用卡', 'yoone-subscriptions'),
'desc_tip' => true,
),
'description' => array(
'title' => __('描述', 'yoone-subscriptions'),
'type' => 'textarea',
'description' => __('用户在结账时看到的支付方式描述', 'yoone-subscriptions'),
'default' => __('使用信用卡安全支付', 'yoone-subscriptions'),
'desc_tip' => true,
),
'testmode' => array(
'title' => __('测试模式', 'yoone-subscriptions'),
'type' => 'checkbox',
'label' => __('启用测试模式', 'yoone-subscriptions'),
'default' => 'yes',
'description' => __('在测试模式下,您可以使用测试卡号进行测试', 'yoone-subscriptions'),
),
'store_id' => array(
'title' => __('Store ID', 'yoone-subscriptions'),
'type' => 'text',
'description' => __('从 Moneris 获取的 Store ID', 'yoone-subscriptions'),
'default' => '',
'desc_tip' => true,
),
'api_token' => array(
'title' => __('API Token', 'yoone-subscriptions'),
'type' => 'password',
'description' => __('从 Moneris 获取的 API Token', 'yoone-subscriptions'),
'default' => '',
'desc_tip' => true,
),
'processing_country' => array(
'title' => __('处理国家', 'yoone-subscriptions'),
'type' => 'select',
'description' => __('选择处理支付的国家', 'yoone-subscriptions'),
'default' => 'CA',
'desc_tip' => true,
'options' => array(
'CA' => __('加拿大', 'yoone-subscriptions'),
'US' => __('美国', 'yoone-subscriptions'),
),
),
);
}
/**
* 加载支付脚本
*/
public function payment_scripts() {
if (!is_cart() && !is_checkout() && !isset($_GET['pay_for_order'])) {
return;
}
if ('no' === $this->enabled) {
return;
}
if (empty($this->store_id) || empty($this->api_token)) {
return;
}
wp_enqueue_script('yoone-moneris-js', YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/js/moneris-payment.js', array('jquery'), YOONE_SUBSCRIPTIONS_VERSION, true);
wp_localize_script('yoone-moneris-js', 'yoone_moneris_params', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('yoone_moneris_nonce'),
'testmode' => $this->testmode,
'store_id' => $this->store_id,
'i18n' => array(
'invalid_card' => __('请输入有效的信用卡号', 'yoone-subscriptions'),
'invalid_cvv' => __('请输入有效的CVV', 'yoone-subscriptions'),
'invalid_date' => __('请输入有效的到期日期', 'yoone-subscriptions'),
)
));
}
/**
* 支付字段
*/
public function payment_fields() {
if ($this->description) {
echo wpautop(wptexturize($this->description));
}
if ($this->testmode) {
echo '<p class="testmode-info">' . __('测试模式已启用。您可以使用测试卡号4242424242424242', 'yoone-subscriptions') . '</p>';
}
echo '<fieldset id="wc-' . esc_attr($this->id) . '-cc-form" class="wc-credit-card-form wc-payment-form" style="background:transparent;">';
// 如果支持令牌化,显示保存的支付方式
if ($this->supports('tokenization') && is_checkout()) {
$this->tokenization_script();
$this->saved_payment_methods();
}
echo '<div class="wc-credit-card-form-card-number">
<label for="' . esc_attr($this->id) . '-card-number">' . __('卡号', 'yoone-subscriptions') . ' <span class="required">*</span></label>
<input id="' . esc_attr($this->id) . '-card-number" class="input-text wc-credit-card-form-card-number" type="text" maxlength="20" autocomplete="cc-number" placeholder="•••• •••• •••• ••••" name="' . esc_attr($this->id) . '-card-number" />
</div>';
echo '<div class="wc-credit-card-form-card-expiry">
<label for="' . esc_attr($this->id) . '-card-expiry">' . __('到期日期 (MM/YY)', 'yoone-subscriptions') . ' <span class="required">*</span></label>
<input id="' . esc_attr($this->id) . '-card-expiry" class="input-text wc-credit-card-form-card-expiry" type="text" autocomplete="cc-exp" placeholder="MM / YY" name="' . esc_attr($this->id) . '-card-expiry" />
</div>';
echo '<div class="wc-credit-card-form-card-cvc">
<label for="' . esc_attr($this->id) . '-card-cvc">' . __('CVV', 'yoone-subscriptions') . ' <span class="required">*</span></label>
<input id="' . esc_attr($this->id) . '-card-cvc" class="input-text wc-credit-card-form-card-cvc" type="text" autocomplete="cc-csc" placeholder="CVV" name="' . esc_attr($this->id) . '-card-cvc" />
</div>';
// 保存支付方式选项
if ($this->supports('tokenization') && is_checkout() && !is_add_payment_method_page()) {
echo '<div class="wc-credit-card-form-save-payment-method">
<input id="wc-' . esc_attr($this->id) . '-new-payment-method" name="wc-' . esc_attr($this->id) . '-new-payment-method" type="checkbox" value="true" style="width:auto;" />
<label for="wc-' . esc_attr($this->id) . '-new-payment-method" style="display:inline;">' . __('保存支付方式以便将来使用', 'yoone-subscriptions') . '</label>
</div>';
}
echo '<div class="clear"></div></fieldset>';
}
/**
* 验证字段
*/
public function validate_fields() {
if (empty($_POST[$this->id . '-card-number'])) {
wc_add_notice(__('请输入信用卡号', 'yoone-subscriptions'), 'error');
return false;
}
if (empty($_POST[$this->id . '-card-expiry'])) {
wc_add_notice(__('请输入到期日期', 'yoone-subscriptions'), 'error');
return false;
}
if (empty($_POST[$this->id . '-card-cvc'])) {
wc_add_notice(__('请输入CVV', 'yoone-subscriptions'), 'error');
return false;
}
return true;
}
/**
* 处理支付
*/
public function process_payment($order_id) {
$order = wc_get_order($order_id);
if (!$order) {
return array(
'result' => 'failure',
'messages' => __('订单不存在', 'yoone-subscriptions')
);
}
// 检查是否包含订阅
$has_subscription = $this->order_contains_subscription($order);
if ($has_subscription) {
return $this->process_subscription_payment($order_id, $order->get_total());
} else {
return $this->process_regular_payment($order);
}
}
/**
* 处理常规支付
*/
protected function process_regular_payment($order) {
$card_data = $this->get_card_data();
if (!$card_data) {
return array(
'result' => 'failure',
'messages' => __('信用卡信息无效', 'yoone-subscriptions')
);
}
// 调用 Moneris Purchase API
$response = $this->moneris_purchase($order, $card_data);
if ($response && $response['success']) {
// 支付成功
$order->payment_complete($response['transaction_id']);
$order->add_order_note(sprintf(__('Moneris 支付完成。交易ID: %s', 'yoone-subscriptions'), $response['transaction_id']));
// 清空购物车
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url($order)
);
} else {
$error_message = isset($response['message']) ? $response['message'] : __('支付处理失败', 'yoone-subscriptions');
wc_add_notice($error_message, 'error');
return array(
'result' => 'failure',
'messages' => $error_message
);
}
}
/**
* 处理订阅支付
*/
public function process_subscription_payment($subscription_id, $amount) {
// 获取订阅对象
if (is_numeric($subscription_id)) {
$subscription = new Yoone_Subscription($subscription_id);
} else {
$order = wc_get_order($subscription_id);
$subscription = $this->get_subscription_from_order($order);
}
if (!$subscription || !$subscription->get_id()) {
return array(
'result' => 'failure',
'messages' => __('订阅不存在', 'yoone-subscriptions')
);
}
// 首次支付需要创建令牌
if (!$subscription->get_payment_token()) {
return $this->process_initial_subscription_payment($subscription, $amount);
} else {
return $this->process_recurring_subscription_payment($subscription, $amount);
}
}
/**
* 处理首次订阅支付
*/
protected function process_initial_subscription_payment($subscription, $amount) {
$card_data = $this->get_card_data();
if (!$card_data) {
return array(
'result' => 'failure',
'messages' => __('信用卡信息无效', 'yoone-subscriptions')
);
}
// 1. 添加信用卡到 Vault
$vault_response = $this->moneris_res_add_cc($card_data);
if (!$vault_response || !$vault_response['success']) {
return array(
'result' => 'failure',
'messages' => __('创建支付令牌失败', 'yoone-subscriptions')
);
}
$data_key = $vault_response['data_key'];
// 2. 使用令牌进行首次支付
$payment_response = $this->moneris_res_purchase_cc($subscription, $data_key, $amount);
if ($payment_response && $payment_response['success']) {
// 保存支付令牌到订阅
$subscription->set_payment_token($data_key);
$subscription->save();
// 保存支付令牌到客户
$this->save_payment_token($subscription->get_customer_id(), $data_key, $card_data);
// 激活订阅
$subscription->activate();
return array(
'result' => 'success',
'redirect' => wc_get_checkout_url() . '/order-received/' . $subscription->get_parent_order_id() . '/?key=' . wc_get_order($subscription->get_parent_order_id())->get_order_key()
);
} else {
// 删除创建的令牌
$this->moneris_res_delete($data_key);
$error_message = isset($payment_response['message']) ? $payment_response['message'] : __('订阅支付失败', 'yoone-subscriptions');
return array(
'result' => 'failure',
'messages' => $error_message
);
}
}
/**
* 处理续费支付
*/
protected function process_recurring_subscription_payment($subscription, $amount) {
$data_key = $subscription->get_payment_token();
if (!$data_key) {
return false;
}
// 使用保存的令牌进行支付
$response = $this->moneris_res_purchase_cc($subscription, $data_key, $amount);
if ($response && $response['success']) {
$subscription->add_log('payment_success', sprintf(__('续费支付成功。交易ID: %s', 'yoone-subscriptions'), $response['transaction_id']));
return true;
} else {
$error_message = isset($response['message']) ? $response['message'] : __('续费支付失败', 'yoone-subscriptions');
$subscription->add_log('payment_failed', $error_message);
return false;
}
}
/**
* 创建支付令牌
*/
public function create_payment_token($payment_data) {
return $this->moneris_res_add_cc($payment_data);
}
/**
* 删除支付令牌
*/
public function delete_payment_token($token_id) {
return $this->moneris_res_delete($token_id);
}
/**
* 验证支付令牌
*/
public function validate_payment_token($token_id) {
$response = $this->moneris_res_lookup_full($token_id);
return $response && $response['success'];
}
/**
* 处理退款
*/
public function process_refund($order_id, $amount = null, $reason = '') {
$order = wc_get_order($order_id);
if (!$order) {
return false;
}
$transaction_id = $order->get_transaction_id();
if (!$transaction_id) {
return new WP_Error('error', __('没有找到交易ID', 'yoone-subscriptions'));
}
$response = $this->moneris_refund($transaction_id, $amount, $reason);
if ($response && $response['success']) {
$order->add_order_note(sprintf(__('退款成功。金额: %s, 原因: %s', 'yoone-subscriptions'), wc_price($amount), $reason));
return true;
} else {
$error_message = isset($response['message']) ? $response['message'] : __('退款失败', 'yoone-subscriptions');
return new WP_Error('error', $error_message);
}
}
/**
* 预授权
*/
public function process_preauth($order_id, $amount) {
$order = wc_get_order($order_id);
$card_data = $this->get_card_data();
return $this->moneris_preauth($order, $card_data, $amount);
}
/**
* 完成预授权
*/
public function complete_preauth($transaction_id, $amount) {
return $this->moneris_completion($transaction_id, $amount);
}
/*
|--------------------------------------------------------------------------
| Moneris API 调用
|--------------------------------------------------------------------------
*/
/**
* Moneris Purchase API
*/
protected function moneris_purchase($order, $card_data) {
$data = array(
'store_id' => $this->store_id,
'api_token' => $this->api_token,
'order_id' => $order->get_id() . '-' . time(),
'amount' => number_format($order->get_total(), 2, '.', ''),
'pan' => $card_data['number'],
'expdate' => $card_data['exp_month'] . $card_data['exp_year'],
'cvd' => $card_data['cvc'],
'crypt_type' => '7'
);
return $this->make_api_request('purchase', $data);
}
/**
* Moneris Res Add CC API
*/
protected function moneris_res_add_cc($card_data) {
$data = array(
'store_id' => $this->store_id,
'api_token' => $this->api_token,
'pan' => $card_data['number'],
'expdate' => $card_data['exp_month'] . $card_data['exp_year'],
'crypt_type' => '7'
);
return $this->make_api_request('res_add_cc', $data);
}
/**
* Moneris Res Purchase CC API
*/
protected function moneris_res_purchase_cc($subscription, $data_key, $amount) {
$data = array(
'store_id' => $this->store_id,
'api_token' => $this->api_token,
'order_id' => 'sub-' . $subscription->get_id() . '-' . time(),
'amount' => number_format($amount, 2, '.', ''),
'data_key' => $data_key,
'crypt_type' => '1'
);
return $this->make_api_request('res_purchase_cc', $data);
}
/**
* Moneris Res Delete API
*/
protected function moneris_res_delete($data_key) {
$data = array(
'store_id' => $this->store_id,
'api_token' => $this->api_token,
'data_key' => $data_key
);
return $this->make_api_request('res_delete', $data);
}
/**
* Moneris Res Lookup Full API
*/
protected function moneris_res_lookup_full($data_key) {
$data = array(
'store_id' => $this->store_id,
'api_token' => $this->api_token,
'data_key' => $data_key
);
return $this->make_api_request('res_lookup_full', $data);
}
/**
* Moneris Refund API
*/
protected function moneris_refund($transaction_id, $amount, $reason = '') {
$data = array(
'store_id' => $this->store_id,
'api_token' => $this->api_token,
'order_id' => 'refund-' . $transaction_id . '-' . time(),
'amount' => number_format($amount, 2, '.', ''),
'txn_number' => $transaction_id,
'crypt_type' => '7'
);
return $this->make_api_request('refund', $data);
}
/**
* Moneris Preauth API
*/
protected function moneris_preauth($order, $card_data, $amount) {
$data = array(
'store_id' => $this->store_id,
'api_token' => $this->api_token,
'order_id' => 'preauth-' . $order->get_id() . '-' . time(),
'amount' => number_format($amount, 2, '.', ''),
'pan' => $card_data['number'],
'expdate' => $card_data['exp_month'] . $card_data['exp_year'],
'cvd' => $card_data['cvc'],
'crypt_type' => '7'
);
return $this->make_api_request('preauth', $data);
}
/**
* Moneris Completion API
*/
protected function moneris_completion($transaction_id, $amount) {
$data = array(
'store_id' => $this->store_id,
'api_token' => $this->api_token,
'order_id' => 'completion-' . $transaction_id . '-' . time(),
'comp_amount' => number_format($amount, 2, '.', ''),
'txn_number' => $transaction_id,
'crypt_type' => '7'
);
return $this->make_api_request('completion', $data);
}
/**
* 发起 API 请求
*/
protected function make_api_request($endpoint, $data) {
$url = $this->get_api_url();
$xml_data = $this->build_xml_request($endpoint, $data);
$response = wp_remote_post($url, array(
'body' => $xml_data,
'headers' => array(
'Content-Type' => 'application/xml',
'User-Agent' => 'Yoone Subscriptions/' . YOONE_SUBSCRIPTIONS_VERSION
),
'timeout' => 30,
'sslverify' => !$this->testmode
));
if (is_wp_error($response)) {
return array(
'success' => false,
'message' => $response->get_error_message()
);
}
return $this->parse_xml_response(wp_remote_retrieve_body($response));
}
/**
* 获取 API URL
*/
protected function get_api_url() {
if ($this->testmode) {
return $this->processing_country === 'US'
? 'https://esqa.moneris.com/gateway2/servlet/MpgRequest'
: 'https://esqa.moneris.com/gateway2/servlet/MpgRequest';
} else {
return $this->processing_country === 'US'
? 'https://esp.moneris.com/gateway2/servlet/MpgRequest'
: 'https://www3.moneris.com/gateway2/servlet/MpgRequest';
}
}
/**
* 构建 XML 请求
*/
protected function build_xml_request($endpoint, $data) {
$xml = '<?xml version="1.0"?>';
$xml .= '<request>';
$xml .= '<store_id>' . htmlspecialchars($data['store_id']) . '</store_id>';
$xml .= '<api_token>' . htmlspecialchars($data['api_token']) . '</api_token>';
$xml .= '<' . $endpoint . '>';
foreach ($data as $key => $value) {
if ($key !== 'store_id' && $key !== 'api_token') {
$xml .= '<' . $key . '>' . htmlspecialchars($value) . '</' . $key . '>';
}
}
$xml .= '</' . $endpoint . '>';
$xml .= '</request>';
return $xml;
}
/**
* 解析 XML 响应
*/
protected function parse_xml_response($xml_string) {
$xml = simplexml_load_string($xml_string);
if (!$xml) {
return array(
'success' => false,
'message' => __('无效的响应格式', 'yoone-subscriptions')
);
}
$response_code = (string) $xml->receipt->ResponseCode;
$success = $response_code !== null && intval($response_code) < 50;
$result = array(
'success' => $success,
'response_code' => $response_code,
'message' => (string) $xml->receipt->Message,
'transaction_id' => (string) $xml->receipt->TransID,
'reference_num' => (string) $xml->receipt->ReferenceNum,
'data_key' => (string) $xml->receipt->DataKey
);
return $result;
}
/*
|--------------------------------------------------------------------------
| 辅助方法
|--------------------------------------------------------------------------
*/
/**
* 获取信用卡数据
*/
protected function get_card_data() {
if (empty($_POST[$this->id . '-card-number'])) {
return false;
}
$card_number = str_replace(' ', '', $_POST[$this->id . '-card-number']);
$card_expiry = $_POST[$this->id . '-card-expiry'];
$card_cvc = $_POST[$this->id . '-card-cvc'];
// 解析到期日期
$expiry_parts = explode('/', str_replace(' ', '', $card_expiry));
if (count($expiry_parts) !== 2) {
return false;
}
$exp_month = str_pad($expiry_parts[0], 2, '0', STR_PAD_LEFT);
$exp_year = strlen($expiry_parts[1]) === 2 ? '20' . $expiry_parts[1] : $expiry_parts[1];
$exp_year = substr($exp_year, -2); // Moneris 需要 2 位年份
return array(
'number' => $card_number,
'exp_month' => $exp_month,
'exp_year' => $exp_year,
'cvc' => $card_cvc
);
}
/**
* 保存支付令牌
*/
protected function save_payment_token($customer_id, $data_key, $card_data) {
global $wpdb;
// 获取卡片信息
$lookup_response = $this->moneris_res_lookup_full($data_key);
$card_type = '';
$last_four = '';
if ($lookup_response && $lookup_response['success']) {
// 从响应中提取卡片信息
$card_type = $this->get_card_type($card_data['number']);
$last_four = substr($card_data['number'], -4);
}
return $wpdb->insert(
$wpdb->prefix . 'yoone_payment_tokens',
array(
'customer_id' => $customer_id,
'gateway_id' => $this->id,
'token' => $data_key,
'token_type' => 'credit_card',
'card_type' => $card_type,
'last_four' => $last_four,
'expiry_month' => $card_data['exp_month'],
'expiry_year' => '20' . $card_data['exp_year'],
'is_default' => 0,
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql')
),
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s')
);
}
/**
* 获取卡片类型
*/
protected function get_card_type($card_number) {
$card_number = str_replace(' ', '', $card_number);
if (preg_match('/^4/', $card_number)) {
return 'visa';
} elseif (preg_match('/^5[1-5]/', $card_number)) {
return 'mastercard';
} elseif (preg_match('/^3[47]/', $card_number)) {
return 'amex';
}
return 'unknown';
}
/**
* 检查订单是否包含订阅
*/
protected function order_contains_subscription($order) {
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->get_meta('_yoone_subscription_enabled') === 'yes') {
return true;
}
}
return false;
}
/**
* 从订单获取订阅
*/
protected function get_subscription_from_order($order) {
global $wpdb;
$subscription_id = $wpdb->get_var($wpdb->prepare("
SELECT id FROM {$wpdb->prefix}yoone_subscriptions
WHERE parent_order_id = %d
LIMIT 1
", $order->get_id()));
return $subscription_id ? new Yoone_Subscription($subscription_id) : null;
}
/**
* 计划订阅支付
*/
public function scheduled_subscription_payment($amount_to_charge, $renewal_order) {
$subscription = $this->get_subscription_from_order($renewal_order);
if ($subscription) {
$result = $this->process_recurring_subscription_payment($subscription, $amount_to_charge);
if ($result) {
$renewal_order->payment_complete();
} else {
$renewal_order->update_status('failed');
}
}
}
/**
* 检查是否可用
*/
public function is_available() {
return parent::is_available() && !empty($this->store_id) && !empty($this->api_token);
}
}