# HissPay 沙箱自测工具（契约 2026-10-27.3）

## 代收自测（quickstart.py）

下载 [quickstart.zip](examples/quickstart.zip)，解压后复制 sandbox-config.example.json 为 sandbox-config.json，填独立沙箱凭据、已签约 product 和自有 HTTPS notifyUrl，再执行 chmod 600 sandbox-config.json。

```sh
python3 quickstart.py balance --currency USD
python3 quickstart.py network
python3 quickstart.py serve
```

serve 监听本机127.0.0.1:8099/notify；由你的HTTPS反向代理将公网443的通知路径转到它。网关不能访问 localhost。保持接收器运行，在另一终端测试（每笔新业务换单号，未知结果先查原单）：

```sh
python3 quickstart.py create --order DEMO_001 --amount 100.00 --pay-type CASHAPP
python3 quickstart.py query --order DEMO_001
python3 quickstart.py report
python3 quickstart.py replay --order 返回的平台orderNo
python3 quickstart.py report
```

工具先记录订单意图，再请求下单；回调验签并核对订单、产品、币种和原金额后，按有效realAmount写一次本地SQLite测试账。replay只对本机接收器重新签名投递已验证通知，报告中至少两次投递且只有一笔credit才完成该样例幂等验收。这不是上游真实重发验收，也不是可直接代替生产账务的服务。失败/处理中仍查单；不要把缺少回调当支付失败。配置与SQLite文件不应上传仓库。

## 代付自测（payout_sandbox.py）

### 美国 USD / 澳洲 AUD 银行卡代付模拟自测

商户沙箱模拟资金服务与正式银行卡代付是两套环境。模拟器不连接银行卡机构，不使用真实卡号，不要求服务商受益人编号，也不证明真实银行卡到账。生产 API 的 extra/收款卡资料只用于已开通的正式产品。

1. 登录沙箱商户后台，分别确认 USD 或 AUD 账户、同币种应用、**代付**产品与路由已启用，并有足够模拟余额。不能用代收 product 或生产凭据。应用缺失请联系平台配置。
2. 沙箱默认不校验代付来源 IP，可跳过本步。若已开启“模拟生产白名单”（开始步骤 5），需将调用服务器真实公网出口 IP 保存到沙箱代付白名单；执行 network 可查看平台看到的来源，10010 表示未加白。
3. 下载 quickstart.zip，配置 sandbox-config.json：保留独立 appId/appSecret，增加 payoutCurrency（USD 或 AUD）及对应 payoutProduct（USD-LOCAL-PAYOUT 或 AUD-LOCAL-PAYOUT，须先签约）；这两个 CARD 模拟产品不传 payoutBizCode/payoutBranchCode，notifyUrl 填您自己的 HTTPS 地址。执行 chmod 600 sandbox-config.json。
4. 启动下面的独立代付接收器。将自有公网 HTTPS /notify 反向代理到 127.0.0.1:8100/notify。不要同时将代收与代付接收器代理到同一个路径。

```sh
python3 payout_sandbox.py balance
python3 payout_sandbox.py network
python3 payout_sandbox.py serve
```

另一终端依次执行（每个 create 为新的模拟订单，使用唯一单号；失败或超时后仅查询原单）：

```sh
python3 payout_sandbox.py create --order OUT_OK_001 --scenario SUCCESS
python3 payout_sandbox.py create --order OUT_FAIL_001 --scenario FAIL
python3 payout_sandbox.py create --order OUT_UNKNOWN_001 --scenario TIMEOUT
python3 payout_sandbox.py query --order OUT_OK_001
python3 payout_sandbox.py query --order OUT_FAIL_001
python3 payout_sandbox.py query --order OUT_UNKNOWN_001
python3 payout_sandbox.py simulation --order OUT_UNKNOWN_001
python3 payout_sandbox.py trigger --order OUT_UNKNOWN_001 --event PAID
python3 payout_sandbox.py query --order OUT_UNKNOWN_001
python3 payout_sandbox.py report
```

初次查询可能仍处理中，间隔至少 3–5 秒再查。预期 SUCCESS 最终 SUCCEEDED/true；FAIL 最终 FAILED/true；TIMEOUT 在手动触发前非终态，不能自行解冻、重付或判失败。trigger 仅在沙箱、该应用自己的可手动场景中有效；生产没有这个接口。可另建 PENDING 单并 trigger FAILED 验证另一终态。下单未知后工具会保留原订单意图，拒绝同号再次提交。

