还款方式说明

1. 背景与目标

本期还款能力基于现金贷现有链路改造,不从零新建支付和销账能力。现金贷项目现状已经具备:

  • 借款环节绑卡:BNS 调 IMS routeRedirect,卡走 token,账户走 Direct Debit + OTP;绑定结果由 IMS 回调 BNS /inner/bindCardCallBack
  • 还款调度:FCS 的 NormalWithholding / OverdueWithholding 生成代扣任务,TxnType 为 RP
  • 主动还款:H5 调 generatePlutusWebOrder4nc 生成 Plutus 网页支付订单,跳支付页,回跳后轮询 FCS activeQueryPlutusPaymentResult
  • 支付执行: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,复用 AfterPaymentFactoryPaymentHier 和 PFS 销账顺序配置。
  • 所有外部支付结果必须由服务端查询/核验后入账,前端回跳不作为入账依据。

2.2 非本期范围

  • 不重建一套独立还款中心。
  • 不建设完整运营后台,对账和异常先通过 DB/人工导出处理。
  • 不支持线下现金还款。
  • 不做复杂退款、调账、减免自动化。
  • 不接 Paystack 特有的 Apple Pay、QR、EFT、Mobile Money 等能力。

3. 现有代码实现口径

3.1 绑卡现状

现金贷借款链路中,用户提交借款前已经完成绑卡:

  1. App/H5 发起绑卡,BNS 调 IMS routeRedirect
  2. IMS 根据通道返回 bindCardType
    • H5:跳 Plutus/IMS 托管页绑卡,BNS 落 BnsCardboundOrder,状态 pending
    • API:原生直绑。
  3. 另一维度 cardType 决定绑定对象:
    • 银行卡:走 token。
    • 银行账户:走 Direct Debit + OTP。
  4. 通道完成后回调 IMS。
  5. IMS 调 BNS /inner/bindCardCallBack,BNS 落客户卡/账户信息。
  6. 后续代扣由 PMT 调 IMS:
    • queryCardTokenByUserId 查询客户 token。
    • cardRepay / 通道 repayment 执行扣款。

借款环节已完成绑卡,FCS 到期发起代扣,PMT 从 IMS 查询客户有效 token 后调用 Monnify 扣款

3.2 主动还款现状

现金贷 H5 当前主路径是 Plutus 网页支付:

  1. H5 查询借据、还款计划、应还金额。
  2. 用户确认还款。
  3. H5 调 BLC plutus/v1/generatePlutusWebOrder4nc
  4. BLC/FCS 创建主动还款订单,TxnType 为 MP
  5. 返回支付 URL。
  6. H5 location.href 跳转收银台。
  7. 支付完成后回跳 /afterRepayment?reference=xxx
  8. H5 轮询 FCS activeQueryPlutusPaymentResult(reference)
  9. 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,业务含义为本次还款需额外承担的支付通道费。

前端不维护通道费计算公式,只负责两类取数:

  1. 页面初始进入时,H5 调用 queryRepayPlanDetailList 获取还款计划。接口返回每一期的 passAmount,前端按当前选中的期次累加后展示到 repaymentDetail.passAmount
  2. 用户编辑还款金额后,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现有入口销账处理
客户主动还款MPactiveCustomerRepaymentafterPaymentMP
人工还款MRartificialRepayafterPaymentMP
到期代扣RPNormalWithholding / OverdueWithholdingafterPaymentRP
部分/催收还款CLPpartialRepaymentafterPaymentCLP
虚拟账户还款VPvitualAccountRepayMP 类销账
超额还款OFR超额处理afterPaymentOFR
退款RFrefund退款处理

销账由 FCS 的 AfterPaymentFactory 分流,最终使用 PaymentHier 按 PFS queryOffsetConfig 返回的销账顺序执行,规则是先费后本,并按账龄 aging 处理。

3.4 虚拟账户代码现状

当前 cashloan 代码中的虚拟账户还款不是完整的 Monnify Reserved Account 动态开户实现,而是既有的 VP / virtual sub account 老链路。

PMT 侧现状:

  1. IInnerPaymentService.vitualAccountRepay 根据 orderId 查询 PmtOrderOriginal
  2. PMT 用 custNamecustIdchannel 组装 CreateAccountNumReq
  3. VitualAccountOutgoing.createOrQueryVitualAccount 调配置项 vp_create_and_query_url,参数为 accountName/channel/bvn
  4. PMT 返回 accountNumber/bankName/accountName,并创建 VP 支付订单。
  5. 这里没有直接调用 Monnify Reserved Account API,也没有保存 Monnify accountReference

PMT 到账回调现状:

  1. /vpRepayCallBack 接收入账通知。
  2. 用回调 BVN 查询近 3 天、可用 VP channel 下的 PmtOrderOriginal
  3. settledAmount == paymentAmt 精确匹配订单。
  4. 匹配成功后更新 PmtOrderOriginalPmtPaymentOrderPmtPaymentFlow 为成功。
  5. notifyBusinessManager.processNotifyBiz(orderId) 通知业务。

FCS 侧还有一条直接入账入口:

  1. /virtualSubAccountPayEnvents 接收虚拟子账户回调。
  2. 按 BVN 加分布式锁。
  3. 通过 tranRef 做幂等,已入库则直接返回成功。
  4. 查询客户正常/逾期未结清借据。
  5. 如果没有未结清借据,金额进入客户 overflowAmt 溢缴款。
  6. 如果有未结清借据,取第一笔借据创建 VP 订单,并立即走 AfterPaymentMP 销账。
  7. 回调原始结果写入 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_channelbusiness_channel 匹配当前业务线。
  • create_time desc 查询。
  • 过滤空 chargeToken
  • 过滤 status = INVALID
  • 若无有效 token,返回失败原因,例如 The user has not bound any tokencharge token is nulltoken is invalid

第二步,PMT routeDeductionForManyChannel 选择本次扣款 token:

场景选择规则
指定卡号只在该卡号对应的 token 列表中选择
有上次成功 token 缓存优先复用成功 token
有上次失败 token 缓存从失败 token 的下一个 token 轮转,最后一个失败则回到第一个
未指定卡号~~优先非 Goldman;~~非 Goldman 内优先merchantName 不以 goldman 结尾的最新卡
只有 Goldman选 Goldman 中最新的一张
特定通道/卡种Paystack、Seerbit、FlutterwaveV3 等按卡种配置决定 batch、partner debit、depart 标识

如果最终无法选出 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 确认支付流程

  1. 前端提交 CommonRepayGatewayReq,其中 repayType = "Card",并携带卡号、CVV、有效期、姓名、isSaveCard 等信息。
  2. PMT commonRepayGateway 创建或更新 PmtOrderOriginal,将订单置为处理中。
  3. PMT 写入 isPartnerDebit = "PLUS",并记录本次输入的 cardNo
  4. PMT 将 PaymentReqVo.txnType 设置为 REPAY,将 PaymentReqVo.repayType 设置为前端传入的 Card
  5. DoRepaymentFactory 根据 PlutusRepayEnum.CARD("Card", "新卡还款") 路由到 DoCardRepay
  6. DoCardRepay 创建 PmtPaymentOrderPmtPaymentFlow,当前代码默认支付通道为 Paystack
  7. PMT 调 IMS cardRepay 发起新卡还款;若 isSaveCard 为保存卡,token 保存由 IMS/通道侧处理。
  8. 支付成功后,FCS 根据 Plutus/Paystack 回调创建 PHP 入账订单并执行销账。

4.5.3 类型记录口径

层级字段记录值说明
H5 展示newCardAvaliableY/N是否展示新卡支付入口
H5 入参repayTypeCard新卡还款实际提交类型
PMT 枚举PlutusRepayEnum.CARDCard路由到 DoCardRepay
PMT 原始单pmt_order_original.is_partner_debitPLUS标识 Plutus H5 还款来源
PMT 原始单pmt_order_original.card_no本次输入卡号记录本次使用的卡号
PMT 支付单pmt_payment_order.debit_strategyCard记录本次还款策略
PMT 支付单pmt_payment_order.payment_channel当前为 Paystack当前新卡还款默认通道
PMT 支付流水pmt_payment_flow.txn_typeREPAY支付域交易类型
FCS 入账单fcs_order_txn.txn_typePHP支付成功回调后的入账销账类型
FCS 入账单fcs_order_txn.channelPlutusWebPay网页还款渠道来源

注意:newCard 是 H5 展示层类型,实际提交到 PMT 的 repayTypeCard。如果用户选择已绑卡支付,repayType 通常为 Token,PMT 会走 DoTokenRepay,并按 AP 代扣消息处理;新卡支付不走该 token 代扣链路。

4.5.4 H5 选择已有卡还款

如果用户不是输入新卡,而是在还款页直接选择已有卡并点击申请还款,前端仍调用 PMT commonRepayGateway,但实际提交的 repayTypeTokencardNum 为用户选择的已绑卡卡号。

处理流程如下:

  1. PMT commonRepayGateway 查询原始还款订单,校验订单状态和在途支付单。
  2. PMT 将 PmtOrderOriginal.paymentStatus 更新为 P,将 isPartnerDebit 设置为 PLUS,并把用户选择的卡号写入 cardNo
  3. PMT 将 PaymentReqVo.repayType 设置为 Token
  4. DoRepaymentFactory 根据 PlutusRepayEnum.TOKEN("Token", "token还款") 路由到 DoTokenRepay
  5. DoTokenRepay 查询客户已绑卡列表:queryCardOrderByCustId(custId, channel, errorChannelList)
  6. 系统会降低 Goldman 主体卡的优先级;但如果前端传入指定 cardNum,会在该卡号匹配的记录中选择可用卡信息。
  7. PMT 更新原始单的 cardTypepaymentMerchantcustName 等信息。
  8. PMT 组装 RepayOrderPushReq,设置 txnType = "AP"type = "Token"isOntime = "Y",并通过 Kafka 发送 AP 还款消息。
  9. 接口先返回 pending 状态,真正扣款由后续 AP 异步代扣流程继续执行。

已有卡还款的记录口径:

层级字段记录值说明
H5 入参repayTypeToken表示选择已有绑卡 token 还款
H5 入参cardNum已绑卡卡号用于匹配客户已有卡信息
PMT 原始单pmt_order_original.is_partner_debitPLUS标识 Plutus H5 主动还款来源
PMT 原始单pmt_order_original.card_no已绑卡卡号记录用户本次选择的卡
PMT 原始单pmt_order_original.payment_statusP还款受理后进入处理中
PMT 原始单pmt_order_original.is_real_time_paymentY主动实时还款
AP 消息RepayOrderPushReq.typeTokentoken 还款类型
AP 消息RepayOrderPushReq.txnTypeAP主动代扣消息类型

注意:已有卡 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 批扣任务类型

批扣类型现有 JobTxnType当前代码状态说明
正常到期批扣normalWithholdingJobNC生效到期日批量生成代扣订单
逾期批扣overdueWithholdingJobOC需确认Reader 有扫描逻辑,但当前 B1222PPaymentOrderInitPlutus 中创建 OC 订单的核心代码被注释

正常到期批扣读取 cfk_acct.next_due_date = businessDate 的账户;逾期批扣读取 past_outstd_bal > 0 且逾期天数落在 (startDay, endDay] 的账户。

4.7.2 参考批扣日历

时间均按尼日利亚本地时间 WAT(UTC+1) 配置。不要在凌晨、深夜、周日或尼日利亚公共假日发起批扣,降低投诉和银行拒付风险。

