feat(cards): report why a card authorization was declined - #960
feat(cards): report why a card authorization was declined#960whoisglover wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-typescript studio · code · diff
✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-go studio · code · diff
✅ grid-python studio · code · diff
✅ grid-php studio · code · diff
✅ grid-cli studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
|
@greptile review |
|
8ba1e7c to
d2a624d
Compare
|
@greptile review |
| Cards decline at auth time if the bound funding source can't cover the | ||
| transaction. The decline code surfaces as `INSUFFICIENT_FUNDS` and is | ||
| visible on the resulting `CardTransaction`. Fund the source the same | ||
| transaction. The reason surfaces as `cardDeclinedReason: | ||
| INSUFFICIENT_FUNDS` on the resulting `CardTransaction`. Fund the source the same |
There was a problem hiding this comment.
Insufficient funds behavior is wrong
This guide says an underfunded source produces a DECLINED transaction with cardDeclinedReason: INSUFFICIENT_FUNDS. However, the current flow approves the authorization and reports the later failed pull as EXCEPTION, which does not carry cardDeclinedReason. The sandbox table now documents that behavior for suffix 002, while its declinedInsufficientFunds example still calls the same suffix a decline. Clients following this guidance will wait for a decline reason that the documented implementation cannot emit. This violates the Mintlify directive to publish accurate, tested documentation.
Context Used: mintlify/CLAUDE.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/cards/cardholder-setup.mdx
Line: 42-44
Comment:
**Insufficient funds behavior is wrong**
This guide says an underfunded source produces a `DECLINED` transaction with `cardDeclinedReason: INSUFFICIENT_FUNDS`. However, the current flow approves the authorization and reports the later failed pull as `EXCEPTION`, which does not carry `cardDeclinedReason`. The sandbox table now documents that behavior for suffix `002`, while its `declinedInsufficientFunds` example still calls the same suffix a decline. Clients following this guidance will wait for a decline reason that the documented implementation cannot emit. This violates the Mintlify directive to publish accurate, tested documentation.
**Context Used:** mintlify/CLAUDE.md ([source](https://github.com/lightsparkdev/grid-api/blob/main/mintlify/CLAUDE.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.d2a624d to
c4870f2
Compare
|
Jira: ENG-11677 under ENG-11645
What this does
When a card authorization is declined, the card transaction reports
status: DECLINED. #957 added that status.A platform can now see that the authorization failed, but not why. A frozen card, a spend cap, and an empty funding source all look the same. Only some of those are something the platform can act on, and it cannot tell them apart.
This PR adds the reason.
How it works
CardDeclinedReasonenum:CARD_NOT_ACTIVE,SPEND_LIMIT_EXCEEDED,INSUFFICIENT_FUNDS,NO_ELIGIBLE_FUNDING_SOURCE,BLOCKED,UNSUPPORTED_NETWORK,OTHER.CardTransactiongainscardDeclinedReason. It is present only whenstatusisDECLINED.CARD_TRANSACTION.DECLINEDwebhook example carries the reason.003documents the reason it produces, which isCARD_NOT_ACTIVE.It also retires a name that was never real. The docs told platforms a frozen card declines with
CARD_PAUSED. No enum has ever had that member. The server's decision vocabulary calls itCARD_NOT_ACTIVE. Every page that saidCARD_PAUSEDnow sayscardDeclinedReason: CARD_NOT_ACTIVE.What has to land with this
The server does not expose the reason yet. It stores it on the decision row,
EntCardEventDecision.decline_reason, and the public projection drops it. The webdev change that joins the decline decision to the transaction and emits the field merges in the same window.One thing to settle before this merges
INSUFFICIENT_FUNDSis in the enum, and nothing produces it.Four of the seven reasons are real in production today.
NO_ELIGIBLE_FUNDING_SOURCE,SPEND_LIMIT_EXCEEDED,BLOCKED, andUNSUPPORTED_NETWORKall come from the shared gates that run after_gen_evaluateindecision/base.py, so they apply on production platforms even thoughProductionCardDecisionEngineitself approves everything.INSUFFICIENT_FUNDSis not one of them. Grid never checks the funding source balance when it decides an authorization. The auth is approved to the card network, and the pull fails afterwards incard_hold_action.py, whose own comment says so: "The auth was already approved to the issuer; if only the pull fails (e.g. the wallet is short) the raw exception propagates as transient so the whole delivery rolls back and Lithic re-delivers." That path resolves asEXCEPTION, and it carries no decline reason.So the docs in this PR now say that plainly, on the sandbox
002row and on the pre-funding guide. What is left to decide is the enum itself. Keeping the member means committing to move the balance check to authorization time, whichproduction.pyalready has a TODO for. Dropping it means adding it back later, which is breaking for strict clients on a response enum.CARD_NOT_ACTIVEhas a smaller version of the same question: onlySandboxDecisionEnginechecks card status, and no shared gate does, so on production platforms a frozen card is declined by the provider rather than by us.Tests
make buildrebundles cleanly.make lintreports 0 errors. The one new finding is informational and is the known$ref-with-example shape thatdirectionandapplicationFeealready report.cd cli && npm testpasses 80 tests. NoCARD_PAUSEDremains anywhere underopenapi/ormintlify/snippets/.History
This is the surviving half of #935. That PR was merged into #934's branch by mistake rather than into
main, and #934 has since been rebuilt onmainwithout it.The other half of #935 was the
DECLINEDstatus itself, which #957 shipped independently. ENG-11676 is therefore already delivered and only the reason was left to land.