收到通知后 report 应显示 outcome、verifiedDeliveries、localResultWrites=1；成功、失败分别记录一次测试结果，未知且未通知时为 PENDING。接收器先验签并核对应用、单号、产品、币种、金额，再事务保存，最后 HTTP200纯文本SUCCESS。重复同状态只增加投递计数，冲突状态标为 REVIEW 并拒绝 ACK。

```sh
python3 payout_sandbox.py replay --order 已验证的HissPay平台订单号
python3 payout_sandbox.py report
python3 payout_sandbox.py resend --order 原商户单号
```

replay 是本机重新签名的函数级去重测试，不是网关发送；只有已验签的通知才允许重放。resend 才请求沙箱平台重发最近终态通知，最短间隔10秒、每单最多20次；实际送达以接收器记录为准。simulation 的 deliveries 是模拟渠道投递到平台的记录，不能代替商户接收日志。测试报告不能把这三类投递混为一谈。

10005 检查代付路由，10011 检查代付产品，10006 检查额度，48001 表示该订单当前不允许手动终态或重发；429 退避。查询与回调最终核对一致才算该模拟用例完成。测试不使用真实资金，SQLite 结果记录不是生产账务系统。

#### 两个币种分开配置

美国测试使用 `payoutCurrency: "USD"`，澳洲测试使用 `payoutCurrency: "AUD"`。分别保存 `sandbox-usd.json`、`sandbox-aud.json`，使用各自币种的应用凭据和代付产品；不要只改币种却沿用另一币种应用或代收产品。缺少应用、产品、路由或模拟余额时请平台配置。默认金额100分别代表100 USD和100 AUD，不进行汇兑。

```sh
chmod 600 sandbox-usd.json sandbox-aud.json
python3 payout_sandbox.py --config sandbox-usd.json --db usd-payout.sqlite balance
python3 payout_sandbox.py --config sandbox-aud.json --db aud-payout.sqlite balance
python3 payout_sandbox.py --config sandbox-aud.json --db aud-payout.sqlite serve --port 8101
python3 payout_sandbox.py --config sandbox-aud.json --db aud-payout.sqlite create --order AUD_OK_001 --scenario SUCCESS
python3 payout_sandbox.py --config sandbox-aud.json --db aud-payout.sqlite create --order AUD_FAIL_001 --scenario FAIL
python3 payout_sandbox.py --config sandbox-aud.json --db aud-payout.sqlite create --order AUD_UNKNOWN_001 --scenario TIMEOUT
python3 payout_sandbox.py --config sandbox-aud.json --db aud-payout.sqlite query --order AUD_UNKNOWN_001
python3 payout_sandbox.py --config sandbox-aud.json --db aud-payout.sqlite trigger --order AUD_UNKNOWN_001 --event FAILED
python3 payout_sandbox.py --config sandbox-aud.json --db aud-payout.sqlite report
```

USD同样依次执行成功、失败、未知、手动终态与resend流程，替换配置、数据库和独立单号即可。USD接收器使用8100，AUD使用8101，各配置的notifyUrl必须指向相应接收器。每次新测试使用全新单号；未知仅查原单，不能重新提交。

#### Python 和 Java 单次请求

ZIP内含 `payout-sandbox-usd.json` / `.properties` 和 `payout-sandbox-aud.json` / `.properties`。这些文件仅用 `SANDBOX-SUCCESS` 合成收款标记，不填写真实卡号、有效期、CVV或受益人。使用已签约的 USD-LOCAL-PAYOUT 或 AUD-LOCAL-PAYOUT 模拟产品，省略 bizCode/branchCode，填写自己的 HTTPS 通知地址；需要失败或未知场景时将account改为SANDBOX-FAIL或SANDBOX-TIMEOUT。

```sh
export HISSPAY_GATEWAY=https://sandbox-api.hisspay.com
# HISSPAY_APP_ID、HISSPAY_APP_SECRET由您在本机安全设置，勿提交仓库。
chmod 600 payout-sandbox-aud.json payout-sandbox-aud.properties
python3 payout_request.py apply payout-sandbox-aud.json
python3 payout_request.py apply payout-sandbox-aud.json --send
python3 payout_request.py query payout-sandbox-aud.json --send
javac PayoutRequest.java
java PayoutRequest apply payout-sandbox-aud.properties
# Java测试须换新单号；不要重复执行刚才Python的下单。
java PayoutRequest apply payout-sandbox-aud.properties --send
java PayoutRequest query payout-sandbox-aud.properties --send
```

