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.
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
| Field | What it seeds |
|---|---|
products | One-off or recurring products, with a price per currency, a billing cycle and a trial. |
customers | Customers, with your own ids if you give them. |
webhookEndpoints | Where events go, which ones, and the signing secret. |
balances | Balances by currency, for payouts. |
destinations | Payout destinations; fails: true makes payouts to one fail. |
recovery | What 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:
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 id | On the hosted pages |
|---|---|
bachs-pay | Pays. |
bachs-cancel | Goes back to your app. |
bachs-total | The total. |
bachs-update-card | The recovery page's button. |
Test cards
The checkout's card field starts with 4242 4242 4242 4242, which succeeds. These fail:
| Card number | Fails with |
|---|---|
4000 0000 0000 0002 | Declined |
4000 0000 0000 9995 | Insufficient funds |
4000 0000 0000 0069 | Expired card |
4000 0000 0000 0119 | A provider error |
What's covered
| Area | Covered |
|---|---|
| Catalog | Products: create, update, list, archive. |
| Checkout | Sessions, the hosted page and the overlay script. |
| Customers | Customers and their portal sessions. |
| Subscriptions | Renewals, trials, failed cards and the recovery page. |
| Payments and refunds | Payments, and full or partial refunds. |
| Payouts | Destinations, quotes and payouts, with account resolution. |
| Money in | Virtual accounts and deposits. |
| Requests | Idempotency keys, request ids and rate-limit headers. |
Faults
| Call | Faults the matrix tries |
|---|---|
A write, such as POST /v1/checkout-sessions | 429, 500, 503, a 10-second delay, connection reset, answer lost after Bachs acted |
A read, such as GET /v1/payments/:id | 429, 500, connection reset |
| Every webhook delivery | arrives 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.
| Events | Sent 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