阶段扣款日推荐批次每日最多扣款次数扣款策略
到期日首扣D009:301优先扣全额应还金额
到期日补扣D013:301首扣失败且非硬拒绝时尝试第二次,可轮转 token
到期日晚间补扣D018:301仅对余额不足、银行临时失败、通道超时类失败重试
早期逾期D+1 至 D+710:001每天最多一次,优先全额;配合短信/WhatsApp 引导 H5 或虚拟账户
中期逾期D+8 至 D+30周一/周三/周五 11:001每周最多三次,避免每天失败扣款
后期逾期D+31 至 D+60周二/周五 11:001每周最多两次,重点转人工催收和主动还款
长期逾期D+61 至 D+90周三 11:001每周最多一次,仅保留低频自动尝试
深度逾期D+91 至 D+365催收承诺日 11:001默认不跑全量批扣;有还款承诺或用户同意时触发
停止自动批扣D+365 之后不配置自动批次0停止自动卡扣

“硬拒绝”包括 token 失效、卡过期、账户关闭、疑似欺诈、用户撤销授权等。硬拒绝不进入当日后续批次,应转绑卡或主动还款引导。

4.7.3 合规与成本控制(供参考)

尼日利亚本地卡 token 扣款需要重点满足授权、透明、低扰动和可追溯要求:

维度要求
用户授权借款/绑卡环节必须明确告知保存卡 token、到期自动扣款、扣款金额范围、扣款频次和撤销方式
扣款前提醒D-1 至少发送一次提醒,说明金额、日期、卡尾号和主动还款入口
扣款后通知成功、失败、部分成功均需通知用户,并展示失败原因和替代还款方式
频次限制不做全天候高频扣款;失败后按规则降频
数据保护token 加密存储,不下发前端;Monnify token 需绑定客户、邮箱和内部用户 ID
结果核验所有成功结果必须通过通道查询或 webhook 验证后再销账
投诉处理用户撤销授权、投诉未授权扣款或卡过期时,立即停止该 token 后续批扣

成本控制口径:

  1. 卡扣成功通常按交易金额收取通道费,失败虽然未必直接收费,但会消耗通道限额、降低成功率评分,并带来用户投诉和运营处理成本。
  2. 到期日最多 3 次是成功率和投诉风险之间的上限;逾期后应快速降频。
  3. 金额较大或多次失败客户,优先引导虚拟账户转账。虚拟账户成本通常低于卡扣,且适合客户主动还款。
  4. 不建议默认开启拆分扣款;只有在通道单笔限额、余额不足识别或成功率策略明确时才启用。
  5. insufficient funds 类失败可保留后续重试;对 invalid tokenexpired carddo not honorrestricted 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 = urlH5 location.href = checkoutUrl
/afterRepayment?reference=xxx 回跳Monnify redirectUrl 回跳同一结果页
轮询 activeQueryPlutusPaymentResult复用或扩展为查询 Monnify 还款订单结果

6.3 支持通道

Monnify 支付方式说明
CARD用户银行卡支付
ACCOUNT_TRANSFER单笔动态虚拟账户转账
USSDUSSD 支付
PHONE_NUMBER手机号支付

6.4 Monnify 官方接口链接

方式三采用 Monnify API-first Hosted Checkout 模式:PMT 服务端调用 Monnify 初始化交易,拿到 checkoutUrl 后返回给 H5;H5 只负责打开收银台和回跳展示,最终入账以 PMT 服务端 webhook + verify 结果为准。

