> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 沙箱模拟范围 FAQ

> Waffo 沙箱到底能复现什么、不能复现什么：哪些保留金额触发哪个错误、为什么退款保留金额不起作用、如何模拟钱包侧取消、为什么模拟的失败续订从不重试，以及哪些场景在沙箱中完全没有路径。

如何进入沙箱以及设置向导会改变什么，在
[沙箱环境与访问 FAQ](/docs/zh/support/sandbox-environment-and-access-faq)。
模拟器的操作说明在
[沙箱模拟器](/docs/zh/developer-docs/tools-and-references/developer-tools/sandbox-simulator)
和[沙箱测试 FAQ](/docs/zh/support/sandbox-testing-faq)。

**本页是边界地图**：沙箱实际能产生哪些场景、
不能产生哪些，以及保留金额在哪里适用、哪里不适用。在
规划验收之前先读它，这样你就不会花一天时间去
复现一个在沙箱中根本没有路径的东西。

## 什么能模拟，什么不能？给我完整列表。

| 场景 | 沙箱中可用？ |
| - | - |
| 支付成功 | 是——收银台页面上的模拟按钮 |
| 支付失败 / 拒绝 | 是——收银台页面上的模拟按钮 |
| 渠道拒绝（`C0005`） | 是——保留的订单金额 |
| 系统错误（`C0001`） | 是——保留金额，在创建和取消时 |
| 未知状态（`E0001`） | 是——保留金额，在创建和取消时 |
| 幂等冲突（`A0011`） | 是——用不同的金额或币种复用请求 ID |
| 退款金额超过原金额（`A0003`） | 是——请求超过已支付的金额 |
| 支付前取消订单 | 是——取消 API |
| 订阅首次扣款成功与失败 | 是——收银台页面 |
| 订阅续订成功或失败 | 是——订阅管理页面 |
| 钱包侧（顾客发起的）取消 | 是，针对受支持的支付方式——见下文 |
| 退款失败 | 是，同步——退款请求上使用创建族的保留金额会返回错误并留下一个 `REFUND_FAILED` 退款订单；见下文 |
| 先进入处理中然后失败的退款 | **否**——没有模拟路径 |
| 拒付 / 争议 | **否**——没有模拟路径 |
| 渠道超时或无响应 | **否**——不能直接模拟；`E0001` 是最接近的类比 |
| Payout、提现或结算结果 | **否**——API 存在，但没有控制手段可以强制产生某个结果 |
| 时间穿越（推进试用结束日期或过期日期） | **否**——不存在此能力 |
| 3DS 挑战 | **否**——没有控制手段可以强制挑战，测试卡列表也没有 3DS 卡 |

"否"行中的所有内容都是确实不存在，而不是未记录，所以请围绕它
规划：用针对录制载荷的单元测试来验证那些代码路径，
并在上线后在生产中验证真实行为。

## 哪些保留金额触发哪个错误，在哪些端点上？

沙箱会拦截特定的订单金额并返回匹配的错误，
而不是正常处理。映射如下：

| 保留金额 | 返回的错误 | 适用于 |
| - | - | - |
| `90`、`990`、`1990`、`19990` | `C0005` Payment Channel Rejection | 订单创建、订阅创建 |
| `9.1`、`91`、`991`、`1991`、`19991` | `C0001` System Error | 订单创建、订阅创建 |
| `9.2`、`92`、`992`、`1992`、`19992` | `E0001` Unknown Status | 订单创建、订阅创建 |
| `9.3`、`93`、`993`、`1993`、`19993` | `C0001` System Error | 订单取消、订阅取消 |
| `9.4`、`94`、`994`、`1994`、`19994` | `E0001` Unknown Status | 订单取消、订阅取消 |

请仔细阅读"适用于"列，因为错误就发生在这里：

* 在**创建**时，金额取自请求——订单创建时的订单金额、
  订阅创建时的金额。
* 在**取消**时，你不传金额。匹配的金额是**存储在
  你正在取消的订单或订阅上的金额**。所以要测试取消错误，你
  必须先以 `9.3` 创建订单，然后取消它。
* 这些金额是**仅沙箱的规则**。生产中不存在类似的东西，
  在生产中这些金额就是普通金额。

## 为什么退款保留金额不起作用？

