KYC 核验需求:NUBAN 收款账户信息核验切换至 Paystack

1. 背景与结论

当前 CRS 的 NUBAN 核验调用 Dojah GET /api/v1/kyc/nuban/status,将账户户名、KYC 状态、身份号码及姓名等结果永久缓存,并通过内部 POST /kyc/nuban/lookup 提供给 BNS。

本紧急需求将收款账户核验切换至 Paystack。经补充评估,推荐使用两个分层接口:GET /bank/resolve 完成同步账户解析和户名展示;POST /customer/{customer_code}/identification 提交 BVN + 收款账户的强归属核验,并以 webhook 异步确认结果。后者比单独 Resolve Account 更接近当前 Dojah 的 NUBAN KYC 场景。

上线结论:

  • 可立即替换:收款账号 + 银行代码的有效性校验、户名回填和展示。
  • 可替换强归属校验,但必须改为异步状态机:Paystack Customer Identification 先返回 202 / in progress,仅收到验签后的 customeridentification.success 才可标记 BVN 与账户强归属通过;failed 或超时均不得放行。
  • 不可将 GET /bank/resolve 成功或 POST /identification 的 202 受理结果当作 BVN 一致性成功。
  • 不以“免费”作为上线前提。Paystack 官方 API 文档未声明该接口免费;商务/财务须以 Paystack 商户 Dashboard、合同与实际 Live 账户额度/限流配置确认费用、支持银行和生产权限。

官方依据:Paystack Verification API — Resolve AccountValidate CustomerCustomer API

2. 目标与范围

目标

  1. 用户录入 10 位 NUBAN 和银行后,由服务端解析真实账户户名;客户端不接触 Paystack 密钥。
  2. 账户号或银行代码无效、账户无法解析、上游异常时,阻断本次收款账户提交/放款前校验,并给出可重试的业务提示。
  3. 明确区分“账户可解析”“姓名匹配”“BVN 强归属已核验”,不得将前两者标记为 BVN 已验证。
  4. 对外 BNS 调用尽量维持 POST /kyc/nuban/lookup 契约,避免前端及非相关业务域直接改造。

本期范围

  • CRS 的 NUBAN provider adapter、Paystack Customer 创建/映射、异步核验编排、webhook、错误映射、审计和监控。
  • BNS 的收款账户户名回填、提交后“核验中”状态,以及 Paystack 成功回调后的准入控制。
  • NUBAN 核验记录的 provider/能力等级/有效期隔离。

非本期范围

  • BVN、NIN、活体、人脸等 Dojah 能力切换。
  • 使用 Paystack POST /bank/validate 代替 BVN 核验。其官方示例为 country_code=ZA,Nigeria/BVN 支持与合规口径未确认,不得直接纳入本期主链路。
  • 将 Paystack 密钥、原始供应商响应或完整 NUBAN 返回客户端/日志。

3. 当前链路与影响面

flowchart LR
  App[App/H5] --> BNS
  BNS -->|POST /kyc/nuban/lookup| CRS
  CRS -->|同步解析| Paystack[Paystack]
  CRS -->|POST identification| Paystack
  Paystack -->|customeridentification webhook| CRS
  CRS --> BNS
层级当前实现本期改动
CRS 客户端DojahApiClient.lookupNubanStatus 调 Dojah新增 PaystackApiClient.resolveAccount,使用 Authorization: Bearer {secret}
CRS 编排KycBusiness.lookupNuban、Dojah DTO、永久成功缓存映射 Paystack 返回;按 provider 与能力等级缓存;设置有限有效期
CRS 表t_kyc_nuban_status_record 存 identity/KYC 字段新增 providerverification_levelprovider_codeprovider_message;旧 Dojah 快照不可作为 Paystack 快照复用
BNS 收款户名查询BankBusiness.verifyBankAccount 读取 accountName可保持调用方式,增加空户名与结果等级校验
BNS BVN-账户一致性BvnBusinessidentityNumber 与输入 BVN 比对必须改成发起异步核验并等待 webhook 成功;Paystack Resolve 无 identityNumber,不可沿用原判断
BNS 进件补全ApplyBusiness 尝试回填 bankAcctName继续回填;失败不应绕过正式收款账户提交校验

4. 目标接口与数据契约

4.1 CRS 调用 Paystack

GET https://api.paystack.co/bank/resolve?account_number={NUBAN}&bank_code={BANK_CODE}
Authorization: Bearer {PAYSTACK_SECRET_KEY}
  • account_number:仅接受 10 位数字 NUBAN。
  • bank_code:使用 Paystack 银行字典的 code;不得默认复用 Dojah/内部银行编码。上线前需建立并验收内部 bank_id -> paystack_bank_code 映射。
  • 成功最小条件:HTTP 成功、status=true、返回 data.account_number 与请求一致、data.account_name 非空。
  • 不记录完整 Authorization、完整 NUBAN、完整户名或上游原文到应用日志;审计记录使用脱敏值、哈希、trace_id 和 provider request id(若有)。

