Agora

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: the queueDispatch hook was declared and never read, so the mode fell through and silently ran durable-or-in-process. Splitting billing.role across 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, and dispatcher: 'durable' is the only mode that can split a deployment. An @adonisjs/queue backend 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. #mapWebhookData returns { gatewayId, amount, currency, customerId?, externalReference? } for every event, including subscription.completed / renewed / cancelled. That satisfies the subscription guard (gatewayId + customerId), so the processor writes a row with status: undefined and planId: ''. Fixing it needs a real AbacatePay subscription payload to map from.

  • MGA has 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.chargedback is deprecated, and its replacement has no published payload. Pagar.me's own docs say migrate to chargeback.received by 2026-09-30. The driver still maps the deprecated event to payment.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 only chargeback.* 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 a status of WAITING_MERCHANT_EVIDENCES, WAITING_ACQUIRER_ANALYSIS, WAITING_ISSUER, MERCHANT_EVIDENCE_DEADLINE_EXPIRED, DEADLINE_EXPIRED, WON, LOST or REVERSED. If chargeback.received's payload turns out to be that object, the mapping is mechanical — chargeIdgatewayId, responseDeadlineactionableUntil, the terminal statuses → payment.dispute_closed. One logged payload from a live account closes this, and the deadline is close.

  • Subscription amounts on Stripe. updateSubscription takes 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. idempotencyKey now rides on charge, createCheckout, refund, createCustomer, createSubscription and updateSubscription, 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 — an externalReference lookup 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 repeated createCheckout makes 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. CheckoutInput has no method, so a Stripe checkout session is always created with the account's default methods. CreateSubscriptionInput.method exists 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.disputed is a canonical webhook event, the processor moves the payment row to disputed instead of leaving it saying paid, and the driver contract carries Dispute, DisputeEvidence, capabilities.disputes and the optional findDispute / submitDisputeEvidence. Dispute carries evidenceDueBy and canSubmitEvidence, which are the two fields an operator actually acts on. payment.dispute_warning and payment.dispute_closed are 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 as actionableUntil. Disputes are persisted (billing_disputes, keyed on the dispute's own gateway id and indexed on (status, evidence_due_by)), payments:health flags 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. submitDisputeEvidence is real on Stripe. Two later additions close the gap the deadline left: an open_disputes health 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 findDispute there 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 than Dispute. capabilities.disputes is 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_payments carries refunded_amount (integer minor units, BIGINT, on BillingPayment, withPayment() and PaymentListItem), and payment.updated — which is what a partial refund arrives as — writes it. A partially refunded charge keeps its status, its amount and its paid_at, and the net is amount - refunded_amount: a subtraction, never a division.
  • Landed: net revenue in the aggregates. store.netRevenue({ from, to }) sums amount - COALESCE(refunded_amount, 0) over the same paid rows and the same paid_at window as revenue(), and billingOverview publishes both — revenue (gross) and net_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_events records 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 answered 400 before an event exists, so it never becomes a ledger row: that audit row is the only trace it leaves, and payments:health's rejected_deliveries check is the only thing that counts them.
  • No per-payment history. billing_payments is 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 a CAST(payload AS TEXT) LIKE scan, reported as matchedBy: 'payload-substring', which is unindexed, can over-match and can miss. The honest fix is a payment_gateway_id column 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. meteredBill and meteredBillForSubscription price 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 billingOverview is 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 updateSubscription throws and tells you to cancel and recreate. It is not behind a finer-grained capability flag — capabilities.subscriptions stays true because 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.

On this page