Roadmap
What is still missing — a queue dispatcher, subscription amounts on Stripe, a per-payment history, splits beyond Asaas, dispute submission beyond Stripe, and fiscal invoice reconciliation.
This page lists what the library does not do yet, and why each gap matters. Anything that already works lives in the guides — start at Providers for the per-gateway matrix.
Gateways
-
billing.dispatcher: 'queue'does not exist. It was in the config type, in the docs and in the dispatcher's own comments, and it was implemented nowhere: thequeueDispatchhook was declared and never read, so the mode fell through and silently ran durable-or-in-process. Splittingbilling.roleacross api/worker was even permitted on it, which produced an api process doing all the work and a worker sitting idle. It is refused at boot now, anddispatcher: 'durable'is the only mode that can split a deployment. An@adonisjs/queuebackend is still worth having — it is a smaller dependency than durable for an app that only needs a channel — but it has to be written. -
AbacatePay maps subscription webhooks with a payment-shaped payload.
#mapWebhookDatareturns{ gatewayId, amount, currency, customerId?, externalReference? }for every event, includingsubscription.completed/renewed/cancelled. That satisfies the subscription guard (gatewayId + customerId), so the processor writes a row withstatus: undefinedandplanId: ''. Fixing it needs a real AbacatePay subscription payload to map from. -
MGAhas no right answer. ISO 4217 gives the Malagasy ariary two decimal places (it is subdivided into five iraimbilanja, not a hundred), while Stripe treats it as zero-decimal. The exponent table follows ISO, so a Stripe charge in MGA will disagree with it. Nobody has hit this; it is written down so that whoever does finds the reason rather than a bug. -
Pagar.me's
charge.chargedbackis deprecated, and its replacement has no published payload. Pagar.me's own docs say migrate tochargeback.receivedby 2026-09-30. The driver still maps the deprecated event topayment.disputed, because the replacement's entire public specification is one sentence — "evento enviado quando um chargeback é criado" — with no payload, no example and no field list anywhere in the reference. It is also the onlychargeback.*event: Pagar.me publishes no won/lost counterpart, so there is no close event to map either.What has changed since this was written is that Pagar.me's Disputes API is now documented, and its dispute object is exactly what the canonical events need:
disputeId,chargeId,responseDeadline,chargebackAmount, and astatusofWAITING_MERCHANT_EVIDENCES,WAITING_ACQUIRER_ANALYSIS,WAITING_ISSUER,MERCHANT_EVIDENCE_DEADLINE_EXPIRED,DEADLINE_EXPIRED,WON,LOSTorREVERSED. Ifchargeback.received's payload turns out to be that object, the mapping is mechanical —chargeId→gatewayId,responseDeadline→actionableUntil, the terminal statuses →payment.dispute_closed. One logged payload from a live account closes this, and the deadline is close. -
Subscription amounts on Stripe.
updateSubscriptiontakes an amount, a description and metadata. Asaas and AbacatePay apply the amount and the description; Stripe applies the description and metadata and drops the amount, because changing a price there needs the subscription item id and the shared input has no room for it. What is missing is a plan/price field on the input with proration handling. Woovi no longer fabricates a result here — it throws, because OpenPix subscriptions are immutable. -
Idempotency where the gateway has none.
idempotencyKeynow rides oncharge,createCheckout,refund,createCustomer,createSubscriptionandupdateSubscription, and every driver either wires it to the gateway's own deduplication or throws rather than accepting a key it cannot honour. Asaas'charge()is the one place the library does the deduplication itself — anexternalReferencelookup before the POST, which is exactly what the refusal message tells callers to do by hand — and it is a retry guard, not a lock: two concurrent calls can still both create. InfinitePay is the clearest remaining case: it documents no idempotency at all, so the key is used only for routing and a repeatedcreateCheckoutmakes a second link. Nothing in the library can fix that — it is a property of the gateway — but which operations are genuinely deduplicated is on each provider page rather than in one table, which it should be. See Idempotency. -
A method on checkout and on subscriptions.
CheckoutInputhas nomethod, so a Stripe checkout session is always created with the account's default methods.CreateSubscriptionInput.methodexists and Stripe ignores it — a Stripe subscription bills whatever the Price and the customer's default payment method say. -
Partial refunds on AbacatePay.
refund()accepts an amount and ignores it — every call is a full refund at the gateway. -
Gateways beyond the eighteen. Eighteen drivers ship. The driver contract is the extension point for the rest, and a driver you write is a first-class citizen of the router and the billing layer — see Custom providers.
Disputes and chargebacks
A chargeback is the one webhook that takes revenue away, and it has a clock: every network gives you a fixed window to respond, and missing it loses the money by default rather than on the merits. What exists today and what does not:
- Landed.
payment.disputedis a canonical webhook event, the processor moves the payment row todisputedinstead of leaving it sayingpaid, and the driver contract carriesDispute,DisputeEvidence,capabilities.disputesand the optionalfindDispute/submitDisputeEvidence.DisputecarriesevidenceDueByandcanSubmitEvidence, which are the two fields an operator actually acts on.payment.dispute_warningandpayment.dispute_closedare mapped on Stripe and Adyen, the two gateways whose dispute vocabulary is documented in enough detail to map without guessing. Both now separate the pre-dispute alert (where no money has moved and a refund still prevents the chargeback) from the chargeback itself, and both carry the response deadline through asactionableUntil. Disputes are persisted (billing_disputes, keyed on the dispute's own gateway id and indexed on(status, evidence_due_by)),payments:healthflags a window closing within 72 hours and names which ones, and the dashboard's Disputes screen leads with the work list. All eighteen drivers have been through the vocabulary pass; the ones with no pre-dispute event say so on their page rather than implying otherwise.submitDisputeEvidenceis real on Stripe. Two later additions close the gap the deadline left: anopen_disputeshealth check that needs no deadline (most gateways publish none, so the deadline check answers zero forever on them) and no threshold, and a console action that records how a dispute ended — status, outcome, note and who said so — for the several gateways that never send a lost-dispute event at all. It sends nothing to a gateway. - Not landed. Adyen's Defend Disputes v30 has no endpoint that reads a dispute back, so
findDisputethere would have to invent the object it returns — the driver refuses instead, and closing that needs either an Adyen API that can read a dispute or a narrower contract thanDispute.capabilities.disputesis false on the other seventeen gateways: PayPal, Square, Razorpay and Mollie all have real dispute APIs and none is wired yet. Nothing decides whether to fight or refund, and nothing will — see below. - Not planned. Deciding for you. The library will carry evidence to the gateway and tell you the window is closing; whether a given dispute is worth fighting or cheaper to refund is a business rule that depends on margin, customer value and fraud history, and a library that guesses it is a library that refunds a customer you wanted to keep. That decision stays in your code.
What the billing tables do not record
- Landed: partial refunds are recorded.
billing_paymentscarriesrefunded_amount(integer minor units,BIGINT, onBillingPayment,withPayment()andPaymentListItem), andpayment.updated— which is what a partial refund arrives as — writes it. A partially refunded charge keeps itsstatus, itsamountand itspaid_at, and the net isamount - refunded_amount: a subtraction, never a division. - Landed: net revenue in the aggregates.
store.netRevenue({ from, to })sumsamount - COALESCE(refunded_amount, 0)over the samepaidrows and the samepaid_atwindow asrevenue(), andbillingOverviewpublishes both —revenue(gross) andnet_revenue— with the console showing them side by side.revenue()was NOT redefined: it was the only revenue figure for two releases and apps read it, so changing what it means would have moved numbers on other people's screens with nothing to announce it. The question of which figure a given business calls "revenue" is now answered by choosing a key, not by patching the library. - Landed: an audit trail.
billing_audit_eventsrecords what no other table did — who refunded from the console, who closed a dispute a gateway will never close, and every delivery the endpoint refused. A rejected delivery is answered400before an event exists, so it never becomes a ledger row: that audit row is the only trace it leaves, andpayments:health'srejected_deliveriescheck is the only thing that counts them. - No per-payment history.
billing_paymentsis a single mutable row upserted in place, so what it used to be is recorded nowhere. The console's per-payment view assembles what is knowable and says so — its ledger strand is aCAST(payload AS TEXT) LIKEscan, reported asmatchedBy: 'payload-substring', which is unindexed, can over-match and can miss. The honest fix is apayment_gateway_idcolumn the processor fills on the way in; no history table has been invented in the meantime. - Splits are not attributed. A split charge stores the full amount on one row; who received what is only in the gateway payload. Per-recipient reporting is on the app today — the manual pattern is in Marketplace splits.
- Fiscal invoices are not persisted. An emitted NFS-e is returned inline and never written anywhere, which is the root of the next section.
Splits beyond Asaas
split sits on the shared charge input, but only the Asaas driver reads it. Stripe, AbacatePay and
Woovi ignore it silently — no error, no split. Woovi models recipients as subaccounts and the driver
can create, find and list them, but a charge cannot be pointed at one, so the subaccounts are
inert as far as the charge path is concerned. The point of a shared input is that a marketplace can
change gateways as a config change; today it cannot.
Fiscal invoices
Invoice providers expose emit and find. Nothing in the library ever calls find, and there is
no cancel. NFS-e issuance is asynchronous at every provider — emit returns a pending note that
becomes issued (or rejected) minutes later — so an app has to poll the provider itself to learn the
outcome, with no place to store it. Three pieces are missing: persistence for emitted notes,
reconciliation of their status, and cancellation, which is a legal requirement rather than a nicety.
See Invoices.
Billing layer
- Auto-invoice the metered amount.
meteredBillandmeteredBillForSubscriptionprice a period's usage, and that is where it stops — nothing charges the overage or emits an invoice for it. The cron, the charge and the guard against billing a period twice are yours to write. See Metered billing. - Dashboard export. The console now has seven screens — overview, payments (with a per-payment
detail view and lookup by your own reference), customers, subscriptions, disputes, webhook events
and activity — plus refund, retry and dispute-resolution actions. What is still missing is an
export: every list is paged JSON and nothing produces a CSV. See
Dashboard; the headless
billingOverviewis still there for a UI of your own.
Coverage
Every gateway has a driver test suite that exercises the request bodies against a stubbed gateway — Stripe's fakes the SDK client and asserts what reaches each call, including the request options the idempotency key rides in. None of them touch a real gateway. The four original drivers were written and reviewed against sandbox behaviour; the fourteen added since are written against each gateway's published API reference and covered by unit tests only, which their pages say outright. Anything that depends on account configuration — whether Pix and boleto are enabled on a Stripe account, whether a Mercado Pago application has the right scopes — is unverified here. The harness the suites use is documented in Testing.
Not planned
- Refunds on Woovi. OpenPix has no refund API. The driver throws a clear error and declares the missing capability so the router rejects the call before it reaches the network — that is the intended behavior, not a placeholder.
- Subscription updates on Woovi. Same shape: OpenPix subscriptions are immutable, so
updateSubscriptionthrows and tells you to cancel and recreate. It is not behind a finer-grained capability flag —capabilities.subscriptionsstaystruebecause Woovi does create and read subscriptions, and splitting the flag per operation would have to reach the webhook event filtering and every driver that declares it. - Gateway-specific features behind the shared contract. Things only one gateway has (Woovi subaccounts, for instance) stay on the concrete driver class rather than being widened into the interface, so the shared contract keeps meaning the same thing everywhere.