diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..7e679bf --- /dev/null +++ b/examples/README.md @@ -0,0 +1,151 @@ +# Django example project + +A ready-to-run **Django** application demonstrating how to integrate every +gateway in `django-nepali-payment` the way you would in a real project, through +views, models, URL callbacks and a management command, rather than as +standalone scripts. + +| Component | Purpose | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `manage.py` / `example_project/` | A minimal Django project (settings, urls, wsgi/asgi). | +| `payments/models.py` | An `Order` model storing the payment lifecycle (status, provider ref, raw payload). | +| `payments/views.py` | `create_order` (initiate), `callback` (provider return), `verify_order` (re-check) for Khalti, eSewa, Fonepay and ConnectIPS. | +| `payments/urls.py` | Routed pages for creating payments and receiving provider callbacks. | +| `payments/management/commands/monitor_fonepay.py` | Background worker that polls Fonepay for settlements (shared-hosting safe). | +| `.env.sample` | Credential template, copy to `.env` and fill in your keys. | + +## Run it + +From this directory, using [uv](https://docs.astral.sh/uv/): + +```bash +# create the venv and install the library + example deps +uv sync + +# copy the credential template and fill in your keys +cp .env.sample .env + +# create migrations from the Order model and set up the database +uv run python manage.py makemigrations payments +uv run python manage.py migrate + +# run the dev server +uv run python manage.py runserver +``` + +Migrations for the example `payments` app are generated on the fly (they're not +committed to the repo), so run `makemigrations` the first time to create the +`Order` table. + +Open to see the home page and start payments. + +## How the flow works per gateway + +All follow the same pattern in `views.py`: + +1. `create_order` builds the gateway request and calls + `PaymentManager.initiate_payment(...)`. +2. **Khalti / eSewa** the customer is redirected to + `result.data.payment_url` (the provider's hosted page). + - Khalti calls back with `?pidx=` to `/callback/khalti/`. + - eSewa calls back with a base64 `?data=` to `/callback/esewa/`. +3. **Fonepay** `result.data.qr_message` is shown for scanning; settlement is + watched by the background monitor (below). +4. **ConnectIPS** `result.data.form_fields` is rendered as a hidden form that + posts to `result.data.target_url` (the bank's sign-in page). The demo ships a + simulated multi-step checkout (login → select bank → confirm) that reports + back to `/callback/connectips/` the same way a real session would. + +The `Order` row is updated as the payment moves through `pending` -> `initiated` +-> `paid` / `failed` / `timeout`. + +**Which order you can "Verify" after the fact:** Khalti re-checks +server-side by `pidx`, and Fonepay is polled by the background monitor. eSewa +and ConnectIPS do not expose a status lookup by transaction id, so the demo only +updates them from their callback payload — the "Verify" button on the orders +table explains this rather than calling the API blindly. + +## Monitor a Fonepay payment + +Start the polling worker in a terminal: + +```bash +uv run python manage.py monitor_fonepay +``` + +It scans for `fonepay` orders still in `initiated` state, starts +`FonepayPaymentMonitor` for each PRN, and updates orders to `paid` / `failed` / +`timeout` when the settlement events fire. This runs on a plain background +thread, no WebSocket, no daemon, nothing that breaks on shared hosting. + +## Going live + +Set these environment variables in `.env` before starting the server: + +```bash +export PAID_MODE=production +export PAID_BASE_URL=... # public HTTPS base for callback URLs +export ESEWA_SECRET=... +export ESEWA_PRODUCT_CODE=... +export KHALTI_SECRET=... +export FONEPAY_SECRET=... +export FONEPAY_MERCHANT=... +export FONEPAY_USERNAME=... +export FONEPAY_PASSWORD=... +# optional monitor tuning (seconds); defaults shown +export FONEPAY_MONITOR_TIMEOUT=900 +export FONEPAY_MONITOR_INTERVAL=5 +export CONNECTIPS_MERCHANT_ID=... +export CONNECTIPS_APP_ID=... +export CONNECTIPS_APP_NAME=... +export CONNECTIPS_APP_PASSWORD=... +export CONNECTIPS_CERT_PATH=... # .pfx / .p12 / .pem +export CONNECTIPS_CERT_PASSWORD=... +export CONNECTIPS_DEMO_USER=... +export CONNECTIPS_DEMO_PASSWORD=... +export CONNECTIPS_DEMO_CAPTCHA=... +export CONNECTIPS_DEMO_TPIN=... +export CONNECTIPS_DEMO_OTP=... +``` + +When `PAID_BASE_URL` is empty the demo derives callback URLs from the incoming +request host, which works locally but is unreliable behind a proxy or on a real +domain — set it to a public HTTPS base before going live. + +Without them the example app runs in **sandbox** mode with placeholder keys so +you can explore the UI and the request/response shapes safely. ConnectIPS runs a +**simulated** checkout when `CONNECTIPS_MERCHANT_ID` is empty. + +### Working test credentials + +The default `.env.sample` ships with test keys that are actually accepted today: + +| Gateway | Test credentials | +| -------- | --------------------------------------------------------------------------- | +| Khalti | Mobile `9800000001/2/3/4/5`, Pin `1111`, OTP `987654`, secret `live_secret_key_68791341fdd94846a146f0457ff7b455` | +| eSewa | Username `9806800001/2/3/4/5`, Password `Nepal@123`, Token `123456`, secret `8gBm/:&EnhH.1/q`, product code `EPAYTEST` | +| Fonepay | See note below — no public sandbox credentials exist | + +### Fonepay sandbox caveat + +Fonepay's **sandbox** API endpoint (`dev-merchantapi.fonepay.com`) currently +serves an **expired TLS certificate**, so any request to it fails with +`SSLCertVerificationError`. This is on Fonepay's side, not the library's. + +The demo targets the **production** endpoint instead (via `PAID_MODE=production` +for Fonepay) and clearly warns that it performs **real transactions**. Fonepay +has not published shared test credentials, so you need your own merchant +account to exercise the QR/status flows. + +## Real-host callback caveat + +`success_url` / `return_url` are built from the incoming request's host, so when +running on a LAN or a real domain point your provider dashboard (and +`ALLOWED_HOSTS` in `example_project/settings.py`) at the reachable public URL. + +## Note on the example server + +This project exists to demonstrate the library's API, it uses the Django +development server and SQLite. For production, run behind a real WSGI server +(e.g. Gunicorn/uWSGI), confirm all callbacks hit HTTPS endpoints, and mark +`PAID_MODE=production` only when you are truly live.