# PIFF Donations

A Next.js 14 (App Router) + TypeScript + Tailwind + Prisma (SQLite) + Stripe
donation app for **Pay It Forward Foundation (PIFF)**, the 501(c)(3)
affiliated with HomeDividend.

The headline feature: donors choose a piece of an illustrated house —
foundation stone, brick, window, door, or roof tile — and dedicate it. Every
donation is permanently recorded on the structure once approved by a human
moderator.

---

## What this is

- **/donate** — interactive house illustration with ~200 SVG hotspot regions.
  Click a piece, write a 60-character dedication, pay via Stripe Payment
  Element. Once approved, the piece colors in with the donor's name, message,
  and optional website link.
- **/donate/donors** — chronological wall of approved donors with running
  total of dollars raised.
- **/donate/about** — copy explaining how PIFF deploys donations as PRIs into
  the HomeDividend SPV.
- **/admin/moderate** — password-gated moderation queue.
- **Stripe Payment Intents** with the Payment Element embedded in a modal
  (no redirect-Checkout). Webhook signature verification enforced.
- **Per-region pricing** — wall bricks priced by row, all overridable in
  `lib/tiers.ts`.

## Quick start

```bash
# 1. Install
npm install

# 2. Configure environment
cp .env.example .env
# Then edit .env with real values (Stripe test keys at minimum).

# 3. Initialize the database
npm run db:push

# 4. Run dev server
npm run dev
# -> http://localhost:3000
```

For Stripe webhooks in dev, use the Stripe CLI:

```bash
stripe listen --forward-to localhost:3000/api/webhook/stripe
# It prints a webhook signing secret — set it as STRIPE_WEBHOOK_SECRET in .env.
```

## Required environment variables

| Variable | Description |
|---|---|
| `STRIPE_PUBLIC_KEY` | (legacy/optional, reads `NEXT_PUBLIC_STRIPE_PUBLIC_KEY` on the client) |
| `NEXT_PUBLIC_STRIPE_PUBLIC_KEY` | Stripe publishable key (`pk_test_...` or `pk_live_...`) |
| `STRIPE_SECRET_KEY` | Stripe secret key (`sk_test_...` or `sk_live_...`) |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret (`whsec_...`) |
| `DATABASE_URL` | Prisma connection string. Default: `file:./dev.db` (SQLite). |
| `ADMIN_PASSWORD` | Single admin password for the moderation queue. |
| `NEXT_PUBLIC_BASE_URL` | Public origin (used for return URLs). |
| `FOUNDATION_EIN` | PIFF EIN, server-side (used in receipt scaffolding). |
| `NEXT_PUBLIC_FOUNDATION_EIN` | PIFF EIN, public (rendered in footer/about). |
| `NEXT_PUBLIC_GOAL_CENTS` | Fundraising goal in cents. Default `25000000` ($250,000). |

## Editing tier prices

All tier prices live in **`lib/tiers.ts`**. Edit `priceCents` for any tier
to change the default. To override a single region's price (e.g. make
`wall-row5-brick17` cost $200 instead of the row-5 default), add to
`regionOverridesCents` in the same file.

```ts
// lib/tiers.ts
export const regionOverridesCents: Record<string, number> = {
  "wall-row5-b17": 20000,    // $200
};
```

## Editing house regions

Region polygons are in **`lib/regions.ts`**. They are fractional (0..1) of
the SVG viewBox (1536 × 1024, matching `public/house.png`). Wall bricks are
generated programmatically by `brickRow()`. Windows, doors, columns, porch
boards, and roof tiles are hand-placed from coordinates derived from the
illustration. To swap the illustration, regenerate coordinates by inspecting
the new image; the viewBox proportions in `components/HouseInteractive.tsx`
must match too.

## Switching SQLite → Postgres

In `prisma/schema.prisma`:

```prisma
datasource db {
  provider = "postgresql"   // was "sqlite"
  url      = env("DATABASE_URL")
}
```

Set `DATABASE_URL` to a Postgres connection string and run:

```bash
npx prisma migrate deploy   # or `npm run db:push` for dev
```

No model changes are required.

## Production deploy notes

### Vercel

1. Add all env vars in Project Settings.
2. Use a managed Postgres (Vercel Postgres, Supabase, Neon, RDS) — SQLite
   does not survive serverless redeploys.
3. Set the Stripe webhook endpoint to
   `https://your-domain/api/webhook/stripe` and copy the signing secret to
   `STRIPE_WEBHOOK_SECRET`.
4. Ensure `prisma generate` runs at build time (it does via the
   `postinstall` script).

### Railway / Fly.io / standalone Node

```bash
npm run build
npm run start
```

The app is a standard Next.js standalone server. Run behind a reverse proxy
that forwards `X-Forwarded-For` and `X-Real-IP` so the rate limiter sees
real client IPs.

### Webhook URL

`POST /api/webhook/stripe` — Stripe must call this URL with a valid signing
secret. Without `STRIPE_WEBHOOK_SECRET`, the route rejects all requests.

## Architecture overview

```
                        ┌─────────────────┐
                        │  Donor browser  │
                        └────────┬────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              │                  │                  │
              ▼                  ▼                  ▼
        /donate page      /api/checkout       Stripe.js
        (server-rendered  (POST → creates    (Payment Element
        with regions +    Stripe Payment     in iframe, talks
        approved          Intent + DB row    directly to Stripe)
        donations)        in PENDING)
                                 │
                          ┌──────┴──────┐
                          ▼             ▼
                      Stripe API     Prisma → SQLite/Postgres
                          │
                          ▼
              /api/webhook/stripe ◀── Stripe sends
              (verifies signature,    payment_intent.succeeded
               annotates DB row)

                        ┌──────────────────┐
                        │  Admin browser   │
                        └────────┬─────────┘
                                 │ password (ADMIN_PASSWORD)
                                 ▼
                        /admin/login → cookie session
                                 │
                                 ▼
                    /admin/moderate (Server Actions)
                    approve | reject | edit text
                                 │
                                 ▼
                    increments Settings.totalRaisedCents,
                    flips Donation.status → APPROVED,
                    revalidates /donate and /donate/donors
```

