Files
scan-receipts/STRIPE_SETUP_GUIDE.md
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

316 lines
12 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Stripe Setup & Integration Guide (ScanReceipts Pro)
This comprehensive guide walks you through configuring **Stripe** for payment processing, subscription management, webhooks, and license generation for **Receipt Scanner to Excel / DATEV (ScanReceipts)**.
---
## 1. Overview & Pricing Architecture
ScanReceipts offers three Pro access tiers:
| Tier / Plan | Price (EUR) | Billing Type | Trial / Terms | Environment Variable |
|---|---|---|---|---|
| **Weekly Pass** | **4,99 €** | Recurring (weekly) | **3-Day Free Trial** | `STRIPE_WEEKLY_PRICE_ID` |
| **Annual Pass** | **39,99 €** | Recurring (yearly) | Full 12 Months Access (~3,33 €/Mo) | `STRIPE_ANNUAL_PRICE_ID` |
| **Lifetime License** | **59,99 €** | One-Time Payment | Perpetual access & all future updates | `STRIPE_LIFETIME_PRICE_ID` |
> 💡 **Inline Fallback**: If you do not create custom Price IDs in Stripe, the application will automatically create ad-hoc inline line items (`price_data`) with the exact prices and trial settings above. Supplying explicit Price IDs is recommended for production accounting and analytics.
---
## 2. Prerequisites & Stripe Account Setup
1. **Sign Up or Log In**: Go to the [Stripe Dashboard](https://dashboard.stripe.com/).
2. **Activate Test Mode**: Toggle the **"Test mode"** switch in the top-right corner of the Stripe Dashboard (the UI will indicate test mode with an orange/yellow banner).
3. Ensure your default currency is set to **EUR (€)**.
---
## 3. Obtaining API Keys
1. Navigate to **Developers > API keys** (`https://dashboard.stripe.com/test/apikeys`).
2. **Publishable key**: Copy the key starting with `pk_test_...`.
3. **Secret key**: Click **"Reveal live key token"** (or create a new restricted/standard key) starting with `sk_test_...`.
4. Add these keys to your `.env.local` file:
```env
STRIPE_PUBLISHABLE_KEY=pk_test_51...
STRIPE_SECRET_KEY=sk_test_51...
```
---
## 4. Creating Products & Prices in the Stripe Dashboard
To track subscriptions and revenue analytics cleanly in Stripe, create the three Pro products:
### 4.1 Product: Weekly Pass (Wochen-Pass)
1. In Stripe Dashboard, go to **Product catalog** (`https://dashboard.stripe.com/test/products`) and click **+ Add product**.
2. **Name**: `Receipt Scanner Pro - Wochen-Pass`
3. **Description**: `Unbegrenzte Belege, Dual-Sheet Excel & Buchhaltungs-CSV (inkl. 3 Tage Trial)`
4. **Pricing**:
- Pricing model: **Standard pricing**
- Price: **4,99 EUR**
- Billing period: **Weekly (Wöchentlich)**
5. Click **Save product**.
6. Under the **Pricing** section of this product, copy the **Price ID** (starts with `price_...`).
7. Add to `.env.local`:
```env
STRIPE_WEEKLY_PRICE_ID=price_1Q...
```
### 4.2 Product: Annual Pass (Jahres-Pass)
1. Click **+ Add product**.
2. **Name**: `Receipt Scanner Pro - Jahres-Pass`
3. **Description**: `Volle 12 Monate unbegrenzte Belege, Dual-Sheet Excel & Prioritäts-Support`
4. **Pricing**:
- Pricing model: **Standard pricing**
- Price: **39,99 EUR**
- Billing period: **Yearly (Jährlich)**
5. Click **Save product**.
6. Copy the **Price ID** (`price_...`).
7. Add to `.env.local`:
```env
STRIPE_ANNUAL_PRICE_ID=price_1Q...
```
### 4.3 Product: Lifetime License (Lebenslange Lizenz)
1. Click **+ Add product**.
2. **Name**: `Receipt Scanner Pro - Lifetime Lizenz`
3. **Description**: `Lebenslanger unbegrenzter Zugriff ohne Folgekosten inklusive aller Updates`
4. **Pricing**:
- Pricing model: **Standard pricing**
- Price: **59,99 EUR**
- Billing period: **One-time (Einmalig)**
5. Click **Save product**.
6. Copy the **Price ID** (`price_...`).
7. Add to `.env.local`:
```env
STRIPE_LIFETIME_PRICE_ID=price_1Q...
```
---
## 5. Webhook Configuration (Production & Staging)
When a customer completes checkout or cancels a subscription, Stripe sends webhook events to your server.
1. Navigate to **Developers > Webhooks** (`https://dashboard.stripe.com/test/webhooks`).
2. Click **+ Add destination** (or **+ Add endpoint**).
3. **Endpoint URL**:
- Production: `https://your-domain.com/api/webhooks/stripe`
- Staging/Preview: `https://staging.your-domain.com/api/webhooks/stripe`
4. **Select events to listen to**:
- `checkout.session.completed` (Creates license in PostgreSQL & triggers Discord notification)
- `customer.subscription.updated` (Updates expiration date and active status)
- `customer.subscription.deleted` (Downgrades user license to cancelled/free)
- `invoice.payment_succeeded` (Renews active subscription period)
- `invoice.payment_failed` (Marks license as past due)
5. Click **Add endpoint**.
6. In the newly created webhook page, find **Signing secret** and click **Reveal**.
7. Copy the signing secret (starts with `whsec_...`) and add to `.env.local`:
```env
STRIPE_WEBHOOK_SECRET=whsec_...
```
### 5.1 Webhook verification & license-granting policy (security notes)
The webhook endpoint (`/api/webhooks/stripe`) enforces, in order:
1. **Signature verification is the sole entry gate.** Every request is verified
with `stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET)`
before any business logic runs. A missing/invalid signature gets a
`400` — nothing else executes. Never disable or bypass this check.
2. **Licenses are only granted for confirmed payments.**
- One-time payments (Lifetime, `mode: "payment"`): only when
`payment_status === "paid"`.
- Subscriptions (Weekly/Annual, `mode: "subscription"`): the subscription is
retrieved from Stripe and only `active` / `trialing` statuses activate a
license; `expiresAt` is derived from `subscription.current_period_end`
(never from the server clock).
- Unconfirmed events are acknowledged with `200` + `received: true` and
logged, but create **no** license and no Stripe retry.
3. **Amount cross-check (defense in depth).** For one-time payments the paid
`amount_total` (minor units) is compared against the shared price catalog
(`src/lib/billing/pricing.ts`, e.g. Lifetime = 5999). On mismatch the event
is acknowledged and logged but no license is created. Subscription trials
legitimately carry `amount_total = 0`, so subscription amounts are not
cross-checked (status gating covers them).
4. **Plan validation.** The plan from checkout metadata is validated with
`isPlanId`/`resolvePlan`; unknown values fall back to the annual plan (same
contract as the checkout route) and are logged as a warning.
> ⚠️ **Testing note:** `stripe trigger checkout.session.completed` generates a
> synthetic session without plan metadata and without a subscription, so it is
> (correctly) treated as unconfirmed and will **not** create a license. To test
> license creation end-to-end, run a real checkout through the app
> (`stripe listen --forward-to localhost:3000/api/webhooks/stripe`) and pay
> with Stripe's test card `4242 4242 4242 4242`.
> **Provider limitation:** Stripe is currently the only integrated payment
> provider (Paddle is not a dependency and no Paddle route exists). If a second
> provider is added later, it must follow the same verification pattern:
> cryptographic signature check as the sole gate, payment-status confirmation
> before granting, amount cross-check against the shared catalog, and
> idempotent license creation.
---
## 6. Local Testing with the Stripe CLI
You can test the entire checkout, webhook, and licensing pipeline on your local machine (`http://localhost:3000`) using the official Stripe CLI.
### 6.1 Install Stripe CLI
- **Windows (Scoop)**:
```powershell
scoop install stripe
```
- **macOS (Homebrew)**:
```bash
brew install stripe/stripe-cli/stripe
```
- **Direct Binary Download**:
Download the latest release executable from [GitHub Releases](https://github.com/stripe/stripe-cli/releases) and add it to your `PATH`.
### 6.2 Authenticate CLI
Run:
```bash
stripe login
```
Follow the in-terminal link to authenticate with your Stripe account.
### 6.3 Forward Webhooks to Local Server
Start your Next.js development server:
```bash
npm run dev
```
In a separate terminal, forward Stripe events to your local webhook route:
```bash
stripe listen --forward-to localhost:3000/api/webhooks/stripe
```
Stripe CLI will print a local webhook signing secret in your terminal:
```
> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
Copy this secret and set `STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxx...` in `.env.local` (and restart `npm run dev` if needed).
### 6.4 Triggering Test Events
You can trigger synthetic Stripe events directly from the CLI:
1. **Test Checkout Session Completion**:
```bash
stripe trigger checkout.session.completed
```
2. **Test Subscription Cancellation**:
```bash
stripe trigger customer.subscription.deleted
```
3. **Test Subscription Renewal Payment**:
```bash
stripe trigger invoice.payment_succeeded
```
4. **Test Failed Payment**:
```bash
stripe trigger invoice.payment_failed
```
Check your terminal logs and database to see the license generated and logged.
---
## 7. Discord Sales Alert Bot (Optional)
ScanReceipts includes real-time sales notifications via Discord Webhook:
1. In your Discord server, open **Server Settings > Integrations > Webhooks**.
2. Click **New Webhook**, name it (e.g. `Receipt Scanner Sales`), select a channel, and click **Copy Webhook URL**.
3. Add the URL to your `.env.local`:
```env
DISCORD_SALES_WEBHOOK_URL=https://discord.com/api/webhooks/123456789/abcdef...
```
4. Whenever a checkout completes, the bot will post an emerald green embed containing the plan purchased, formatted amount in EUR, customer email, and timestamp.
---
## 8. License Verification API Reference
### `GET /api/license/verify`
Verify an active license key or Stripe checkout session ID.
**Query Parameters:**
- `key` or `licenseKey`: The generated license key (e.g., `RS-PRO-A1B2-C3D4`)
- `sessionId` or `session_id`: The Stripe checkout session ID (`cs_test_...` or `cs_live_...`)
**Example Request:**
```bash
curl "http://localhost:3000/api/license/verify?key=RS-PRO-A1B2-C3D4"
```
**Success Response (HTTP 200):**
```json
{
"valid": true,
"plan": "lifetime",
"status": "active",
"expiresAt": null,
"licenseKey": "RS-PRO-A1B2-C3D4",
"source": "database"
}
```
---
## 9. Environment Variables Reference (.env.local)
Here is the complete template for your environment configuration:
```env
# ==============================================
# Next.js Application URL
# ==============================================
NEXT_PUBLIC_APP_URL=http://localhost:3000
# ==============================================
# Stripe API Keys & Secrets
# ==============================================
STRIPE_SECRET_KEY=sk_test_51...
STRIPE_PUBLISHABLE_KEY=pk_test_51...
STRIPE_WEBHOOK_SECRET=whsec_...
# ==============================================
# Optional Stripe Price IDs (Dashboard Created)
# ==============================================
STRIPE_WEEKLY_PRICE_ID=price_...
STRIPE_ANNUAL_PRICE_ID=price_...
STRIPE_LIFETIME_PRICE_ID=price_...
# ==============================================
# Discord Sales Notification Webhook (Optional)
# ==============================================
DISCORD_SALES_WEBHOOK_URL=https://discord.com/api/webhooks/...
# ==============================================
# Database Persistence (PostgreSQL)
# ==============================================
DATABASE_URL=postgresql://receipt_user:receipt_password@localhost:5432/receipt_scanner
```
---
## 10. Troubleshooting & FAQ
### Issue: "Invalid signature" error in webhook logs
- Ensure `STRIPE_WEBHOOK_SECRET` matches the signing secret displayed by Stripe CLI (`stripe listen`) during local development, or the signing secret in Stripe Dashboard under Webhooks for production.
- Ensure the raw request body is read cleanly without intermediate JSON serialization before signature validation (ScanReceipts handles this via `await req.text()`).
### Issue: Stripe Checkout redirects to 404 or localhost in production
- Set `NEXT_PUBLIC_APP_URL` to your production domain (e.g. `https://scanreceipts.io`).
### Issue: Database is offline during checkout
- ScanReceipts uses a **Local-First Architecture**. If PostgreSQL is unreachable or `DATABASE_URL` is omitted, the checkout and verification routes gracefully fall back to zero-friction local validation without crashing or returning errors to the user.