因为退款路径不使用退款专用的金额。已发布的
[沙箱模拟器](/docs/zh/developer-docs/tools-and-references/developer-tools/sandbox-simulator)
页面为退款 API 列出的 `9.5` 和 `9.6` 族**不**生效——`995` 的退款就是一笔普通退款。

退款路径*会*响应的是**创建**族：
`refundAmount` 等于 `90` / `9.1` / `9.2` 族中某个值（即在订单创建时
触发 `C0005`、`C0001` 和 `E0001` 的同一组金额）时，会使
退款请求在请求时以匹配类别的错误失败——`90` 和 `9.1` 族为
渠道或系统错误，`9.2` 族为未知状态错误。
为该请求打开的退款订单以 `REFUND_FAILED` 结束，
并向你的退款通知 URL 发送一条报告失败的退款通知。这让你能够验证
对被拒绝的退款请求和失败的退款记录的处理；它不能给你的是一笔
先进入处理中、之后再失败的退款，而且在沙箱中没有其他方式可以强制产生这种情况。
在退款路径上你*仍然*可以验证的：

| 退款情形 | 方法 |
| - | - |
| 退款成功，全额和部分 | 对已支付的沙箱订单退款 |
| 退款超过原金额 → `A0003` | 请求超过已支付的金额 |
| 退款幂等冲突 → `A0011` | 用不同的金额或币种复用 `refundRequestId` |
| 退款通知处理 | 上述情形都会触发退款通知，包括同步失败 |

对于真正的退款*失败*，请用录制的载荷测试你的处理程序，
而不是尝试触发一个。

## 我的真实价格正好落在保留金额上，沙箱订单一直失败

这是一个已知情况，并且有一个不是"改价格"的解决办法：
**沙箱商户 ID 可以被免除金额模拟。** 带上你的沙箱商户 ID 联系
集成支持并申请免除；一旦
应用，保留金额在你的沙箱中就表现为普通金额。

在此之前，变通方法是避开上述五个族。实际上这
意味着避开以 `90` 结尾的金额，以及 `9.x` / `x91`–`x96` 模式——
加一分钱（例如用 `90.01` 代替 `90`）就够了，因为匹配
是精确的。

## 沙箱真的会发送Webhook 通知吗？

会。沙箱的Webhook 通知投递遵循与生产相同的契约——相同的
签名、相同的可接受响应体、相同的重试计划。
[Webhook 通知投递契约 FAQ](/docs/zh/support/webhook-delivery-contract-faq)中的一切
都原样适用。

两件*并不*不同、却被归咎为看起来不同的事：

* **你的 `notifyUrl` 必须可公开访问。** 沙箱无法访问
  `localhost` 或私有地址，你的 TLS 证书必须是公开可信的。
  本地开发请使用隧道工具。
* **感觉上的沙箱延迟通常是重试计划。** 如果第一次投递
  失败，后续尝试会按递增的退避进行——计划参见
  [Webhook 通知重试](/docs/zh/developer-docs/webhook/retry)。
  一条"花了几分钟才到"的通知几乎总是意味着最初的
  尝试被拒绝了——在断定沙箱慢之前，先检查 Portal Webhook 通知日志中
  同一记录上更早的失败尝试。

## 我应该用哪些测试卡号？

当流程要求你输入卡时，使用**沙箱收银台页面上的快速填充按钮**。
它们会一键填入卡号、有效期和 CVV，对应成功
或拒绝，按卡品牌，覆盖你的沙箱已启用的品牌
——这既消除了抄写错误，也消除了对
该环境期望什么值的猜测。

如果你需要写下来的值，Portal 自己的列表在
**Merchant Portal → Integration → Test Cards**，每张卡有复制操作，
以及两个场景类别：**successful** 和 **failed**。

[沙箱测试 FAQ](/docs/zh/support/sandbox-testing-faq)中的表格与
Portal 列表一致。较早发布的模拟器页面并非每个卡组织都一致，
所以**优先使用产品内的来源而不是任何文档**，并优先使用收银台
快速填充而不是两者。

## 有效期和 CVV 填什么？会校验吗？

任何未来的有效期，以及任何长度正确的 CVV——三位，使用四位的
卡组织则为四位。收银台页面上的快速填充会为你提供有效的
组合。

这对覆盖范围的启示是：因为有效期和 CVV 没有实质性的
校验，沙箱无法验证你对有效期拒绝或
CVV 不匹配的处理。这些属于"用录制载荷测试"的列表。

