PAYATHON 2026

如何在 ThriveCart Test Mode 测试订阅创建与取消

支付小周

结论

ThriveCart Test Mode 会生成用于测试的订单和订阅记录。创建成功后,通常可以直接测试取消订阅,无需切换到真实付款环境。

You must provide matching customer IDs. 通常不代表 Test Mode 没有创建订阅。它表示 ThriveCart 根据 order_id 和 subscription_id 找到的记录不属于同一个测试客户或同一次购买。还有一种常见情况:创建和取消请求使用了不同模式。例如,订单来自 Test Mode,但取消请求的 X-TC-Mode 实际上传成了 live。

取消订阅时需要同时满足以下条件:

  • order_id 来自创建该订阅的同一笔订单。
  • subscription_id 来自该订单对应的订阅。
  • 两个值都取自 ThriveCart 返回的数据,不能使用 Laravel 本地订单号或套餐 ID。
  • 取消请求使用的 Test Mode 与下单时一致。
  • 测试购买的商品包含 recurring pricing,而不是一次性付款。

为什么会出现 customer IDs 不匹配

取消接口虽然不要求直接提交 customer_id,但 ThriveCart 会在后台根据 order_id 和 subscription_id 分别查找订单、订阅及其客户,再检查它们是否属于同一个客户。

以下情况都可能触发这个错误:

  1. order_id 是 ThriveCart 订单号,但 subscription_id 来自另一笔测试购买。
  2. 数据库保存的是应用自己的订单主键,而不是 ThriveCart 返回的 order_id。
  3. 代码将产品 ID、套餐 ID、invoice ID 或 transaction ID 当成了 subscription_id。
  4. 同一测试邮箱购买过多次,代码把旧订阅和新订单组合到了一起。
  5. 创建订阅时使用 Test Mode,取消时却发送了 live 模式头。
  6. webhook 被重复处理或处理顺序发生变化,导致原本正确的 ID 被后续事件覆盖。

如果订阅完全不存在,接口更可能返回找不到订阅之类的错误。当前错误明确提到 matching customer IDs,因此应该先检查两个 ID 的来源,以及它们是否正确配对。

正确的测试流程

1. 确认测试商品是订阅商品

ThriveCart 中的产品或价格选项必须配置为周期性收费。一次性付款即使能够生成成功订单,也不会创建可供取消的 recurring subscription。

还要确认结账页面处于 Test Mode,并使用 ThriveCart 提供或当前支付处理器支持的测试付款信息。

2. 从同一次 webhook 中保存标识符

收到购买成功或订阅创建事件后,保存 ThriveCart 返回的原始字段:

$purchase->thrivecart_order_id = $payload['order_id'] ?? null;
$purchase->thrivecart_subscription_id = $payload['subscription_id'] ?? null;
$purchase->thrivecart_mode = 'test';
$purchase->save();

这些字段的实际位置可能因 webhook 类型和当前 ThriveCart payload 结构而异。如果字段位于嵌套对象中,应按照实际收到的 JSON 读取,不要假设它们一定在顶层。

调试时可以记录脱敏后的 payload 结构:

Log::info('ThriveCart webhook identifiers', [
    'event' => $payload['event'] ?? null,
    'order_id' => $payload['order_id'] ?? null,
    'subscription_id' => $payload['subscription_id'] ?? null,
    'mode' => $payload['mode'] ?? null,
]);

不要记录 API key、完整支付信息或其他敏感数据。

3. 检查数据库中的 ID 是否成对保存

调用取消接口前,至少要验证:

if (
    empty($purchase->thrivecart_order_id) ||
    empty($purchase->thrivecart_subscription_id)
) {
    throw new RuntimeException(
        'Missing ThriveCart order_id or subscription_id.'
    );
}

如果同一用户有多次购买,不要只通过邮箱查询”最新订单”,再拼接另一条订阅记录。更稳妥的做法是让每条本地购买记录同时绑定 ThriveCart 的 order_id 和 subscription_id。

4. 使用一致的 Test Mode 调用取消接口

可以将模式明确固定为 test,以免环境变量配置错误:

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    'base_uri' => 'https://thrivecart.com',
    'timeout' => 15,
]);

try {
    $response = $client->post('/api/external/cancelSubscription', [
        'headers' => [
            'Authorization' => 'Bearer ' . $apiKey,
            'X-TC-Mode' => 'test',
            'Accept' => 'application/json',
        ],
        'form_params' => [
            'order_id' => $purchase->thrivecart_order_id,
            'subscription_id' => $purchase->thrivecart_subscription_id,
        ],
    ]);

    $result = json_decode(
        (string) $response->getBody(),
        true,
        512,
        JSON_THROW_ON_ERROR
    );

    Log::info('ThriveCart subscription cancellation response', [
        'order_id' => $purchase->thrivecart_order_id,
        'subscription_id' => $purchase->thrivecart_subscription_id,
        'response' => $result,
    ]);
} catch (RequestException $e) {
    $body = $e->hasResponse()
        ? (string) $e->getResponse()->getBody()
        : null;

    Log::error('ThriveCart subscription cancellation failed', [
        'order_id' => $purchase->thrivecart_order_id,
        'subscription_id' => $purchase->thrivecart_subscription_id,
        'status' => $e->hasResponse()
            ? $e->getResponse()->getStatusCode()
            : null,
        'response' => $body,
    ]);

    throw $e;
}

X-TC-Mode 的具体取值和大小写应以当前 ThriveCart API 文档及账户配置为准。如果创建请求使用的不是字面值 test,取消请求也必须使用相同的有效值。

原代码中的:

?passthrough[type]=cancel

通常不是用来指定取消操作的,因为请求路径本身已经是 cancelSubscription。除非业务确实需要 passthrough 数据,否则可以先移除它,减少排查变量。它通常也不是 customer ID 不匹配的直接原因。

建议的排查顺序

先在 ThriveCart Dashboard 中打开这笔 Test Mode 购买,确认它确实显示为订阅交易。然后逐项核对 Dashboard、创建 webhook 和数据库中的以下内容:

mode
order_id
subscription_id
customer/email
product
purchase time

重点确认 order_id 和 subscription_id 来自同一次 webhook 或同一笔购买。取消前还可以增加一条日志:

Log::info('Canceling ThriveCart subscription', [
    'mode' => 'test',
    'order_id' => $purchase->thrivecart_order_id,
    'subscription_id' => $purchase->thrivecart_subscription_id,
]);

取消完成后,不要只根据 HTTP 状态码更新本地订阅状态。应解析接口响应,并结合 ThriveCart 后续 webhook 或 Dashboard 状态确认取消结果。同时注意区分”立即取消”和”在当前计费周期结束时取消”,具体行为取决于 ThriveCart 当前接口及产品配置。

如果模式一致,而且两个 ID 确实来自同一笔测试购买,但接口仍然返回相同错误,应保存完整响应、请求时间和对应的测试交易信息,再交给 ThriveCart 支持团队核对。不要为了绕过错误而自行猜测或提交 customer_id,除非当前官方 API 文档明确要求这个参数。

备注:内容仅供参考。