IUPBCP 登录、账户与账单 Clean-room 协议包
本目录是一套 clean-room、厂商中立(vendor-neutral) 的开发契约,用于演示印度 UPI 与 Bharat Connect 风格的登录、账户、账单、充值和售后聚合。它不是 Paytm 私有协议,也不是 Paytm 或任何机构的生产 API;示例不包含真实端点、真实密钥、真实签名、私有签名算法或任何认证/风控绕过方式。本文中的标识符、凭据和证明都只能指向隔离的本地 Mock。
协议包不代表已取得 NPCI、PSP、TPAP、Bharat Connect、合作银行或监管机构的接入资格,也不能据此处理真实资金。
文件职责
| 文件 | 职责 |
|---|---|
openapi.yaml | 同步 HTTP API 的路径、operationId、安全方案、请求/响应、错误与 JSON Schema 真源 |
asyncapi.yaml | 聚合状态事件、事件 envelope、channel 与消费者接口真源 |
state-machines.yaml | 聚合初态、终态、合法迁移、守卫与父子支付映射 |
examples/requests.http | 覆盖全部 86 个 operationId 的安全 REST Client 示例及变量传递 |
scripts/validate_contracts.rb | 跨文件引用、状态、安全字段与示例的静态验证 |
Quick start
以下命令只面向本地开发。不同工具对 OpenAPI 3.1、JSON Schema 2020-12 和 AsyncAPI 3.0 的支持不同,应在团队里固定工具版本并把生成差异纳入评审。
-
先做仓库内语义校验:
ruby protocols/paytm-cleanroom/scripts/validate_contracts.rb -
用支持 OpenAPI 3.1 的 linter 校验
openapi.yaml,用支持 AsyncAPI 3.0 的 linter 校验asyncapi.yaml。例如可在已固定依赖的项目中执行:npx redocly lint protocols/paytm-cleanroom/openapi.yaml npx asyncapi validate protocols/paytm-cleanroom/asyncapi.yaml -
从 OpenAPI 3.1 生成类型化客户端或服务端接口;生成物不要反向覆盖契约:
openapi-generator-cli generate -i protocols/paytm-cleanroom/openapi.yaml -g typescript-fetch -o build/generated/client openapi-generator-cli generate -i protocols/paytm-cleanroom/openapi.yaml -g spring -o build/generated/server -
从 AsyncAPI 3.0 生成事件消费者骨架,再按
aggregateVersion规则实现 inbox:asyncapi generate fromTemplate protocols/paytm-cleanroom/asyncapi.yaml <pinned-template> -o build/generated/events -
启动由 schema 驱动的本地 Mock,并把
baseUrl配置为该 Mock 的/v1地址。Mock 必须无真实支付能力,绝不能连接真实资金轨、银行、PSP 或账单网络。schema mock 只用于契约测试,不可用于资金验证。 -
在 REST Client 中打开
examples/requests.http,配置文档保留值和本地文件变量,按章节执行。不要把示例凭据写入源码、日志或测试快照。
内置 validator 使用 File.read 配合 YAML.safe_load(..., aliases: true),
已在 Ruby 2.6.10 / Psych 3.1 上验证。CI 仍应同时运行当前受支持的
Ruby 版本;任何兼容性失败都不得通过跳过 OpenAPI、AsyncAPI 或状态机校验来规避。
推荐实现顺序
- 固定公共类型、错误 envelope、请求追踪、
Idempotency-Key、ETag/If-Match和鉴权中间件。 - 实现身份、设备登记、标准 OTP、注册与会话;随后实现 step-up、换号、恢复和账户关闭。
- 实现客户、consent、KYC、证据上传、银行发现/绑定、VPA 和余额授权。
- 实现账单方目录、runtime schema、账单快照、quote、充值目录与 quote。
- 实现 UPI AuthorizationSession、唯一子 PaymentOrder、父级 Bill/Recharge saga、receipt、退款和争议。
- 最后接入 outbox/inbox、AsyncAPI 消费者、对账、审计、可观测性、灾备和全链路契约测试。
每一步都先实现 state-machines.yaml 的迁移守卫和拒绝路径,再开放对应 HTTP 写操作。
服务边界、聚合与存储
建议按 Identity/Device、Customer/KYC、Account/VPA、Bill/Recharge、UPI Orchestrator、Refund/Dispute、Reconciliation 与 Audit 分界。每个聚合独占自己的状态和版本;跨聚合只传不透明 ID、冻结快照或事件,不共享可写表。
- Identity 持有 enrollment、OTPChallenge、Session、step-up、换号和恢复;OTP 原文只在验证边界短暂处理。
- Account 持有 Customer、Consent、KycCase、BankAccountLink 与 VPA;银行账号只存结构化掩码显示。
- Bill/Recharge 持有业务快照、quote、父 saga、履约/结算/退款维度,但不能写 UPI 表、创建网络尝试或接触银行授权秘密。
- UPI Orchestrator 独占 AuthorizationSession 与 PaymentOrder,并以
(aggregateId, aggregateVersion)发布事件。 - Evidence 存元数据、内容摘要、扫描状态与受控对象引用;普通 bearer 域和 recovery 域必须物理或逻辑隔离,不能跨域复用 uploadId。
- Reconciliation 读取不可变流水、外部确认和 outbox,不把超时直接写成失败。
存储层建议使用聚合版本的条件更新、事务 outbox、消费者 inbox、不可变业务快照和受限审计索引。不要以手机号、VPA、账号或 recovery ID 作为可枚举主键或 bearer credential。
登录、注册与会话 ETag
登录顺序是 device enrollment → OTP challenge → 标准 OTP verification → registration 或 session。OTP challenge 响应永不回传验证码;登录验证只返回一次性 authenticationGrant。注册和既有客户创建会话是互斥分支,同一 grant 不得消费两次。
SessionBundle.sessionEtag 或 GET /sessions/current 的 ETag 只用于 refresh/撤销当前会话;GET /sessions/{sessionId} 的 ETag 只用于该命名会话;GET /sessions 的 collection ETag 只用于 revoke-all。不要混用这三类版本。
refresh 仍是公开鉴权交换,但必须同时提供原 refresh credential、设备证明、幂等键和当前会话 ETag。凭据轮换后立即使旧值失效;日志只记录凭据指纹或安全分类,不记录值。
账户维护、银行绑定、VPA 与余额
客户偏好 PATCH、VPA PATCH、银行绑定 action、KYC action、账户关闭 action、step-up 验证和换号 proof attachment 都要求使用刚读取的 ETag。换号需分别验证旧侧和新侧标准 OTP,并逐次采用响应中的新 ETag;cooling-off 不是可跳过的客户端计时器。
银行流程是:银行目录 → 账户发现 POST/GET 轮询 → ACCOUNT_BINDING AuthorizationSession POST/GET → 使用一次性的 authorizationSessionRef 创建账户绑定 → 读取掩码账户 → 创建/读取/更新 VPA。发现中的 UNKNOWN 不是“无账户”。
余额流程必须先创建并轮询 BALANCE_INQUIRY AuthorizationSession,再 POST balance inquiry,并用 GET /upi/balance-inquiries/{balanceInquiryId} 轮询。只有 SUCCEEDED 才读取 balance;余额来源是发行银行的短期视图,不是本地账本余额。
AuthorizationSession 在 ACTION_REQUIRED 时只返回启动受批准组件(approved component)的短期 launch credential。公共 API 绝不接收、记录或转发 UPI PIN;UPI PIN 只能在经批准的银行/PSP 组件内采集和处理,也不能出现在日志、事件、证据或 Mock fixture。
账单、充值与唯一子 Payment saga
先读取 biller,再读取 /billers/{billerId}/parameter-schema。Biller 参数完全由返回的 runtime schema(运行时 schema) 驱动;customerRef 只是示例字段,客户端不得为所有 biller 硬编码它,也不得提交运行时 schema 未声明的字段。
BillFetch 成功后产生不可变、会过期的 BillSnapshot。quote 必须绑定该快照与 funding instrument;BillOrder 又必须绑定同一快照和 quote。金额全部是 INR minor-unit 十进制字符串,例如 {"amountMinor":"100","currency":"INR"},禁止浮点金额、主单位推断或其他币种。
BillOrder 和 RechargeOrder 都先进入 PAYMENT_PROVISIONING,此时 paymentId 必须为 null 且不得发起资金网络调用。轮询至 AUTH_REQUIRED 后,父聚合恰好出现一个不可变 child PaymentOrder。客户端按父级 nextAction 创建/轮询 AuthorizationSession,然后 GET child 取得 ETag,再以 SUBMIT 和授权引用修改这个 child。不得创建替代 paymentId;父服务只消费 child 状态事件。
充值同样执行 operator → plan → subscriber validation → quote → order → provisioning poll → authorization → child GET/SUBMIT → order/receipt poll。subscriber validation 返回的是短期不透明 token,不应长期存储原始订户标识。
Receipt 是账单/充值聚合的投影,不是银行对账单,也不证明尚未完成的 fulfilment 或 settlement 已成功。展示层必须同时显示 payment、fulfilment、settlement 和 refund 状态。
证据、退款、争议与恢复
普通 KYC/争议证据链是 create upload → PUT .../content → completion → 各自 GET 轮询到 READY → attach;recovery 域也必须在 completion 后用同一 recovery request 下的专用 GET 轮询到 READY 才能 submit。普通 create body 必须显式携带 target: ResourceRef:KYC 使用 KYC_CASE 与后续路径中的同一 kycCaseId,争议使用 DISPUTE 与后续路径中的同一 disputeId。创建时冻结 purpose、target、media type、字节数、主体、租户、凭据域与 creator context;消费证据时服务端以存储的冻结绑定校验 READY、purpose、target 与调用上下文,不能信任客户端重报。PUT 的 Content-Digest 和 completion 的 contentDigest 必须完全相同。
退款是独立聚合:以原始业务资源、业务幂等引用、INR 金额和原因创建,随后轮询 Refund。争议是独立 case:create → GET → 普通域证据上传/扫描 → evidence attach → 可选 escalation。退款成功不能重写原 PaymentOrder 的历史事实。
恢复严格 non-enumeration:请求是否命中客户都返回统一公开投影。完整顺序是新设备 enroll → recovery request → public GET → recovery challenge → 取得 otpChallengeId → 调用标准 OTP verification 得到 verificationId → attach recovery verification → 交换 recovery grant → restricted recovery session。
若要求证据,必须使用专用 recovery evidence 域:create → PUT content → completion → submit。所有步骤都绑定同一 recovery request、session、主体、租户和 recovery credential audience,不能调用普通 /evidence-uploads 代替。最后仅在状态允许时执行 CONFIRM_RESTORE;公开 GET、recovery ID 或 challenge ID 本身都不是授权。
幂等、并发、精确重放与对账
所有写请求都带调用方生成的 Idempotency-Key,其作用域是 method + canonical path。相同键只允许精确重放:规范化 body、目标资源、认证主体、媒体类型、文件字节和关键头必须相同,服务返回原结果;同键不同表示必须冲突。对同一业务意图不要在超时后换新键制造第二个支付。
要求 If-Match 的写操作只能使用紧邻目标资源的最新 ETag;412/STATE_CONFLICT 后重新 GET、重新评估状态,再决定是否重试。绝不能盲目覆盖或把父聚合 ETag 用于 child PaymentOrder。
PENDING、UNKNOWN、NEEDS_RECONCILIATION 都是对账中的不确定状态,不是失败、成功、可安全重付或可安全履约信号。保持同一 operation/child,按 nextStatusCheckAt 或退避策略轮询,并把最终判定交给 reconciliation。只有 safeToCreateNewOperation:true 且业务策略明确允许时,才可创建新操作。
事件、错误、重试、审计与隐私
错误处理、重试、审计、隐私必须作为同一条端到端安全链路设计和验收。
事件消费必须幂等:inbox 以 (event.id, consumer) 去重,以 (aggregateId, aggregateVersion) 拒绝旧事件、重复事件和乱序覆盖。父 Bill/Recharge 只接受唯一 child 的版本单调事件;outbox 发布和聚合提交同事务。
错误处理以 error.code/category/retryable/safeToCreateNewOperation/correlationId 为准,HTTP 状态只作传输分类。仅对 retryable:true 做带抖动的指数退避;不确定资金结果即使网络可重试,也不能创建替代业务操作。保留服务端 X-Request-Id/Trace-Id 以便审计,但不要把凭据、OTP、设备证明、手机号、VPA、账号或证据内容放进日志、指标标签和异常消息。
审计记录应追加写、最小化、按角色隔离并设置留存/删除策略;隐私实现需支持目的限制、数据最小化、访问审批、加密、删除/法定保留冲突处理和可追溯导出。应用日志与审计日志不要共用宽权限存储。
测试矩阵
| 维度 | 必测内容 |
|---|---|
| 契约 | YAML 解析、本地$ref、86 个 operationId 唯一且示例全覆盖、请求/响应 schema、OpenAPI/AsyncAPI lint |
| 安全 | public/bearer/recoveryAuth 隔离;OTP 作用域;敏感字段、真实端点、完整账号和凭据泄漏扫描 |
| 幂等 | 首次写、精确重放、同键异 body/主体/路径/文件、过期上传、并发重复提交 |
| 并发 | 正确/陈旧/跨资源 ETag;换号双侧、KYC、closure、payment、dispute 与 recovery 状态冲突 |
| Saga | provisioning 唯一 child、授权过期、提交前取消、提交中断、父子乱序、无替代支付 |
| 不确定性 | PENDING、UNKNOWN、NEEDS_RECONCILIATION、对账恢复、权威失败证据、退款补偿 |
| Evidence | 普通域/recovery 域隔离、摘要/长度/media type 一致、恶意文件、扫描未 READY 禁止 attach |
| 事件 | event.id 去重、aggregateVersion 单调、乱序/重复/缺口、outbox/inbox 崩溃恢复 |
| 隐私与运行 | non-enumeration、日志脱敏、权限、限流、审计留存、备份恢复与灾备演练 |
如何查阅 86 个操作
openapi.yaml 的 operationId 是稳定索引;路径和 method 才是实际调用地址。列出全部 86 个操作:
ruby -ryaml -e 'o=YAML.safe_load(File.read("protocols/paytm-cleanroom/openapi.yaml"), [], [], true); o["paths"].each { |p, i| i.each { |m, op| puts "#{op["operationId"]}\t#{m.upcase}\t#{p}" if op.is_a?(Hash) && op["operationId"] } }'
在 examples/requests.http 中搜索 # @operationId <名称> 可定位安全请求。一个 operationId 可能出现多次,以展示不同状态分支或前序变量;覆盖统计按唯一 operationId 计算。
生产接入前置条件
本协议只给出通用合规提示,不构成法律意见,也不声称任何主体已获资质。生产前至少需要:
生产控制面应同时覆盖 HSM、数据保护、风控、审计、灾备,且均需形成可验证证据。
- 由合格法务与合规团队确认适用的 RBI/NPCI、支付、KYC/AML、Bharat Connect、消费者保护、争议/退款、数据本地化与数据保护要求;
- 与 NPCI、PSP、TPAP、Bharat Connect 参与方及合作银行分别完成适用的准入、合同、认证、联调、沙盒和生产审批;
- 建立机构级身份、证书生命周期、密钥托管与轮换、HSM、双人控制、职责分离和紧急吊销;
- 完成数据分类、加密、数据保护影响评估、目的限制、保留删除、跨境/本地化评估与主体权利流程;
- 上线交易风控、欺诈/账户接管防控、AML/制裁筛查、限额、人工复核、投诉与事件响应;
- 通过代码、基础设施、供应链、渗透、移动端、API、审计和监管要求的独立评估;
- 验证容量、可用性、对账、不可变审计、监控告警、备份恢复、RTO/RPO、灾备切换与定期演练。
上述前置条件必须由真实合作方和适用主管机构确认;本地 Mock、代码生成成功或契约测试通过均不能替代资质、法务、认证和生产安全审批。

1218

被折叠的 条评论
为什么被折叠?