## Portal 测试卡页面只显示成功和失败。3DS、余额不足、CVC 错误和风控拒绝的卡在哪里？

它们不存在。Portal 测试卡列表只有两个场景类别，
成功和失败，没有任何卡号映射到特定的
拒绝原因。

对验收规划的影响：沙箱能证明你的代码在成功和失败时
走了正确的分支。它不能让你枚举拒绝
原因。当你的产品按拒绝原因有不同的行为时——余额不足时提示重试，
硬拒绝时提示"换一张卡"——请在单元测试中用 `orderFailedReason` 驱动这些分支，
使用
[错误码](/docs/zh/developer-docs/tools-and-references/developer-tools/error-codes)中的代码。

## 我如何模拟续订，能连续模拟多个周期吗？

从订阅管理页面，通过调用
`POST /api/v1/subscription/manage` 并打开返回的 `managementUrl` 获得。它
提供模拟下一次支付成功和模拟下一次支付失败，
每次点击推进一个周期——所以你可以串联：第 2 期成功，
第 3 期失败。

前提条件，因为这些正是让按钮不可用的原因：

| 要求 | 没有它时按钮为什么不见了 |
| - | - |
| 订阅为 `ACTIVE` | 任何其他状态都拒绝模拟 |
| 订阅在 Waffo 管理的计费计划上 | 计费由支付方式一侧驱动的订阅通过另一条路径模拟 |
| 你在沙箱中 | 模拟入口在生产中不起任何作用 |

如果页面渲染了但模拟控件不见了，请先检查
订阅的状态——未激活或已取消的订阅是
最常见的原因。

## 模拟的失败周期之后会按正常计划重试吗？

**不会——这是本页最重要的一点。** 模拟失败的
续订会**先把续订重试额度设为零**，所以失败的周期
直接进入终态，而不是进入重试计划。

需要围绕它规划的两个后果：

* **你无法通过模拟失败在沙箱中观察续订重试行为。**
  生产中运行的重试计划被模拟本身
  绕过了。
* **该更改会持久保留在该沙箱订阅的配置上。** 之后续订
  重试保持禁用，所以一个你用来模拟失败的订阅
  不再是重试测试的干净对象。任何与重试相关的测试请创建一个新的
  订阅。

要测试你对重试序列耗尽后*结果*的处理，模拟失败
正合适——它产生同样的终态周期状态
和同样的通知。要测试序列本身，你需要生产
行为或一组录制的载荷。

## 我能模拟顾客在钱包 App 内取消吗？

可以，针对支持它的支付方式。模拟会产生
与真实顾客侧取消相同的带 `USER_CANCELLED` 的
`SUBSCRIPTION_STATUS_NOTIFICATION`，所以你的处理程序看到的是真实的东西。

两道门槛决定它对某个订阅是否可用：

1. **订阅必须是 `ACTIVE`。**
2. **支付方式必须支持它。** 钱包侧取消是通过
   复现该支付方式在取消时实际发送的消息来模拟的，
   所以它只存在于该消息已建模的方式上。
   未建模时，调用会以专门的"this payment method does not
   support channel cancel"错误失败，而不是静默地什么都不做——这
   本身就回答了你的方式是否被覆盖：试一下并读取
   错误。

如果你的方式未被覆盖，请直接用
`USER_CANCELLED` 载荷测试处理程序。通知的形态不因支付方式而异。

## 我能在沙箱中测试拒付吗？

不能。沙箱中没有拒付或争议模拟——没有保留
金额、没有按钮、没有端点。

替代做法：

| 目标 | 方法 |
| - | - |
| 验证你的拒付Webhook 通知处理程序 | 用录制的 `CHARGEBACK_NOTIFICATION` 载荷测试；字段列表在[Webhook 通知载荷字段 FAQ](/docs/zh/support/webhook-payload-field-faq) |
| 验证你的证据提交流程 | 按[拒付字段参考](/docs/zh/support/chargeback-field-reference)中的 API 契约走一遍 |
| 了解生产中会发生什么 | [拒付 FAQ](/docs/zh/support/chargeback-faq)和[拒付证据指南](/docs/zh/support/chargeback-evidence-guide) |

