yoone-wc-moneris-payments/includes/class-yoone-gateway-moneris...

721 lines
34 KiB
PHP
Raw 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
if (! defined('ABSPATH')) {
exit;
}
/**
* Yoone Moneris 网关(骨架)
*/
class Yoone_Gateway_Moneris extends WC_Payment_Gateway_CC
{
// 直连 Moneris 配置(用于表单保存)
public $sandbox; // 沙盒模式
/** 商店ID由支付网关提供的唯一标识符 */
public $store_id;
/** API令牌用于身份验证的密钥 */
public $api_token;
/** 国家代码,支持 "CA"(加拿大)和 "US"(美国) */
public $country_code; // CA/US
/** 是否启用状态检查,默认为 false */
public $status_check; // 布尔
public $crypt_type; // 默认 7
/** 国家/地区CA 或 US */
// 提示:仅直连一种形式时,使用通用命名即可,无需 前缀
public $protocol; // https
public $port; // 443
public $host; // 可选:覆盖默认主机
public $path; // 可选:覆盖默认路径
public $http_method; // POST
public $request_timeout; // 30
/**
* 构造函数:初始化网关配置与能力,并读取后台保存的设置。
*
* - 定义网关 ID/标题/描述/支持的能力
* - 初始化设置表单字段并加载已保存的设置值
* - 挂载后台保存钩子以持久化管理员输入
*/
public function __construct()
{
$this->id = 'yoone_moneris';
$this->method_title = __('Moneris (Yoone)', 'yoone-moneris');
$this->method_description = __('使用 Moneris 支付,支持订阅自动续费(需接入 Moneris 令牌化)。', 'yoone-moneris');
$this->has_fields = true;
/**
* $this->supports 数组告诉 WooCommerce 本支付网关“支持”哪些功能。
* 每一项都是一个字符串标识Woo 核心/扩展会根据这些标识判断能否调用对应功能:
* - 'products' : 支持普通商品付款(默认就有,可省略)
* - 'default_credit_card_form': 可使用 WC_Payment_Gateway_CC 自带的信用卡表单
* - 'tokenization' : 支持“令牌化”,即保存/复用信用卡Woo 会显示“保存此卡”复选框)
* - 'refunds' : 支持后台订单退款(需要实现 process_refund 方法)
* - 'pre-orders' : 支持 WooCommerce Pre-Orders 插件
* - 'subscriptions' : 支持 WooCommerce Subscriptions 插件
* - 'subscription_cancellation' : 支持订阅取消
* - 'subscription_suspension' : 支持订阅暂停
* - 'subscription_reactivation' : 支持订阅重新激活
* - 'subscription_amount_changes' : 支持修改订阅金额
* - 'subscription_date_changes' : 支持修改订阅日期
* - 'multiple_subscriptions' : 支持一次性购买多个订阅
* - 'add_payment_method' : 支持“我的账户 → 付款方式”里手动添加卡片
*
* 只要将对应字符串写进数组Woo 就会在合适场景下触发对应钩子或显示对应 UI
* 如果缺少某项Woo 会认为本网关不具备该能力,从而隐藏相关按钮或流程。
*/
$this->supports = array(
'products',
'default_credit_card_form',
'tokenization',
'refunds',
'pre-orders',
// Subscriptions 相关
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'multiple_subscriptions',
'add_payment_method',
);
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title', 'Moneris');
$this->description = $this->get_option('description', 'Pay with your credit card via Moneris.');
$this->enabled = $this->get_option('enabled', 'yes');
$this->sandbox = $this->get_option('sandbox', 'yes');
$this->store_id = $this->get_option('store_id', '');
$this->api_token = $this->get_option('api_token', '');
$this->country_code = $this->get_option('country_code', 'CA');
$this->crypt_type = $this->get_option('crypt_type', '7');
$this->status_check = 'yes' === $this->get_option('status_check', 'no');
$this->protocol = $this->get_option('protocol', 'https');
$this->port = absint($this->get_option('port', 443));
$this->host = $this->get_option('host', '');
$this->path = $this->get_option('path', '');
$this->http_method = $this->get_option('http_method', 'POST');
$this->request_timeout= absint($this->get_option('request_timeout', 30));
// 下面这一行 add_action 的作用是:
// 当管理员在 WooCommerce → 设置 → 付款 → Moneris 里点击“保存设置”按钮时,
// Woo 会触发钩子woocommerce_update_options_payment_gateways_{网关ID}。
// 我们把当前网关类里自带的 process_admin_options() 方法挂到这个钩子上,
// 从而把用户在后台表单里填的 store_id、api_token、是否启用沙箱等选项写进数据库。
add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
}
/**
* init_form_fields()
*
* 这是 WooCommerce 支付网关的标准方法用于定义网关在后端设置页面WooCommerce → 设置 → 付款 → Moneris
* 需要显示的所有配置字段(表单控件)。把这些字段写进 $this->form_fields 数组后WooCommerce 会自动:
* 1. 在后台渲染对应的输入框、下拉框、复选框等;
* 2. 接收管理员提交的值;
* 3. 通过 process_admin_options() 把值保存到 wp_options 表(键名格式类似 woocommerce_yoone_moneris_settings
*
* 换句话说init_form_fields 就是“告诉 WooCommerce 我的网关有哪些设置项”。
*/
/**
* 初始化后台设置表单字段。
*
* WooCommerce 根据 $this->form_fields 渲染后台“设置 → 付款 → Moneris (Yoone)”页面,
* 并在保存时将值存入 wp_options供 $this->get_option() 读取。
*/
public function init_form_fields()
{
$this->form_fields = array(
'enabled' => array(
'title' => __('启用/禁用', 'yoone-moneris'),
'label' => __('启用 Moneris', 'yoone-moneris'),
'type' => 'checkbox',
'default' => 'yes',
),
'title' => array(
'title' => __('标题', 'yoone-moneris'),
'type' => 'text',
'default' => __('Moneris', 'yoone-moneris'),
),
'description' => array(
'title' => __('描述', 'yoone-moneris'),
'type' => 'textarea',
'default' => 'Pay with your credit card via Moneris.',
),
'sandbox' => array(
'title' => __('Sandbox', 'yoone-moneris'),
'label' => __('启用沙箱模式', 'yoone-moneris'),
'type' => 'checkbox',
'default' => 'yes',
),
'store_id' => array(
'title' => __('Store ID', 'yoone-moneris'),
'type' => 'text',
'desc_tip' => __('Moneris 账户 Store ID。', 'yoone-moneris'),
),
'api_token' => array(
'title' => __('API Token', 'yoone-moneris'),
'type' => 'text',
'desc_tip' => __('Moneris API Token。', 'yoone-moneris'),
),
'country_code' => array(
'title' => __('国家/地区', 'yoone-moneris'),
'type' => 'select',
'description' => __('选择 CA加拿大或 US美国。将影响请求路径与主机。', 'yoone-moneris'),
'default' => 'CA',
'options' => array(
'CA' => 'CA',
'US' => 'US',
),
),
'crypt_type' => array(
'title' => __('Crypt Type', 'yoone-moneris'),
'type' => 'text',
'description' => __('默认 7。用于交易加密类型。', 'yoone-moneris'),
'default' => '7',
),
'status_check' => array(
'title' => __('状态检查', 'yoone-moneris'),
'label' => __('启用 statusCheck', 'yoone-moneris'),
'type' => 'checkbox',
'default' => 'no',
),
'protocol' => array(
'title' => __('协议', 'yoone-moneris'),
'type' => 'text',
'default' => 'https',
),
'port' => array(
'title' => __('端口', 'yoone-moneris'),
'type' => 'number',
'default' => 443,
),
'host' => array(
'title' => __('自定义主机(高级)', 'yoone-moneris'),
'type' => 'text',
'description' => __('可选:覆盖默认主机。例如生产 CA: www3.moneris.com测试: esqa.moneris.com。US 可参考文档。', 'yoone-moneris'),
'default' => '',
),
'path' => array(
'title' => __('自定义路径(高级)', 'yoone-moneris'),
'type' => 'text',
'description' => __('默认 CA: /gateway2/servlet/MpgRequestUS: /gateway_us/servlet/MpgRequestMPI: /mpi/servlet/MpiServlet。', 'yoone-moneris'),
'default' => '',
),
'http_method' => array(
'title' => __('HTTP 方法', 'yoone-moneris'),
'type' => 'text',
'description' => __('默认 POSTMoneris XML API。仅供调试或特殊需求。', 'yoone-moneris'),
'default' => 'POST',
),
'request_timeout' => array(
'title' => __('请求超时(秒)', 'yoone-moneris'),
'type' => 'number',
'default' => 30,
),
);
}
/**
* 在结账页渲染本支付网关的表单字段(信用卡输入区)。
* 1. 先读取并拼接网关描述(含沙箱提示),用 wp_kses_post 过滤后输出。
* 2. 若网关声明支持 'default_credit_card_form',则调用父类方法输出 Woo 默认信用卡表单:
* - 已登录用户会看到“使用已保存卡片/使用新卡片”的单选列表;
* - 未登录或选“新卡片”则显示卡号、有效期、CVC 输入框。
* 注意:本实现针对经典短代码结账;若使用 WooCommerce Blocks 结账需另行集成。
*/
public function payment_fields()
{
// 结账页(例如 https://canadiantails.local/checkout/ )会调用此方法渲染网关的前端表单。
// 这里我们使用 WooCommerce 默认的信用卡表单classic checkout即 WC_Payment_Gateway_CC 提供的字段:
// - 卡号、有效期、CVC字段名形如yoone_moneris-card-number / -card-expiry / -card-cvc
// 若你使用 WooCommerce Blocks 的结账(新式块编辑器结账页),需要另外实现 Blocks 支付集成,
// 当前插件仅针对经典结账表单;后续可添加 blocks 集成以支持新版结账。
$description = $this->get_description();
error_log('【yoone moneris 】这里想记的东西' . print_r($description, true));
if ('yes' === $this->sandbox) {
$description .= ' ' . __('沙箱模式开启:使用测试卡(例如 4242 4242 4242 4242。', 'yoone-moneris');
}
if ($description) {
// 在结账页输出网关描述(含沙箱提示),并渲染默认信用卡表单
echo wp_kses_post(wpautop(wptexturize(trim($description))));
}
if ($this->supports('default_credit_card_form')) {
// 输出默认信用卡表单。若用户已保存过卡片Woo 会在此处同时渲染“选择已保存的卡/使用新卡”的单选列表。
parent::payment_fields();
}
}
/**
* 控制本网关在结账页的可用性。
*
* 返回 true 才会在结账页显示本支付方式。这里做最基本的判断:
* - 网关已启用;
* - 站点货币为 CAD 或 USDMoneris 支持的主币种);
* - 若站点未启用 SSL仍允许在沙箱模式下使用生产建议强制 SSL。
*
* 如需更细粒度控制(如按国家/运送方式/订单总额限制),可在此扩展。
*
* @return bool
*/
public function is_available()
{
return true;
// 111
if ('yes' !== $this->enabled) {
return false;
}
// 货币限制Moneris 主要支持 CAD/USD
$currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'CAD';
if (! in_array($currency, array('CAD', 'USD'), true)) {
return false;
}
// 在生产环境建议强制 SSL沙箱允许非 SSL避免本地开发阻塞
$is_ssl = function_exists('is_ssl') ? is_ssl() : false;
$is_sandbox = ('yes' === strtolower((string) $this->sandbox));
if (! $is_ssl && ! $is_sandbox) {
return false;
}
return true;
}
/**
* 验证结账页输入的信用卡字段。
*
* 若选择已保存令牌则跳过校验;否则要求卡号、有效期(月/年)和 CVC 均存在。
* @return bool 通过返回 true否则添加错误提示并返回 false
*/
public function validate_fields()
{
// 若用户选择了已保存的令牌,则无需验证卡片字段
$selected_token_id = $this->get_selected_token_id();
if ($selected_token_id) {
return true;
}
// 新卡:需要确保卡片字段完整
$card = $this->get_posted_card();
error_log('【Yoone Moneris】validate_fields 读取到卡片字段:' . print_r($card, true));
if (empty($card['number']) || empty($card['exp_month']) || empty($card['exp_year']) || empty($card['cvc'])) {
wc_add_notice(__('请填写完整的银行卡信息。', 'yoone-moneris'), 'error');
error_log('【Yoone Moneris】validate_fields 缺少字段number=' . (empty($card['number']) ? '空' : '已填') . ' exp_month=' . (empty($card['exp_month']) ? '空' : $card['exp_month']) . ' exp_year=' . (empty($card['exp_year']) ? '空' : $card['exp_year']) . ' cvc=' . (empty($card['cvc']) ? '空' : '已填'));
return false;
}
return true;
}
// 覆盖父类的字段 name 逻辑:始终输出 name 属性MVP便于后端读取 $_POST 中的卡信息。
// 注意:生产环境建议采用前端令牌化方案,不在服务器接收原始卡号。
/**
* 返回输入字段的 name 属性字符串以便后端读取。
*
* 某些主题/表单可能省略 name本方法强制输出。
* @param string $name 字段基名(如 card-number、card-expiry、card-cvc
* @return string 形如 name="yoone_moneris-card-number" 的片段
*/
public function field_name($name)
{
return ' name="' . esc_attr($this->id . '-' . $name) . '" ';
}
// 移除自定义 form()恢复使用父类默认信用卡表单渲染default_credit_card_form
// 注意:仍保留 field_name() 的覆盖以确保输入框拥有 name 属性,后端可从 $_POST 读取卡片信息。
/**
* 从 $_POST 中提取并规范化用户输入的卡片信息。
*
* 兼容多种字段命名移除非数字字符解析组合有效期MM/YY为分拆的月/年;
* 若本次提交的支付方式不是当前网关,返回空卡数据。
* @return array{number:string,exp_month:string,exp_year:string,cvc:string}
*/
protected function get_posted_card()
{
// 记录原始支付方式选择
$raw_pm = isset($_POST['payment_method']) ? wc_clean(wp_unslash($_POST['payment_method'])) : '';
error_log('【Yoone Moneris】get_posted_card payment_method=' . ($raw_pm ?: '未提交') . ' 当前网关=' . $this->id);
// 如果当前提交并非选择我们网关,直接返回空卡数据,避免误读其他网关字段
$pm = $raw_pm;
if ($pm && $pm !== $this->id) {
error_log('【Yoone Moneris】get_posted_card 非本网关提交,返回空卡数据');
return array('number' => '', 'exp_month' => '', 'exp_year' => '', 'cvc' => '');
}
// 助手:按优先级从 $_POST 中读取第一个非空值
$read_post = function ($candidates) {
foreach ((array) $candidates as $k) {
if (isset($_POST[$k])) {
$val = wc_clean(wp_unslash($_POST[$k]));
if ('' !== $val) {
return $val;
}
}
}
return '';
};
// 兼容不同主题/插件的字段命名
$number = $read_post(array(
$this->id . '-card-number',
'wc-' . $this->id . '-card-number',
'card-number',
'wc-card-number',
$this->id . '_card_number',
'cardnumber',
'card_number',
));
$expiry_raw = $read_post(array(
$this->id . '-card-expiry',
'wc-' . $this->id . '-card-expiry',
'card-expiry',
'wc-card-expiry',
'expiry',
'card_expiry',
));
$cvc = $read_post(array(
$this->id . '-card-cvc',
'wc-' . $this->id . '-card-cvc',
'card-cvc',
'wc-card-cvc',
'cvc',
'card_cvc',
));
error_log('【Yoone Moneris】get_posted_card 原始读取number=' . (isset($number) ? ('len=' . strlen((string)$number)) : '未读') . ' expiry_raw=' . ($expiry_raw ?: '空') . ' cvc=' . ($cvc ? ('len=' . strlen((string)$cvc)) : '空'));
// 如果没有组合有效期,尝试读取分拆的 月/年 字段
$exp_month = $read_post(array(
$this->id . '-exp-month',
'wc-' . $this->id . '-exp-month',
$this->id . '-card-expiry-month',
'wc-' . $this->id . '-card-expiry-month',
'exp-month',
'card-expiry-month',
'expiry_month',
));
$exp_year = $read_post(array(
$this->id . '-exp-year',
'wc-' . $this->id . '-exp-year',
$this->id . '-card-expiry-year',
'wc-' . $this->id . '-card-expiry-year',
'exp-year',
'card-expiry-year',
'expiry_year',
));
error_log('【Yoone Moneris】get_posted_card 原始读取exp_month=' . ($exp_month ?: '空') . ' exp_year=' . ($exp_year ?: '空'));
// 规范化:仅保留数字
$number = preg_replace('/\D+/', '', (string) $number);
$cvc = preg_replace('/\D+/', '', (string) $cvc);
// 规范化有效期:优先解析组合字段(支持 "MM / YY" 或 "MM/YY" 或包含中文空格)
$exp_month = preg_replace('/\D+/', '', (string) $exp_month);
$exp_year = preg_replace('/\D+/', '', (string) $exp_year);
if ($expiry_raw && (! $exp_month || ! $exp_year)) {
$parts = array_map('trim', preg_split('/\s*\/\s*/', (string) $expiry_raw));
$m = isset($parts[0]) ? preg_replace('/\D+/', '', $parts[0]) : '';
$y = isset($parts[1]) ? preg_replace('/\D+/', '', $parts[1]) : '';
if ($m) {
$exp_month = $m;
}
if ($y) {
$exp_year = $y;
}
}
// 两位年转四位年
if (strlen($exp_year) === 2) {
$exp_year = '20' . $exp_year;
}
$masked = $number ? str_repeat('*', max(strlen($number) - 4, 0)) . substr($number, -4) : '';
error_log('【Yoone Moneris】get_posted_card 规范化card=' . ($masked ?: '空') . ' len=' . strlen($number) . ' exp_month=' . ($exp_month ?: '空') . ' exp_year=' . ($exp_year ?: '空') . ' cvc_len=' . ($cvc ? strlen($cvc) : 0));
return array(
'number' => $number,
'exp_month' => $exp_month,
'exp_year' => $exp_year,
'cvc' => $cvc,
);
}
/**
* 读取结账页提交的已保存支付令牌 ID。
*
* 当用户选择“使用新卡”时值为 'new';选择已有令牌时返回其整数 ID。
* @return int 令牌 ID若未选择或为 'new',返回 0
*/
protected function get_selected_token_id()
{
// 该方法用于解析结账页(/checkout提交的“选择已保存支付方式”的字段
// Woo 默认字段名:'wc-' . gateway_id . '-payment-token'例如wc-yoone_moneris-payment-token
// 当用户选择“已保存的卡”,此字段会传递一个 token 的 ID当选择“使用新卡”该字段值为 'new'。
// 注意:此字段由 Woo 默认信用卡表单生成,适用于经典结账;如果使用 Blocks 结账,需要对应 Blocks 集成来产生等效数据。
$field = 'wc-' . $this->id . '-payment-token';
if (isset($_POST[$field])) {
$posted = wc_clean(wp_unslash($_POST[$field]));
error_log('【Yoone Moneris】get_selected_token_id 提交字段 ' . $field . ' = ' . $posted);
}
if (isset($_POST[$field]) && 'new' !== $_POST[$field]) {
// 将提交的令牌 ID 转为整数,后续用 WC_Payment_Tokens::get( $id ) 读取具体令牌对象
return absint($_POST[$field]);
}
error_log('【Yoone Moneris】get_selected_token_id 未选择已保存令牌或选择了新卡');
return 0;
}
/**
* 构造并返回直连 Moneris 的 API 客户端。
*
* 将后台直连配置打包为 $direct 传入 API 层。
* @return Yoone_Moneris_API
*/
protected function api()
{
// 仅直连 Moneris
if (! class_exists('Yoone_Moneris_API')) {
// 防御:在极端情况下文件未加载
require_once dirname(__FILE__) . '/class-yoone-moneris-api.php';
}
$config = array(
'country_code' => $this->country_code,
'crypt_type' => $this->crypt_type,
'status_check' => (bool) $this->status_check,
'protocol' => $this->protocol,
'port' => absint($this->port),
'host' => $this->host,
'path' => $this->path,
'http_method' => $this->http_method,
'timeout' => absint($this->request_timeout),
);
return new Yoone_Moneris_API($this->store_id, $this->api_token, $this->sandbox, $config);
}
/**
* 根据令牌化结果创建并保存 WooCommerce 支付令牌。
*
* @param array $res tokenize_card 返回的结构,需包含 token/last4/exp_month/exp_year
* @param int $user_id 用户 ID
* @return int WC_Payment_Token_CC 的 ID
*/
protected function create_wc_token($res, $user_id)
{
error_log('【yoone moneris 】创建 WC 支付令牌tokenize_card 返回结果' . print_r($res, true));
$token = new WC_Payment_Token_CC();
// 兼容 data_key 命名
if (empty($res['token']) && ! empty($res['data_key'])) {
$res['token'] = $res['data_key'];
error_log('【Yoone Moneris】create_wc_token 使用 data_key 作为 token');
}
$token->set_token($res['token']);
$token->set_gateway_id($this->id);
$token->set_user_id($user_id);
$token->set_last4(substr(preg_replace('/\D+/', '', (string) $res['last4'] ?? ''), -4) ?: '0000');
$token->set_expiry_month(preg_replace('/\D+/', '', (string) ($res['exp_month'] ?? '')) ?: '01');
$token->set_expiry_year(preg_replace('/\D+/', '', (string) ($res['exp_year'] ?? '')) ?: (date('Y') + 1));
// 设置卡类型(若有),避免 Woo 对必填字段校验失败
if (! empty($res['brand'])) {
$token->set_card_type((string) $res['brand']);
}
$token->save();
$tid = $token->get_id();
if (! $tid) {
error_log('【Yoone Moneris】create_wc_token 保存失败WC_Token 未生成 ID检查必填字段。');
}
return $tid;
}
/**
* 处理首笔支付:支持选择已保存令牌或新卡令牌化后扣款。
*
* 成功时标记订单已支付并返回重定向地址;失败时返回 fail 并提示错误。
* @param int $order_id 订单 ID
* @return array{result:string,redirect?:string}
*/
public function process_payment($order_id)
{
error_log('【Yoone Moneris】process_payment 开始order_id=' . $order_id);
$order = wc_get_order($order_id);
if (! $order) {
error_log('【Yoone Moneris】无法获取订单对象终止');
return array('result' => 'fail');
}
$user_id = $order->get_user_id();
$pm_meta = $order->get_meta('_yoone_moneris_pm_data', true);
if (! empty($pm_meta)) {
error_log('【Yoone Moneris】订单存在 Blocks/Store API 支付数据 _yoone_moneris_pm_data=' . print_r($pm_meta, true));
} else {
error_log('【Yoone Moneris】订单未找到 _yoone_moneris_pm_data 元数据');
}
// 选择已保存的令牌或创建新令牌
$selected_token_id = $this->get_selected_token_id();
$token_string = '';
if ($selected_token_id) {
$token_obj = WC_Payment_Tokens::get($selected_token_id);
if ($token_obj && $token_obj->get_user_id() == $user_id && $token_obj->get_gateway_id() === $this->id) {
$token_string = $token_obj->get_token();
} else {
wc_add_notice(__('选择的支付令牌不可用。', 'yoone-moneris'), 'error');
return array('result' => 'fail');
}
} else {
// 创建新令牌(占位)
// 优先使用 Blocks/Store API 保存到订单的支付数据
$card = array();
if (is_array($pm_meta) && ! empty($pm_meta)) {
$card = array(
'number' => preg_replace('/\D+/', '', (string) ($pm_meta['number'] ?? '')),
'exp_month' => preg_replace('/\D+/', '', (string) ($pm_meta['exp_month'] ?? '')),
'exp_year' => preg_replace('/\D+/', '', (string) ($pm_meta['exp_year'] ?? '')),
'cvc' => preg_replace('/\D+/', '', (string) ($pm_meta['cvc'] ?? '')),
);
error_log('【Yoone Moneris】使用 Blocks/Store API 数据作为卡片来源:' . print_r($card, true));
}
if (empty($card['number']) || empty($card['exp_month']) || empty($card['exp_year']) || empty($card['cvc'])) {
// 退回到经典结账表单的 POST 字段
$card = $this->get_posted_card();
error_log('【Yoone Moneris】使用经典结账 POST 字段作为卡片来源:' . print_r($card, true));
}
$res = $this->api()->tokenize_card($card);
error_log('【Yoone Moneris】tokenize_card 返回=' . print_r($res, true));
if (empty($res['success'])) {
wc_add_notice(__('令牌化失败:', 'yoone-moneris') . $res['error'], 'error');
error_log('【Yoone Moneris】令牌化失败' . ($res['error'] ?? '未知错误'));
return array('result' => 'fail');
}
// 兼容 data_key 命名
if (empty($res['token']) && ! empty($res['data_key'])) {
$res['token'] = $res['data_key'];
error_log('【Yoone Moneris】tokenize_card 返回使用 data_key 作为 token');
}
$token_string = $res['token'];
error_log('【Yoone Moneris】新令牌创建成功token_len=' . strlen($token_string) . ' last4=' . ($res['last4'] ?? ''));
// 保存到用户
if ($user_id) {
$wc_token_id = $this->create_wc_token($res + ['exp_month' => $card['exp_month'], 'exp_year' => $card['exp_year']], $user_id);
error_log('【Yoone Moneris】已保存令牌到用户wc_token_id=' . $wc_token_id . ' user_id=' . $user_id);
$order->add_payment_token($wc_token_id);
}
}
// 首笔扣款
$amount = $order->get_total();
$currency = $order->get_currency();
error_log('【Yoone Moneris】准备扣款amount=' . $amount . ' currency=' . $currency . ' order_id=' . $order_id);
$charge = $this->api()->charge_token($token_string, $amount, $currency, $order_id);
error_log('【Yoone Moneris】charge_token 返回=' . print_r($charge, true));
if (empty($charge['success'])) {
wc_add_notice(__('支付失败:', 'yoone-moneris') . ($charge['error'] ?? ''), 'error');
error_log('【Yoone Moneris】支付失败' . ($charge['error'] ?? '未知错误'));
return array('result' => 'fail');
}
// 标记订单已支付
$order->payment_complete($charge['transaction_id'] ?? '');
$order->add_order_note(sprintf('Moneris 首付成功,交易号:%s', $charge['transaction_id'] ?? 'N/A'));
error_log('【Yoone Moneris】支付成功transaction_id=' . ($charge['transaction_id'] ?? ''));
return array(
'result' => 'success',
'redirect' => $this->get_return_url($order),
);
}
/**
* Woo Subscriptions 自动续费扣款。
*
* 从订单或用户查找与本网关关联的令牌后进行扣款,并写入订单备注。
* @param float|int $amount_to_charge 本次应扣金额
* @param int|WC_Order $order 订单或订单 ID
* @return void
*/
public function scheduled_subscription_payment($amount_to_charge, $order)
{
if (is_numeric($order)) {
$order = wc_get_order($order);
}
if (! $order) return;
$user_id = $order->get_user_id();
// 找到一个我们网关的令牌(订单或用户)
$tokens = WC_Payment_Tokens::get_order_tokens($order->get_id());
$token_string = '';
foreach ($tokens as $t) {
if ($t->get_gateway_id() === $this->id) {
$token_string = $t->get_token();
break;
}
}
if (! $token_string && $user_id) {
$user_tokens = WC_Payment_Tokens::get_customer_tokens($user_id, $this->id);
if (! empty($user_tokens)) {
$first = array_shift($user_tokens);
$token_string = $first->get_token();
}
}
if (! $token_string) {
$order->update_status('failed', '未找到可用的支付令牌,自动续费失败。');
return;
}
$currency = $order->get_currency();
$charge = $this->api()->charge_token($token_string, $amount_to_charge, $currency, $order->get_id());
if (empty($charge['success'])) {
$order->update_status('failed', '自动续费扣款失败:' . ($charge['error'] ?? ''));
return;
}
$order->payment_complete($charge['transaction_id'] ?? '');
$order->add_order_note(sprintf('Moneris 自动续费成功,交易号:%s', $charge['transaction_id'] ?? 'N/A'));
}
/**
* “我的账户 → 付款方式”添加新卡片并保存令牌。
*
* @return array|void 成功返回重定向地址;失败添加错误提示
*/
public function add_payment_method()
{
$user_id = get_current_user_id();
if (! $user_id) {
wc_add_notice(__('请先登录再添加支付方式。', 'yoone-moneris'), 'error');
return;
}
$card = $this->get_posted_card();
$res = $this->api()->tokenize_card($card);
if (empty($res['success'])) {
wc_add_notice(__('添加支付方式失败:', 'yoone-moneris') . $res['error'], 'error');
return;
}
$wc_token_id = $this->create_wc_token($res + ['exp_month' => $card['exp_month'], 'exp_year' => $card['exp_year']], $user_id);
wc_add_notice(__('支付方式已添加。', 'yoone-moneris'), 'success');
return array('result' => 'success', 'redirect' => wc_get_endpoint_url('payment-methods'));
}
/**
* 后台订单退款。
*
* 从订单获取交易号,调用 API 退款并记录订单备注。
* @param int $order_id 订单 ID
* @param float|null $amount 退款金额
* @param string $reason 退款原因
* @return true|WP_Error 成功返回 true失败返回 WP_Error
*/
public function process_refund($order_id, $amount = null, $reason = '')
{
$order = wc_get_order($order_id);
$transaction_id = $order ? $order->get_transaction_id() : '';
if (! $transaction_id) {
return new WP_Error('no_tx', __('缺少交易号,无法退款。', 'yoone-moneris'));
}
$res = $this->api()->refund($transaction_id, $amount);
if (empty($res['success'])) {
return new WP_Error('refund_failed', __('退款失败:', 'yoone-moneris') . ($res['error'] ?? ''));
}
$order->add_order_note(sprintf('Moneris 已退款:%s', wc_price($amount)));
return true;
}
}