# BizNode Stripe Payment System — Complete Documentation

> **Audience:** BizNode owner / admin operator
> **Scope:** Admin-only infrastructure (tools/admin/) — never shipped in customer ZIP
> **Last updated:** 2026-03-09

---

## Table of Contents

1. [Architecture Overview](#1-architecture-overview)
2. [Payment Flow — End to End](#2-payment-flow--end-to-end)
3. [Stripe Configuration](#3-stripe-configuration)
4. [Payment Link Categories](#4-payment-link-categories)
5. [Webhook Handler (webhook.php)](#5-webhook-handler-webhookphp)
6. [Payment Dispatcher (Python)](#6-payment-dispatcher-python)
7. [Workers — What Each Category Does](#7-workers--what-each-category-does)
8. [Database Tables](#8-database-tables)
9. [DZIT Credit Ledger System](#9-dzit-credit-ledger-system)
10. [Admin API Endpoints](#10-admin-api-endpoints)
11. [Automation & Scheduling](#11-automation--scheduling)
12. [Service Verification — How to Know It Works](#12-service-verification--how-to-know-it-works)
13. [Security — Signature vs IP Whitelisting](#13-security--signature-vs-ip-whitelisting)
14. [Monitoring & Troubleshooting](#14-monitoring--troubleshooting)
15. [File Reference](#15-file-reference)

---

## 1. Architecture Overview

```
                    CUSTOMER
                       |
                 (clicks buy link)
                       |
                       v
              ┌─────────────────┐
              │  Stripe Checkout │
              │  (hosted by      │
              │   Stripe.com)    │
              └────────┬────────┘
                       │  checkout.session.completed
                       │  event (POST + HMAC signature)
                       v
              ┌─────────────────┐
              │  webhook.php    │  <-- biznode.1bz.biz/webhook.php
              │  (PHP on 1bz    │      Runs on your web server
              │   web server)   │
              └────────┬────────┘
                       │
          ┌────────────┼────────────┐
          v            v            v
    ┌──────────┐ ┌──────────┐ ┌──────────┐
    │ Category │ │ Track in │ │ Forward  │
    │ Lookup   │ │ MySQL    │ │ to Main  │
    │ (stripe_ │ │ (dzit_   │ │ Node API │
    │  plinks) │ │  stripe_ │ │ (best-   │
    │          │ │  tracker)│ │  effort) │
    └──────────┘ └──────────┘ └──────────┘
          │
          v
    ┌──────────────────────┐
    │  Worker Dispatch     │
    │  DZITPACKAGE→voucher │
    │  TOPUP→topup         │
    │  BOOKS→book_sale     │
    └──────────────────────┘
          │
          v
    ┌──────────────────────┐
    │  dzit_ledger         │  Credits recorded
    │  ubc_book_sales      │  Sale recorded (BOOKS only)
    └──────────────────────┘
          │
          v
    ┌──────────────────────┐
    │  dzit_sync.py        │  Periodic: MySQL → PostgreSQL
    │  (scheduler job)     │  Keeps local node in sync
    └──────────────────────┘
```

**Two processing paths exist (redundancy):**

| Path | Handler | Location | When Used |
|------|---------|----------|-----------|
| **Primary** | `webhook.php` | Web server (biznode.1bz.biz) | Always — Stripe calls this directly |
| **Secondary** | `admin_api.py → PaymentDispatcher` | MainNode (localhost:7777) | When webhook.php forwards the event via curl |

The PHP handler is the primary receiver because it's on a publicly accessible web server. The Python handler is a backup that also enables the dashboard API to access payment data.

---

## 2. Payment Flow — End to End

### Step-by-step:

1. **Customer clicks a Stripe payment link** (e.g., `https://buy.stripe.com/xxx`)
2. **Stripe hosts the checkout page** — collects card details, processes payment
3. **Stripe sends `checkout.session.completed` webhook** → `POST https://biznode.1bz.biz/webhook.php`
4. **webhook.php verifies the HMAC-SHA256 signature** using your webhook secret
5. **Category lookup** — queries `stripe_plinks` table using the `payment_link` ID from the event
   - Falls back to `stripe_links` if not found in `stripe_plinks`
   - Defaults to `UNKNOWN` if neither table has a match
6. **Records payment in `dzit_stripe_tracker`** — global audit log, every payment regardless of category
7. **Routes to the appropriate worker:**
   - `DZITPACKAGE` → `process_voucher()` → generates DZIT voucher code, credits `dzit_ledger`
   - `TOPUP` → `process_topup()` → adds DZIT credits directly, credits `dzit_ledger`
   - `BOOKS` → `process_book_sale()` → records in `ubc_book_sales`, increments `copies_sold`
   - `UNKNOWN` → logged, no action taken
8. **Forwards event to MainNode** (best-effort, 5s timeout) → `http://localhost:7777/api/admin/stripe/webhook`
9. **Returns HTTP 200** to Stripe — Stripe considers the webhook delivered

### Timing:
- Stripe waits up to **20 seconds** for a 200 response before retrying
- Stripe retries failed webhooks for up to **3 days** with exponential backoff
- Your webhook typically responds in **< 500ms**

---

## 3. Stripe Configuration

### Stripe Dashboard Setup

| Setting | Value |
|---------|-------|
| **Webhook URL** | `https://biznode.1bz.biz/webhook.php` |
| **Events to listen for** | `checkout.session.completed` |
| **Signing secret** | `whsec_8jWs59GpMFKWz8HLiKtOoyw6CdT6lt4A` |

### Local Configuration

**File:** `tools/admin/admin.env` (NEVER in customer ZIP)

```env
ADMIN_STRIPE_SECRET_KEY=sk_live_51S2pnk...
ADMIN_STRIPE_WEBHOOK_SECRET=whsec_8jWs59GpMFKWz8HLiKtOoyw6CdT6lt4A
```

**File:** `webhook.php` (on web server)

```php
define('STRIPE_WEBHOOK_SECRET', 'whsec_8jWs59GpMFKWz8HLiKtOoyw6CdT6lt4A');
```

Both must have the **same** webhook signing secret.

---

## 4. Payment Link Categories

Your Stripe payment links are pre-registered in the `stripe_plinks` table with categories:

| Category | Count | Description | Worker |
|----------|-------|-------------|--------|
| **DZITPACKAGE** | 5 links | One-time DZIT membership packages | `voucher` |
| **TOPUP** | 4 links | Balance topup packages | `topup` |
| **BOOKS** | 1 link | Book sales | `book_sale` |

### Adding a New Payment Link

1. Create a payment link in [Stripe Dashboard → Payment Links](https://dashboard.stripe.com/payment-links)
2. Copy the `plink_xxx` ID
3. Insert into MySQL:

```sql
INSERT INTO stripe_plinks (payment_link_id, category, stripe_buy_link)
VALUES ('plink_NEW_ID', 'TOPUP', 'https://buy.stripe.com/xxx');
```

Or via admin API:
```bash
# Future: POST /api/admin/stripe-links (not yet implemented)
```

4. The local PostgreSQL copy syncs automatically via `dzit_sync.py`

### How Category Lookup Works

```
stripe_plinks (canonical, checked first)
       ↓ not found?
stripe_links (legacy, checked second)
       ↓ not found?
category = "UNKNOWN" (logged, no worker action)
```

---

## 5. Webhook Handler (webhook.php)

**Location:** `https://biznode.1bz.biz/webhook.php`
**Server path:** `/home/1bzbiz/public_html/biznode/webhook.php`
**Source:** `tools/admin/website/webhook.php` (local)

### Request/Response:

| Method | Status | Meaning |
|--------|--------|---------|
| GET | 405 | Method Not Allowed — only POST accepted |
| POST (no body) | 400 | Empty request body |
| POST (bad signature) | 401 | Invalid Stripe signature |
| POST (non-checkout event) | 200 | Event ignored (not checkout.session.completed) |
| POST (valid checkout) | 200 | Payment processed successfully |
| POST (DB error) | 500 | Database error (still tries to forward) |

### Signature Verification

The handler verifies **every** request using HMAC-SHA256:

```
signed_payload = timestamp + "." + raw_body
expected = HMAC-SHA256(webhook_secret, signed_payload)
compare(expected, signature_from_header)
```

**Key points:**
- Timestamp tolerance: **300 seconds** (5 minutes) — rejects replayed old events
- Uses `hash_equals()` for timing-safe comparison
- `BYPASS_SIGNATURE = false` in production (set `true` only for local testing)

### Logging

All activity is logged to `/home/1bzbiz/public_html/biznode/webhook_log.txt`:

```
[2026-03-09 14:23:01 UTC] [INFO] Received event: checkout.session.completed (evt_xxx)
[2026-03-09 14:23:01 UTC] [INFO] Tracked: event=evt_xxx cat=TOPUP worker=topup email=user@example.com amount=500
[2026-03-09 14:23:01 UTC] [INFO] Topup: user@example.com +500 credits
[2026-03-09 14:23:01 UTC] [INFO] Mainnode forward OK (HTTP 200)
[2026-03-09 14:23:01 UTC] [INFO] Completed: event=evt_xxx worker=topup result=ok
```

**The log file is protected** by `.htaccess` — not accessible via browser.

---

## 6. Payment Dispatcher (Python)

**File:** `tools/admin/payment_dispatcher.py`

The Python version provides the same logic as webhook.php but runs inside the MainNode's FastAPI server. It's used when:

1. webhook.php forwards the event to `http://localhost:7777/api/admin/stripe/webhook`
2. Admin triggers a manual reprocess via the dashboard

The endpoint is defined in `tools/admin/admin_api.py`:

```python
@admin_router.post("/stripe/webhook")
async def stripe_webhook(request: Request):
    body = await request.body()
    sig_header = request.headers.get("stripe-signature", "")
    if admin_config.STRIPE_WEBHOOK_SECRET:
        if not PaymentDispatcher.verify_stripe_signature(body, sig_header):
            raise HTTPException(status_code=400, detail="Invalid signature")
    event_data = await request.json()
    result = PaymentDispatcher.handle_stripe_webhook(event_data)
    return result
```

---

## 7. Workers — What Each Category Does

### DZITPACKAGE → `voucher` worker → Blockchain Account Creation

**Purpose:** Customer purchases a DZIT membership — creates a virtual identity (DZID)
on the DZITSystem smart contract (Polygon).

**Smart Contract:** `0x14811c0497c958ffdc4ccf5b4f576b0bc19c44fd` ([PolygonScan](https://polygonscan.com/address/0x14811c0497c958ffdc4ccf5b4f576b0bc19c44fd#code))
**DZIT Token:** `0x4B37872c868571d4ad1f822c43d4e1B123FB43d8`
**Admin Wallet (BACKEND_ROLE):** `0xb96e2C9f4b05042eb6D39DA4cd9c6B1b4ab9afe3`

**Full flow:**

```
Stripe payment received
    ↓
1. Create service ticket (BN-YYYYMMDD-XXXXX)
    ↓
2. Generate random 6-char DZID (e.g., A9F2K7)
    ↓
3. Call EmailCreationAccount(keccak256(email))
   → Costs 1 DZIT from admin wallet → contract
   → TX hash recorded as creation_tx
    ↓
4. Call confirmOTP(dzid, emailHash, walletAddress)
   → Admin (BACKEND_ROLE) activates the account
   → walletAddress = customer wallet or 0x0 if unknown
   → TX hash recorded as confirm_tx
    ↓
5. Call adminDepositPoints(dzid, creditPoints)
   → Credit points = amount_cents (1 cent = 1 credit point)
   → Points are DZIT-backed (contract requires DZIT deposit)
   → TX hash recorded as deposit_tx
    ↓
6. Transfer 1 DZIT to contract as proof-of-service
   → TX hash = proof_tx_hash (blockchain proof that service was rendered)
    ↓
7. Record in dzid_accounts table + dzit_ledger + service_tickets
    ↓
8. Email customer: DZID code, credit points, PolygonScan proof link
```

**DZID Format:** Random 6 chars (A-Z + 0-9): `A9F2K7`, `X3Q9PL`, `7ZKD4T`
~2.1 billion combinations. Not predictable. User-friendly.

**Proof of Service:** 1 DZIT token transferred from admin wallet → DZITSystem contract.
The TX hash is stored in `service_tickets.proof_tx_hash`. Anyone can verify on PolygonScan.

**Legacy Fallback:** If blockchain is unavailable (no private key, RPC down, insufficient DZIT),
the system falls back to a deterministic voucher code `DZIT-{SHA256(event_id)[:12]}` and
credits `dzit_ledger` without blockchain interaction.

### TOPUP → `topup` worker → Blockchain Credit Deposit

**Purpose:** Customer tops up credit points on their existing DZID account.

**Flow:**
```
Stripe payment received
    ↓
1. Look up DZID by email in dzid_accounts table
   → If no DZID exists: create one first (full DZITPACKAGE flow)
    ↓
2. Call adminDepositPoints(dzid, creditPoints)
   → Requires DZIT backing (contract enforces collateral)
    ↓
3. Transfer 1 DZIT as proof-of-service
    ↓
4. Service ticket + email notification
```

### BOOKS → `book_sale` worker → PDF Delivery (planned)

**Purpose:** Customer purchases a book.

**Current flow:**
1. Looks up book title from `book_payment_links` table
2. Records sale in `ubc_book_sales` + increments `copies_sold`
3. Creates service ticket

**Planned additions:**
- Send PDF book to customer email
- Transfer 1 DZIT to UBC smart contract (to be created) as proof
- Record email delivery details in service ticket

---

## 8. Database Tables

### MySQL (Site DB: crn0s&hahi @ 144.91.114.245)

| Table | Purpose |
|-------|---------|
| **stripe_plinks** | Payment link → category mapping (canonical, 10 rows) |
| **stripe_links** | Legacy payment link mapping (12 rows, fallback) |
| **dzit_stripe_tracker** | Global payment audit log (every payment recorded) |
| **dzit_ledger** | DZIT credit/debit ledger per email |
| **dzid_accounts** | DZID virtual accounts (dzid, email, wallet, blockchain txs) |
| **service_tickets** | Service ticket tracking (ticket_id, tx_hash, proof_tx_hash) |
| **book_payment_links** | Book catalog with stripe_plink_id and copies_sold |
| **ubc_book_sales** | Individual book sale records |

### PostgreSQL (Local Node: biznode DB)

| Table | Purpose |
|-------|---------|
| **stripe_plinks** | Synced copy of MySQL stripe_plinks |
| **dzit_ledger** | Synced copy of MySQL dzit_ledger |
| **dzid_accounts** | Synced copy of DZID virtual accounts |
| **service_tickets** | Synced copy of service tickets |

### Key Schemas

**dzit_stripe_tracker** (audit log):
```sql
stripe_event_id  VARCHAR(255) -- Stripe event ID (unique per payment)
payment_link     VARCHAR(255) -- Stripe payment link ID (plink_xxx)
amount_cents     INT          -- Payment amount in cents
currency         VARCHAR(10)  -- e.g., "usd"
customer_email   VARCHAR(255) -- Customer's email
worker_type      VARCHAR(50)  -- voucher | topup | book_sale | unknown
category         VARCHAR(50)  -- DZITPACKAGE | TOPUP | BOOKS | UNKNOWN
created_at       DATETIME     -- When the payment was processed
```

**dzit_ledger** (credit system):
```sql
email           VARCHAR(255) -- Customer email
credit_amount   INT          -- Credits added (in cents/DZIT)
debit_amount    INT          -- Credits subtracted
reason          VARCHAR(500) -- Human-readable description
reference       VARCHAR(255) -- Stripe event ID (for dedup)
created_at      DATETIME     -- Transaction timestamp
```

---

## 9. DZIT Credit Ledger System

### Balance Calculation

```sql
SELECT SUM(credit_amount) - SUM(debit_amount) AS balance
FROM dzit_ledger
WHERE email = 'customer@example.com'
```

### Credit Sources
- DZITPACKAGE purchase → credits = amount_cents
- TOPUP purchase → credits = amount_cents
- Admin manual credit → via `/api/admin/dzit/add`

### Debit Sources
- Admin manual deduction → via `/api/admin/dzit/deduct`
- Transfer between users → via `/api/admin/dzit/transfer`
- (Future) Node-to-node service consumption

### Idempotency

The `reference` column (set to Stripe event ID) prevents double-crediting:
- `dzit_sync.py` checks existing references before backfilling
- webhook.php doesn't check (Stripe event IDs are unique), but the tracker insert will fail on duplicate event IDs

---

## 10. Admin API Endpoints

All endpoints at `http://localhost:7777/api/admin/`:

### Payment Monitoring
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/payments` | Recent payments (optional `?email=` filter) |
| GET | `/payments?email=x&limit=50` | Filtered payment history |
| GET | `/payments/stats` | Total count, amount, breakdown by category, last 24h |

### DZIT Credits
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/dzit/balance/{email}` | Check DZIT balance for an email |
| GET | `/dzit/history/{email}` | Transaction history for an email |
| POST | `/dzit/add` | Add credits `{"email":"x","amount":100,"reason":"..."}` |
| POST | `/dzit/deduct` | Deduct credits `{"email":"x","amount":50,"reason":"..."}` |
| POST | `/dzit/transfer` | Transfer between users |

### Stripe / Products
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/stripe-links` | List all payment links with categories |
| GET | `/books` | Book catalog with sales counts |
| POST | `/stripe/webhook` | Receive forwarded Stripe events |

### System
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/status` | Connectivity check (MySQL, Stripe, Polygon configured?) |

### Example Usage

```bash
# Check a customer's DZIT balance
curl http://localhost:7777/api/admin/dzit/balance/user@example.com

# View recent payments
curl http://localhost:7777/api/admin/payments?limit=10

# Payment statistics
curl http://localhost:7777/api/admin/payments/stats

# Manually credit a customer
curl -X POST http://localhost:7777/api/admin/dzit/add \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","amount":1000,"reason":"Promotional credit"}'
```

---

## 11. Automation & Scheduling

### dzit_sync.py — Ledger Synchronization

**File:** `tools/admin/dzit_sync.py`

The `sync_payments()` function:
1. Ensures `dzit_ledger` tables exist in both MySQL and PostgreSQL
2. Backfills MySQL `dzit_ledger` from `dzit_stripe_tracker` (voucher/topup records only)
3. Syncs MySQL `dzit_ledger` → PostgreSQL `dzit_ledger`

**To run manually:**
```bash
cd C:\nodesrunning\biznodemainnode
python -m tools.admin.dzit_sync
```

**To add to scheduler** (in `automation/scheduler.py`):
```python
# Not yet wired — add this to register_admin_jobs():
scheduler.add_job(
    tools.admin.dzit_sync.sync_payments,
    'interval', hours=1,
    id='dzit_ledger_sync',
    name='DZIT Ledger Sync (MySQL → PostgreSQL)',
)
```

### Recommended Automation Schedule

| Job | Frequency | Purpose |
|-----|-----------|---------|
| `sync_payments()` | Every 1 hour | Keep local PostgreSQL in sync with MySQL |
| Payment stats digest | Daily 9 AM | Email payment summary to admin |
| Webhook log rotation | Weekly | Archive webhook_log.txt on server |

---

## 12. Service Verification — How to Know It Works

### Quick Health Checks

**1. Is the webhook URL reachable?**
```bash
# Should return 405 (Method Not Allowed) — correct, it only accepts POST
curl -s -o /dev/null -w "%{http_code}" https://biznode.1bz.biz/webhook.php
# Expected: 405
```

**2. Does signature verification work?**
```bash
# POST without signature → should return 401
curl -s -X POST https://biznode.1bz.biz/webhook.php \
  -H "Content-Type: application/json" \
  -d '{"type":"test"}' | python -m json.tool
# Expected: {"error": "Invalid signature"}
```

**3. Are payment links registered?**
```bash
curl http://localhost:7777/api/admin/stripe-links | python -m json.tool
# Should show 12 payment links with categories
```

**4. Is the admin system connected?**
```bash
curl http://localhost:7777/api/admin/status | python -m json.tool
# Should show: mysql_configured: true, stripe_configured: true
```

**5. Check recent payments:**
```bash
curl http://localhost:7777/api/admin/payments?limit=5 | python -m json.tool
```

**6. Check webhook log on server:**
```bash
ssh -p 28 root@144.91.114.245 "tail -20 /home/1bzbiz/public_html/biznode/webhook_log.txt"
```

### End-to-End Test (Stripe Test Mode)

1. Switch to Stripe test mode in dashboard
2. Use webhook secret from test mode
3. Create a test payment link
4. Register it in `stripe_plinks` with a test category
5. Complete a test checkout with card `4242 4242 4242 4242`
6. Check `webhook_log.txt` on server
7. Check `dzit_stripe_tracker` via admin API
8. Check `dzit_ledger` for credit entry

### Verifying Service Was Rendered

For each payment, check these in order:

| Check | What to Look For | Query |
|-------|-----------------|-------|
| **Webhook received?** | Entry in webhook_log.txt | `ssh ... "grep EVENT_ID webhook_log.txt"` |
| **Tracked?** | Row in dzit_stripe_tracker | `GET /api/admin/payments?email=X` |
| **Credited?** | Row in dzit_ledger | `GET /api/admin/dzit/history/EMAIL` |
| **Balance updated?** | Correct sum | `GET /api/admin/dzit/balance/EMAIL` |
| **Book delivered?** | Row in ubc_book_sales | `GET /api/admin/books` (check copies_sold) |

---

## 13. Security — Signature vs IP Whitelisting

### Does the webhook need IP whitelisting?

**No. The HMAC-SHA256 signature alone is sufficient.**

Here's why:

| Security Layer | How It Works |
|---------------|--------------|
| **HMAC-SHA256 Signature** | Stripe signs every webhook payload with your secret key. The webhook verifies this signature before processing. An attacker cannot forge the signature without knowing your `whsec_xxx` secret. |
| **Timestamp validation** | Events older than 5 minutes (300 seconds) are rejected. Prevents replay attacks. |
| **Timing-safe comparison** | Uses `hash_equals()` (PHP) / `hmac.compare_digest()` (Python) to prevent timing side-channel attacks. |
| **HTTPS** | Webhook URL uses HTTPS — payload is encrypted in transit. |

### When would you add IP whitelisting?

Only if you want **defense in depth** (belt + suspenders). Stripe publishes their webhook IP ranges at [https://stripe.com/docs/ips](https://stripe.com/docs/ips), but they explicitly state:

> *"We recommend verifying webhook signatures rather than relying on IP allowlisting."*

**Our recommendation: Signature verification is the correct and complete security mechanism. No IP whitelisting needed.**

### What the secret key protects against:

| Attack | Protected? | How |
|--------|-----------|-----|
| Forged webhook events | Yes | Attacker can't compute valid HMAC without secret |
| Replay attacks | Yes | Timestamp check rejects old events |
| Man-in-the-middle | Yes | HTTPS encryption + HMAC verification |
| Event tampering | Yes | Any modification invalidates the signature |
| Brute force signature | Yes | SHA-256 with long secret = computationally infeasible |

---

## 14. Monitoring & Troubleshooting

### Common Issues

| Symptom | Cause | Fix |
|---------|-------|-----|
| Webhook returns 401 | Signing secret mismatch | Ensure `whsec_xxx` matches in both Stripe dashboard and webhook.php |
| Category shows UNKNOWN | Payment link not in stripe_plinks | Add the plink_xxx to the table |
| No credits added | Wrong category or DB error | Check webhook_log.txt for errors |
| Mainnode forward fails | Dashboard not running or SSH tunnel down | Start dashboard: `python run_dashboard.py` |
| Duplicate credits | Same event processed twice | reference column dedup prevents this in ledger |
| PostgreSQL out of sync | sync_payments() not running | Run manually or add to scheduler |

### Webhook Log Locations

| Log | Location | Access |
|-----|----------|--------|
| PHP webhook | `/home/1bzbiz/public_html/biznode/webhook_log.txt` | `ssh -p 28 root@144.91.114.245 "tail -50 ..."` |
| Python dispatcher | Dashboard stdout / log file | Check uvicorn output |
| Stripe dashboard | Stripe → Developers → Webhooks → Event log | View delivery attempts and responses |

### Stripe Dashboard Verification

1. Go to [dashboard.stripe.com/webhooks](https://dashboard.stripe.com/webhooks)
2. Click your webhook endpoint
3. Check **Recent deliveries** — each shows:
   - HTTP status code received
   - Response body
   - Number of retry attempts
   - Time taken

---

## 15. File Reference

| File | Location | Purpose |
|------|----------|---------|
| `tools/admin/webhook.php` | Web server + local | Primary Stripe webhook handler (PHP) |
| `tools/admin/webhook.htaccess` | Web server | Apache config: allow POST, block log file |
| `tools/admin/payment_dispatcher.py` | MainNode | Python webhook handler + worker routing |
| `tools/admin/payment_orchestrator.py` | MainNode | Full blockchain orchestration (DZID creation, points, proof) |
| `tools/admin/blockchain_service.py` | MainNode | Web3 interaction with DZITSystem contract on Polygon |
| `tools/admin/service_ticket.py` | MainNode | Service ticket + DZID account CRUD |
| `tools/admin/site_db.py` | MainNode | MySQL database helper (stripe_plinks, ledger, tracker queries) |
| `tools/admin/dzit_sync.py` | MainNode | MySQL ↔ PostgreSQL ledger sync |
| `tools/admin/dzit_service.py` | MainNode | DZIT credit management (balance, add, deduct, transfer) |
| `tools/admin/admin_api.py` | MainNode | FastAPI admin router (all /api/admin/ endpoints) |
| `tools/admin/config.py` | MainNode | Admin config loader (reads admin.env) |
| `tools/admin/admin.env` | MainNode only | Keys, credentials, wallet private key (NEVER in ZIP) |

### Server Deployment

| File | Server Path |
|------|-------------|
| webhook.php | `/home/1bzbiz/public_html/biznode/webhook.php` |
| webhook.htaccess | `/home/1bzbiz/public_html/biznode/.htaccess` |
| webhook_log.txt | `/home/1bzbiz/public_html/biznode/webhook_log.txt` (auto-created) |

---

## Summary

The BizNode Stripe payment system is a **self-contained, webhook-driven pipeline** that:

1. **Receives** Stripe events via HMAC-verified webhook
2. **Categorizes** payments using pre-registered payment links
3. **Processes** each payment with the appropriate worker (voucher, topup, or book sale)
4. **Tracks** everything in MySQL (global audit log + credit ledger)
5. **Syncs** to local PostgreSQL for dashboard queries
6. **Exposes** all data via admin API endpoints

**No IP whitelisting required** — HMAC-SHA256 signature verification with timestamp validation provides complete security. The signing secret (`whsec_xxx`) is the sole authentication mechanism, as recommended by Stripe.
