dánwòdánwò

Examples

Python booking app

Fern Studio, a class-booking app in two parts: a SvelteKit site on a FastAPI JSON API. Browse the timetable, pay with Stripe Checkout, get a confirmation through Resend, see your bookings. Postgres through psycopg.

Fern Studio, a class-booking app in two parts: a SvelteKit site on a FastAPI JSON API. Browse the timetable, pay with Stripe Checkout, get a confirmation through Resend, see your bookings. Postgres through psycopg. See it on GitHub.

What it shows

  • Two apps in two languages behind one proxy: stackEnvironment starts the API, then the site, and wires each to the other's address
  • An app outside Node behind danwo's proxy: the official Stripe and Resend Python SDKs reach the emulators through the proxy and CA variables danwo sets, with no test hooks
  • Every hop on the record: the browser, the SvelteKit server calling the API, the browser calling the API, and the API calling Stripe and Resend
  • Invariants that read Postgres against Stripe and Resend: one booking per paid session, one confirmation per booking, amounts equal to what Stripe charged
  • danwo matrix faulting Stripe, Resend, the webhooks and Postgres itself (54 trials), and the app surviving every one
  • A STUDIO_BUG switch that turns one safeguard off, so you can watch the matrix catch it

Stack

PythonFastAPISvelteKitPostgresuv

Emulates: Stripe, Resend. Runs on: A FastAPI API started with uv and a SvelteKit site on Node, in Chromium, with Postgres in Docker.

Run it

Copy examples/with-python anywhere, then from its folder:

npm install
npm test

Its scripts:

npm run build          # cd web && vite build
npm run typecheck      # cd web && svelte-kit sync && svelte-check --fail-on-warnings && cd .. && tsc --noEmit
npm test               # danwo test
npm run test:agentic   # danwo test tests/agentic.danwo.ts
npm run matrix         # danwo matrix tests/deterministic.danwo.ts --grep "books a class"
npm run matrix:plan    # danwo matrix tests/deterministic.danwo.ts --grep "books a class" --plan
npm run serve          # danwo serve

Its README says what else it needs and what the last full matrix found.

Until the danwo version it pins is on npm, install it from a local build instead: from the repository's root, bun run release <version> --pack, then node examples/link.ts with-python, which installs a copy and prints where it is.

Apps in other languages explains what this example leans on.

The test

tests/deterministic.danwo.ts
import { resendInbox, test as base } from "@danwo/sdk"

import {
  CUSTOMER,
  DECLINED_CARD,
  expectBooksBalance,
  expectOneSessionPerBooking,
  pay,
  type StudioFixtures,
} from "./studio.ts"

const test = base.extend<StudioFixtures>()

test("browses the timetable", async ({ app, screen, expect }) => {
  await app.open("/")
  await expect(screen.getByRole("heading", { name: "This week's classes" })).toBeVisible()
  await expect(screen.getByRole("link", { name: /Spin sprint/ })).toContainText("Full")

  await screen.getByRole("link", { name: /Pottery wheel/ }).click()

  await expect(screen.getByRole("heading", { name: "Pottery wheel" })).toBeVisible()
  await expect(screen.getByTestId("class-price")).toHaveText("$45.00")
  await expect(screen.getByRole("button", { name: "Book and pay $45.00" })).toBeVisible()
}).invariant(async (fixtures) => {
  // Looking is not booking: nothing is paid, so nothing is owed.
  await expectBooksBalance(fixtures)
  fixtures.expect(await fixtures.sql("select id from bookings")).toHaveLength(0)
})

test("books a class and pays through Stripe Checkout", async ({ app, screen, expect }) => {
  await pay({ app, screen, expect }, { klass: "sunrise-flow", email: "[email protected]" })

  await expect(screen.getByRole("heading", { name: "You're booked" })).toBeVisible()
  await expect(screen.getByTestId("booking-total")).toHaveText("$18.00")

  await app.open("/")
  await expect(screen.getByRole("link", { name: /Sunrise flow/ })).toContainText("11 places left")
}).invariant(async (fixtures) => {
  await expectBooksBalance(fixtures)
  await expectOneSessionPerBooking(fixtures)
})

test("a declined card shows Stripe's error and books nothing", async ({ app, screen, expect }) => {
  await pay(
    { app, screen, expect },
    { klass: "breathwork", email: "[email protected]", card: DECLINED_CARD },
  )

  await expect(screen.getByRole("alert")).toHaveText("Your card was declined.")

  await app.open("/bookings?email=kit%40fernstudio.test")
  await expect(screen.getByText("No bookings yet")).toBeVisible()
}).invariant(async (fixtures) => {
  await expectBooksBalance(fixtures)
  fixtures.expect(await fixtures.sql("select id from bookings")).toHaveLength(0)
  fixtures.expect(resendInbox(fixtures.resend).list("[email protected]")).toHaveLength(0)
})

test("lists my bookings", { session: "returning-customer" }, async ({ app, screen, expect }) => {
  await app.open("/")
  await screen.getByRole("link", { name: "My bookings" }).click()

  const row = screen.getByRole("row", { name: /Sunrise flow/ })

  await expect(row).toContainText("$18.00")
  await expect(row).toContainText("Sent")

  // Refresh asks the API from the browser, not through the site's server.
  await screen.getByRole("button", { name: "Refresh" }).click()
  await expect(screen.getByRole("status")).toHaveText("Up to date")
  await expect(row).toContainText("Sunrise flow")
}).invariant(async (fixtures) => {
  await expectBooksBalance(fixtures)
  fixtures.expect(await fixtures.sql("select email from bookings")).toEqual([{ email: CUSTOMER }])
})

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