PAYATHON 2026

如何使用 Moneris API 自动跟踪网站订阅续费状态?

支付小周

结论

Moneris 的 recurring payment 配置不应视为可查询的“订阅对象”。创建 recurring payment 后,Moneris 会按计划执行后续扣款。如果当前接入的产品没有 webhook、POST back 或查询接口,网站就无法仅凭 recurring payment ID 实时获取每期扣款结果。

有两种可靠的实现方式:

  1. **推荐:由网站调度续费,Moneris 负责扣款。**保存 Moneris 返回的支付凭证或 token,通过定时任务在续费日期发起交易,并立即记录响应。
  2. **继续使用 Moneris 托管的 recurring billing:定期导入 Moneris 交易报告并完成对账。**可用的报表下载、批次查询或自动传输功能取决于商户账户及所用的 Moneris 产品,需要向 Moneris 支持团队确认,不能假设存在未在文档中公布的查询 API。

无论选择哪种方式,网站都应在自己的数据库中维护订阅状态和交易历史,不要等到用户登录时再临时查询支付网关。

推荐的数据模型

至少需要保存订阅和交易两类数据:

CREATE TABLE subscriptions (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    customer_id BIGINT UNSIGNED NOT NULL,
    payment_token VARCHAR(255) NULL,
    status VARCHAR(32) NOT NULL,
    next_billing_at DATETIME NULL,
    retry_count INT NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL
);

CREATE TABLE payment_transactions (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    subscription_id BIGINT UNSIGNED NOT NULL,
    merchant_order_id VARCHAR(64) NOT NULL,
    gateway_transaction_id VARCHAR(128) NULL,
    amount DECIMAL(10, 2) NOT NULL,
    currency CHAR(3) NOT NULL DEFAULT 'CAD',
    status VARCHAR(32) NOT NULL,
    response_code VARCHAR(32) NULL,
    response_message VARCHAR(255) NULL,
    processed_at DATETIME NULL,
    raw_response JSON NULL,
    UNIQUE KEY uq_merchant_order_id (merchant_order_id)
);

订阅状态可以分为:

  • active:当前有效;
  • past_due:本期扣款失败,正处于宽限期或重试阶段;
  • suspended:已经超过重试次数或宽限期限,服务暂停;
  • cancelled:已经取消;
  • expired:订阅自然结束。

交易状态应单独记录,例如 pending、approved、declined、refunded。一次扣款失败不代表订阅需要永久取消。

方案一:网站自行发起每期扣款

首次付款或绑卡时,通过 Moneris 支持的 tokenization/Vault 功能取得支付凭证。网站只保存 token,不保存完整卡号或 CVV。此后,定时任务负责查找已经到期的订阅,并调用当前 Moneris 产品对应的购买或预授权接口。

下面的 PHP 代码只展示业务结构。MonerisGateway 是应用自己的适配层,实际使用的请求类和参数必须以当前 Moneris SDK 文档为准:

<?php

final class SubscriptionRenewalService
{
    public function __construct(
        private PDO $db,
        private MonerisGateway $gateway
    ) {
    }

    public function renew(array $subscription): void
    {
        $orderId = sprintf(
            'sub-%d-%s',
            $subscription['id'],
            (new DateTimeImmutable())->format('Ymd')
        );

        $this->createPendingTransaction(
            (int) $subscription['id'],
            $orderId,
            (string) $subscription['amount']
        );

        try {
            $result = $this->gateway->chargeToken(
                paymentToken: $subscription['payment_token'],
                orderId: $orderId,
                amount: number_format((float) $subscription['amount'], 2, '.', '')
            );

            if ($result->isApproved()) {
                $this->markApproved($orderId, $result);
                $this->activateAndAdvanceBillingDate((int) $subscription['id']);
            } else {
                $this->markDeclined($orderId, $result);
                $this->markPastDue((int) $subscription['id']);
            }
        } catch (Throwable $exception) {
            // 超时或网络错误不等于交易失败,必须先标记为结果未知并进行对账。
            $this->markUnknown($orderId, $exception->getMessage());
        }
    }

    // 数据库方法略
}

定时任务可以每隔几分钟运行一次:

*/10 * * * * /usr/bin/php /var/www/app/bin/process-renewals.php

处理到期订阅时,需要使用数据库锁或任务锁,以免多个进程重复扣款。例如,可以在事务中使用 SELECT ... FOR UPDATE,并为 merchant_order_id 建立唯一索引。

这种方案可以同步取得每次扣款的响应,网站也能自行控制交易历史、重试策略和账户状态。

方案二:对账 Moneris 托管的 recurring billing

如果每期扣款已经交给 Moneris 自动执行,就需要建立周期性对账流程:

  1. 创建 recurring payment 时,保存本地订阅 ID、Moneris 返回的标识、金额及扣款计划。
  2. 每日或按照结算批次获取 Moneris 交易报告。
  3. 使用商户订单号、reference number、transaction number 等稳定字段匹配本地订阅。
  4. 将批准、拒绝、退款及撤销结果写入 payment_transactions。
  5. 把未匹配、重复或结果不明确的记录交给人工复核。
  6. 客户查看历史记录时,从本地数据库读取,不要直接查询 Moneris 后台。

Moneris 开放哪些报表功能,可能因产品、账户地区和商户配置而异。需要向 Moneris 确认以下问题:

  • recurring billing 的每期交易结果能否通过 API 查询;
  • 是否支持自动下载结算或交易报告;
  • 报告何时生成,以及应该使用哪个字段关联 recurring payment;
  • 拒绝交易、退款、撤销和 chargeback 是否记录在同一数据源中;
  • 报告采用的时区、结算日边界及数据保留期限。

如果只能从管理后台手动下载报告,就无法做到完全自动化。此时应考虑改用网站自行调度续费的方案。

失败、超时与卡片过期处理

交易结果不能只分为“成功”和“失败”:

  • 明确拒绝:记录拒绝码,将订阅改为 past_due,再按照业务规则重试,并通知客户更新付款方式。
  • 网络超时或响应丢失:将交易标记为 unknown,先查询或对账原交易。不要立即重新扣款,否则可能造成重复收费。
  • 卡片过期:可以提前提醒客户更新,但不要只依赖网站保存的到期日,因为 token 对应的卡片信息可能由发卡行或网关更新。
  • 临时拒绝:可以间隔一段时间后重试,但应限制重试次数。
  • 永久性问题:停止自动重试,要求客户更换付款方式。
  • 退款、撤销和 chargeback:作为后续交易记录单独保存,不要覆盖原付款记录。

是否暂停账户由业务规则决定。常见做法是在首次失败后进入宽限期,经过多次失败或超过指定天数后再暂停服务,而不是第一次失败就删除或取消账户。

安全与一致性注意事项

  • 不要在网站数据库中保存完整卡号、CVV 或其他不必要的持卡人数据。
  • 金额应使用十进制定点值或最小货币单位,不要直接使用浮点数计算。
  • 每次扣款都应使用唯一且可以重复识别的 merchant order ID,以便进行幂等处理。
  • 保存网关原始响应前,需要移除敏感字段。
  • 只有收到明确的批准结果,或者通过后续查询、报表确认批准后,才能将交易标记为 approved。
  • 即使由网站主动发起扣款,也应定期对账,不能只依赖同步响应。
  • Moneris 的具体 SDK 类名、响应字段和批准判断规则可能因产品及接口版本而异,应以商户账户当前对应的官方文档和认证环境结果为准。

备注:内容仅供参考。