4.2 建议内部响应扩展

保留现有 accountNameaccountNumberbank 字段的兼容读取,同时新增以下字段;identityNumber 等 Dojah 专属字段在 Paystack 结果中必须为 null,不得伪造。

字段说明
provider固定为 PAYSTACK
verificationLevelACCOUNT_RESOLVED / NAME_MATCHED / STRONG_OWNERSHIP_VERIFIED
accountResolved是否满足 4.1 成功最小条件
nameMatchResultMATCH / MISMATCH / NOT_AVAILABLE
verifiedAtexpireAt本次结果与重新核验时间
reasonCode对外稳定业务码;不直接透传供应商文本

4.3 强归属校验:Customer Identification

用户给出的 curl https://api.paystack.co/customer/{customer_code}/identification 缺少 method 和 body;该接口是 POST,不是 GET。

POST https://api.paystack.co/customer/{customer_code}/identification
Authorization: Bearer {PAYSTACK_SECRET_KEY}
Content-Type: application/json
 
{
  "country": "NG",
  "type": "bank_account",
  "account_number": "{NUBAN}",
  "bvn": "{BVN}",
  "bank_code": "{PAYSTACK_BANK_CODE}",
  "first_name": "{BVN_KYC_FIRST_NAME}",
  "last_name": "{BVN_KYC_LAST_NAME}",
  "middle_name": "{BVN_KYC_MIDDLE_NAME}"
}
  • customer_code 不是 BNS 的 cust_id,必须是同一 Paystack 商户/密钥下已创建 Customer 返回的 CUS_* 值。当前 BNS bns_cust 有 email/姓名/手机号字段,但没有 Paystack Customer 映射;现有 IMS 仅在绑卡交易结果中保存 customer_code,不能作为 KYC 主链路的可靠来源。
  • 先由 CRS 为业务客户 POST /customer 创建(或复用自建映射表中同一 merchant 的 Customer),持久化 cust_id + paystack_merchant + customer_code;email 为空或未经确认时须先补齐合规可用的唯一 email,不能依赖绑卡时临时生成的 email。
  • 接口成功只表示已受理(官方示例为 202Customer Identification in progress)。核验结论来自验签 webhook:customeridentification.success 写入 STRONG_OWNERSHIP_VERIFIEDcustomeridentification.failed 写入失败原因(官方原因包括账户无法解析、账户名或 BVN 错误、账户号或 BVN 错误)。
  • success webhook 仅回传脱敏 BVN/NUBAN 等识别信息;户名展示仍以 /bank/resolve 返回为准,或成功后以 GET /customer/{customer_code} 读取 Paystack 已更新的客户姓名。两者均须与本地请求指纹(客户、BVN、银行、NUBAN、规则版本)严格对应。
  • 新建 CRS webhook handler,验 X-Paystack-Signature(HMAC-SHA512 原始请求体),按事件 ID/请求指纹幂等处理。现有 PMT/IMS 的验签代码只处理 charge.success 等支付事件,不能直接承担 KYC 事件路由。

5. 业务规则

  1. **账户可解析:**Paystack 成功且回传账号一致、户名非空,标记 ACCOUNT_RESOLVED;允许回填/展示脱敏户名。
  2. **姓名匹配:**在等待异步强核验时,以已完成 BVN KYC 的法定姓名与 Paystack account_name 做服务端标准化匹配(大小写、首尾空格、连续空格、标点;规则版本需留痕)。命中标记 NAME_MATCHED,但不能放宽强归属场景。
  3. **BVN 强归属:**仅收到验签的 customeridentification.success,且回调与本地 pending 请求的客户、银行、NUBAN、BVN 指纹匹配时,产生 STRONG_OWNERSHIP_VERIFIED;Paystack Resolve 或 Identification 受理结果永远不得产生该等级。
  4. 放款/绑卡准入:
    • 仅要求收款账户可用的场景:最低 ACCOUNT_RESOLVED
    • 规则要求“本人账户”的场景:最低 NAME_MATCHED;风控/合规若要求 BVN 强归属,则本需求上线前必须提供独立强核验通道,否则该场景不切换。
  5. 银行、账号、BVN KYC 姓名任一变化,旧结果立即失效并重新解析。
  6. Paystack 调用超时、5xx、限流、网络异常或 webhook 超时均为“未知”,不等同于账号不存在;允许用户稍后重试,严禁自动伪成功或静默降级。重复提交前必须按请求指纹查询 pending 状态,避免重复创建 Customer 或重复提交核验。
  7. 不做 Dojah→Paystack 静默回退。若紧急期需要双跑,仅允许灰度观测时同请求并行比对,用户决策仍以被选主 provider 的结果为准,且不得因任一方异常放行。

