# Gap analysis: commerce, NGO donations, tourism bookings & payments

This document inventories **what the codebase ships today**, **remaining gaps**, and **recommended next work**. Use it with admin areas **E‑commerce** (donations ledger, tourism packages/bookings) and **Settings → Payments**.

---

## Executive summary

The CMS exposes **donations** (`/donations`), **tourism enquiries** (`/bookings`), and **shop checkout** with **Stripe Checkout**, **PayPal Orders API (capture on return)**, **PayFast hosted payments + ITN**, and **PayNow Initiate Transaction + status webhook**. Orders are still created **before** the customer pays online; stock is decremented at order placement — align this with your risk tolerance (see §4).

---

## Feature maturity matrix

| Capability | Status | Notes |
|------------|--------|--------|
| Donation campaigns + public pledge form | **Shipped** | Offline + gateway choices from settings. |
| Donations ledger (admin) | **Shipped** | Filters / listing; payment state from gateways / webhooks. |
| Tourism packages + enquiry form | **Shipped** | No automated deposit or calendar availability. |
| Tourism bookings admin | **Shipped** | Status workflow (pending → confirmed → completed / cancelled). |
| Shop checkout + order totals | **Shipped** | COD / bank transfer + online redirects when configured. |
| Gateway metadata on orders/donations | **Shipped** | `payment_gateway`, `payment_gateway_reference` / `gateway_payment_id` (`pending:uuid` until gateway assigns its own id). |
| **Stripe Checkout** (shop + donations) | **Shipped** | Creates Checkout Session after DB save; **`POST /payments/webhooks/stripe`** verifies **`Stripe-Signature`** and handles `checkout.session.completed`. |
| **PayPal** | **Shipped** | OAuth client-credentials, create order, redirect to approve URL, **capture on signed return URL**; marks paid locally. |
| **PayFast** | **Shipped** | Auto-submit POST to PayFast **process** URL; **`POST /payments/webhooks/payfast`** validates MD5 signature + `payment_status`; requires **`payment_payfast_merchant_key`**. |
| **PayNow (Zimbabwe)** | **Shipped** | **`PayNowClient`** initiates via `/interface/initiatetransaction`, redirects to `browserurl`; **`POST /payments/webhooks/paynow`** validates SHA512 hash (integration key **lower-cased** for signing, matching PayNow’s PHP SDK). |
| Idempotent webhook processing | **Missing** | Duplicate ITNs/webhooks may repeat updates; add dedupe store if you see double posts. |
| Booking deposits | **Missing** | No checkout linked to `booking_id` metadata yet. |

Legend: **Shipped** = primary client + server path implemented; **Missing** = not implemented.

---

## Gateway endpoints & return URLs (reference)

| Gateway | Customer redirect | Server callback |
|---------|-------------------|-----------------|
| Stripe | Stripe-hosted Checkout | `POST /payments/webhooks/stripe` |
| PayPal | `links[].rel=approve` | Capture on `GET /shop/checkout/paypal/return/{order}` (signed) or donations equivalent |
| PayFast | `POST` to sandbox/live **eng/process** | `POST /payments/webhooks/payfast` (ITN) |
| PayNow | `browserurl` from initiate response | `POST /payments/webhooks/paynow` |

Signed return routes (12h expiry): `frontend.shop.checkout.paynow.return`, `frontend.shop.checkout.payfast.return`, `frontend.shop.checkout.paypal.return`, and matching `frontend.donations.*` routes.

---

## PayNow (Zimbabwe) — operator notes

- Docs: [Paynow Developer Hub](https://developers.paynow.co.zw/), [Initiate Transaction](https://developers.paynow.co.zw/docs/initiate_transaction.html).  
- Initiate URL: `{base}/interface/initiatetransaction` (`PayNowClient`).  
- **`resulturl`** passed on initiate must reach **`{APP_URL}/payments/webhooks/paynow`** (absolute URL).  
- Hash signing uses the **lower-cased** integration key (PayNow PHP SDK convention).

Optional improvements: persist **`pollurl`** server-side and poll on timeouts ([quickstart](https://developers.paynow.co.zw/docs/paynow/quickstart)).

---

## PayFast — operator notes

- Hosted URL: sandbox `https://sandbox.payfast.co.za/eng/process`, live `https://www.payfast.co.za/eng/process`.  
- Settings: **`payment_payfast_merchant_id`**, **`payment_payfast_merchant_key`**, optional **`payment_payfast_passphrase`**, **`payment_payfast_sandbox`**.  
- ITN notify URL: **`{APP_URL}/payments/webhooks/payfast`**.  
- Signature: alphabetical parameter string + optional **`&passphrase=`** suffix → **MD5** (`PayFastService`).  
- Successful payment ITN uses **`payment_status`** **`COMPLETE`** (handled case-insensitively as `complete`).

---

## Major gaps (prioritized)

### 1. Inventory vs async abandonment

Stock drops when the order row is saved. Mitigations: pending-order TTL + auto-cancel, stock reservation, or create order only after payment — choose per tenant.

### 2. Webhook idempotency & reconciliation

Add idempotency keys (`event.id`, PayFast `pf_payment_id`, PayNow `paynowreference`) before updating rows if duplicates appear in production logs.

### 3. Tourism deposit checkout

Reuse Stripe / PayNow / PayFast flows with metadata `{ booking_id }` and a booking-specific success URL.

### 4. Tourism domain depth

Availability calendars, capacity, seasonal pricing, channel managers, reminder emails, ICS exports.

### 5. NGO extras

Recurring gifts, donor portal, PDF receipts, CRM sync.

### 6. Observability & compliance

Structured logs with correlation IDs, admin payout audit trail, GDPR retention, rate limiting / honeypot on public forms.

### 7. Frontend UX

Shop checkout styling may differ from themed donate/booking screens — optional consolidation.

---

## Testing checklist (manual)

| Scenario | Expect |
|----------|--------|
| Stripe Checkout | Redirect to Stripe; webhook with valid signature marks paid. |
| Stripe webhook invalid signature | HTTP 400. |
| PayPal | Approve → return route captures → `payment_status=paid` when successful. |
| PayFast | Browser POST to PayFast; ITN `COMPLETE` + valid signature marks paid. |
| PayNow | Initiate → PayNow UI; status POST + hash marks paid/failed. |
| Wrong PayNow hash | HTTP 400. |

---

## Configuration checklist for operators

1. **Settings → Payments**: enable gateways; paste keys (use secrets manager in production).  
2. **Stripe**: Dashboard webhook → `{APP_URL}/payments/webhooks/stripe`, event **`checkout.session.completed`**, paste **`payment_stripe_webhook_secret`**.  
3. **PayPal**: **`payment_paypal_sandbox`**, Client ID + Secret; return URLs must be **HTTPS** in production.  
4. **PayFast**: Merchant ID + **Merchant key** + passphrase (if configured on PayFast); ITN URL **`/payments/webhooks/payfast`**.  
5. **PayNow**: Integration ID + key; **`resulturl`** on initiate points to **`/payments/webhooks/paynow`**.  
6. Run **`ensureDefaultSettings()`** (or save Payments group) after pulling new settings keys.

---

## Suggested roadmap (remaining)

| Sprint | Focus |
|--------|--------|
| S1 | Stock reservation or post-payment order creation for high-volume shops |
| S2 | Tourism booking deposits (`booking_id` metadata + admin reconcile) |
| S3 | Webhook idempotency store + admin reconciliation dashboard |
| S4 | Tourism availability model + calendar UI |

---

*Tenant-specific priorities differ for pure NGO vs mixed commerce vs resort-only deployments.*
