# Resumen Backend v6 — iKhokha + Pesapal + PPD + gated /api/export

## What's new since v5

1. **/api/export is now gated.** Requires `Authorization: Bearer <Firebase ID token>`.
   Server verifies the token, then:
   - if `users/{uid}.membership === 'Premium'` and `membershipEnds > now` → free render
   - else if `users/{uid}.downloadCredits >= 1` → atomically decrements 1 and renders (PPD)
   - else → `402 Payment Required`
     Every successful render appends a row to `downloads/{auto}`.

2. **IPN handlers actually grant entitlements** (no more `// TODO`):
   - `POST /api/ikhokha/callback` (HMAC-verified) → `grantSubscription` or `grantPpdCredits`
   - `POST|GET /api/pesapal/ipn` (re-queries `GetTransactionStatus`, only grants when description == COMPLETED)
     Idempotent on `externalTransactionID` / merchant reference — replays are safe.

3. **New status passthrough** for the SPA's return pages:
   - `GET /api/ikhokha/status/:txn`
   - `GET /api/pesapal/status?orderTrackingId=...&ref=...`

## Firestore schema written by the server

| collection         | fields                                                          |
| ------------------ | --------------------------------------------------------------- | ------ |
| users/{uid}        | (sub) `membership='Premium'`, `membershipEnds: Date`            |
|                    | (ppd) `downloadCredits: number`, `downloadCreditsUpdated: ts`   |
| subscriptions/{id} | legacy ledger row — same shape as the old client-side `addSbs`  |
| payments/{ref}     | NEW: per-transaction record + idempotency. Keyed by gateway ref |
| downloads/{id}     | NEW: one row per successful /api/export. `mode: 'sub'           | 'ppd'` |
| data/earnings      | `amount` incremented by `priceUsd` on each successful grant     |

## Required env (.env)

See `.env.example`. New things on top of v5:

```
FIREBASE_SERVICE_ACCOUNT_B64=<base64 of your serviceAccount.json>
```

Generate from Firebase Console → Project Settings → Service accounts →
Generate new private key, then `base64 -w0 serviceAccount.json`.

## Install

```
npm install            # picks up new firebase-admin dep
npm start
```

## Firestore Security Rules — IMPORTANT

The fields the server writes (`membership`, `membershipEnds`, `downloadCredits`,
docs in `payments/`, `downloads/`, `data/earnings`) MUST be **read-only to
clients** so users cannot self-grant. Example:

```
match /users/{uid} {
  allow read:   if request.auth.uid == uid;
  allow update: if request.auth.uid == uid
                && !(request.resource.data.diff(resource.data).affectedKeys()
                       .hasAny(['membership','membershipEnds','downloadCredits','downloadCreditsUpdated']));
}
match /payments/{p}      { allow read: if false; allow write: if false; }
match /downloads/{d}     { allow read: if request.auth.uid == resource.data.userId; allow write: if false; }
match /subscriptions/{s} { allow read: if request.auth.uid == resource.data.userId; allow write: if false; }
match /data/earnings     { allow read, write: if false; }
```

## How the pieces fit

```
[Plans/Checkout]
   |  POST /api/{ikhokha|pesapal}/{create-payment|submit-order}
   v
[backend] persists payments/{ref}{status:'pending'}, returns redirect URL
   |
   v  (user pays on gateway)
[gateway] hits /api/.../callback or /ipn  ──► verifies, then grants:
                                                users.{membership|downloadCredits},
                                                subscriptions/  or  payments/{ref}.status='granted',
                                                data/earnings += priceUsd
   |
[user is redirected] to /billing/success | /billing/return | /billing/failure | /billing/cancel
                       └─ pages poll /status/* and show outcome

[/api/export] gated by ID token → consumeDownload() → render PDF (free if Premium, else -1 credit)
```