使用环节Monnify 能力 / 接口官方文档接口路径 / 说明
获取访问令牌Authenticate / LoginQuickstart - AuthenticatePOST /api/v1/auth/login,用 apiKey:secretKey Base64 后换取 Bearer token
创建 H5 收银台交易Initialize Transaction / Checkout APICheckout APIPOST /api/v1/merchant/transactions/init-transaction,入参包含 amountcustomerNamecustomerEmailpaymentReferencepaymentDescriptioncurrencyCodecontractCoderedirectUrlpaymentMethods
打开支付页Hosted Checkout URLCheckout API - Hosted Checkout初始化交易成功后返回 checkoutUrl,H5 location.href = checkoutUrl;官方说明 checkoutUrl 有效期为 40 分钟
限定 H5 支付方式Payment MethodsPayment MethodspaymentMethods 可传 CARDACCOUNT_TRANSFERUSSDPHONE_NUMBER,用于控制收银台展示的支付方式
支付完成通知WebhooksWebhooks在 Monnify 后台配置 Transaction Completion webhook;PMT 需验签、幂等、落原始报文
服务端验单Verify by Payment ReferenceVerify TransactionsGET /api/v2/merchant/transactions/query?paymentReference={paymentReference},用我方传入的 paymentReference 查询权威支付状态
服务端验单Verify by Transaction ReferenceVerify TransactionsGET /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
redirectUrlMonnify 支付完成回跳地址
paymentMethodsCARDACCOUNT_TRANSFERUSSDPHONE_NUMBER

6.7 异常处理

场景处理
用户关闭 H5订单保持 PENDING,等待 webhook 或查询补偿
支付链接过期订单置为 EXPIRED,允许重新生成
前端回跳但未入账结果页展示处理中,继续轮询
Monnify 成功但 FCS 未销账补偿任务重放/查询结果,幂等销账
金额不一致不自动销账,进入异常处理

7. 统一入账与销账逻辑

入账逻辑不按还款方式各自实现,统一由 PMT 回流 FCS,FCS 按 TxnType 分流销账。

7.1 统一入账入口

来源PMT/FCS 入口TxnType销账处理
卡代扣repayResult / 还款结果事件RPafterPaymentRP
H5 主动还款repayResult / 主动还款查询结果MPafterPaymentMP
虚拟账户还款vitualAccountRepay / vpRepayCallBackVPMP 类销账
部分还款partialRepaymentCLPafterPaymentCLP
超额还款超额处理OFRafterPaymentOFR

7.2 标准处理步骤

  1. PMT 接收通道 webhook 或查询结果。
  2. PMT 校验签名、金额、币种、交易状态。
  3. PMT 保存原始报文和通道交易流水。
  4. PMT 以 paymentReferencetransactionReferenceaccountReference 做幂等。
  5. PMT 回流 FCS:
    • 代扣:RP
    • 主动 H5:MP
    • 虚拟账户:VP
  6. FCS AfterPaymentFactory 按 TxnType 选择处理器。
  7. FCS 加载销账上下文:
    • 单借据:loadDataForLoanRepay
    • 客户级:loadDataForCustRepay
  8. FCS 调 PFS queryOffsetConfig 获取销账顺序。
  9. PaymentHier 按余额组件、账龄和配置顺序执行销账。
  10. 生成复式记账分录和销账分录。
  11. 判断期次/借据是否结清。
  12. 更新还款计划、借据状态和还款历史。

7.3 销账顺序

销账顺序沿用现有 FCS/PFS:

  • bal_compt 区分本金、利息、费用、罚息。
  • 按账龄 aging 优先处理更早欠款。
  • 按 PFS pfs_offset_order 配置顺序销账。
  • 支持客户级还款在多借据/多期次间分配。

7.4 金额状态

状态处理
足额正常销账,期次结清
少付部分销账,剩余继续待还
多付先销可销账余额,多余进入超额还款/挂账
重复支付第二笔作为独立资金流水,按可销账余额或超额处理
冲正走冲正/人工处理,不能简单删除原销账流水

7.5 幂等键(参考)

用途
paymentReference我方还款订单和 Monnify 交易主关联
transactionReferenceMonnify 单笔交易幂等
accountReference虚拟账户入账匹配客户/订单
repaymentOrderNo我方还款订单幂等
loanId + repaymentPlanId + attemptNo代扣尝试幂等

7.6 人工还款与部分还款

人工还款和部分还款不是新增 Monnify 能力,但它们会影响还款后账务余额,需在本期入账口径中明确。

人工还款 MR

