672 lines
30 KiB
PHP
672 lines
30 KiB
PHP
<?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/MpgRequest;US: /gateway_us/servlet/MpgRequest;MPI: /mpi/servlet/MpiServlet。', 'yoone-moneris'),
|
||
'default' => '',
|
||
),
|
||
'http_method' => array(
|
||
'title' => __('HTTP 方法', 'yoone-moneris'),
|
||
'type' => 'text',
|
||
'description' => __('默认 POST(Moneris 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 或 USD(Moneris 支持的主币种);
|
||
* - 若站点未启用 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();
|
||
if (empty($card['number']) || empty($card['exp_month']) || empty($card['exp_year']) || empty($card['cvc'])) {
|
||
wc_add_notice(__('请填写完整的银行卡信息。' . $card, 'yoone-moneris'), 'error');
|
||
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()
|
||
{
|
||
// 如果当前提交并非选择我们网关,直接返回空卡数据,避免误读其他网关字段
|
||
$pm = isset($_POST['payment_method']) ? wc_clean(wp_unslash($_POST['payment_method'])) : '';
|
||
if ($pm && $pm !== $this->id) {
|
||
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',
|
||
));
|
||
|
||
// 如果没有组合有效期,尝试读取分拆的 月/年 字段
|
||
$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',
|
||
));
|
||
|
||
// 规范化:仅保留数字
|
||
$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;
|
||
}
|
||
|
||
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]) && 'new' !== $_POST[$field]) {
|
||
// 将提交的令牌 ID 转为整数,后续用 WC_Payment_Tokens::get( $id ) 读取具体令牌对象
|
||
return absint($_POST[$field]);
|
||
}
|
||
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)
|
||
{
|
||
$token = new WC_Payment_Token_CC();
|
||
$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));
|
||
$token->save();
|
||
return $token->get_id();
|
||
}
|
||
|
||
/**
|
||
* 处理首笔支付:支持选择已保存令牌或新卡令牌化后扣款。
|
||
*
|
||
* 成功时标记订单已支付并返回重定向地址;失败时返回 fail 并提示错误。
|
||
* @param int $order_id 订单 ID
|
||
* @return array{result:string,redirect?:string}
|
||
*/
|
||
public function process_payment($order_id)
|
||
{
|
||
$order = wc_get_order($order_id);
|
||
$user_id = $order->get_user_id();
|
||
|
||
// 支持 Blocks:若从 Store API 捕获到支付数据,优先使用该数据
|
||
$blocks_data = is_object($order) && method_exists($order, 'get_meta') ? (array) $order->get_meta('_yoone_moneris_pm_data') : array();
|
||
|
||
// 选择已保存的令牌或创建新令牌
|
||
$selected_token_id = $this->get_selected_token_id();
|
||
if (! $selected_token_id && ! empty($blocks_data['saved_token_id']) && 'new' !== $blocks_data['saved_token_id']) {
|
||
$selected_token_id = absint($blocks_data['saved_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 {
|
||
// 创建新令牌(占位)
|
||
$card = $this->get_posted_card();
|
||
// 若 Blocks 提供了卡片数据,使用其值覆盖经典表单读取结果
|
||
if (! empty($blocks_data)) {
|
||
$normalize = function($v){ return preg_replace('/\D+/', '', (string) $v); };
|
||
$number = isset($blocks_data['number']) ? $normalize($blocks_data['number']) : '';
|
||
$cvc = isset($blocks_data['cvc']) ? $normalize($blocks_data['cvc']) : '';
|
||
$exp_month = isset($blocks_data['exp_month']) ? $normalize($blocks_data['exp_month']) : '';
|
||
$exp_year = isset($blocks_data['exp_year']) ? $normalize($blocks_data['exp_year']) : '';
|
||
if (strlen($exp_year) === 2) { $exp_year = '20' . $exp_year; }
|
||
$card = array(
|
||
'number' => $number ?: $card['number'],
|
||
'exp_month' => $exp_month ?: $card['exp_month'],
|
||
'exp_year' => $exp_year ?: $card['exp_year'],
|
||
'cvc' => $cvc ?: $card['cvc'],
|
||
);
|
||
}
|
||
$res = $this->api()->tokenize_card($card);
|
||
if (empty($res['success'])) {
|
||
wc_add_notice(__('令牌化失败:', 'yoone-moneris') . $res['error'], 'error');
|
||
return array('result' => 'fail');
|
||
}
|
||
$token_string = $res['token'];
|
||
// 保存到用户
|
||
if ($user_id) {
|
||
$wc_token_id = $this->create_wc_token($res + ['exp_month' => $card['exp_month'], 'exp_year' => $card['exp_year']], $user_id);
|
||
$order->add_payment_token($wc_token_id);
|
||
}
|
||
}
|
||
|
||
// 首笔扣款
|
||
$amount = $order->get_total();
|
||
$currency = $order->get_currency();
|
||
$charge = $this->api()->charge_token($token_string, $amount, $currency, $order_id);
|
||
if (empty($charge['success'])) {
|
||
wc_add_notice(__('支付失败:', 'yoone-moneris') . ($charge['error'] ?? ''), 'error');
|
||
return array('result' => 'fail');
|
||
}
|
||
|
||
// 标记订单已支付
|
||
$order->payment_complete($charge['transaction_id'] ?? '');
|
||
$order->add_order_note(sprintf('Moneris 首付成功,交易号:%s', $charge['transaction_id'] ?? 'N/A'));
|
||
|
||
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;
|
||
}
|
||
}
|