# PesaPal Integration (API 3.0 – UGX)

Simplified reference for integrating PesaPal as a payment provider in this application. Currency is **UGX** by default. For full request/response samples, see [PesaPal Developer Docs](https://developer.pesapal.com/how-to-integrate/e-commerce/api-30-json/api-reference).

---

## Overview

- **PesaPal API 3.0** uses JSON over HTTPS. All operations (except Auth) use a **Bearer token** obtained from the Auth endpoint; token is valid for about 5 minutes and is cached by our library.
- **Default currency** for orders in this integration is **UGX**.
- **IPN (Instant Payment Notification)** is required: PesaPal calls our IPN URL on payment status change. We register one IPN URL and use the returned `ipn_id` as `notification_id` in every Submit Order request.

---

## Authentication

- **Endpoint**: `POST /Auth/RequestToken`
- **Body**: `{ "consumer_key": "...", "consumer_secret": "..." }`
- **Response**: `token`, `expiryDate` (UTC). Use the token as `Authorization: Bearer <token>` for all other API calls.
- **Handling**: The application library requests and caches the token; you do not need to call Auth yourself.

**URLs**:
- Sandbox: `https://cybqa.pesapal.com/pesapalv3/api/Auth/RequestToken`
- Production: `https://pay.pesapal.com/v3/api/Auth/RequestToken`

---

## IPN (Instant Payment Notification)

- **Purpose**: PesaPal notifies your server when a payment’s status changes (e.g. completed, failed). Required for reliable status updates.
- **Our IPN URL**: Built from the app base URL in `.env` (e.g. `https://mm.finflo.online/public/pesapal-ipn`). Override with `pesapal.ipn_url` in `.env` if needed.
- **Method**: We register and use **POST** (preferred).
- **Registration**: Call **Register IPN** once (see “Our endpoints” below). Store the returned `ipn_id` as `pesapal.notification_id` and use it in every Submit Order.
- **Response**: The IPN endpoint must respond with JSON:  
  `{"orderNotificationType":"IPNCHANGE","orderTrackingId":"...","orderMerchantReference":"...","status":200}` (or `500` on error).

---

## Get IPN List

- **Endpoint**: `GET /URLSetup/GetIpnList`
- **Auth**: Bearer token.
- **Use**: List all registered IPN URLs for your merchant account (e.g. to confirm registration or pick an existing `ipn_id`).

---

## Submit Order

- **Endpoint**: `POST /Transactions/SubmitOrderRequest`
- **Required**: `id` (merchant reference, max 50 chars; alphanumeric, `-`, `_`, `.`, `:` only), `currency`, `amount`, `description`, `callback_url`, `notification_id` (IPN id), `billing_address` (must include `email_address` or `phone_number`).
- **Optional**: `cancellation_url`, `branch`, `redirect_mode` (e.g. `TOP_WINDOW`), `account_number` (for recurring), `subscription_details` (recurring).
- **Default currency**: **UGX** (unless overridden).
- **Response**: `order_tracking_id`, `redirect_url`. Redirect the customer to `redirect_url` to complete payment.

---

## Get Transaction Status

- **Endpoint**: `GET /Transactions/GetTransactionStatus?orderTrackingId=...`
- **Use**: After callback or IPN, call this to get the real status and **confirmation_code** (needed for refunds). Map PesaPal status to your system:
  - **COMPLETED** → success
  - **FAILED**, **INVALID**, **REVERSED** → failed
- **Important**: Store `confirmation_code` (e.g. in transaction metadata) for the Refund API.

---

## Recurring / Subscription Payments

- Use the same **Submit Order** endpoint with:
  - `account_number`: your reference (e.g. invoice/account id).
  - Optionally `subscription_details`: `{ "start_date": "dd-MM-yyyy", "end_date": "dd-MM-yyyy", "frequency": "DAILY"|"WEEKLY"|"MONTHLY"|"YEARLY" }`.
- IPN for recurring payments uses **OrderNotificationType** `RECURRING`. Handle the same way as IPNCHANGE (fetch status via Get Transaction Status and update your records).

---

## Refund Request

- **Endpoint**: `POST /Transactions/RefundRequest`
- **Body**: `confirmation_code`, `amount`, `username`, `remarks`.
- **Rules**: Only for **COMPLETED** payments; one refund per payment; card payments can be partial, mobile money full only.
- **confirmation_code**: From Get Transaction Status; we store it in transaction metadata so you can resolve it from `order_tracking_id` if needed.

---

## Order Cancellation

- **Endpoint**: `POST /Transactions/CancelOrder`
- **Body**: `order_tracking_id`.
- **Rules**: Only for **failed** or **pending** orders; can be cancelled only once.

---

## Callback and result page (including WebView)

When the customer finishes payment, PesaPal redirects to your **callback URL** (e.g. `https://mm.finflo.online/public/pesapal-callback`). We then:

1. Fetch the transaction status from PesaPal and update `payment_transactions`.
2. Redirect the user to the **result page** (`/pesapal-result`) with `status=success` or `status=failed`, plus `order_tracking_id` and `merchant_reference`.

The result page shows either **"Payment successful"** or **"Payment failed"** and a single **"Close window"** button. This works for:

- **Browser**: User clicks "Close window" (may close the tab if it was opened by script).
- **In-house app (WebView)**: Set `pesapal.webview_close_url` in `.env` to your app’s custom URL scheme with placeholders, for example:
  - `myapp://pesapal-done?status={status}&order_tracking_id={order_tracking_id}&merchant_reference={merchant_reference}`
  When the user taps "Close window", the page redirects to that URL. Your app should register for that scheme, intercept the navigation, read the query params, close the WebView, and then handle the result (e.g. refresh order status) in the app.

**Callback URL (base):**  
`https://mm.finflo.online/public/pesapal-callback`  
PesaPal appends `OrderTrackingId`, `OrderMerchantReference`, and `OrderNotificationType=CALLBACKURL` when redirecting.

**Multiple apps (different URL schemes):** When creating the order, each app sends its own `callback_url` including a `return_url` param, e.g.  
`https://mm.finflo.online/public/pesapal-callback?return_url=` + encodeURIComponent(`appa://pesapal-done?status={status}&order_tracking_id={order_tracking_id}&merchant_reference={merchant_reference}`)  
We pass `return_url` through to the result page; the Close button then redirects to that URL (with placeholders replaced). So App A uses `appa://...`, App B uses `appb://...`, etc., with no shared config.

---

## Environment and configuration

Set these in `.env` (or equivalent):

| Variable | Description |
|--------|-------------|
| `pesapal.consumer_key` | PesaPal merchant consumer key |
| `pesapal.consumer_secret` | PesaPal merchant consumer secret |
| `pesapal.environment` | `sandbox` or `production` (drives base URL) |
| `pesapal.ipn_url` | (Optional) Full IPN URL. Defaults to base URL from `.env` + `pesapal-ipn` (e.g. `…/public/pesapal-ipn`) |
| `pesapal.callback_base_url` | (Optional) Base URL for callback/redirects. Defaults to base URL from `.env` (e.g. `…/public`) |
| `pesapal.notification_id` | IPN id (GUID) returned from Register IPN – required for Submit Order |
| `pesapal.webview_close_url` | (Optional) Default URL when user taps "Close window". Used only when no per-order `return_url` is passed. For **multiple apps**, omit this and pass `callback_url` with `?return_url=` + your app scheme (with placeholders) when creating each order. |
| `pesapal.currency` | (Optional) Default currency; default in code is **UGX** |

**Base URLs** (used when `pesapal.api_base_url` is not set):
- Sandbox: `https://cybqa.pesapal.com/pesapalv3/api`
- Production: `https://pay.pesapal.com/v3/api`

---

## Base URL (single source)

The app **base URL** is configured in **`.env`** (e.g. `app.baseURL`). All PesaPal application URLs use this base (typically including `/public`). PesaPal config (IPN URL, callback base URL) defaults to this base when not overridden in `.env`.

Example: if base URL in `.env` is `https://mm.finflo.online/public/`, the base for links is **`https://mm.finflo.online/public`** (no trailing slash in the list below).

---

## Full PesaPal links (our endpoints)

Using **base URL** = `https://mm.finflo.online/public` (from `.env`):

| Method | Full URL | Description |
|--------|----------|-------------|
| POST | `https://mm.finflo.online/public/pesapal-ipn` | PesaPal IPN callback (do not call manually) |
| GET | `https://mm.finflo.online/public/pesapal-callback` | User redirect after payment; we fetch status and redirect to result page |
| GET | `https://mm.finflo.online/public/pesapal-result` | Result page: "Payment successful" or "Payment failed" with a single **Close window** button |
| POST | `https://mm.finflo.online/public/api/pesapal/order` | Submit order (JSON: id, amount, description, billing_address, etc.); returns `redirect_url`, `order_tracking_id` |
| GET | `https://mm.finflo.online/public/api/pesapal/status/{order_tracking_id}` | Get transaction status and update local record |
| POST | `https://mm.finflo.online/public/api/pesapal/refund` | Refund (JSON: order_tracking_id or confirmation_code, amount, username, remarks) |
| POST | `https://mm.finflo.online/public/api/pesapal/cancel` | Cancel order (JSON: order_tracking_id) |
| POST | `https://mm.finflo.online/public/api/pesapal/register-ipn` | Register IPN URL (optional JSON: url); returns `ipn_id` – set as `pesapal.notification_id` |
| GET | `https://mm.finflo.online/public/api/pesapal/ipn-list` | List registered IPNs |
| GET | `https://mm.finflo.online/public/api/pesapal/transaction/{order_tracking_id}` | Get local transaction by PesaPal order tracking id |

**Path-only reference (relative to base URL with `/public`):**

| Method | Path |
|--------|------|
| POST | `/public/pesapal-ipn` |
| GET | `/public/pesapal-callback` |
| GET | `/public/pesapal-result` |
| POST | `/public/api/pesapal/order` |
| GET | `/public/api/pesapal/status/{order_tracking_id}` |
| POST | `/public/api/pesapal/refund` |
| POST | `/public/api/pesapal/cancel` |
| POST | `/public/api/pesapal/register-ipn` |
| GET | `/public/api/pesapal/ipn-list` |
| GET | `/public/api/pesapal/transaction/{order_tracking_id}` |

---

## Quick flow

1. Set env vars and register IPN once (`POST /api/pesapal/register-ipn`). Set `pesapal.notification_id` to the returned `ipn_id`.
2. Create order: `POST /api/pesapal/order` with merchant reference, amount, description, billing_address (and optional callback_url, cancellation_url). Use **UGX** (default).
3. Redirect the customer to the returned `redirect_url`.
4. Customer pays; PesaPal calls our IPN (`/pesapal-ipn`) and redirects the customer to our callback (`/pesapal-callback`). We fetch status, update `payment_transactions`, and redirect to the **result page** (`/pesapal-result`). The result page shows "Payment successful" or "Payment failed" and a single **Close window** button. For multiple in-house apps: pass `callback_url` with `?return_url=` + your app’s scheme (with placeholders `{status}`, `{order_tracking_id}`, `{merchant_reference}`) when creating the order so each app gets its own close redirect. Optionally set `pesapal.webview_close_url` in `.env` as a single default when no per-order return_url is used.
5. For refunds: use `confirmation_code` (stored in transaction metadata after status fetch) in `POST /api/pesapal/refund`.
