# Payment Integration Guide

> [!CAUTION]
> **INTERNAL ENGINEERING DOCUMENT — DO NOT SHARE WITH MERCHANTS.**
> It names the acquiring gateways, internal tables, and routes.
>
> Merchant-facing documentation is in [`merchant/`](merchant/):
> [INTEGRATION.md](merchant/INTEGRATION.md) and [PAYMENT_FLOW.md](merchant/PAYMENT_FLOW.md).

Merchants integrate **once**, against shukriapg. Which acquiring gateway actually runs a
payment — YSP/Narada or Mastercard (MPGS) — is decided by us, server-side, from the
merchant's configuration. The merchant's code is identical either way and does not change
when they move between gateways.

```
ShukriaPayment.process({ merchantId, amount, orderId, ... })   ← the whole integration
        │
        └──→ shukriapg                                          ← always the first call
                  │
            routing lookup by merchantId
                  │
        ┌─────────┴──────────┐
      YSP                  MPGS
   (Narada)          (hosted checkout / hosted session)
```

| | |
|---|---|
| **Merchant-facing contract** | One JS call, one result envelope, one status API |
| **Gateway choice** | A row in `merchant_payment_routing` — not a merchant setting |
| **Switching a merchant** | One `UPDATE`. No merchant-side change. |
| **MPGS status** | Implemented, **never run against a live gateway** |

Companion documents: [MPGS_INTEGRATION.md](MPGS_INTEGRATION.md) for MPGS internals and
flow diagrams, and [`postman/`](postman/) for runnable collections.

---

## Contents

