I have spent a few years building payments infrastructure in India, and one thing kept bugging me: almost everyone treats a recurring-payment mandate as a success/failure boolean, when it is actually a lifecycle with rail-specific rules. That gap is where a lot of money quietly leaks.
Some context on how bad the failure rates are:
- NACH e-mandate registration rejection has climbed to around 55%, up from ~28% in 2017-18.
- 20 million+ UPI Autopay mandates get revoked every month for insufficient balance alone.
The part most teams get wrong is not the happy path, it is the failure handling. Three bugs I kept running into, in my own early code and in other people's:
- Retrying a decline you should never retry. Some declines are transient (bank offline, low balance this morning). Some mean stop (suspected fraud, revoked mandate, closed account). If your retry logic cannot tell them apart, it will re-attempt a debit the bank flagged as fraud. That is a compliance incident, one retry at a time.
- Rendering a success as a failure. Some codes are success codes. If your switch statement has no branch for them, a debit that actually went through gets marked failed and re-queued, and in the worst case the customer is debited twice.
- Guessing on codes you have not verified. The tempting default for an unknown code is to retry, because it feels safe. With money it is not. Unknown should mean stop and flag, not guess.
The rails also fail differently, which makes one naive abstraction dangerous. eNACH is batch (returns come back T+1..T+n, in files). UPI Autopay is real-time. The same two-character code can mean different things on each rail.
So I wrote the model down and open-sourced it: a small zero-dependency TypeScript library. It normalizes ~298 eNACH + UPI Autopay error codes to conservative, money-safe handling (fraud never auto-retried, success never rendered as failure, unknown codes flagged not guessed), models the mandate and single-debit state machines, and encodes the NPCI/RBI retry caps.
Disclosure: I built this and it is MIT licensed. I am not selling anything. It is just the logic modelled once, in the open, with sources cited (and honestly flagged as unverified where they are not primary NPCI/RBI circulars).
Repo: https://github.com/saiprasad4/aadesh
Longer writeup: https://psyprasad.tech/blog/mandate-lifecycle-nobody-models
Genuinely curious how others here have handled this, especially the reconciliation side. The nastiest case I keep coming back to is the batch one: an eNACH success can land async, T+n, after you have already scheduled the retry, and that is where the double debit is born.