Dog walker booking site with Stripe payments and SMS
Next.js booking app where clients pick a walk slot, pay via Stripe Checkout, and receive a Twilio SMS reminder 24 hours before.
nextjstypescripttailwindprismapostgresstripe
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add dog-walker-booking-site-with-stripe-payments-and-sms-d0163fCLAUDE.md# Dog walker booking site
## What this is
A booking website for one dog walker. Clients choose a time slot, pay online with Stripe, and receive an SMS reminder via Twilio 24 hours before the walk.
## Stack
- Next.js App Router, TypeScript strict
- Tailwind CSS
- Prisma ORM with PostgreSQL
- Stripe Checkout with webhooks
- Twilio SMS
- Vitest for unit tests
- Playwright for end-to-end tests
## Commands
- `npm run dev`: start the dev server on http://localhost:3000
- `npm run verify`: lint, typecheck, unit tests, build. Must pass before any change is called done.
- `npm run test:e2e`: Playwright end-to-end tests
- `npx prisma migrate dev`: apply schema changes locally
## Verification
- After every change, run `npm run verify`.
- If it fails, fix it before reporting back.
- Use the `verify` skill for the full checklist.
## Rules
### Money
- Never store card data. Stripe handles all card entry.
- Store only Stripe IDs and amounts in cents as integers.
- Show dollars to the user, formatted from cents.
### Secrets
- Never write real keys into any file.
- Use placeholders such as `YOUR_STRIPE_SECRET_KEY` in `.env.example`.
- Real values live only in `.env.local`, which is git-ignored.
### Bookings
- A slot is held only after the Stripe webhook confirms payment.
- Never trust the client redirect as proof of payment.
### Time
- Store all times in UTC.
- Display times in the walker's timezone, read from a config value.
- The default `America/New_York` is a placeholder. The user changes it.
### Phone numbers
- Store phone numbers in E.164 format.
- Send SMS only to clients who opted in.
## Testing
- For long test output, use the `test-runner` subagent so only failures come back to the main chat.
## Ask before
- Running Prisma migrations that drop or rename columns
- Changing Stripe webhook logic
- Deleting files
- Pushing to git
## Review
- `REVIEW.md` holds the review checklist. It is imported below so `/code-review` applies the same rules.
@REVIEW.md
## Gotchas
- Stripe webhooks need the Stripe CLI locally. Run this in a second terminal:
`stripe listen --forward-to localhost:3000/api/stripe/webhook`
- Twilio trial accounts can only text verified numbers. Verify each test number in the Twilio console first.
- Prisma migrations that rename or drop columns need approval (see Ask before).
.claude/agents/test-runner.md---
name: test-runner
description: Use to run unit or end-to-end tests and report only the failures, so long test output stays out of the main chat.
model: haiku
tools: Bash, Read
maxTurns: 8
---
You run test commands for this project and report results briefly.
## Steps
1. Run the test command you were given, for example `npx vitest run` or `npx playwright test`. If no command is given, use `npx vitest run`.
2. Read the output.
3. Reply in one of two formats.
## Reply format
If all tests pass, reply with one line:
`PASS: <number> tests passed in <command>`
If any test fails, reply with one block per failing test:
- Test name
- File path
- The first 15 lines of the error message
Do not add commentary, summaries of passing tests, or suggested fixes.
## Rules
- Never edit files.
- Never run database migrations, deploy commands, or git commands that change history.
- Never print environment variables or the contents of .env files.
- If the command cannot start (missing dependency, wrong directory), reply with the first 15 lines of the error and stop.
.claude/settings.json{
"model": "sonnet",
"permissions": {
"defaultMode": "default",
"allow": [
"Bash(npm run *)",
"Bash(npx vitest *)",
"Bash(npx playwright *)",
"Bash(npx prisma generate)",
"Bash(npx tsc *)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)"
],
"ask": [
"Bash(npx prisma migrate *)",
"Bash(git push *)",
"Bash(git commit *)",
"Bash(stripe *)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git reset --hard *)",
"Read(./.env)",
"Read(./.env.*)"
],
"blockWritesOutsideWorkingDirectories": true
}
}
.claude/skills/verify/SKILL.md---
name: verify
description: Runs after any code change to confirm the app still works. Use it before saying a task is done.
disable-model-invocation: false
---
# Verify
Use this skill after any code change. Do not report a task as done until it passes.
## Steps in order
Run each step from the project root. Run them in this order.
1. `npm run lint`
2. `npx tsc --noEmit`
3. `npx vitest run`, run through the test-runner subagent. Report only failing tests, with file and message.
4. `npm run build`
5. If the change touches pages or the booking flow, run `npx playwright test` for the affected spec only.
If the change does not touch pages or the booking flow, skip step 5 and say so in the report.
## Pass rule
All five steps must succeed. If a step fails:
1. Find and fix the cause.
2. Rerun from the failed step.
3. Rerun the whole list only if code changed in an earlier step.
Do not skip a failing step. Do not edit tests to make them pass unless the test itself is wrong, and say so if you do.
## Smoke check for the walker page
Run this after step 4 passes.
1. Start the dev server in the background: `npm run dev`.
2. Wait until the server responds on port 3000.
3. Fetch `http://localhost:3000` and expect HTTP status 200.
4. Stop the dev server.
If the fetch returns anything other than 200, the result is BLOCKED. Include the status code in the reason.
## Report format
Show a short table with one row per step:
| Step | Result | Time |
|------|--------|------|
| lint | pass or fail | seconds |
| typecheck | pass or fail | seconds |
| unit tests | pass or fail | seconds |
| build | pass or fail | seconds |
| e2e (affected spec) | pass, fail, or skipped | seconds |
| smoke check | 200 or other status | seconds |
Then add one line:
- `READY` if every step passed and the smoke check returned 200.
- `BLOCKED: <reason>` if any step failed. Name the failing step and the first error line.
Do not add anything after that line.
INSTALL.md# Setup guide
This guide walks you through setting up the dog walker booking site. You do not need to know how to code to follow it. Work from top to bottom, and do not skip steps.
## Before you start
You need these accounts and tools. Each one has a one-sentence reason.
- **Node.js 20 or newer**: runs the website on your computer and runs the test commands.
- **Git**: saves a history of your changes so you can undo a phase if something goes wrong.
- **GitHub account**: stores the project online so it can be deployed later.
- **Stripe account in test mode**: takes card payments. Test mode uses fake cards, so no real money moves.
- **Stripe CLI**: forwards payment events from Stripe to your computer so you can test payments locally.
- **Twilio account with a phone number**: sends the reminder text messages.
- **PostgreSQL database**: stores bookings, clients and availability. Use a free Neon database, or Docker if you prefer to run it locally.
- **Claude Code**: the AI assistant that writes most of the code, following the plan in this folder.
## About the stack
The stack (Next.js, TypeScript, Tailwind, Prisma, PostgreSQL, Stripe, Twilio) was chosen by default because you did not name one. It is a common, well-documented combination for a booking site with payments.
If you want something different, ask Claude before Phase 1 starts. For example, say "Use Remix instead of Next.js" or "Use SQLite instead of PostgreSQL." Claude will update CLAUDE.md, SPEC.md and PLAN.md before any code is written. Do this first. Changing the stack after Phase 2 means rewriting work that is already done.
## Step 1: Install the tools
1. Install Node.js 20 or newer from nodejs.org. Check it with `node -v`.
2. Install Git. Check it with `git --version`.
3. Install the Stripe CLI from stripe.com/docs/stripe-cli. Check it with `stripe --version`.
4. Install Claude Code with the native installer from the official Claude Code docs. Do not use `npm install -g`. Check it with `claude --version`.
## Step 2: Create the accounts
1. **GitHub**: create a new repository for the project. Clone it to your computer.
2. **Stripe**: create an account. Switch to test mode. Copy your test secret key to a private note. You will paste it into `.env.local` later.
3. **Twilio**: create an account. Buy one phone number. Note your Account SID, Auth Token and the phone number. A trial account can only text phone numbers you have verified in the Twilio console. Verify your own number now.
4. **Neon**: create a free PostgreSQL database. Copy the connection string.
## Step 3: Add the project files
1. Copy these files from the plan into the project folder: `CLAUDE.md`, `SPEC.md`, `PLAN.md`, `REVIEW.md`, `INSTALL.md`.
2. Copy `.claude/settings.json`, `.claude/skills/verify/SKILL.md` and `.claude/agents/test-runner.md` into the matching folders.
3. Create a file named `.env.example` with placeholder names only:
```
DATABASE_URL=YOUR_DATABASE_URL
STRIPE_SECRET_KEY=YOUR_STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET=YOUR_STRIPE_WEBHOOK_SECRET
TWILIO_ACCOUNT_SID=YOUR_TWILIO_SID
TWILIO_AUTH_TOKEN=YOUR_TWILIO_TOKEN
TWILIO_FROM_NUMBER=YOUR_TWILIO_NUMBER
CRON_SECRET=YOUR_CRON_SECRET
```
## Step 4: Set up your keys
Keys are passwords. Keep them private.
1. Copy `.env.example` to a new file named `.env.local`.
2. Replace each `YOUR_...` value with the real value from your accounts.
3. Never paste keys into a chat with Claude, into a file that Git tracks, or into a screenshot.
`.env.local` is ignored by Git. Check that it is not listed when you run `git status`.
## Step 5: Start Claude Code
1. Open a terminal in the project folder.
2. Run `claude`.
3. Accept the permission prompts the first time. Claude asks before it edits files or runs commands.
4. Run `/init` only if `CLAUDE.md` is missing. This project already includes one.
### Permission levels
Claude Code has three permission modes you can switch between:
- **default**: Claude asks before each edit and each command. This is the safest choice for a beginner.
- **auto**: Claude decides which actions are safe and runs them without asking. Terminal sessions start in auto mode unless `.claude/settings.json` sets `defaultMode`. This project sets `default`, so you will be asked first.
- **plan**: Claude reads and plans but does not change files. Use it to review what Claude intends to do.
## Step 6: Install the plugins
Run these commands inside Claude Code, one at a time:
```
/plugin install code-review@claude-plugins-official
/plugin install feature-dev@claude-plugins-official
/plugin install commit-commands@claude-plugins-official
/plugin install security-guidance@claude-plugins-official
/plugin marketplace add anthropics/skills
/plugin install frontend-design@anthropic-agent-skills
/plugin install webapp-testing@anthropic-agent-skills
```
What they do:
- **code-review**: checks the payment and booking code against `REVIEW.md`.
- **feature-dev**: helps plan each phase before coding.
- **commit-commands**: makes clean commits, so you can undo one phase.
- **security-guidance**: flags unverified webhooks and leaked secrets.
- **frontend-design**: helps the booking pages look good on phones.
- **webapp-testing**: gives patterns for browser tests of the booking flow.
## Step 7: Start the first phase
1. Paste the kickoff prompt from the plan as your first message. It starts Phase 1 only.
2. Read the result. Check that `npm run verify` exits with code 0 and that `.env.local` is not tracked.
3. Only then move on to Phase 2. Run `/clear` before each new phase.
4. Commit after each phase with `/commit`.
## Local payment testing
Payments are confirmed by a Stripe webhook. Your computer cannot receive that webhook unless you forward it. You need two terminals.
1. In the first terminal, start the app with `npm run dev`.
2. In the second terminal, run:
```
stripe listen --forward-to localhost:3000/api/stripe/webhook
```
3. The Stripe CLI prints a webhook signing secret starting with `whsec_`. Put it in `.env.local` as `STRIPE_WEBHOOK_SECRET`. Restart the app after you change it.
4. Make a test booking and pay with Stripe's test card number `+00 000 000 000`, any future expiry date, and any CVC.
Keep the second terminal running while you test payments.
## Deploy checklist
Work through this list in order. Mark each item when it is done.
- [ ] Set all the variables from `.env.example` in your hosting provider, with real values.
- [ ] Run `npx prisma migrate deploy` against the production database.
- [ ] Register the Stripe webhook URL in the Stripe dashboard, pointing to `/api/stripe/webhook` on your live site.
- [ ] Set a daily schedule that calls `/api/cron/reminders` and sends the `CRON_SECRET` header.
- [ ] Send one real test SMS to your own verified phone number.
- [ ] Make one full test booking end to end, using a Stripe test card.
## Troubleshooting
**`npm run verify` fails.** Copy the failing step name and the first error lines into Claude. Ask it to fix the cause and rerun only the failed step. Do not move to the next phase until verify passes.
**A test payment does not mark the booking paid.** Check three things in order:
1. The Stripe CLI listener is running in the second terminal.
2. `STRIPE_WEBHOOK_SECRET` in `.env.local` matches the `whsec_` value the CLI printed. Restart the app after changing it.
3. The webhook appears in the Stripe CLI output with a 200 response. A 400 response means the signature check failed.
**The SMS does not arrive.** Check these:
1. Your Twilio account is a trial. Trial accounts text only verified numbers. Verify the client's number in the Twilio console.
2. The `ReminderLog` table has a row for the booking. If it does, the reminder was sent. If not, the cron route did not find the booking.
3. The booking is at least 24 hours away and the client opted in.
PLAN.md# Build plan
Six phases. Run them in order. Each phase ends with a `/goal` prompt that Claude can check against a measurable end state.
## How to run a phase
1. Type `/clear`.
2. Paste the phase's `/goal` prompt.
3. Wait for the check output and read it.
4. Commit with `/commit`.
Use `/compact` only between sub-tasks inside a phase, never as a substitute for `/clear` at the start.
## Model and effort
- Default model is Sonnet (set in `.claude/settings.json`).
- Use `/effort high` only in Phase 4 (Stripe webhook) and Phase 5 (reminder dedupe).
- To switch models for a hard problem, use `/model` at the start of a phase. Switching models breaks the prompt cache, so avoid it mid-task.
## Phase 1: Scaffold
**Goal:** A Next.js project with the full toolchain and a passing `npm run verify`.
**Files likely touched:** `package.json`, `tsconfig.json`, `tailwind.config.ts`, ESLint config, `vitest.config.ts`, `playwright.config.ts`, `app/layout.tsx`, `app/page.tsx`, `.env.example`, `.gitignore`.
**Check:** `npm run verify` exits 0.
**Constraints:**
- Use only the stack in CLAUDE.md.
- Write no real keys or tokens.
- Do not run database migrations.
- Ask before deleting any file.
**Turn bound:** 25 turns.
**`/goal` prompt:**
```
/goal Phase 1 of PLAN.md is complete. End state: a Next.js project with TypeScript strict, Tailwind CSS, ESLint, Vitest and Playwright is set up in this folder. A placeholder home page at / shows the text "Dog Walker Booking". A .env.example file exists with placeholder names only (YOUR_ values, no real keys). The package.json has a "verify" script that runs lint, typecheck, unit tests and build. Check: run `npm run verify`, then show the exit code and the last 20 lines of output. Run `git status` and confirm .env.local is not tracked. Constraints: use only the stack in CLAUDE.md; do not write real keys or tokens; do not run database migrations; do not install packages outside that stack; ask me before deleting any file; keep all changes inside this project folder. Use the verify skill for the final checks. Stop when the check passes or after 25 turns. If you stop early, report what is blocking and what you tried. Do not start Phase 2.
```
## Phase 2: Data and availability
**Goal:** Prisma schema for the SPEC.md entities, a seed script, and slot math that is tested.
**Files likely touched:** `prisma/schema.prisma`, `prisma/seed.ts`, `lib/slots.ts`, `lib/slots.test.ts`.
**Check:** `npx vitest run` passes, including the slot math tests. `npx prisma migrate dev` creates the first migration.
**Constraints:**
- Entities come from SPEC.md only: Walker, Service, AvailabilityRule, Booking, Client, ReminderLog.
- Store all times in UTC.
- Ask before any migration that drops or renames a column.
- Seed one walker and two services (30-minute and 60-minute walks).
**Turn bound:** 25 turns.
**`/goal` prompt:**
```
/goal Phase 2 of PLAN.md is complete. End state: prisma/schema.prisma defines Walker, Service, AvailabilityRule, Booking (status pending, paid, cancelled, completed), Client and ReminderLog as described in SPEC.md. Times are stored in UTC. prisma/seed.ts creates one walker and two services (a 30-minute walk and a 60-minute walk, prices in cents). lib/slots.ts exports a function that returns free slots for the next 14 days from availability rules minus existing bookings. Check: run `npx vitest run` and show the pass count, and confirm all slot math tests pass. Run `npx prisma migrate dev --name init` and confirm it succeeds. Run `npm run verify` and show the exit code. Constraints: entities come only from SPEC.md; store times in UTC; prices in integer cents; do not write real keys; ask me before any migration that drops or renames a column; ask before deleting any file. Use the verify skill for final checks. Stop when the check passes or after 25 turns. If you stop early, report what is blocking and what you tried. Do not start Phase 3.
```
## Phase 3: Booking UI and API
**Goal:** A client can pick a slot, fill in a validated form, and create a pending booking.
**Files likely touched:** `app/book/page.tsx`, `components/SlotPicker.tsx`, `components/BookingForm.tsx`, `lib/validation.ts`, `app/api/bookings/route.ts`, `e2e/booking.spec.ts`.
**Check:** `npm run test:e2e -- e2e/booking.spec.ts` passes. `npm run verify` exits 0.
**Constraints:**
- Use a calendar-style slot picker.
- Validate form input and API input with zod.
- The redirect after booking goes to a placeholder URL (`/checkout/pending`) until Phase 4.
- Phone numbers are stored in E.164 format.
- Use no raw SQL string concatenation.
**Turn bound:** 25 turns.
**`/goal` prompt:**
```
/goal Phase 3 of PLAN.md is complete. End state: a calendar-style slot picker on /book shows open slots for the next 14 days. A booking form collects name, phone, email and the chosen service, and validates them with zod. POST /api/bookings creates a booking with status pending and rejects invalid input with a 400 response. Phone numbers are stored in E.164 format. After submitting, the browser goes to a placeholder URL /checkout/pending. Check: run `npm run test:e2e -- e2e/booking.spec.ts` and show the result. The test must book a slot up to the payment redirect. Run `npm run verify` and show the exit code. Constraints: validate all input with zod; no raw SQL string concatenation; no real keys; ask before deleting any file. Use the verify skill for the final checks. Stop when the check passes or after 25 turns. If you stop early, report what is blocking and what you tried. Do not start Phase 4.
```
## Phase 4: Payments
**Goal:** Stripe Checkout takes payment, and only a verified webhook marks a booking paid.
**Files likely touched:** `lib/stripe.ts`, `app/api/stripe/checkout/route.ts`, `app/api/stripe/webhook/route.ts`, `lib/bookings.ts`, `tests/stripe-webhook.test.ts`, `tests/double-booking.test.ts`.
**Check:** `npx vitest run` passes, including a test with a fake signed webhook that marks a booking paid, and a double-booking test that fails as expected: a second booking on a taken slot is rejected. `npm run verify` exits 0.
**Constraints:**
- Use `/effort high` for this phase.
- Verify the Stripe signature before reading the request body.
- Booking status changes only from webhook events.
- Handle repeated webhook deliveries idempotently.
- Check for overlapping bookings inside a database transaction.
- Never store card data.
- Tests never call live Stripe.
- Ask before changing webhook logic in a way that alters the verification step.
**Turn bound:** 25 turns.
**`/goal` prompt:**
```
/goal Phase 4 of PLAN.md is complete. End state: POST /api/stripe/checkout creates a Stripe Checkout session for a pending booking, using the amount in cents from the Service record. POST /api/stripe/webhook verifies the Stripe signature before it reads the body, and moves a booking from pending to paid on checkout.session.completed. Repeated deliveries of the same event do not change the result. Slot creation checks for overlapping bookings inside a database transaction. Check: run `npx vitest run` and show the output. The tests must include (a) a fake signed webhook that marks a booking paid, (b) an invalid signature that gets a 400, and (c) a double-booking test that fails as expected: a second booking on a taken slot is rejected. Run `npm run verify` and show the exit code. Constraints: never store card data; store only Stripe IDs and amounts in integer cents; no test calls live Stripe or Twilio; do not write real keys; ask me before changing webhook logic in a way that alters the signature check; ask before deleting any file. Use the verify skill for final checks. Stop when the check passes or after 25 turns. If you stop early, report what is blocking and what you tried. Do not start Phase 5.
```
## Phase 5: Reminders
**Goal:** A cron route sends one SMS per booking, 24 hours before the walk, and never sends a duplicate.
**Files likely touched:** `lib/twilio.ts`, `lib/reminders.ts`, `lib/reminders.test.ts`, `app/api/cron/reminders/route.ts`, `prisma/schema.prisma` (ReminderLog only if a change is needed).
**Check:** `npx vitest run` passes, including a test that proves no duplicate sends with Twilio mocked. `npm run verify` exits 0.
**Constraints:**
- Use `/effort high` for this phase.
- The cron route requires the CRON_SECRET header.
- Send SMS only to clients who opted in.
- ReminderLog prevents duplicates; the check happens before the send, inside a transaction.
- Twilio is mocked in all tests.
- Ask before any schema change that drops or renames a column.
**Turn bound:** 25 turns.
**`/goal` prompt:**
```
/goal Phase 5 of PLAN.md is complete. End state: lib/twilio.ts wraps the Twilio client and reads credentials only from environment variables. app/api/cron/reminders/route.ts returns 401 unless the request has the correct CRON_SECRET header. It finds paid bookings whose walk starts about 24 hours from now, skips clients who have not opted in, and sends one SMS per booking. Each send writes a ReminderLog row, and a booking that already has a ReminderLog row is never sent again. Check: run `npx vitest run` and show the output. The tests must include a test with Twilio mocked that calls the cron logic twice and confirms exactly one SMS is sent, and a test that a request without CRON_SECRET gets 401. Run `npm run verify` and show the exit code. Constraints: no real phone numbers in code, tests or logs; no live Twilio calls in tests; read all secrets from environment variables; ask me before any schema change that drops or renames a column; ask before deleting any file. Use the verify skill for final checks. Stop when the check passes or after 25 turns. If you stop early, report what is blocking and what you tried. Do not start Phase 6.
```
## Phase 6: Hardening and deploy
**Goal:** The walker can see upcoming walks, errors are handled, the full flow is tested end to end, and the deploy checklist is complete.
**Files likely touched:** `app/walker/page.tsx`, `app/error.tsx`, `app/not-found.tsx`, `README.md`, `INSTALL.md`, `e2e/full-flow.spec.ts`.
**Check:** `npm run verify` and `npm run test:e2e` both exit 0. Every checklist box in INSTALL.md is ticked.
**Constraints:**
- The walker list shows today's and upcoming walks.
- Pages load under 2 seconds on mobile and work in phone browsers.
- Money is shown in dollars, converted from cents.
- Do not deploy from this phase. You run the live deploy steps in INSTALL.md yourself.
- Ask before deleting any file.
**Turn bound:** 25 turns.
**`/goal` prompt:**
```
/goal Phase 6 of PLAN.md is complete. End state: /walker lists today's and upcoming walks in a simple list, with money shown in dollars converted from cents. app/error.tsx and app/not-found.tsx exist and show plain-language messages. README.md has deploy steps. e2e/full-flow.spec.ts runs the whole path: set availability, pick a slot, pay with Stripe test mode through a stubbed checkout, see the confirmation, and see the walk on /walker. INSTALL.md has a deploy checklist and every box in it is ticked. Check: run `npm run verify` and show the exit code. Run `npm run test:e2e` and show the pass count. Open INSTALL.md and list any unticked boxes; there should be none. Constraints: do not deploy anything; do not write real keys or phone numbers; do not call live Stripe or Twilio in tests; ask me before deleting any file. Use the verify skill for final checks. Stop when the check passes or after 25 turns. If you stop early, report what is blocking and what you tried.
```
## After Phase 6
- Run the live steps in INSTALL.md yourself: set env vars on the host, run `prisma migrate deploy`, register the Stripe webhook URL, set the cron schedule with CRON_SECRET, and send one real test SMS to your own phone.
- Use `/usage` if a phase runs longer than expected.
REVIEW.md# Review checklist
Use this file with `/code-review`. Apply every section to the diff. Payments, bookings and reminders are the risky parts of this app, so check them first.
## Money
- Amounts are integers in cents. Flag any float math on money, such as `price * 0.1` or `parseFloat` on a price.
- Currency is USD unless SPEC.md says otherwise.
- Convert cents to dollars only at display time.
## Payments
- The Stripe webhook verifies the signature before it reads the body. Flag any JSON parsing or field access that happens first.
- Booking status changes only from verified webhook events. The client redirect never marks a booking paid.
- Repeated webhook deliveries are idempotent. Processing the same event twice must not create a second payment, a second booking or a second status change.
- Card data is never stored or logged. Only Stripe IDs and cent amounts are stored.
## Double booking
- Slot creation checks for overlapping bookings inside a database transaction.
- The check and the insert happen in the same transaction. Flag any gap between them.
- A slot that is already booked cannot be booked again. A test covers this.
## Secrets
- No keys, tokens or phone numbers in code, logs, tests or fixtures.
- `.env` and `.env.local` are not committed. `.env.example` contains placeholder names only, such as `YOUR_STRIPE_SECRET_KEY`.
- Error messages and logs do not echo request bodies that contain phone numbers or secrets.
## Reminders
- Duplicate protection uses ReminderLog. A reminder is never sent twice for the same booking.
- The cron route `/api/cron/reminders` requires the CRON_SECRET header and rejects requests without it.
- No SMS goes to a number that has not opted in.
- Phone numbers are stored in E.164 format.
## Input
- All API input is validated with zod before use.
- No raw SQL string concatenation. Use Prisma query methods or tagged template queries.
- Dates and times from the client are converted to UTC before storage.
## Tests
- Every bug fix has a test that fails without the fix.
- No test calls live Stripe or Twilio. Use the Stripe test fixtures and mock Twilio.
- Unit tests for slot math and the reminder dedupe logic cover the edge cases in SPEC.md.
## Output format
List each finding under one of three labels:
- **blocker**: must fix before merge (security, money, double booking, duplicate SMS).
- **should-fix**: real problem, fix in this change or the next one.
- **nit**: style or clarity, optional.
Each finding gives the file and line, one sentence on the problem, and one sentence on the fix. If there are no findings in a section, say so in one line.
SPEC.md# Dog walker booking site: product spec
## Purpose
A booking site for one solo dog walker. Clients pick a walk time, pay online, and get a text reminder 24 hours before the walk.
## Users
- **Walker:** the admin. One account. Sets availability and services, and sees upcoming walks.
- **Clients:** no account needed. They book with name, phone and email.
## Core flows
1. Walker sets weekly availability and service types (30-minute walk, 60-minute walk). Each service has a price in cents.
2. Client sees open slots for the next 14 days and picks one.
3. Client pays through Stripe Checkout.
4. When the Stripe webhook confirms payment, the slot is booked and a confirmation page shows.
5. An SMS reminder is sent 24 hours before the walk.
6. Walker sees today's and upcoming walks in a simple list.
## Out of scope for MVP
- Multiple walkers
- Recurring bookings
- Refunds UI
- Client accounts
- Native apps
## Data entities
- **Walker:** name, timezone, contact email. One row.
- **Service:** name, duration in minutes, price in cents.
- **AvailabilityRule:** weekday, start time, end time, service. Times are in the walker's timezone and converted to UTC for storage.
- **Booking:** status (pending, paid, cancelled, completed), start time in UTC, service, client, Stripe checkout session ID, Stripe payment intent ID, amount in cents.
- **Client:** name, email, phone in E.164 format, SMS opt-in flag.
- **ReminderLog:** booking, sent at. One reminder per booking.
A pending booking does not hold a slot. Only a paid booking blocks its time.
## Acceptance criteria
Each item is a statement a test or a manual check can confirm.
- A slot already booked (paid) cannot be booked again. Two clients racing for the same slot: the second gets a clear "slot taken" message and no second paid booking exists.
- A webhook with an invalid signature returns 400 and changes nothing.
- A replayed webhook event (same Stripe event ID) leaves the booking paid once, with no duplicate changes.
- A booking becomes paid only after a verified webhook event. The client redirect alone never marks it paid.
- A reminder is never sent twice for the same booking.
- Reminders go only to clients who opted in to SMS.
- A call to the reminder cron route without the correct CRON_SECRET header returns 401.
- All API input is validated. Invalid input returns 400 with a readable message.
- Money is stored as integer cents and shown in dollars, for example $25.00.
- `npm run verify` passes on every phase's final commit.
## Non-functional
- Pages load in under 2 seconds on a mobile connection.
- All pages work in phone browsers.
- Times are stored in UTC and shown in the walker's timezone.
- Timezone placeholder: America/New_York. The walker changes it in config.
## Open questions for the user
- Which timezone should the walker use?
- What is the cancellation policy? (Not in MVP. Needed before launch.)
- What should the reminder text say?
- Should the walker take a deposit, or full payment at booking?