dánwòdánwò

Payments

Bachs

Bachs checkout sessions, subscriptions, card recovery, refunds and payouts on its sandbox and live hosts, settling on the emulator's clock.

danwo's Bachs emulator answers Bachs's live and sandbox APIs, its hosted checkout and overlay script, the card recovery page and the customer portal. Payments settle, subscriptions renew and payouts complete on the emulator's clock.

Set up

Add bachsService. A well-formed key is accepted: sk_sandbox_… on the sandbox host and sk_live_… on the live one, as Bachs checks them, so your app keeps its own key.

danwo.config.ts
import { appEnvironment, bachsService, defineConfig } from "@danwo/sdk"

const url = "http://localhost:3000"

export default defineConfig({
  testDir: "tests",
  readiness: { mode: "page+backend", settle: true },
  environment: () =>
    appEnvironment({
      app: { command: "npm run dev", url },
      services: [
        bachsService({
          seed: {
            products: [
              {
                id: "prod_pro",
                name: "Pro",
                price: { currency: "USD", amount: "29.00" },
                billing_cycle: { interval: "month", frequency: 1 },
              },
            ],
            webhookEndpoints: [
              {
                url: `${url}/api/webhooks/bachs`,
                events: ["checkout.completed", "invoice.paid", "invoice.payment_failed"],
                secret: "whsec_danwo",
              },
            ],
          },
        }),
      ],
    }),
})

Seed it

FieldWhat it seeds
productsOne-off or recurring products, with a price per currency, a billing cycle and a trial.
customersCustomers, with your own ids if you give them.
webhookEndpointsWhere events go, which ones, and the signing secret.
balancesBalances by currency, for payouts.
destinationsPayout destinations; fails: true makes payouts to one fail.
recoveryWhat the card recovery page does when a customer updates a card.

Use it in a test

Subscribe on the hosted checkout, fail the next renewal, and check the app sent its customer to the recovery page:

tests/renewal.danwo.ts
import { type BachsFixtures, bachsLedger, test as base } from "@danwo/sdk"

const test = base.extend<BachsFixtures>()

test("asks for a new card when a renewal fails", async ({ app, screen, expect, time, bachs }) => {
  await app.open("/pricing")
  await screen.getByRole("button", { name: "Choose Pro" }).click()
  await screen.getByTestId("bachs-pay").click()
  await expect(screen.getByText("Plan: Pro")).toBeVisible()

  const ledger = bachsLedger(bachs)
  const [subscription] = ledger.subscriptions()

  ledger.failCard(subscription?.id ?? "", "insufficient_funds")
  await time.travel({ days: 31 })

  await app.open("/billing")
  await expect(screen.getByText("Update your card")).toBeVisible()
  expect(ledger.recoveryEmails()).toHaveLength(1)
})
bachsLedger(bachs)What it's for
payments(), succeeded()Every payment, or only those that went through.
checkouts(), subscriptions()Checkout sessions and subscriptions.
refunds(), payouts()Money going back to customers and out to you.
balance(currency)The account's balance.
failCard(id), updateCard(id)Make a subscription's card decline, or fix it.
deposit(…)Money arriving in a virtual account.
Test idOn the hosted pages
bachs-payPays.
bachs-cancelGoes back to your app.
bachs-totalThe total.
bachs-update-cardThe recovery page's button.

Test cards

The checkout's card field starts with 4242 4242 4242 4242, which succeeds. These fail:

Card numberFails with
4000 0000 0000 0002Declined
4000 0000 0000 9995Insufficient funds
4000 0000 0000 0069Expired card
4000 0000 0000 0119A provider error

What's covered

AreaCovered
CatalogProducts: create, update, list, archive.
CheckoutSessions, the hosted page and the overlay script.
CustomersCustomers and their portal sessions.
SubscriptionsRenewals, trials, failed cards and the recovery page.
Payments and refundsPayments, and full or partial refunds.
PayoutsDestinations, quotes and payouts, with account resolution.
Money inVirtual accounts and deposits.
RequestsIdempotency keys, request ids and rate-limit headers.

Faults

CallFaults the matrix tries
A write, such as POST /v1/checkout-sessions429, 500, 503, a 10-second delay, connection reset, answer lost after Bachs acted
A read, such as GET /v1/payments/:id429, 500, connection reset
Every webhook deliveryarrives twice, arrives 10 minutes late, forged signature, 1 hour stale

Webhooks

Each delivery is signed with the endpoint's secret. The X-Bachs-Signature-V2 header holds the timestamp and the signature, in the t=…,v1=… form.

EventsSent when
checkout.*A checkout opens, completes, closes or expires.
collection.*Money is collected, or collecting it fails.
customer.*A customer is created or updated.
invoice.*A renewal invoice is created, paid, or fails.
payout.*A payout is created, paid or fails.
refund.*A refund is created, paid or fails.

Fidelity

Live. danwo fidelity bachs runs 2 scenarios against the Bachs sandbox. Both match.

Not covered yet

  • Coupons and discounts
  • Usage-based prices
  • Disputes

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI