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>
316 lines
12 KiB
Markdown
316 lines
12 KiB
Markdown
# 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.
|