- [Mount prefix](#mount-prefix--read-this-first)
- [1. The integration](#1-the-integration)
- [2. The result envelope](#2-the-result-envelope)
- [3. Gateway routing](#3-gateway-routing)
- [4. Payment status](#4-payment-status)
- [5. Direct routes](#5-direct-routes-testing-and-advanced-use)
- [6. YSP specifics](#6-ysp-specifics)
- [cURL reference](#curl-reference)
- [Postman collections](#postman-collections)

---

## Mount prefix — read this first

`payment_gateway_app` is **forwarded by the main router at `/pgpayments`**
(`lib/da_product_app_web/router.ex`). Every path in this document lives under that prefix
in the integrated deployment:

| | Path |
|---|---|
| ✅ Correct | `https://shukriapg.ariticapp.com/pgpayments/pay/status/ORD_123` |
| ❌ 404 | `https://shukriapg.ariticapp.com/pay/status/ORD_123` |

A bare path reaches `DaProductAppWeb.Router`, which has no such route and answers
`Phoenix.Router.NoRouteError`. The prefix disappears only when running
`payment_gateway_app` standalone on its own port.

---

## 1. The integration

### Embed the widget

```html
<script src="/pgpayments/shukria-payment-widget.js"></script>
```

### Take a payment

```js
ShukriaPayment.process({
  merchantId:    '879400008771022',
  merchantKey:   'X9a7bC3dE6fG8hJ2kL5mN7pQ0rS4tU9v',
  amount:        6297.90,
  currency:      'AED',
  orderId:       'ORD_' + Date.now(),
  customerName:  'Meghana Rao',
  customerEmail: 'meghana@example.com',
  customerPhone: '+971501234567',

  returnUrl:   window.location.origin + '/order/confirmation',
  callbackUrl: window.location.origin + '/checkout.html',

  onSuccess: function (result) { /* result.status_code === 1200 */ },
  onFailure: function (result) { /* anything else */ },
  onClose:   function ()       { /* payer dismissed the modal */ }
});
```

`merchantId`, `merchantKey`, `amount`, `orderId`, `customerName`, `customerEmail` and
`customerPhone` are required; the widget refuses to open without them.

**That is the entire integration.** Nothing here names a gateway, and nothing changes when
a merchant is moved between them.

### What happens next

The widget encodes the payment into a token and opens an iframe on shukriapg. The entry
point decodes `credentials.user` (the `merchantId`), looks up the merchant's routing, and
redirects to the appropriate flow:

| Routed to | Payer sees |
|---|---|
| **YSP** | Our card form; details post to Narada |
| **MPGS hosted checkout** | Mastercard's hosted page (redirect or embedded) |
| **MPGS hosted session** | Our card form; the fields themselves are gateway-owned |

Whichever runs, the result comes back in the same envelope.

> [!NOTE]
> `merchantKey` is still required and is visible in the merchant's page source. It is the
> HMAC key for YSP requests. MPGS-routed merchants do not need it — their credentials are
> resolved server-side — but the widget still validates its presence, so keep sending it
> until the key moves server-side for YSP too.

---

## 2. The result envelope

Every payment route reports the same shape. This is the **existing YSP envelope**,
reproduced exactly, so integrations written against the old behaviour keep working.

```json
{
  "status_code": 1200,
  "status": "success",
  "transaction_id": "SHK-1042-1",
  "order_id": "ORD_123",
  "amount": "6297.90",
  "currency": "AED",
  "tab_id": "tab_9",
  "message": "Payment successful",
  "timestamp": "2026-08-18T12:01:14Z",
  "approval_code": "OK1234",
  "masked_card": "512345xxxxxx0008"
}
```

| Field | Notes |
|---|---|
| `status_code` | **Integer.** The field to branch on. `1200` success, `1400` failed, `1100` pending |
| `status` | `success` / `failed` / `pending` |
| `transaction_id` | Gateway transaction reference |
| `order_id` | Your own order number, as you sent it |
| `amount` | String, 2 decimal places |
| `tab_id` | Matches a result to the browser tab that started the payment |
| `approval_code`, `masked_card` | Present only when the gateway supplied them |

**`pending` is not a failure.** It means the payment has not resolved yet and may still be
settled by a gateway notification. Treat it as unresolved, not declined.

Nothing in the envelope names the acquirer. That is deliberate — the gateway is recorded in
`pg_transactions.gateway` for our reconciliation and is not merchant-facing.

### Where you receive it

**1. Widget callbacks** — the normal case. `onSuccess` / `onFailure` receive the envelope.

**2. `return_url` redirect** — the same fields as query parameters:

```
https://shop.example/thanks?status_code=1200&status=success&order_id=ORD_123&...
```

**3. `postMessage`**, if you are hosting the iframe yourself:

```js
window.addEventListener('message', function (event) {
  var result = event.data && event.data.response;
  if (result && result.status_code === 1200) { /* paid */ }
});
```

**4. [Status API](#4-payment-status)** — poll for it.

> [!IMPORTANT]
> Do not rely on the redirect or `postMessage` alone. A payer who closes the tab produces
> neither, and the payment may still have succeeded. Poll the status API, or let the
> gateway notification settle it server-side.

---

## 3. Gateway routing

Routing lives in `merchant_payment_routing`, keyed by the merchant reference the widget
sends (`credentials.user`, i.e. `merchantId`).

```elixir
# Move a merchant to Mastercard
PaymentGatewayApp.Routing.put("879400008771022", :mpgs, "UAT cutover 18 Aug")

# Roll them back — their MPGS credentials stay in place
PaymentGatewayApp.Routing.put("879400008771022", :ysp, "reverted, 3DS issue")

# Inspect
PaymentGatewayApp.Routing.gateway_for("879400008771022")   #=> :mpgs
PaymentGatewayApp.Routing.list()
```

### It fails safe

`gateway_for/1` returns `:ysp` for **every** uncertain case: no row, an inactive row, an
unrecognised gateway name, a blank reference, or the lookup raising outright.

That means an unpopulated table — or an unreachable database — leaves every merchant on the
gateway they already use, rather than routing live payments somewhere untested. Deploying
the routing code changes nothing until you insert a row.

### Before moving a merchant

1. Create their `mpgs_merchant_credentials` row (see
   [MPGS_INTEGRATION.md](MPGS_INTEGRATION.md#configuration))
2. Verify: `PaymentGatewayApp.Mpgs.verify_credentials(user_id)` → `{:ok, ...}`
3. Then, and only then, `Routing.put(merchant_ref, :mpgs)`

Rolling back is one call and takes effect on the next payment.

---

## 4. Payment status

```
GET /pgpayments/pay/status/:id
```

`:id` accepts whichever identifier you hold:

| Identifier | Example | Who has it |
|---|---|---|
| Your order number | `ORD_123` | **You** — the `orderId` you sent |
| Gateway order reference | `SHK-1042` | Allocated server-side; appears in reconciliation |
| Our transaction id | `12` | `pg_transactions.id` |

Response — the standard envelope, plus support fields:

```json
{
  "success": true,
  "payment": {
    "status_code": 1200,
    "status": "success",
    "transaction_id": "SHK-1042-1",
    "order_id": "ORD_123",
    "amount": "6297.90",
    "currency": "AED",
    "tab_id": "",
    "message": "Payment successful",
    "timestamp": "2026-08-18T12:01:14Z",
    "approval_code": "OK1234",
    "masked_card": "512345xxxxxx0008",
    "reference": "SHK-1042",
    "attempt": 1,
    "created_at": "2026-08-18T12:00:00",
    "completed_at": "2026-08-18T12:01:14"
  }
}
```

`404` with `{"success": false, "error": "not_found"}` when the reference is unknown.

Add `?refresh=true` to re-read the order from the gateway and re-settle from it — for
missed notifications or payments stuck at `pending`. That makes a live gateway call, so
don't poll it tightly.

> [!CAUTION]
> **Unauthenticated**, matching the existing `/api/status` route. Transaction ids are
> sequential and therefore enumerable. Add a merchant API key before exposing this beyond
> internal testing.

> [!NOTE]
> This endpoint currently answers for MPGS-routed payments only. YSP payments are recorded
> in `pg_transactions` but have no read path yet — see [YSP specifics](#6-ysp-specifics).

---

## 5. Direct routes (testing and advanced use)

Merchants should use the widget. These exist for testing and for back-ends that would
rather build the URL themselves.

| Method | Path | Purpose |
|---|---|---|
| `GET` `POST` | `/pay/start` | Dispatches on the merchant's MPGS configuration |
| `GET` `POST` | `/pay/checkout` | Force hosted checkout |
| `GET` `POST` | `/pay/session` | Force hosted session |
| `GET` | `/pay/status/:id` | Payment status |
| `POST` | `/pay/submit` | Internal — the card form posts here |
| `GET` `POST` | `/pay/3ds-return` | Internal — return from 3-D Secure |
| `GET` `POST` | `/pay/return` | Internal — payer returns from the gateway |
| `GET` `POST` | `/pay/cancel` | Payer abandoned the payment |
| `POST` | `/pay/callback` | Gateway notification (server-to-server) |

Parameters for the start routes:

| Parameter | Required | Notes |
|---|---|---|
| `user_id` | ✅ | The merchant's MPGS configuration key |
| `amount` | ✅ | Decimal string, e.g. `6297.90` |
| `currency` | | ISO alpha-3, defaults to `AED` |
| `order_id` | | Your order number; recorded as `order_number` |
| `customer_name` | | Split into first/last for the gateway |
| `email`, `phone` | | Passed on when present |
| `description`, `merchant_name` | | Shown on the payment page |
| `return_url` | | Where the payer lands once settled |
| `tabId` | | Echoed back in the result envelope |

The widget's token format is also accepted on these routes.

---

## 6. YSP specifics

### The callback fix

The YSP callback page previously posted the raw Narada response, which carries no
`status_code`. The widget branches on that field, so **a successful payment arriving by
that route was reported to the merchant as a failure**.

The callback now posts the raw response *merged with* the standard envelope. Raw `nar_*`
fields are preserved, so anything already reading them still works, and `status_code` is
now present.

### Environments

`ShukriaPayment.environment` selects the Narada endpoint for YSP-routed merchants. It has
no effect on MPGS-routed merchants, whose endpoint comes from their stored `region`.

| Environment | Card form submits to |
|---|---|
| `sandbox` | `https://shukriapg.ariticapp.com:4065/pgpayments` (client-side simulator) |
| `local_gateway` | `http://demo.ctrmv.com:4055/pgpayments` |
| `uat` | `https://uat2.yalamanchili.in/mpacqpg/verifyOrder` |
| `production` | `https://secure.yalamanchili.in/mpacqpg/mercpg` |

### No status endpoint yet

`GET /api/status/:payment_id` exists but routes to `MockProvider`:

```elixir
status: Enum.random(["pending", "completed", "failed"])
```

It returns a **random status** and never reads the database. Do not build against it.

YSP payment state is in `pg_transactions`, reachable by SQL:

```sql
SELECT id, order_number, closure_status, response_code, response_message,
       total_amount, created_dateTime, completed_dateTime
FROM pg_transactions
WHERE gateway = 'ysp' AND order_number = 'ORD_123'
ORDER BY id DESC LIMIT 1;
```

Giving YSP the same status endpoint is small work — the data is there and the envelope
already exists; only the read path is missing.

---

## cURL reference

```bash
BASE=https://shukriapg.ariticapp.com/pgpayments
```

### Check a payment's status

```bash
curl -s "$BASE/pay/status/ORD_123" | jq
curl -s "$BASE/pay/status/ORD_123?refresh=true" | jq       # re-read from gateway
```

Just the merchant-facing verdict:

```bash
curl -s "$BASE/pay/status/ORD_123" | jq '.payment | {status_code, status, message}'
```

### Start a payment directly (returns HTML — open in a browser)

```bash
curl -s "$BASE/pay/start?user_id=42&amount=10.00&currency=AED\
&order_id=ORD_$(date +%s)&customer_name=Test+Payer&email=test@example.com\
&phone=%2B971500000000&return_url=https://example.com/thanks"
```

### Simulate a gateway notification

```bash
curl -s -X POST "$BASE/pay/callback" \
  -H 'Content-Type: application/json' \
  -H 'X-Notification-Secret: YOUR_WEBHOOK_SECRET' \
  -d '{
    "result": "SUCCESS",
    "status": "CAPTURED",
    "order": {"id": "SHK-1042", "amount": "6297.90", "currency": "AED"},
    "transaction": {"id": "SHK-1042-1", "type": "PAYMENT", "authorizationCode": "OK1234"},
    "response": {"gatewayCode": "APPROVED"}
  }' -w '\nHTTP %{http_code}\n'
```

`200` = applied or ignored as duplicate. `401` = bad secret **or** unknown order — the two
are deliberately indistinguishable so the endpoint cannot be probed for valid references.

### Verify a merchant's MPGS credentials

```bash
curl -s -u "merchant.$MERCHANT_ID:$API_PASSWORD" \
  -X POST "https://eu-gateway.mastercard.com/api/rest/version/100/merchant/$MERCHANT_ID/paymentOptionsInquiry" \
  | jq '.result'
```

The cheapest authenticated call the gateway offers. `401` means the credentials are wrong.
Run this **before** routing a merchant to MPGS.

### Read the gateway's own view of an order

```bash
curl -s -u "merchant.$MERCHANT_ID:$API_PASSWORD" \
  "https://eu-gateway.mastercard.com/api/rest/version/100/merchant/$MERCHANT_ID/order/SHK-1042" \
  | jq '{result, status, transactions: [.transaction[]? | {id: .transaction.id, code: .response.gatewayCode}]}'
```

This is the authority. When our record and this disagree, this one is right.

---

## Postman collections

There are **two**, deliberately separated.

### `Shukria-Payments.postman_collection.json` — share this

[postman/Shukria-Payments.postman_collection.json](postman/Shukria-Payments.postman_collection.json)

Everything a merchant integration touches. **Every URL is a shukriapg URL.** No acquirer
names, no acquirer hostnames, no gateway credentials — verified by a scan, not by eye.

Set `base_url` and `pg_prefix`, then run **Widget entry point** to see which route a
merchant is dispatched to, and **Payment status** to check the envelope contract.

### `INTERNAL-Acquirer-Operations.postman_collection.json` — do not share

[postman/INTERNAL-Acquirer-Operations.postman_collection.json](postman/INTERNAL-Acquirer-Operations.postman_collection.json)

Direct authenticated calls to the acquirer, for work with no merchant-facing equivalent:
verifying credentials before routing traffic, reading the gateway's own view of an order
during a dispute, and issuing a refund or capture when the application path is unavailable.
Notification simulation lives here too, since the acquirer sends those, not merchants.

> [!CAUTION]
> `mpgs_api_password` can issue refunds. Put it in a Postman **environment**, never in the
> file, and never in a shared workspace. Refund and Capture move real money.

**Why two collections rather than one with a folder:** a merchant must never hold acquirer
credentials or call the acquirer directly. With no folder to hand over, that is difficult
to get wrong by accident.
