# QPAY integration into 2enable.africa

## Hosting placement

QPAY API and checkout run on a Docker-capable VPS or cloud environment. The merchant package runs on PHP 8.3 with PDO MySQL, cURL and JSON, with only its public directory exposed. If 2enable.africa is WordPress, Laravel, Moodle, Node or another platform, adapt the routes and authentication bridge to that platform rather than replacing the existing website. No hosting credentials or existing site source were supplied, and the public site could not be inspected through the web tool.

Suggested QPAY domains are examples, not verified ownership. Configure approved HTTPS domains. Route QPAY ingress through an external TLS terminator to loopback port 8080 on the Docker host. Register the return and webhook URLs in QPAY environment configuration.

## Contract

Create example, executed only from the merchant server:

```http
POST /v1/payment-intents
Authorization: Bearer qpk_test_...
Idempotency-Key: 2enable_ORDERS_NONCE
Content-Type: application/json

{"amount":100,"currency":"USD","country":"ZA","merchant_reference":"32_hex_order_id","return_url":"https://2enable.africa/payments/return.php"}
```

The response includes id, amount, currency, merchant_reference, status and checkout_url. Save id and checkout_url against the existing order before redirecting. Retry with the same body and key after a timeout. A different body with the same key returns 409. USD amounts 100 and 200 are the user's sandbox tests; production amounts will be ZAR minor units.

Read status with the server key using GET /v1/payment-intents/{id}. A different merchant cannot read the object. Scope names are payments:read and payments:write. The key and webhook secret never reach the browser.

Merchant event format:

```json
{"id":"evt_32hex","type":"payment_intent.succeeded","data":{"id":"pi_32hex","status":"succeeded","amount":100,"currency":"USD","merchant_reference":"32_hex_order_id"}}
```

Signature is lowercase hex HMAC-SHA256(secret, timestamp + "." + exact raw HTTP body). Headers are QPAY-Timestamp and QPAY-Signature; timestamps are Unix seconds and must be within 300 seconds. Provider and merchant webhook secrets must be different. Deduplicate the stable event ID. Retry responses are 503; acknowledgment is 200. This implementation always fetches canonical status before fulfilment.

## Activation

The subscription grant holds a per-user MySQL advisory lock, locks the order, inserts one subscription with a unique order ID and marks the order paid in one transaction. It extends from the later of now and the user's latest end date. A free plan follows the same grant path but sends no payment request. Duration is provisionally 30 days; this is not a monthly calendar renewal. All plans currently grant the same placeholder test access and require business-defined benefits before production.

The existing learning app must query active grants and enforce access. It must not infer access from the return URL, a screenshot, a submitted receipt or a client-side status. Refund/revocation policies are not implemented in this test package.

## Staging acceptance

1. Verify Plan 1 creates one free access record and no QPAY intent. Refreshing or repeating the same order must not duplicate the record.
2. Plan 2 creates amount 100 USD; Plan 3 creates amount 200 USD. Changing a browser amount has no effect because prices come from enable_plans.
3. Confirm sandbox approval stays processing before a signed fixture. No receipt/access exists yet.
4. Send a succeeded signed sandbox event. Check one QPAY outbox event, merchant delivery acknowledgment, one merchant subscription and receipt.
5. Retry the same event and revisit the return URL. No duplicate access.
6. Submit a signed wrong amount/currency. QPAY quarantines the event and access remains inactive.
7. Send invalid/stale signatures. Both webhook endpoints reject them.
8. Sign in as a second user. The first user's order, return and receipt cannot be read.
9. Drop the provider connection or merchant delivery. Reuse the saved order/idempotency key; observe worker retries.
10. Restart services, check saved state and confirm UTC timestamps. Test backup restoration on a separate database.

## Live provider handover

MTN's South Africa collection page describes merchant collections but contains country-specific inconsistencies. Obtain the contracted ZA API and signature/query contract directly. FNB's catalogue must be narrowed to the contracted inbound collection product; never equate an outbound EFT API with checkout collection. Implement each adapter, provider route/capability and reconciliation mapping, then certify success, decline, timeout, delayed confirmation and supported refund/reversal. Do not enable live keys merely by changing environment variables: this release rejects them by design.

References checked 5 October 2026:
- MTN MoMo South Africa: https://momoapi.mtn.com/SouthAfrica_Widget_productDetails
- FNB Integration Channel: https://www.fnb.co.za/integration-channel/catalogue/serviceCatalogue-ZA/services.html?catalogue=serviceCatalogue-ZA&service=EFT-Domestic-Collections&type=API

## Deployment status

Integration code and configuration contract are established in the supplied packages. The real host has not been modified. Live installation needs the existing 2Enable source/login and learning authorization interface, hosting access, approved domains/TLS, merchant credentials and certified provider details.
