PAYATHON 2026

PHP:Moneris Vault 免费试用后无法设置 Recurring Bills

支付老李

结论

data_key 没有问题。它是 Moneris Vault 中永久支付资料对应的令牌,可以用于 res_purchase_cc。

在 Live 环境中,如果 getRecurSuccess() 返回空值,通常表示响应里没有 recurring 结果字段。常见原因是生产商户账户尚未启用 Recurring Billing,或当前 store 没有相应权限。Sandbox 通常预先开放了测试功能,所以同一份代码在 Sandbox 成功,并不代表生产账户也已开通该功能。

当前代码还有一个配置问题:它设置的是每天扣款一次,而不是每月扣款一次。

'recur_unit' => 'day',
'period'     => '1',

首先检查生产账户权限

联系 Moneris 支持或账户经理,确认以下事项:

  • Live store_id 是否已启用 Recurring Billing。
  • Recurring Billing 是否允许与 Vault 的 data_key 配合使用。
  • 生产账户是否接受金额为 0.00 的 res_purchase_cc。
  • 当前交易是否需要 Credential on File(COF)相关字段。
  • 使用的 crypt_type 是否符合该商户账户和交易场景。

Sandbox 和生产环境的这些配置通常互相独立。如果生产端没有启用 recurring,主交易仍可能返回响应,但系统不会创建 recurring 计划,此时 getRecurSuccess() 可能为空。

不要只检查 getRecurSuccess()

读取 recurring 结果之前,先确认主交易是否获批,并记录完整的响应字段:

$mpgResponse = $mpgHttpPost->getMpgResponse();

$responseCode = $mpgResponse->getResponseCode();
$message      = $mpgResponse->getMessage();
$complete     = $mpgResponse->getComplete();
$receiptId    = $mpgResponse->getReceiptId();
$recurSuccess = $mpgResponse->getRecurSuccess();

error_log('Moneris response: ' . print_r([
    'response_code' => $responseCode,
    'message'       => $message,
    'complete'      => $complete,
    'receipt_id'    => $receiptId,
    'recur_success' => $recurSuccess,
], true));

如果 PHP SDK 可以访问原始响应或 response tags,也应一并记录。不要把卡号、CVV、API token 或其他敏感支付数据写入日志。

getRecurSuccess() 为空与返回 "false" 是两种不同情况:

  • "true":Recurring 计划已创建。
  • "false":响应中明确包含失败结果。
  • 空值:响应里没有该字段。常见原因包括主交易失败、Recurring 未启用,或请求没有被当作 recurring 交易处理。

修正每月计费配置

如果业务需求是免费试用一个月,之后每月收取 99.00,应将周期设为 month:

$startDate = (new DateTimeImmutable('today'))
    ->modify('+1 month')
    ->format('Y/m/d');

$recurArray = [
    'recur_unit'   => 'month',
    'start_now'    => 'false',
    'start_date'   => $startDate,
    'num_recurs'   => '98',
    'period'       => '1',
    'recur_amount' => '99.00',
];

$mpgRecur = new mpgRecur($recurArray);
$mpgTxn->setRecur($mpgRecur);

当前的 day + period 1 会请求每天扣款一次,共执行 98 次。如果生产端接受了这项配置,实际扣款安排会严重偏离按月订阅的业务目标。

建议的完整处理方式

$clientName = $_POST['cust_id'] ?? '';
$billingKey = $_POST['data_key'] ?? '';

if ($clientName === '' || $billingKey === '') {
    throw new RuntimeException('Missing cust_id or data_key');
}

$txnArray = [
    'type'       => 'res_purchase_cc',
    'data_key'   => $billingKey,
    'order_id'   => $orderID,
    'cust_id'    => $clientName,
    'amount'     => '0.00',
    'crypt_type' => '7',
];

$mpgTxn = new mpgTransaction($txnArray);

$startDate = (new DateTimeImmutable('today'))
    ->modify('+1 month')
    ->format('Y/m/d');

$recurArray = [
    'recur_unit'   => 'month',
    'start_now'    => 'false',
    'start_date'   => $startDate,
    'num_recurs'   => '98',
    'period'       => '1',
    'recur_amount' => '99.00',
];

$mpgTxn->setRecur(new mpgRecur($recurArray));

$mpgRequest  = new mpgRequest($mpgTxn);
$mpgHttpPost = new mpgHttpsPost(
    $store_id,
    $api_token,
    $mpgRequest
);

$mpgResponse = $mpgHttpPost->getMpgResponse();

$responseCode = $mpgResponse->getResponseCode();
$message      = $mpgResponse->getMessage();
$recurSuccess = $mpgResponse->getRecurSuccess();

if ($recurSuccess !== 'true') {
    error_log('Recurring setup failed or was not processed: ' . print_r([
        'response_code' => $responseCode,
        'message'       => $message,
        'recur_success' => $recurSuccess,
        'order_id'      => $orderID,
    ], true));

    throw new RuntimeException(
        'The payment profile was saved, but recurring billing was not created.'
    );
}

即使 Vault 中已经出现永久 profile,也不能直接把订阅标记为有效。Vault 保存成功与 recurring 计划创建成功是两个独立的结果。

关于 data_key 与 Credential on File

data_key 是 Vault 令牌。Credential on File 则是一组规则或交易标识,用来描述支付凭据的保存方式、客户授权以及后续交易的发起方。两者不能互相替代。

因此:

  • 使用 data_key 这个方向是正确的。
  • 是否必须同时提交 COF 字段,取决于 Moneris 当前接口、账户配置、SDK 版本和交易类型。
  • 缺少 COF 信息可能导致生产交易被拒绝或不符合要求,但不能仅凭 getRecurSuccess() 为空就认定 COF 是根因。
  • 应请 Moneris 根据生产 store_id、订单号、交易时间和响应码检查服务端日志,同时确认 COF 与 crypt_type 的具体配置要求。

还需要注意

+30 day 不等于下个月的同一天。例如跨越二月时,两种计算方式可能得到不同的日期。按月订阅应优先使用 recur_unit => month,并明确处理月末日期规则。

如果生产账户不接受 0.00 的 res_purchase_cc,不要擅自改成小额扣款后立即退款。应向 Moneris 确认其支持的无扣款验证方式,或正确的 Vault 和订阅建立流程,因为这涉及持卡人授权、账单展示和支付合规要求。

建议按以下顺序排查:先确认 Live Recurring Billing 已开通,再检查主交易响应,然后确认零金额交易、COF 字段和 crypt_type 在生产环境中的要求,最后把周期从 day 修正为 month。

备注:内容仅供参考。