# PIFF Donations — Build Notes

One-page summary of the PIFF donation page handed to the dev team.

## What this is

A Next.js 14 (App Router) + TypeScript + Tailwind + Prisma (SQLite) +
Stripe donation app for Pay It Forward Foundation, the 501(c)(3) affiliated
with HomeDividend. The donor lands on `/donate`, hovers an illustrated
house, clicks any of ~200 hotspot regions (foundation stones, brick courses
priced by row, windows, door, sidelights, porch columns and boards, roof
shingles, gable peak shingles, plus a one-shot naming-rights polygon), pays
via embedded Stripe Payment Element, and after admin moderation the dedication
renders permanently on the chosen piece.

## Build summary

| Metric | Value |
|---|---|
| Source files (ts/tsx/css/prisma/md/json/mjs) | **39** |
| Lines of code (source, excludes node_modules and lockfile) | **~2,870** |
| Total hotspot regions on the house | **201** |
| Tiers configured in `lib/tiers.ts` | **17** |
| Routes wired (pages + API) | **17** (10 pages + 5 API + checkout + webhook) |
| `npm run build` | ✅ passes |
| `npm run lint` | ✅ zero warnings/errors |
| `npm run db:push` | ✅ creates SQLite at `prisma/dev.db` |
| Smoke test (`npm run start`) | ✅ all 10 routes return 200 (admin/moderate 307→login) |

## Routes wired

**Pages**
- `/` landing
- `/donate` interactive house
- `/donate/about` PIFF explainer (verbatim copy from spec)
- `/donate/donors` chronological wall of donors + running total
- `/donate/success` post-payment thank you
- `/donate/cancel` payment cancelled
- `/donate/terms` Terms of Donation (verbatim)
- `/privacy` privacy placeholder
- `/admin/login` password gate
- `/admin/moderate` moderation queue (Server Actions)

**API routes**
- `POST /api/checkout` — Zod-validated, rate-limited (10/IP/min),
  creates Stripe Payment Intent + DB row in PENDING.
- `POST /api/webhook/stripe` — Stripe signature verification enforced;
  annotates donation when `payment_intent.succeeded` fires.
- `POST /api/admin/approve|reject|edit` — Cookie-gated admin endpoints.

## House region count + how generated

**201 regions total**, broken down:

| Tier | Regions |
|---|---|
| foundation (stones) | 28 |
| wall-row1 | 12 |
| wall-row2 | 3 |
| wall-row3 | 3 |
| wall-row4 | 12 |
| wall-row5 | 30 |
| wall-row6 | 24 |
| wall-row7 | 30 |
| window-upper | 3 |
| window-ground | 2 |
| door | 1 |
| door-sidelight | 2 |
| porch-column | 2 |
| porch-board | 5 |
| roof-shingle | 39 (main + porch roof) |
| roof-gable | 5 (gable peak triangle) |

Wall-row 2 and 3 counts are intentionally low: those courses are mostly
hidden behind the ground-floor windows in the illustration. The 12 bricks
each in rows 1 and 4 sit below and above the windows respectively.

**Generation method (in `lib/regions.ts`):**
- Wall bricks generated programmatically by `brickRow()` — produces
  evenly subdivided polygons across a given (x0,y0)→(x1,y1) bounding
  box with optional half-brick offset for running bond. Bricks whose
  centerpoint falls inside a window opening are dropped.
- Foundation stones: same `brickRow()` helper, single row of 28 stones.
- Porch boards: linear subdivision of a single horizontal strip.
- Main roof + porch roof: 5×6 and 5×3 grids of rectangular tiles. Cells
  whose center sits inside the gable peak triangle are skipped (so the
  main roof and gable peak don't double-cover).
- Gable peak triangle: 5 horizontal trapezoidal strips computed from the
  apex/base coords.
- Windows, door, sidelights, columns: hand-placed rectangles with
  fractional coordinates measured visually from `house_reference.png`
  (1536×1024).

All polygons are stored as fractional [0..1] coordinates and rendered
inside an SVG viewBox of `0 0 1536 1024` with `preserveAspectRatio="none"`,
so they scale tightly with any responsive width.

## README legal-TODO checklist (complete?) — YES

The `README.md` includes a `⚠️ CRITICAL LEGAL TODO CHECKLIST` section with
**11 items**, every one explicitly required by the spec:

1. Counsel opinion letter on PIFF→SPV transfer mechanism (PRI / grant / LP)
2. Stripe nonprofit account in PIFF's EIN
3. Insert PIFF EIN (search/replace `[INSERT EIN]`, env vars)
4. Privacy policy review
5. Donor receipt automation (IRS Pub. 1771 substantiation) review
6. Content moderation policy review with counsel
7. State charitable-solicitation registrations (flagged as out-of-scope but
   critical)
8. Webhook signing secret rotation
9. Backups (Postgres in production)
10. Rate limiter (replace with Redis for multi-instance)
11. PCI DSS attestation (SAQ-A scope)

The README also documents quick-start, env vars, tier-price editing, the
SQLite→Postgres switch, deploy notes for Vercel and standalone Node, and an
ASCII architecture diagram.

## Notable design decisions

- **Payment Intents + embedded Payment Element** (not redirect Checkout).
  Modal overlays directly on the house page; donor never leaves.
- **Moderation = explicit second step**. Stripe webhook only annotates the
  donation; admin must hit Approve in `/admin/moderate` for the brick to
  color in publicly. The transaction increments `Settings.totalRaisedCents`
  atomically with the status change.
- **Region claim race protection**: `/api/checkout` checks for any
  existing APPROVED or PENDING donation on the same `regionId` before
  creating the Payment Intent, returning 409 if claimed. (For high-volume
  sites, consider adding a unique index on `(regionId, status)` partial.)
- **Constant-time admin password comparison** via `crypto.timingSafeEqual`.
  Cookie token = SHA-256(password + ":piff-admin"), so the cookie value
  alone is not the password.
- **Donor messages** sanitized server-side via `sanitizeForDisplay()` (strips
  control chars, collapses whitespace, hard-caps at 60 chars), and React
  renders them as text nodes — never `dangerouslySetInnerHTML`.
- **URL validation**: `safeUrl()` only allows `http:` / `https:` schemes.
  `rel="nofollow noopener noreferrer"` on every donor-supplied link.
- **Naming rights** is a special "virtual region" — no polygon; clicked via
  a separate CTA button below the house. Approving a naming-rights donation
  flips `Settings.namingRightsClaimed` and renders a banner above the house.
- **Per-region price overrides** via `regionOverridesCents` map in
  `lib/tiers.ts`, e.g. to make `wall-row5-b17` cost $200 instead of the
  row-5 default.

## Confirmation against spec

(a) **Total file count: 39** source files (ts/tsx/css/prisma/md/json/mjs).
(b) **Total LOC: ~2,870** lines of source code (excludes node_modules,
    next build output, and `package-lock.json`).
(c) **All routes wired** — 10 pages + 7 API/webhook endpoints, all return
    successfully on smoke test.
(d) **Build/lint pass** — `next build` and `next lint` both clean.
(e) **README's legal TODO section is complete** with all 11 required items.
(f) **House regions: 201**, generated by a mix of programmatic brick-grid
    helpers (walls, foundation, porch boards, roof tiles, gable strips) and
    hand-placed rectangles for windows, door, sidelights, and columns.
