还款方式说明
1. 背景与目标
本期还款能力基于现金贷现有链路改造,不从零新建支付和销账能力。现金贷项目现状已经具备:
- 借款环节绑卡:BNS 调 IMS
routeRedirect,卡走 token,账户走 Direct Debit + OTP;绑定结果由 IMS 回调 BNS/inner/bindCardCallBack。 - 还款调度:FCS 的
NormalWithholding/OverdueWithholding生成代扣任务,TxnType 为RP。 - 主动还款:H5 调
generatePlutusWebOrder4nc生成 Plutus 网页支付订单,跳支付页,回跳后轮询 FCSactiveQueryPlutusPaymentResult。 - 支付执行:FCS 通过
BeforePaymentPlutusPublisher派发还款事件,PMT 走IThirdPayment.repayment对接通道。 - 结果回流:PMT 通过
repayResult/ 还款结果事件回流 FCS,FCS 通过AfterPaymentFactory+PaymentHier销账。 - 虚拟账户入口:PMT 现有
IInnerPaymentService.vitualAccountRepay,TxnType 可归类到VP,走 MP 类销账处理。
本期要在此基础上支持 3 类 Monnify 还款方式:
| 还款方式 | Monnify 能力 | 本期实现口径 |
|---|---|---|
| 卡代扣 | Card Tokenization / Charge Card Token | 借款环节已有绑卡,代扣时 PMT 通过 IMS 查 token 后扣款 |
| 虚拟账户还款 | Customer Reserved Account | 为客户或订单创建虚拟账号,到账后通过 PMT 回流 FCS 销账 |
| 打开支付通道 H5 自行还款 | Checkout API / Web SDK | 复用现有 H5 主动还款模式,将 Plutus 收银台替换/扩展为 Monnify checkoutUrl |
2. 范围
2.1 本期范围
- Monnify 作为本期还款供应商之一接入 PMT。
- 卡代扣复用现金贷“借款环节已绑卡”的现有模式,不要求用户在还款页重新绑卡。
- 虚拟账户还款补充账号创建、展示、到账匹配和异常挂账。
- H5 自行还款支持打开 Monnify 支付通道页面。
- 入账/销账统一回流 FCS,复用
AfterPaymentFactory、PaymentHier和 PFS 销账顺序配置。 - 所有外部支付结果必须由服务端查询/核验后入账,前端回跳不作为入账依据。
2.2 非本期范围
- 不重建一套独立还款中心。
- 不建设完整运营后台,对账和异常先通过 DB/人工导出处理。
- 不支持线下现金还款。
- 不做复杂退款、调账、减免自动化。
- 不接 Paystack 特有的 Apple Pay、QR、EFT、Mobile Money 等能力。
3. 现有代码实现口径
3.1 绑卡现状
现金贷借款链路中,用户提交借款前已经完成绑卡:
- App/H5 发起绑卡,BNS 调 IMS
routeRedirect。 - IMS 根据通道返回
bindCardType:H5:跳 Plutus/IMS 托管页绑卡,BNS 落BnsCardboundOrder,状态pending。API:原生直绑。
- 另一维度
cardType决定绑定对象:- 银行卡:走 token。
- 银行账户:走 Direct Debit + OTP。
- 通道完成后回调 IMS。
- IMS 调 BNS
/inner/bindCardCallBack,BNS 落客户卡/账户信息。 - 后续代扣由 PMT 调 IMS:
queryCardTokenByUserId查询客户 token。cardRepay/ 通道 repayment 执行扣款。
借款环节已完成绑卡,FCS 到期发起代扣,PMT 从 IMS 查询客户有效 token 后调用 Monnify 扣款。
3.2 主动还款现状
现金贷 H5 当前主路径是 Plutus 网页支付:
- H5 查询借据、还款计划、应还金额。
- 用户确认还款。
- H5 调 BLC
plutus/v1/generatePlutusWebOrder4nc。 - BLC/FCS 创建主动还款订单,TxnType 为
MP。 - 返回支付 URL。
- H5
location.href跳转收银台。 - 支付完成后回跳
/afterRepayment?reference=xxx。 - H5 轮询 FCS
activeQueryPlutusPaymentResult(reference)。 - FCS 确认 PMT 回流结果后展示成功/失败/处理中。
本期 Monnify H5 自行还款应复用该交互模型:生成订单后返回 checkoutUrl,前端跳转 Monnify H5,回跳后继续轮询 FCS 结果。
3.2.1 H5 还款页面截图及交互说明
当前 H5 还款入口会先展示应还金额、还款计划、通道费提示和还款按钮;用户确认后进入支付方式选择页,支付完成后进入结果页。
| 页面 | 截图 |
|---|---|
| 还款首页 | ![]() |
| 还款计划/期次选择 | ![]() |
| 支付方式选择 | ![]() |
| 支付处理中 | ![]() |
| 支付成功 | ![]() |
| 支付失败 | ![]() |
3.2.2 通道费展示与计算逻辑
还款首页文案 Including ₦xxx fees charged by payment channels 展示的是前端字段 repaymentDetail.passAmount,业务含义为本次还款需额外承担的支付通道费。
前端不维护通道费计算公式,只负责两类取数:
- 页面初始进入时,H5 调用
queryRepayPlanDetailList获取还款计划。接口返回每一期的passAmount,前端按当前选中的期次累加后展示到repaymentDetail.passAmount。 - 用户编辑还款金额后,H5 调用
queryPassAmount重新计算通道费。请求参数为当前还款金额和当前借据号:
{
"data": {
"amount": "当前还款金额",
"loanId": "当前借据号"
}
}接口返回:
{
"passAmount": "重新计算后的通道费"
}后端计算由 fund-control 的 FCS 资金控制模块负责,核心代码在 fcs-business / financial / FinancialBusiness.queryPassAmount。计算依赖配置项 passRate:
BigDecimal rate = new BigDecimal(1)
.divide((new BigDecimal(1).subtract(passRate)), 4, BigDecimal.ROUND_UP)
.subtract(new BigDecimal(1));
BigDecimal passAmt = AmountHelper.mul(
AmountHelper.divide(req.getAmount(), new BigDecimal(1).add(rate)),
rate
);因此通道费不是 H5、BNS 或 PMT 临时拼出来的固定金额,而是 FCS 根据 passRate 和本次还款金额计算并返回。后续接 Monnify 时,如果 Monnify 的收费方式、费率或承担方发生变化,需要明确是继续复用 FCS passRate 口径,还是在 PMT/通道配置中新增 Monnify 独立费率并回传给 FCS/H5。
3.3 还款入账现状
FCS 现有还款分流如下:
| 场景 | TxnType | 现有入口 | 销账处理 |
|---|---|---|---|
| 客户主动还款 | MP | activeCustomerRepayment | afterPaymentMP |
| 人工还款 | MR | artificialRepay | afterPaymentMP |
| 到期代扣 | RP | NormalWithholding / OverdueWithholding | afterPaymentRP |
| 部分/催收还款 | CLP | partialRepayment | afterPaymentCLP |
| 虚拟账户还款 | VP | vitualAccountRepay | MP 类销账 |
| 超额还款 | OFR | 超额处理 | afterPaymentOFR |
| 退款 | RF | refund | 退款处理 |
销账由 FCS 的 AfterPaymentFactory 分流,最终使用 PaymentHier 按 PFS queryOffsetConfig 返回的销账顺序执行,规则是先费后本,并按账龄 aging 处理。
3.4 虚拟账户代码现状
当前 cashloan 代码中的虚拟账户还款不是完整的 Monnify Reserved Account 动态开户实现,而是既有的 VP / virtual sub account 老链路。
PMT 侧现状:
IInnerPaymentService.vitualAccountRepay根据orderId查询PmtOrderOriginal。- PMT 用
custName、custId、channel组装CreateAccountNumReq。 VitualAccountOutgoing.createOrQueryVitualAccount调配置项vp_create_and_query_url,参数为accountName/channel/bvn。- PMT 返回
accountNumber/bankName/accountName,并创建 VP 支付订单。 - 这里没有直接调用 Monnify Reserved Account API,也没有保存 Monnify
accountReference。
PMT 到账回调现状:
/vpRepayCallBack接收入账通知。- 用回调 BVN 查询近 3 天、可用 VP channel 下的
PmtOrderOriginal。 - 按
settledAmount == paymentAmt精确匹配订单。 - 匹配成功后更新
PmtOrderOriginal、PmtPaymentOrder、PmtPaymentFlow为成功。 - 调
notifyBusinessManager.processNotifyBiz(orderId)通知业务。
FCS 侧还有一条直接入账入口:
/virtualSubAccountPayEnvents接收虚拟子账户回调。- 按 BVN 加分布式锁。
- 通过
tranRef做幂等,已入库则直接返回成功。 - 查询客户正常/逾期未结清借据。
- 如果没有未结清借据,金额进入客户
overflowAmt溢缴款。 - 如果有未结清借据,取第一笔借据创建
VP订单,并立即走AfterPaymentMP销账。 - 回调原始结果写入
fcs_virtual_sub_account_order。
因此本期接 Monnify Reserved Account 时,不能简单认为现有 VP 已等价支持 Monnify。需要明确是复用 VP 入账模型,还是新增 Monnify reserved account 适配层,并统一 PMT/FCS 两条回调入口。
4. 方式一:卡代扣
4.1 业务目标
到期还款日由系统自动扣用户在借款环节已绑定的银行卡。扣款成功后 FCS 销账;扣款失败后记录失败原因,并引导用户使用 H5 自行还款或虚拟账户转账。
4.2 实现逻辑
sequenceDiagram participant Job as FCS Job participant FCS as FCS participant PMT as PMT participant IMS as IMS participant Monnify as Monnify Job->>FCS: NormalWithholding / OverdueWithholding FCS->>FCS: 生成 RP 代扣订单 FCS->>PMT: BeforePaymentPlutusPublisher 发送还款事件 PMT->>IMS: queryCardTokenByUserId(userId) IMS-->>PMT: 返回有效 cardToken PMT->>Monnify: Charge Card Token Monnify-->>PMT: 返回受理/扣款结果 Monnify-->>PMT: webhook / 查询结果 PMT->>FCS: repayResult / 还款结果事件 FCS->>FCS: afterPaymentRP + PaymentHier 销账
4.3 关键规则
- 卡信息来源:IMS 现有绑卡结果,不从还款页重新采集。
- token 查询:PMT 按客户号调用 IMS
queryCardTokenByUserId。 - 扣款触发:FCS 到期任务触发,正常代扣和逾期代扣均走
RP。 - 通道执行:PMT 路由到 Monnify 后调用
charge-card-token。 - 结果回流:PMT 回流 FCS,FCS 只按回流结果销账。
- 失败兜底:失败后保留账单未还状态,用户可进入 H5 主动还款或使用虚拟账户还款。
4.4 多 token 选择规则
如果用户存在多个 token,当前代码不是固定取第一张卡,而是经过 PMT 的 token 筛选和扣款路由。
第一步,PMT selectCardToken(custId, businessChannel) 查询可用于还款的卡:
- 查询条件包含
identity_number = custId。 - 排除
error.channel中配置的通道。 - 支持
share_channel或business_channel匹配当前业务线。 - 按
create_time desc查询。 - 过滤空
chargeToken。 - 过滤
status = INVALID。 - 若无有效 token,返回失败原因,例如
The user has not bound any token、charge token is null、token is invalid。
第二步,PMT routeDeductionForManyChannel 选择本次扣款 token:
| 场景 | 选择规则 |
|---|---|
| 指定卡号 | 只在该卡号对应的 token 列表中选择 |
| 有上次成功 token 缓存 | 优先复用成功 token |
| 有上次失败 token 缓存 | 从失败 token 的下一个 token 轮转,最后一个失败则回到第一个 |
| 未指定卡号 | ~~优先非 Goldman;~~非 Goldman 内优先merchantName 不以 goldman 结尾的最新卡 |
如果最终无法选出 token,PMT 会把订单置为失败,失败原因是 the order has not valid token,并通知业务侧。
现状风险:多 token 策略依赖 Redis 中成功/失败 token 缓存、通道配置和 merchantName 字符串规则。接 Monnify tokenization 时,需要确认 Monnify token 是否纳入同一
CustBankInfo.chargeToken模型,并补齐 Monnify 在扣款路由中的优先级和失败轮转规则。
4.5 H5 新卡还款与类型记录
当前代码中,用户在还款页选择“新卡支付”、输入卡信息并点击确认,会触发 PMT 的 H5 公共还款入口;该链路不是 FCS activeCustomerRepayment(MP) 人工还款链路。
4.5.1 页面展示控制
- H5 调用 PMT
repayTypeForH5获取可用还款方式。 PlusRepayEnum.NEW_CARD("newCard", "新卡支付")控制新卡支付入口。- PMT 返回
newCardAvaliable,前端据此展示或隐藏新卡支付。
4.5.2 确认支付流程
- 前端提交
CommonRepayGatewayReq,其中repayType = "Card",并携带卡号、CVV、有效期、姓名、isSaveCard等信息。 - PMT
commonRepayGateway创建或更新PmtOrderOriginal,将订单置为处理中。 - PMT 写入
isPartnerDebit = "PLUS",并记录本次输入的cardNo。 - PMT 将
PaymentReqVo.txnType设置为REPAY,将PaymentReqVo.repayType设置为前端传入的Card。 DoRepaymentFactory根据PlutusRepayEnum.CARD("Card", "新卡还款")路由到DoCardRepay。DoCardRepay创建PmtPaymentOrder和PmtPaymentFlow,当前代码默认支付通道为Paystack。- PMT 调 IMS
cardRepay发起新卡还款;若isSaveCard为保存卡,token 保存由 IMS/通道侧处理。 - 支付成功后,FCS 根据 Plutus/Paystack 回调创建
PHP入账订单并执行销账。
4.5.3 类型记录口径
| 层级 | 字段 | 记录值 | 说明 |
|---|---|---|---|
| H5 展示 | newCardAvaliable | Y/N | 是否展示新卡支付入口 |
| H5 入参 | repayType | Card | 新卡还款实际提交类型 |
| PMT 枚举 | PlutusRepayEnum.CARD | Card | 路由到 DoCardRepay |
| PMT 原始单 | pmt_order_original.is_partner_debit | PLUS | 标识 Plutus H5 还款来源 |
| PMT 原始单 | pmt_order_original.card_no | 本次输入卡号 | 记录本次使用的卡号 |
| PMT 支付单 | pmt_payment_order.debit_strategy | Card | 记录本次还款策略 |
| PMT 支付单 | pmt_payment_order.payment_channel | 当前为 Paystack | 当前新卡还款默认通道 |
| PMT 支付流水 | pmt_payment_flow.txn_type | REPAY | 支付域交易类型 |
| FCS 入账单 | fcs_order_txn.txn_type | PHP | 支付成功回调后的入账销账类型 |
| FCS 入账单 | fcs_order_txn.channel | PlutusWebPay | 网页还款渠道来源 |
注意:
newCard是 H5 展示层类型,实际提交到 PMT 的repayType是Card。如果用户选择已绑卡支付,repayType通常为Token,PMT 会走DoTokenRepay,并按AP代扣消息处理;新卡支付不走该 token 代扣链路。
4.5.4 H5 选择已有卡还款
如果用户不是输入新卡,而是在还款页直接选择已有卡并点击申请还款,前端仍调用 PMT commonRepayGateway,但实际提交的 repayType 为 Token,cardNum 为用户选择的已绑卡卡号。
处理流程如下:
- PMT
commonRepayGateway查询原始还款订单,校验订单状态和在途支付单。 - PMT 将
PmtOrderOriginal.paymentStatus更新为P,将isPartnerDebit设置为PLUS,并把用户选择的卡号写入cardNo。 - PMT 将
PaymentReqVo.repayType设置为Token。 DoRepaymentFactory根据PlutusRepayEnum.TOKEN("Token", "token还款")路由到DoTokenRepay。DoTokenRepay查询客户已绑卡列表:queryCardOrderByCustId(custId, channel, errorChannelList)。- 系统会降低 Goldman 主体卡的优先级;但如果前端传入指定
cardNum,会在该卡号匹配的记录中选择可用卡信息。 - PMT 更新原始单的
cardType、paymentMerchant、custName等信息。 - PMT 组装
RepayOrderPushReq,设置txnType = "AP"、type = "Token"、isOntime = "Y",并通过 Kafka 发送 AP 还款消息。 - 接口先返回
pending状态,真正扣款由后续 AP 异步代扣流程继续执行。
已有卡还款的记录口径:
| 层级 | 字段 | 记录值 | 说明 |
|---|---|---|---|
| H5 入参 | repayType | Token | 表示选择已有绑卡 token 还款 |
| H5 入参 | cardNum | 已绑卡卡号 | 用于匹配客户已有卡信息 |
| PMT 原始单 | pmt_order_original.is_partner_debit | PLUS | 标识 Plutus H5 主动还款来源 |
| PMT 原始单 | pmt_order_original.card_no | 已绑卡卡号 | 记录用户本次选择的卡 |
| PMT 原始单 | pmt_order_original.payment_status | P | 还款受理后进入处理中 |
| PMT 原始单 | pmt_order_original.is_real_time_payment | Y | 主动实时还款 |
| AP 消息 | RepayOrderPushReq.type | Token | token 还款类型 |
| AP 消息 | RepayOrderPushReq.txnType | AP | 主动代扣消息类型 |
注意:已有卡 H5 还款与后台批扣都会使用 token,但入口不同。H5 已有卡还款由用户选择卡并触发
Token/AP;后台批扣由定时任务触发RP,再按多 token 路由策略选择 token。
4.5.5 新卡保存后的后续代扣关系
新卡还款是否生成可用于后续代扣的 token,取决于前端提交的 isSaveCard 和通道返回结果。
- 如果
isSaveCard = "Y",IMS 会将绑卡订单备注记为repay|Y。通道成功返回authorization_code后,IMS 会保存或更新CustBankInfo.chargeToken,该 token 后续可进入批扣候选池。 - 如果
isSaveCard = "N",IMS 识别remark = "repay|N"后不保存卡信息,不会新增可用于后续代扣的 token。 - 后续批扣通过
selectCardToken(custId, businessChannel)查询CustBankInfo。只有chargeToken非空、状态非INVALID、业务线或共享渠道匹配的卡,才会进入代扣候选列表。 - 新生成的 token 进入候选列表后,不代表每次都优先使用;实际使用哪张卡仍由多 token 路由策略决定。
4.6 异常处理
| 场景 | 处理 |
|---|---|
| 未查到有效 token | 本次代扣失败,提示用户重新绑卡或主动还款 |
| token 失效 | IMS/PMT 标记 token 不可用,引导重新绑卡 |
| 余额不足/银行拒绝 | 记录失败原因,按策略重试或进入提醒 |
| 通道处理中 | 保持 PENDING,等待 webhook 或查询补偿 |
| 重复回调 | 按 paymentReference + transactionReference 幂等 |
4.7 批扣批次配置
4.7.1 批扣任务类型
| 批扣类型 | 现有 Job | TxnType | 当前代码状态 | 说明 |
|---|---|---|---|---|
| 正常到期批扣 | normalWithholdingJob | NC | 生效 | 到期日批量生成代扣订单 |
| 逾期批扣 | overdueWithholdingJob | OC | 需确认 | Reader 有扫描逻辑,但当前 B1222PPaymentOrderInitPlutus 中创建 OC 订单的核心代码被注释 |
正常到期批扣读取 cfk_acct.next_due_date = businessDate 的账户;逾期批扣读取 past_outstd_bal > 0 且逾期天数落在 (startDay, endDay] 的账户。
4.7.2 参考批扣日历
时间均按尼日利亚本地时间 WAT(UTC+1) 配置。不要在凌晨、深夜、周日或尼日利亚公共假日发起批扣,降低投诉和银行拒付风险。
| 阶段 | 扣款日 | 推荐批次 | 每日最多扣款次数 | 扣款策略 |
|---|---|---|---|---|
| 到期日首扣 | D0 | 09:30 | 1 | 优先扣全额应还金额 |
| 到期日补扣 | D0 | 13:30 | 1 | 首扣失败且非硬拒绝时尝试第二次,可轮转 token |
| 到期日晚间补扣 | D0 | 18:30 | 1 | 仅对余额不足、银行临时失败、通道超时类失败重试 |
| 早期逾期 | D+1 至 D+7 | 10:00 | 1 | 每天最多一次,优先全额;配合短信/WhatsApp 引导 H5 或虚拟账户 |
| 中期逾期 | D+8 至 D+30 | 周一/周三/周五 11:00 | 1 | 每周最多三次,避免每天失败扣款 |
| 后期逾期 | D+31 至 D+60 | 周二/周五 11:00 | 1 | 每周最多两次,重点转人工催收和主动还款 |
| 长期逾期 | D+61 至 D+90 | 周三 11:00 | 1 | 每周最多一次,仅保留低频自动尝试 |
| 深度逾期 | D+91 至 D+365 | 催收承诺日 11:00 | 1 | 默认不跑全量批扣;有还款承诺或用户同意时触发 |
| 停止自动批扣 | D+365 之后 | 不配置自动批次 | 0 | 停止自动卡扣 |
“硬拒绝”包括 token 失效、卡过期、账户关闭、疑似欺诈、用户撤销授权等。硬拒绝不进入当日后续批次,应转绑卡或主动还款引导。
4.7.3 合规与成本控制(供参考)
尼日利亚本地卡 token 扣款需要重点满足授权、透明、低扰动和可追溯要求:
| 维度 | 要求 |
|---|---|
| 用户授权 | 借款/绑卡环节必须明确告知保存卡 token、到期自动扣款、扣款金额范围、扣款频次和撤销方式 |
| 扣款前提醒 | D-1 至少发送一次提醒,说明金额、日期、卡尾号和主动还款入口 |
| 扣款后通知 | 成功、失败、部分成功均需通知用户,并展示失败原因和替代还款方式 |
| 频次限制 | 不做全天候高频扣款;失败后按规则降频 |
| 数据保护 | token 加密存储,不下发前端;Monnify token 需绑定客户、邮箱和内部用户 ID |
| 结果核验 | 所有成功结果必须通过通道查询或 webhook 验证后再销账 |
| 投诉处理 | 用户撤销授权、投诉未授权扣款或卡过期时,立即停止该 token 后续批扣 |
成本控制口径:
- 卡扣成功通常按交易金额收取通道费,失败虽然未必直接收费,但会消耗通道限额、降低成功率评分,并带来用户投诉和运营处理成本。
- 到期日最多 3 次是成功率和投诉风险之间的上限;逾期后应快速降频。
- 金额较大或多次失败客户,优先引导虚拟账户转账。虚拟账户成本通常低于卡扣,且适合客户主动还款。
- 不建议默认开启拆分扣款;只有在通道单笔限额、余额不足识别或成功率策略明确时才启用。
- 对
insufficient funds类失败可保留后续重试;对invalid token、expired card、do not honor、restricted card类失败应停止或降级处理。
5. 方式二:虚拟账户还款
5.1 业务目标
通过 Monnify Reserved Account 为客户或订单生成虚拟账户。用户向虚拟账户转账后,PMT 根据 Monnify 入账通知识别账户引用并回流 FCS 入账。
5.2 虚拟账号创建口径
本期建议区分两类虚拟账户:
| 类型 | accountReference 建议 | 适用场景 | 复用策略 |
|---|---|---|---|
| 客户级还款账户 | RA-{custId} | 现金贷/分期长期还款 | 客户长期复用 |
| 订单级账户 | RP-{repaymentOrderNo} 或 RA-{loanId}-{term} | 手机分期首付款 | 单笔还款使用,过期关闭 |
MVP 优先建议:
- 现金贷/普通贷后还款:客户级虚拟账户,便于客户长期保存。
- 首付、订单强匹配场景:订单级虚拟账户,避免同一客户多笔订单错配。—— 可在手机分期业务中再实现
如果 Monnify 合同限制同一自然客户只能有一个 active reserved account,则客户级复用为主;订单级诉求需改用 invoice/dynamic account 或产品限制同一客户同一时间只存在一笔待支付订单。
5.3 创建虚拟账号流程
sequenceDiagram participant App as App/H5 participant BNS as BNS/BFF participant PMT as PMT participant Monnify as Monnify App->>BNS: 进入虚拟账户还款页 BNS->>PMT: 查询客户虚拟账户 alt 已存在有效账户 PMT-->>BNS: 返回 accountNumber + bankName + accountReference else 不存在或已失效 PMT->>PMT: 生成 accountReference PMT->>Monnify: Create Reserved Account Monnify-->>PMT: 返回虚拟账号和银行信息 PMT->>PMT: 保存虚拟账户 PMT-->>BNS: 返回虚拟账户信息 end BNS-->>App: 展示银行名、账号、户名、应还金额
5.4 创建入参
| 字段 | 要求 | 说明 |
|---|---|---|
accountReference | 必填 | 我方唯一账户引用,必须可反查客户/订单 |
accountName | 必填 | 银行侧展示户名,建议包含品牌和客户姓名脱敏 |
currencyCode | 必填 | NGN |
contractCode | 必填 | Monnify 合同号 |
customerEmail | 必填 | 客户邮箱;无真实邮箱时需统一生成系统邮箱,邮箱域名按照bizcode进行差异配置 |
customerName | 必填 | 客户姓名 |
bvn / nin | 至少一个 | 按 Monnify 生产要求和合规口径确认 |
getAllAvailableBanks | 可选 | 是否返回多个银行虚拟账号 |
preferredBanks | 可选 | 指定优先银行 |
restrictPaymentSource | 可选 | 是否限制付款来源 |
allowedPaymentSources | 可选 | 限制付款账号/BVN,降低误入账 |
5.5 展示与复用规则
- 用户进入还款页时先查本地虚拟账户,不重复开户。
- 账户有效则直接展示。
- 账户不存在、失效或被关闭时才重新创建。
- 客户级账户可长期展示;订单级账户需要展示有效期。
- 页面必须展示本期应还金额,但不能假设用户一定转入精确金额。
5.6 到账流程
sequenceDiagram participant User as 用户 participant Monnify as Monnify participant PMT as PMT participant FCS as FCS User->>Monnify: 转账至虚拟账户 Monnify-->>PMT: Transaction Completion webhook PMT->>PMT: 验签、落原始报文、幂等校验 PMT->>PMT: 根据 accountReference 定位客户/订单 PMT->>Monnify: Verify Transaction / 查询交易 Monnify-->>PMT: 返回 PAID、金额、交易号 PMT->>FCS: vitualAccountRepay / repayResult FCS->>FCS: VP类销账
5.7 金额处理
| 入账金额 | 处理规则 |
|---|---|
| 等于本期应还 | 结清本期 |
| 小于本期应还 | 记为部分还款,剩余金额继续待还 |
| 大于本期应还 | 先销本期及可销账金额,多余进入超额/挂账/人工退款 |
| 无法匹配账单 | 进入客户级挂账,待人工或账务任务分配 |
| 订单关闭后到账 | 不自动推进业务状态,进入异常挂账 |
6. 方式三:打开支付通道 H5 自行还款
6.1 业务目标
用户在还款页点击“立即还款”,系统生成 Monnify Checkout 交易并打开 checkoutUrl。用户可在 Monnify H5 中选择卡、动态虚拟账户转账、USSD 或手机号支付。
6.2 与现有 Plutus H5 的关系
现有现金贷 H5 已经是“生成支付订单 -> 跳外部收银台 -> 回跳 -> 轮询结果”的模式。本期不改变前端主流程,只替换/扩展支付 URL 生成逻辑:
| 现有 Plutus | 本期 Monnify H5 |
|---|---|
generatePlutusWebOrder4nc 返回支付 URL | 新增/扩展接口返回 Monnify checkoutUrl |
H5 location.href = url | H5 location.href = checkoutUrl |
/afterRepayment?reference=xxx 回跳 | Monnify redirectUrl 回跳同一结果页 |
轮询 activeQueryPlutusPaymentResult | 复用或扩展为查询 Monnify 还款订单结果 |
6.3 支持通道
| Monnify 支付方式 | 说明 |
|---|---|
CARD | 用户银行卡支付 |
ACCOUNT_TRANSFER | 单笔动态虚拟账户转账 |
USSD | USSD 支付 |
PHONE_NUMBER | 手机号支付 |
6.4 Monnify 官方接口链接
方式三采用 Monnify API-first Hosted Checkout 模式:PMT 服务端调用 Monnify 初始化交易,拿到 checkoutUrl 后返回给 H5;H5 只负责打开收银台和回跳展示,最终入账以 PMT 服务端 webhook + verify 结果为准。
| 使用环节 | Monnify 能力 / 接口 | 官方文档 | 接口路径 / 说明 |
|---|---|---|---|
| 获取访问令牌 | Authenticate / Login | Quickstart - Authenticate | POST /api/v1/auth/login,用 apiKey:secretKey Base64 后换取 Bearer token |
| 创建 H5 收银台交易 | Initialize Transaction / Checkout API | Checkout API | POST /api/v1/merchant/transactions/init-transaction,入参包含 amount、customerName、customerEmail、paymentReference、paymentDescription、currencyCode、contractCode、redirectUrl、paymentMethods |
| 打开支付页 | Hosted Checkout URL | Checkout API - Hosted Checkout | 初始化交易成功后返回 checkoutUrl,H5 location.href = checkoutUrl;官方说明 checkoutUrl 有效期为 40 分钟 |
| 限定 H5 支付方式 | Payment Methods | Payment Methods | paymentMethods 可传 CARD、ACCOUNT_TRANSFER、USSD、PHONE_NUMBER,用于控制收银台展示的支付方式 |
| 支付完成通知 | Webhooks | Webhooks | 在 Monnify 后台配置 Transaction Completion webhook;PMT 需验签、幂等、落原始报文 |
| 服务端验单 | Verify by Payment Reference | Verify Transactions | GET /api/v2/merchant/transactions/query?paymentReference={paymentReference},用我方传入的 paymentReference 查询权威支付状态 |
| 服务端验单 | Verify by Transaction Reference | Verify Transactions | GET /api/v2/transactions/{transactionReference},用 Monnify 返回或 webhook 中的 transactionReference 查询 |
6.5 系统流程
sequenceDiagram participant H5 as App/H5 participant BNS as BNS/BFF participant FCS as FCS participant PMT as PMT participant Monnify as Monnify H5->>BNS: 点击立即还款 BNS->>FCS: activeCustomerRepayment / 创建 MP 主动还款订单 FCS->>PMT: 创建 Monnify checkout 交易 PMT->>Monnify: Initialize Transaction(paymentMethods) Monnify-->>PMT: checkoutUrl PMT-->>FCS: 返回 checkoutUrl 和 reference FCS-->>BNS: 返回支付 URL BNS-->>H5: checkoutUrl H5->>Monnify: 打开 Monnify H5 并完成支付 Monnify-->>H5: redirectUrl 回跳 Monnify-->>PMT: webhook PMT->>Monnify: Verify Transaction PMT->>FCS: repayResult H5->>FCS: 轮询支付结果 FCS-->>H5: SUCC / FAIL / PENDING
6.6 订单要求
| 字段 | 说明 |
|---|---|
repaymentOrderNo | 我方还款订单号 |
paymentReference | 传给 Monnify 的唯一引用 |
custId | 客户号 |
loanId | 借据号 |
repaymentPlanId | 期次 |
amount | 用户本次还款金额 |
txnType | 主动还款为 MP |
redirectUrl | Monnify 支付完成回跳地址 |
paymentMethods | CARD、ACCOUNT_TRANSFER、USSD、PHONE_NUMBER |
6.7 异常处理
| 场景 | 处理 |
|---|---|
| 用户关闭 H5 | 订单保持 PENDING,等待 webhook 或查询补偿 |
| 支付链接过期 | 订单置为 EXPIRED,允许重新生成 |
| 前端回跳但未入账 | 结果页展示处理中,继续轮询 |
| Monnify 成功但 FCS 未销账 | 补偿任务重放/查询结果,幂等销账 |
| 金额不一致 | 不自动销账,进入异常处理 |
7. 统一入账与销账逻辑
入账逻辑不按还款方式各自实现,统一由 PMT 回流 FCS,FCS 按 TxnType 分流销账。
7.1 统一入账入口
| 来源 | PMT/FCS 入口 | TxnType | 销账处理 |
|---|---|---|---|
| 卡代扣 | repayResult / 还款结果事件 | RP | afterPaymentRP |
| H5 主动还款 | repayResult / 主动还款查询结果 | MP | afterPaymentMP |
| 虚拟账户还款 | vitualAccountRepay / vpRepayCallBack | VP | MP 类销账 |
| 部分还款 | partialRepayment | CLP | afterPaymentCLP |
| 超额还款 | 超额处理 | OFR | afterPaymentOFR |
7.2 标准处理步骤
- PMT 接收通道 webhook 或查询结果。
- PMT 校验签名、金额、币种、交易状态。
- PMT 保存原始报文和通道交易流水。
- PMT 以
paymentReference、transactionReference、accountReference做幂等。 - PMT 回流 FCS:
- 代扣:
RP - 主动 H5:
MP - 虚拟账户:
VP
- 代扣:
- FCS
AfterPaymentFactory按 TxnType 选择处理器。 - FCS 加载销账上下文:
- 单借据:
loadDataForLoanRepay - 客户级:
loadDataForCustRepay
- 单借据:
- FCS 调 PFS
queryOffsetConfig获取销账顺序。 PaymentHier按余额组件、账龄和配置顺序执行销账。- 生成复式记账分录和销账分录。
- 判断期次/借据是否结清。
- 更新还款计划、借据状态和还款历史。
7.3 销账顺序
销账顺序沿用现有 FCS/PFS:
- 按
bal_compt区分本金、利息、费用、罚息。 - 按账龄 aging 优先处理更早欠款。
- 按 PFS
pfs_offset_order配置顺序销账。 - 支持客户级还款在多借据/多期次间分配。
7.4 金额状态
| 状态 | 处理 |
|---|---|
| 足额 | 正常销账,期次结清 |
| 少付 | 部分销账,剩余继续待还 |
| 多付 | 先销可销账余额,多余进入超额还款/挂账 |
| 重复支付 | 第二笔作为独立资金流水,按可销账余额或超额处理 |
| 冲正 | 走冲正/人工处理,不能简单删除原销账流水 |
7.5 幂等键(参考)
| 键 | 用途 |
|---|---|
paymentReference | 我方还款订单和 Monnify 交易主关联 |
transactionReference | Monnify 单笔交易幂等 |
accountReference | 虚拟账户入账匹配客户/订单 |
repaymentOrderNo | 我方还款订单幂等 |
loanId + repaymentPlanId + attemptNo | 代扣尝试幂等 |
7.6 人工还款与部分还款
人工还款和部分还款不是新增 Monnify 能力,但它们会影响还款后账务余额,需在本期入账口径中明确。
人工还款 MR
人工还款由 FCS artificialRepay 触发,典型场景是运营或后台录入线下还款,不走 PMT 通道。
现有处理逻辑:
- 按
loanId查询借据。 - 检查批处理是否运行中,运行中则阻断。
- 校验人工还款金额不能大于借据当前未还余额。
- 锁客户。
- 检查客户是否存在在途
MR订单。 - 创建
TxnType.MR订单。 - 使用请求中的
txnDate作为线下还款交易日。 - 订单直接置为支付成功。
- 立即走
AfterPaymentMP销账。
人工还款的特殊点:
MR不扣通道费;AfterPaymentMP中只有非MR才会按passRate扣通道费。MR/MRJ冲销后如有剩余金额,不计入客户溢缴款。- 入口已限制还款金额不能大于借据未还余额,因此人工还款原则上不应产生超额。
部分/催收还款 CLP
部分还款由 FCS partialRepayment 触发,TxnType 为 CLP。当前代码注释为“催收还款”,但账务效果就是客户级部分入账。
现有处理逻辑:
- 校验
repayAmt > 0。 - 检查批处理是否运行中。
- 查询并锁客户。
- 校验客户存在正常或逾期借据。
- 检查客户是否存在在途
CLP订单。 - 走
BeforePaymentCLP创建还款订单。 - 发送还款消息到 PMT 执行扣款。
- 支付成功后走
AfterPaymentCLP。
AfterPaymentCLP 的记账方式:
- 加载客户级还款上下文
loadDataForCustRepay。 - 从成功金额中扣除通道费:
successAmt - successAmt * passRate。 - 走
C40还款逻辑模块。 - 由
PaymentHier按 PFSqueryOffsetConfig返回的顺序冲销。 - 冲销顺序按账龄 aging 和余额成分执行,余额成分包括本金、利息、费用、罚息等。
- 金额不足时,只冲销到可覆盖的计划和余额成分,未冲销部分继续保留为待还余额。
- 如果本次部分还款刚好结清某笔借据,会触发结清后的额度/规则处理。
部分还款后的账务结果:
| 情况 | 后续账务表现 |
|---|---|
| 只还了一部分费用/罚息 | 本金、利息等余额继续待还 |
| 还清某一期部分成分 | 该成分余额归零,其他成分继续待还 |
| 还清某一期全部余额 | 该期计划可置为结清,继续冲后续期次 |
| 还清全部借据 | 借据结清,后续触发结清相关处理 |
| 金额超过可销账余额 | 普通还款进入溢缴款;MR/MRJ 不进溢缴款 |
因此,“部分还款后后续的账”不是重算新计划,而是在原还款计划和余额成分上减少已冲销金额;剩余 outstdBal 继续按原计划、逾期和日终逻辑流转。
8. 统一状态
| 状态 | 说明 |
|---|---|
INIT | 我方已创建还款订单 |
PENDING | 已提交通道或等待用户支付 |
PROCESSING | 已收到回调或前端回跳,正在核验 |
SUCCESS | 已确认成功并完成销账 |
PARTIAL | 已部分销账 |
OVERPAID | 超额还款,待分配或退款 |
FAILED | 支付失败 |
EXPIRED | H5 链接或动态账号过期 |
REVERSED | 成功后发生冲正 |
9. 前后端职责
| 模块 | 职责 |
|---|---|
| App/H5 | 展示还款计划、卡代扣状态、虚拟账户、打开 Monnify H5、轮询结果 |
| BNS/BFF | 聚合还款页信息,调用 FCS/PMT 创建还款订单或查询虚拟账户 |
| IMS | 保存借款环节绑卡结果,向 PMT 提供 token 查询 |
| PMT | 调 Monnify、处理 webhook、验签、幂等、交易查询、回流 FCS |
| FCS | 生成还款计划、代扣调度、创建主动还款订单、接收结果并销账 |
| PFS | 提供产品和销账顺序配置 |
| Job | 代扣调度、结果查询补偿、过期订单处理 |
10. 验收标准
10.1 卡代扣
- 借款环节已绑卡客户,到期可由 FCS 自动发起代扣。
- PMT 能通过 IMS 查询客户有效 token。
- Monnify 扣款成功后,FCS 以
RP完成销账。 - 扣款失败能记录失败原因,并支持用户改用 H5 或虚拟账户还款。
10.2 虚拟账户还款
- 客户进入还款页时可查询或创建 Monnify 虚拟账户。
- 虚拟账户创建使用唯一
accountReference,可反查客户/订单。 - 用户转账后 PMT 能根据
accountReference识别资金归属。 - FCS 能按
VP/ MP 类销账处理入账。 - 少付、多付、重复转账不导致重复销账或错误推进业务状态。
10.3 H5 自行还款
- 用户点击立即还款后可打开 Monnify
checkoutUrl。 - H5 支持
CARD、ACCOUNT_TRANSFER、USSD、PHONE_NUMBER。 - 回跳后前端展示处理中,并轮询 FCS 支付结果。
- Monnify 成功交易经 PMT 回流 FCS 后,以
MP完成销账。
10.4 统一入账
- 三种还款方式均回流 FCS,不在 PMT 或 BNS 直接改账。
- 同一通道交易重复回调不会重复销账。
- FCS 销账顺序沿用 PFS 配置,不因通道变化改变。
- 可通过还款订单、通道交易号、虚拟账户引用追踪完整链路。
11. 待确认事项
| 事项 | 说明 |
|---|---|
| Monnify Card Tokenization 权限 | 需确认商户是否已开通 charge-card-token |
| 超额还款策略 | 多余金额进入余额、抵扣下期还是退款 |
| 重试策略 | 卡代扣失败后的重试次数、间隔和时间窗 |
12. 参考文档
- 现状:
09-现状代码架构/13-现金贷业务时序与接口落地/03-借款.md - 现状:
09-现状代码架构/13-现金贷业务时序与接口落地/05-还款.md - 现状:
09-现状代码架构/16-现金贷前端交互时序/04-还款.md - 现状:
09-现状代码架构/14-服务依赖与边界/06-ims.md - 现状:
09-现状代码架构/14-服务依赖与边界/07-pmt.md - Monnify Quickstart: https://developers.monnify.com/docs/collections/quickstart
- Monnify Checkout API: https://developers.monnify.com/docs/collections/one-time-payments/checkout-api
- Monnify Payment Methods: https://developers.monnify.com/docs/collections/payment-methods
- Monnify Card Tokenization: https://developers.monnify.com/docs/collections/recurring-payments/card-tokenization
- Monnify Customer Reserved Account: https://developers.monnify.com/docs/collections/recurring-payments/reserved-accounts
- Monnify Verify Transactions: https://developers.monnify.com/docs/collections/manage-payments/verify-transactions