6. 数据、缓存与迁移

  • 成功记录不再永久有效;建议 expire_at = verified_at + 24h(最终 TTL 由风控/支付确认)。放款、签约和提现/收款账户变更前必须复验。
  • 缓存/唯一查询键至少包含 provider + bank_code + account_number + verification_rule_version,避免读到 Dojah 历史成功记录而跳过 Paystack。
  • 既有 Dojah 历史记录只作审计,不得被标记为 PAYSTACK 或覆盖其 provider。新增字段先允许空,回填 provider 为 DOJAH,再启用 Paystack 主链路。
  • 原始响应如需留存,仅加密存储、最小权限访问和明确保留期;业务日志只存脱敏账号(如 ******8151)与户名摘要。

7. 异常码与前端交互

场景内部码(建议)用户提示处理
参数/格式错误BANK_ACCOUNT_INVALID_INPUT请检查银行和 10 位账户号不调用三方
内部银行未映射 Paystack codeBANK_CODE_NOT_SUPPORTED当前银行暂不支持,请更换银行或稍后重试告警并补字典
上游明确无法解析BANK_ACCOUNT_NOT_RESOLVED未能核验该收款账户,请核对后重试不允许提交
上游超时/5xx/限流BANK_ACCOUNT_VERIFY_UNAVAILABLE账户核验暂不可用,请稍后重试不允许提交、可重试
姓名不匹配BANK_ACCOUNT_NAME_MISMATCH收款账户户名与认证信息不一致不允许本人账户场景提交;进入人工复核策略(如启用)
强归属能力缺失BANK_ACCOUNT_OWNERSHIP_UNVERIFIED当前无法完成账户归属核验强归属场景阻断,不伪造成功

前端沿用“选择银行 + 输入 NUBAN → 服务端核验 → 展示脱敏户名/结果”的交互;不得直接请求 Paystack,不展示供应商名、密钥或原始错误。

8. 发布、监控与回滚

  1. **上线前门禁:**确认 Live secret key、费用/额度、生产 IP/权限、Paystack 支持的 Nigeria 银行清单及内部银行码映射;使用已获授权的脱敏样本完成成功、账户不存在、错银行码、超时和限流联调。
  2. **灰度:**按配置 nuban.provider=DOJAH|PAYSTACK 和灰度比例启用;首日保留 Dojah 只读审计能力,不改写历史结果。严禁将两家结果混存为同一快照。
  3. **监控:**按 provider、银行、接口状态、错误码统计请求量、成功率、P95/P99、超时率、NAME_MATCHED 率、内部银行码未映射数、缓存命中率;不输出明文 PII。
  4. **告警建议:**5 分钟请求失败率/超时率、任一银行异常集中、未映射银行码、成功率相对灰度基线显著下降均触发 P1/P2 告警。
  5. **回滚:**关闭 Paystack 灰度并切回 DOJAH 配置;不删除 Paystack 审计记录。若本次已将强归属场景降为姓名匹配,回滚须同时恢复对应准入策略,防止标准漂移。

9. 验收标准

  • AC-01:输入有效 NUBAN + 已映射银行,CRS 使用 Paystack /bank/resolve,返回账号一致且户名非空时,BNS 能得到户名和 provider=PAYSTACKverificationLevel=ACCOUNT_RESOLVED
  • AC-02:/bank/resolve 成功或 /identification 返回 202 时,系统均不产生强归属成功;只有验签且指纹匹配的 customeridentification.success 才产生强归属成功。
  • AC-03:需要本人账户的流程只在姓名匹配规则命中后通过;不匹配时返回稳定业务码并阻断。
  • AC-04:错银行码、账号不存在、4xx、超时、5xx、限流分别映射为可辨别的内部业务码;上游异常绝不被当作“账号不存在”或成功。
  • AC-05:银行映射缺失时不调用 Paystack,记录告警并返回 BANK_CODE_NOT_SUPPORTED
  • AC-06:更换银行/账号、KYC 姓名变化或结果过期后会重新核验;旧 Dojah 永久快照不会命中 Paystack 缓存。
  • AC-07:日志、监控、DB 审计中无 Secret Key、完整 NUBAN、完整户名或完整上游响应;具备 trace_id 追踪。
  • AC-08:Customer 不存在时按同一 merchant 创建并保存 customer_code 映射;重复请求不重复创建/提交,webhook 重放不重复改变业务状态。
  • AC-09:灰度关闭可恢复 Dojah 主链路,且不会修改 BVN/NIN/活体/人脸的 Dojah 调用。

10. 待确认的决策(阻塞强归属场景上线)

  1. 强归属场景是否以 Paystack customeridentification.success 作为最终准入证据;若不接受,必须保留/补充独立核验供应商。
  2. NAME_MATCHED 的姓名规范化规则、允许差异范围和人工复核策略由风控/合规确认。
  3. Paystack Live 账户对 Nigeria GET /bank/resolve 的费用、日限额、QPS、可用银行和 SLA,以商户侧书面确认及实测为准。
  4. 24 小时建议缓存有效期是否满足资金风控;若不满足,确认更短 TTL 及放款前强制复验时点。