不带--send仅预览。单次请求示例不维护本地订单意图或回调账，完整回调与幂等验收请使用payout_sandbox.py；不要混用两种工具对同一笔订单下单。USD示例替换文件并设置USD应用凭据。合成收款标记禁止发送到正式网关。AUD模拟通过不代表正式AUD银行卡代付已开放；真实产品需另行确认资格及接口验收。


## 泰国 THB 模拟

沙箱 THB 产品与生产 THB 使用同一套字段规则（开发文档第 16 节），不连接任何银行或 PromptPay，不产生真实付款。沙箱商户需已签约 `THB-PROMPTPAY-PAYIN`（代收）和 `THB-BANK-PAYOUT`（代付），并有 THB 账户；新注册的沙箱商户自动开通，已有沙箱商户由平台统一补开（如仍未开通请联系平台）。

**代收**：`currency=THB`、`product=THB-PROMPTPAY-PAYIN`，**不传 payType**；`cardExtra` 为字符串 `{"payerAccountNo":"1234567890","payerBankCode":"SCB"}`（账号 6–20 位数字，银行为 SCB/KTB/BAY/GSB/KBANK/TTB），`name` 用字母户名，`phone` 6–15 位数字可带 `+`，`email` **必填**（不传或空字符串返回 400 `email invalid`，格式形如 `payer@example.com`，不超过 64 个字符），`returnUrl` 不传或为不含 `?`/`#` 的 https 地址（不超过 128 个字符）。字段不合规时返回 9999，原单查单为 `CANCELLED`，没有付款链接——这与生产一致，用来自查请求。沙箱 `payUrl` 是模拟收银台（带“SANDBOX 测试环境 · 不会扣款”水印，链接默认 30 分钟内有效，过期后请用 trigger 或重新下单），代替生产的 PromptPay 二维码页；尾数 .09 可在页面上点「支付成功/失败」。

```sh
python3 quickstart.py create --order THB1026_DEMO_001 --amount 1000.00 --currency THB --payer-bank SCB
python3 quickstart.py query --order THB1026_DEMO_001
```

**代付**：`currency=THB`、`product=THB-BANK-PAYOUT`、`bizCode=BANK`、`branchCode` 为 GSB/TTB/BBL/BAY/SCB/KBANK/KTB、`account` 为 6–20 位数字（不能用 `SANDBOX-` 前缀）、`accountName` 字母户名（沙箱默认不超过 **10 个字符**，与生产付款通道的默认上限一致；生产以开通通知为准）、`phone`、`email` 同上，`payCode` 填 `BANK`。沙箱默认 7 家银行都可用（生产可用银行以开通通知为准，可能更少）。字段不合规时与开发文档第 11 节「代付下单的拒绝与 msg」及 16.4 所述的生产现行行为一致：下单同步被拒、**不建单**，用原单号查单返回 30001；按 16.4 自查清单修正后用新单号重下。尾数 .03（结果未知）的订单保持处理中，用于验证「超时不当失败、不重复代付」；可在模拟工具里手动触发终态。

```sh
# sandbox-config.json: "payoutCurrency":"THB","payoutProduct":"THB-BANK-PAYOUT"，可选 "payoutBranchCode":"KBANK"
python3 payout_sandbox.py create --order THB1026_OUT_001 --amount 2000 --scenario SUCCESS
python3 payout_sandbox.py create --order THB1026_OUT_002 --amount 2000 --scenario TIMEOUT
python3 payout_sandbox.py query --order THB1026_OUT_002
```

THB 场景只由金额尾数决定：`--scenario` 把整数金额的两位小数设成对应尾数（SUCCESS .00、FAIL .01、PENDING .02、TIMEOUT .03、DELAYED_SUCCESS .04、DUPLICATE_CALLBACK .05、DELAYED_FAIL .07、REJECTED .08），金额须在 500.00–49999.00 之间。沙箱代付手续费按沙箱签约费率从 THB 测试余额中扣除。沙箱成功不代表生产 THB 已为您开通。