人工还款由 FCS artificialRepay 触发,典型场景是运营或后台录入线下还款,不走 PMT 通道。

现有处理逻辑:

  1. loanId 查询借据。
  2. 检查批处理是否运行中,运行中则阻断。
  3. 校验人工还款金额不能大于借据当前未还余额。
  4. 锁客户。
  5. 检查客户是否存在在途 MR 订单。
  6. 创建 TxnType.MR 订单。
  7. 使用请求中的 txnDate 作为线下还款交易日。
  8. 订单直接置为支付成功。
  9. 立即走 AfterPaymentMP 销账。

人工还款的特殊点:

  • MR 不扣通道费;AfterPaymentMP 中只有非 MR 才会按 passRate 扣通道费。
  • MR/MRJ 冲销后如有剩余金额,不计入客户溢缴款。
  • 入口已限制还款金额不能大于借据未还余额,因此人工还款原则上不应产生超额。

部分/催收还款 CLP

部分还款由 FCS partialRepayment 触发,TxnType 为 CLP。当前代码注释为“催收还款”,但账务效果就是客户级部分入账。

现有处理逻辑:

  1. 校验 repayAmt > 0
  2. 检查批处理是否运行中。
  3. 查询并锁客户。
  4. 校验客户存在正常或逾期借据。
  5. 检查客户是否存在在途 CLP 订单。
  6. BeforePaymentCLP 创建还款订单。
  7. 发送还款消息到 PMT 执行扣款。
  8. 支付成功后走 AfterPaymentCLP

AfterPaymentCLP 的记账方式:

  • 加载客户级还款上下文 loadDataForCustRepay
  • 从成功金额中扣除通道费:successAmt - successAmt * passRate
  • C40 还款逻辑模块。
  • PaymentHier 按 PFS queryOffsetConfig 返回的顺序冲销。
  • 冲销顺序按账龄 aging 和余额成分执行,余额成分包括本金、利息、费用、罚息等。
  • 金额不足时,只冲销到可覆盖的计划和余额成分,未冲销部分继续保留为待还余额。
  • 如果本次部分还款刚好结清某笔借据,会触发结清后的额度/规则处理。

部分还款后的账务结果:

情况后续账务表现
只还了一部分费用/罚息本金、利息等余额继续待还
还清某一期部分成分该成分余额归零,其他成分继续待还
还清某一期全部余额该期计划可置为结清,继续冲后续期次
还清全部借据借据结清,后续触发结清相关处理
金额超过可销账余额普通还款进入溢缴款;MR/MRJ 不进溢缴款

因此,“部分还款后后续的账”不是重算新计划,而是在原还款计划和余额成分上减少已冲销金额;剩余 outstdBal 继续按原计划、逾期和日终逻辑流转。

8. 统一状态

状态说明
INIT我方已创建还款订单
PENDING已提交通道或等待用户支付
PROCESSING已收到回调或前端回跳,正在核验
SUCCESS已确认成功并完成销账
PARTIAL已部分销账
OVERPAID超额还款,待分配或退款
FAILED支付失败
EXPIREDH5 链接或动态账号过期
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 支持 CARDACCOUNT_TRANSFERUSSDPHONE_NUMBER
  • 回跳后前端展示处理中,并轮询 FCS 支付结果。
  • Monnify 成功交易经 PMT 回流 FCS 后,以 MP 完成销账。

10.4 统一入账

  • 三种还款方式均回流 FCS,不在 PMT 或 BNS 直接改账。
  • 同一通道交易重复回调不会重复销账。
  • FCS 销账顺序沿用 PFS 配置,不因通道变化改变。
  • 可通过还款订单、通道交易号、虚拟账户引用追踪完整链路。

11. 待确认事项

事项说明
Monnify Card Tokenization 权限需确认商户是否已开通 charge-card-token
超额还款策略多余金额进入余额、抵扣下期还是退款
重试策略卡代扣失败后的重试次数、间隔和时间窗

12. 参考文档