152 lines
7.2 KiB
Markdown
152 lines
7.2 KiB
Markdown
# 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 <http://127.0.0.1:8000/> 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 `9711111111/2/3/4`, 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.
|