---

## ⚠️ CRITICAL LEGAL TODO CHECKLIST

**Do not go live without completing every item below.** This codebase is
correct as a software artifact, but several items require legal review and
are out of scope for the dev work.

- [ ] **Counsel opinion letter on the PIFF → HomeDividend SPV transfer
      mechanism.** The plan as described in `/donate/about` (PRI into the
      SPV) implicates IRS rules on Program-Related Investments
      (Treas. Reg. § 53.4944-3) and on private benefit / private inurement
      for 501(c)(3)s. Get a written opinion from tax counsel that the
      contemplated mechanism — grant, PRI, or LP interest — is permissible
      and properly documented. Do not collect the first dollar without it.
- [ ] **Stripe nonprofit account.** Open a Stripe account in PIFF's name
      under PIFF's EIN (not HomeDividend's). Apply for nonprofit pricing.
      Verify the legal entity type is set to "Non-profit organization".
- [ ] **Insert PIFF EIN.** Search the codebase for `[INSERT EIN]` and replace
      with the real EIN. Set both `FOUNDATION_EIN` and
      `NEXT_PUBLIC_FOUNDATION_EIN` env vars.
- [ ] **Privacy policy.** Replace the placeholder at `/privacy` with a
      counsel-reviewed policy. State the legal basis for processing donor
      data (consent + legitimate interest), retention period, donor rights
      under GDPR/CCPA if applicable, and how donor identity is shared (if at
      all) with HomeDividend or the SPV.
- [ ] **Donor receipt automation.** A scaffolded receipt acknowledgement is
      shown on `/donate/success` and sent via Stripe `receipt_email`.
      **This is not enough on its own.** IRS Pub. 1771 requires a written
      acknowledgement for donations of $250 or more, including: the
      organization's name, the contribution amount, a statement of whether
      goods or services were provided in return (and if so, a good-faith
      estimate of their value), and — for the donor — the EIN and a
      statement that PIFF is a 501(c)(3). Build this email pipeline (e.g.
      Postmark/SES) and have counsel sign off on the template before
      go-live. The dedication banner / brick on the house is **not**
      "goods or services" for tax purposes (it is purely commemorative and
      has no inherent value), but counsel should confirm that position in
      writing.
- [ ] **Content moderation policy.** Codify what counts as a refusable
      message (profanity, hate speech, defamation, commercial solicitation
      unrelated to donor identity, etc.). The Terms of Donation page is a
      starting point; have counsel review the moderation rubric.
- [ ] **State charitable-solicitation registrations.** Most U.S. states
      require a 501(c)(3) to register before soliciting donations from
      residents of that state. PIFF will likely need to register with each
      state attorney general's office where it solicits — typically all 40+
      states that require it. Use a service like Harbor Compliance or
      counsel for this. **Out of scope for the dev work, but flagging it
      here so it does not get missed.**
- [ ] **Webhook signing secret rotation.** Confirm the `STRIPE_WEBHOOK_SECRET`
      is set in production and rotation is on the runbook.
- [ ] **Backups.** SQLite is the default. Production must use Postgres with
      backups (not SQLite). Confirm before launch.
- [ ] **Rate limiter.** The default is in-memory (10 req/IP/min). For
      multi-instance deploys, replace with Redis (Upstash or similar).
- [ ] **PCI DSS attestation.** Stripe handles card data via the Payment
      Element; PIFF is in SAQ-A scope. Confirm with Stripe.

---

## File map (high level)

```
app/
  layout.tsx                Root layout, header, footer.
  page.tsx                  Landing.
  donate/
    page.tsx                The house page (server-rendered).
    about/page.tsx
    donors/page.tsx
    success/page.tsx
    cancel/page.tsx
    terms/page.tsx
  privacy/page.tsx
  admin/
    login/page.tsx          Password gate.
    moderate/page.tsx       Moderation queue.
    moderate/actions.ts     Server actions: approve/reject/edit.
  api/
    checkout/route.ts       POST → creates Payment Intent.
    webhook/stripe/route.ts POST → verifies + annotates.
    admin/approve/route.ts
    admin/reject/route.ts
    admin/edit/route.ts
components/
  HouseInteractive.tsx      Client; renders house + SVG overlay + tooltip.
  DonateModal.tsx           Client; form + Stripe Payment Element.
lib/
  tiers.ts                  Edit prices here.
  regions.ts                Hotspot polygons (fractional coords).
  db.ts                     Prisma client.
  stripe.ts                 Stripe SDK (server-only).
  sanitize.ts
  rate-limit.ts
  admin-auth.ts             Constant-time password compare + cookie.
prisma/
  schema.prisma             Donation + Settings models.
public/
  house.png                 The watercolor house illustration.
```

## Smoke test

```bash
npm run db:push
npm run build
npm run lint
npm run dev
# In a separate terminal:
curl http://localhost:3000        # → 200, landing page
curl http://localhost:3000/donate # → 200, house page
```

Then visit `http://localhost:3000/donate`, hover bricks, click one, fill the
form, complete a test payment with Stripe test card `4242 4242 4242 4242`,
sign in to `/admin/login` with `ADMIN_PASSWORD`, and approve.