在围绕它规划之前还值得了解：拒付通知只有在 Portal 中配置了
Chargeback Notify URL 时才会投递，而且它
只能在 Portal 配置——没有请求参数。所以首先要
验证的是该 URL 在两个环境中都已设置。

## 订单卡在未知状态。我如何把它推进到最终结果？

`E0001` 在创建时由保留金额返回，请求在
**任何订单创建之前**就被拒绝——对该请求 ID 的查询
返回"order does not exist"。这正是重点：未知状态响应
不能告诉你订单是否存在，所以要验证的行为是
你的恢复逻辑——**把结果视为未确定，查询，并根据
查询返回的内容行动；绝不要自己把订单标记为失败或关闭它**。当
查询确认不存在任何订单时，用完全相同的参数重新提交（你的
`paymentRequestId` 使这样做是安全的）。

沙箱中没有控制手段能产生一个先停留在
未知状态、之后再翻转为最终结果的订单。所以要展示的行为
是查询-然后-决定的循环以及"没有自行关闭"这一属性，而不是
一个你手工产生的最终状态。验收标准对此的表述是：不要
关闭订单，用相同参数重试，并等待查询结果
或通知。参见
[集成验收标准](/docs/zh/developer-docs/tools-and-references/references/acceptance-criteria)。

## 我需要在特定国家的网络出口才能测试吗？

沙箱不需要。沙箱支付路径中没有任何东西检查你的
请求或浏览器来自哪里，而且由于沙箱不连接
真实的支付渠道，也没有渠道侧的地理限制需要
满足。从任何地方运行的沙箱测试都是有效的沙箱测试。

地理确实重要的地方是对真正
依赖顾客所在地区的方式做生产验证——移动钱包产品的
设备或账户地区设置，以及这些产品公布的支持国家列表。
这些检查列在
[沙箱与测试](/docs/zh/developer-docs/getting-started/sandbox)的
各支付方式章节下，它们是生产检查，不是沙箱检查。

## 哪些事情只能在生产中验证？

把这份列表放在你的上线计划旁边，因为每一项都是
沙箱跑绿了也什么都证明不了的地方：

| 只能在生产中验证 | 原因 |
| - | - |
| 真实支付方式的行为及其自身页面 | 沙箱显示的是模拟器，不是真实支付页面 |
| 拒绝原因及其分布 | 沙箱只有成功和失败 |
| 续订重试行为 | 模拟失败会禁用重试 |
| 退款失败、拒付 | 没有沙箱路径 |
| Payout、提现和结算结果 | 沙箱中没有结果控制手段 |
| 移动钱包的设备、账户和地区要求 | 依赖真实设备和账户 |
| 移动钱包产品的生产访问审批 | 由钱包提供方而非 Waffo 授予 |
| 你的生产凭据、主机和回调 URL | 按设计与沙箱的值不同 |

移动钱包的检查尤其有自己的前提条件列表——
账户、卡、设备地区、App 内浏览器配置，以及必须在发布前
获批的生产访问申请。请按
[沙箱与测试](/docs/zh/developer-docs/getting-started/sandbox)和
[支付方式集成说明](/docs/zh/developer-docs/tools-and-references/references/payment-method-integration-notes)
逐项处理，而不是在切换时才发现它们。

## 我必须逐个测试每个已签约的支付方式吗？

不必。验收标准规定的是代表性覆盖而不是
穷尽覆盖：至少一张卡、每个国家每种类型至少一个、至少
一个虚拟账户、每个有特殊必填参数的方式，以及
移动钱包的手动设备测试。完整规则集在
[集成验收标准](/docs/zh/developer-docs/tools-and-references/references/acceptance-criteria)。

两个经常出现的追问：

* **你的协议不允许退款的方式**不需要退款
  用例。只对你的集成实际包含的 API 和行为运行条件
  用例——模板正是出于这个原因把它们标记为条件项。
* **第二次集成不是完整重跑。** 一旦你至少通过过一次验收，
  后续的一次性支付集成使用附加
  集成模板，只覆盖增量部分。你的第一次集成
  始终使用标准模板。

***

下一步：[上线清单](/docs/zh/developer-docs/getting-started/go-live)用于
切换本身，以及
[沙箱环境与访问 FAQ](/docs/zh/support/sandbox-environment-and-access-faq)
了解哪些内容会、哪些不会从沙箱带过去。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.