# PayRam Module

PayRam hosted checkout and webhook payment module.

Sources: `src/core/modules/payram/payram.class.php`, `src/core/modules/payram/payram.api.php`, `src/core/modules/payram/payram.js`, `src/core/modules/payram/payram.php`

## File Map

| Concern | File | Role |
|---|---|---|
| Configuration | `src/settings/config.php`, `config.example.php` | PayRam connection and behavior settings |
| Provider bootstrap | `src/core/modules/payram/payram.php` | Loads models/classes and dispatches the webhook |
| API wrapper | `src/core/modules/payram/payram.api.php` | Performs reusable PayRam HTTP requests |
| Provider adapter | `src/core/modules/payram/payram.class.php` | Creates checkout, renders modes, validates status, and processes callbacks |
| Browser widget | `src/core/modules/payram/payram.js` | Polls status and controls popup/new_tab widgets |
| Payment helper | `src/core/modules/PaymentModule/PaymentHelpers.php` | Loads the provider and returns provider HTML |
| Cart controller | `src/pages/private/cart/cart.controller.php` | Starts checkout and serves authenticated status polling |
| Cart view | `src/pages/private/cart/cart.view.php` | Shows provider content and hides normal cart panels during checkout |
| Temporary transaction model | `src/model/base/order-temp-txn.model.php` | Stores provider-to-order references |
| Crypto data model | `src/model/base/order-crypto-data.model.php` | Stores on-chain transaction details |
| Schema | `installation/cms.sql` | V9 PayRam transaction tables |

## Configuration

The module is configured in a string-keyed `PAYMENT_MODULES` entry. The selected key is persisted and is used in callback URLs; `payment_module => 'payram'` only selects this implementation:

| Key | Description |
|---|---|
| `enabled` | Enables the provider |
| `label` | Store button label |
| `prod_mode` | Controls non-admin checkout visibility and development logs; also selects the default API URL |
| `base_url` | Optional PayRam server URL; defaults to the test or production server based on `prod_mode` |
| `api_key` | PayRam project API key |
| `display_mode` | `popup`, `new_tab`, `iframe`; defaults to `popup` |
| `order_prefix` | Prefix stored with local PayRam references to isolate multiple web instances sharing one PayRam API |
| `partial_payment_tolerance` | Percentage of underpayment accepted for `PARTIALLY_FILLED`; defaults to `1.5` |
| `timeout` | PayRam API request timeout in seconds |
| `polling_interval` | Browser status polling interval in seconds; defaults to `20` |
| `discount` | Shared payment discount or fee percentage |

The PayRam server settings control available networks and currencies. The CMS sends the calculated final price as `amountInUSD`; the hosted PayRam page handles customer token and network selection.

## Request Flow

1. `PaymentHelpers::startPayment()` loads `payram/payram.php` and calculates the final order price.
2. `PayramPayment::createRequest()` reuses the order's temporary PayRam reference when available.
3. A first request posts `customerEmail`, the account UID as `customerID`, and `amountInUSD` to `/api/v1/payment`.
4. The returned `reference_id` is stored in `cms_orders_temp_txn` for the order and payment method.
5. On later PayRam button presses, the existing reference is checked against PayRam before the hosted URL is rendered to check if have been already paid. This manual check is limited to once every 10 seconds per PHP session.
6. The returned hosted URL is rendered as an iframe, popup, or new_tab according to configuration. The cart and pending-order panels are hidden while the provider content is active.

The iframe and new-tab modes poll the authenticated cart status endpoint at `polling_interval` seconds. A confirmed payment redirects to purchase history. A closed popup or new-tab provider page refreshes the cart; if the order is still unpaid, the customer can retry PayRam or choose another method.

## Reference Identifiers

PayRam returns a provider reference such as a UUID. The CMS stores a local reference:

```text
local_reference = order_prefix + provider_reference
```

The local reference is stored in:

- `cms_orders_temp_txn.txn_id` while the payment session exists.
- `cms_orders.txn_id` after the order is paid.
- `cms_payments.txn_id` as the provider audit transaction ID.
- `cms_orders_crypto_data.reference_id` for crypto transaction details.

PayRam API requests and hosted URLs always use the raw provider reference. Webhooks receive the raw provider reference, prepend the configured prefix, and then look up `cms_orders_temp_txn`. A reference absent from that table is acknowledged and ignored. This isolates multiple websites using the same PayRam instance.

Successful, cancelled, and expired temporary rows are intentionally retained for audit and callback investigation.

## Checkout Modes

`PaymentHelpers::startPayment()` returns provider HTML when PayRam does not new_tab or terminate the request. The cart view renders that HTML and hides the `cart-content` and `pending-order` panels while keeping their block structure intact.

| Mode | Behavior |
|---|---|
| `iframe` | Embeds the hosted PayRam checkout and polls the site's status endpoint every 10 seconds |
| `popup` | Shows instructions and the provider URL, opens a popup, detects closure, then checks status and reloads or redirects |
| `new_tab` | Shows instructions, opens a new tab, monitors status and tab closure |

The status polling URL is:

```text
WEB_URL + LANGUAGE + '/cart?payment_status=1&order_id=<order_id>'
```

It requires the authenticated account session, resolves the order through `OrderModel::getForAccount()`, loads the provider stored in `cms_orders.payment_method` through `PaymentHelpers`, and performs the provider status check.

PayRam remains available for status refreshes on existing orders even when `prod_mode` hides it from non-admin checkout.

PayRam HTTP calls are implemented by `src/core/modules/payram/payram.api.php`. `PayramPayment` resolves the configured or environment-default base URL and passes it to the wrapper; the wrapper does not select PayRam environments. It returns the HTTP status, decoded body, raw response, and cURL error. Webhook validation remains in `payram.class.php`.

## Webhook

Register `WEB_URL.'store?action=ipn&method=<payment-key>'` in the PayRam project webhook settings. Configure the key manually for each payment entry.

The endpoint:

1. Reads the raw JSON request body.
2. Validates `X-Payram-Signature` using HMAC-SHA256 and the project API key. The legacy `API-Key` header is also accepted.
3. Finds the local order through `cms_orders_temp_txn`.
4. Verifies the account customer ID and reference ID.
5. Calls `GET /api/v1/payment/reference/<reference_id>` using the API key.
6. Uses the verified PayRam status and USD-filled amount, falling back to `filledAmount` when `filledAmountInUsd` is empty.
7. Stores each available on-chain transaction in `cms_orders_crypto_data`.
8. Completes `FILLED` and `OVER_FILLED` statuses through `PaymentModule::completePayment()`, or approves a `PARTIALLY_FILLED` payment within `partial_payment_tolerance` using PayRam-specific amount mapping.
9. Keeps the successful temporary transaction row and stores the prefixed PayRam reference in `cms_orders.txn_id`.

Incoming provider references are prefixed before the temporary-transaction lookup. Webhooks for references absent from this table are acknowledged and ignored. `prod_mode` controls console output only; provider logs are still written in production. Duplicate `data_received` entries are filtered by critical payment fields.

## Status Rules

| Status | Default behavior |
|---|---|
| `OPEN` | Audit/log and acknowledge; never fulfill |
| `PARTIALLY_FILLED` | Approved within `partial_payment_tolerance`; otherwise logged and ignored |
| `FILLED` | Fulfill through the shared payment completion path |
| `OVER_FILLED` | Fulfill through the shared payment completion path and return overfill details |
| `CANCELLED` | Audit/log and acknowledge; never fulfill |

The default partial tolerance is `1.5%`. When `filledAmountInUsd` is empty, the numeric `filledAmount` is used for the near-full tolerance decision.

PayRam-specific mapping handles the tolerance without changing `PaymentModule.php`:

- `mc_gross`: expected CMS amount when a near-full partial payment is approved.
- `net_money`: actual received amount, preferring `filledAmountInUsd` and falling back to `filledAmount`.
- `payment_gross`: provider-reported filled amount.

This preserves the shared module's exact amount contract for every other payment provider.

## Duplicate Callback Logs

PayRam can deliver repeated `OPEN` and `PARTIALLY_FILLED` callbacks. File-log deduplication uses only these critical fields:

- `paymentState`
- `filledAmount`
- `filledAmountInUsd`
- `confirmationRequired`

Changing unrelated payload fields does not create another log. A different critical signature creates a new log, allowing separate confirmation-required states to remain visible. The database payment audit uses `REPLACE` semantics and is independent from file-log deduplication.

## Storage

| Table | Purpose |
|---|---|
| `cms_orders_temp_txn` | Maps a provider reference, including its configured local prefix, to an order and payment method |
| `cms_orders_crypto_data` | Stores network, token, transaction hash, addresses, block, amount, and confirmation data |
| `cms_orders.txn_id` | Stores the prefixed local PayRam reference after successful completion |
| `cms_payments` | Stores the normal provider audit row |

`cms_orders_crypto_data` stores one row per unique provider transaction hash and includes the order, payment method, local reference, network, token/currency, transaction hash, source and destination addresses, block number, token amount, USD amount, and confirmation counts.

Do not use the blockchain transaction hash as `cms_orders.txn_id`; the local PayRam reference is the stable payment-session identifier.

## Change Rules

- Keep PayRam-specific amount handling inside `PayramPayment`.
- Do not change `PaymentModule.php` for PayRam-only behavior.
- Keep webhook validation synchronous and re-query PayRam before fulfillment.
- Keep unknown prefixed references harmless and acknowledged.
- Keep successful, cancelled, and expired temporary references for audit.
- Keep API keys out of provider payload logs.
- Update this document when the integration contract changes.
