Every gateway is a Single DocType whose controller extends both Document and PaymentGatewayBase:
from frappe.model.document import Document
from bwh_payments.base_class import PaymentGatewayBase
class AcmePayGatewaySettings(Document, PaymentGatewayBase): ...
| Method | Required | Called when |
|---|---|---|
create_session |
✅ | A Gateway Payment Request is created |
get_payment_status |
✅ | An app checks a payment without waiting for a webhook |
refund_payment |
✅ | A refund is issued |
handle_webhook |
✅ | The gateway posts to the webhook endpoint |
cancel_session |
Your app calls release_if_unpaid() on an open checkout |
|
get_gateway_name |
Never by BWH Payments. It's for your own use. |
Rules for every method
- Money is in major units.
49.99means $49.99. Convert with the currency helpers below, and use the same conversion for charges and refunds. - Statuses are BWH Payments statuses, already mapped from the gateway's own wording:
Pending,Paid,Not Paid,Cancelled,Expired. - To report failure, throw. Use
frappe.throwwith a message a person can act on.
create_session
def create_session(
self,
amount: float,
currency: str,
reference: str | None = None,
customer: dict | None = None,
) -> dict
Opens a hosted checkout. Runs when a Gateway Payment Request is saved without a gateway session, which normally means once, on insert.
Parameters
| Name | Type | Description |
|---|---|---|
amount |
float |
Amount to charge, in major units |
currency |
str |
ISO 4217 code, for example USD or KWD |
reference |
str |
The Gateway Payment Request name. Send it as the gateway's merchant reference. |
customer |
dict |
The shopper. See Customer below. Any field may be empty. |
Returns
| Key | Required | Description |
|---|---|---|
session_id |
✅ | The gateway's id for this checkout, stored as the request's order_ref. It must be the id the gateway's webhooks and status endpoint use. |
redirect_url |
✅ | The hosted checkout URL the shopper is sent to |
success_url |
Where the shopper returns after paying | |
cancel_url |
Where the shopper returns after cancelling | |
failure_url |
Where the shopper returns after a failed payment |
Throw when the currency isn't supported, a required setting is missing, or the gateway refuses to open a checkout. The request isn't saved, and your message is raised to the app that created it.
get_payment_status
def get_payment_status(self, session_id: str) -> str
Reads the current status from the gateway's API. It is called:
- when the shopper returns to the site
- by your own scheduled job, if you run one to settle Pending payments no one confirmed
- by
release_if_unpaid(), before an open checkout is cancelled
For gateways with no trustworthy webhook, this is the only source of truth.
Parameters
| Name | Type | Description |
|---|---|---|
session_id |
str |
The session_id your create_session returned |
Returns one of Pending, Paid, Not Paid, Cancelled, Expired.
- Map anything you don't recognise to
Pending. - Return
Paidonly when the money has actually been captured. - Only a request that is still
Pendingchanges. Once it's Paid, Cancelled, Not Paid or Expired, a later answer is ignored.
refund_payment
def refund_payment(self, session_id: str, amount: float, currency: str | None = None) -> dict
Refunds part or all of a paid checkout. Before your method runs, BWH Payments has already:
- checked the payment is refundable
- checked
amountisn't more than what's left - skipped a refund Payment Entry (or an amendment of one) that was already refunded
Parameters
| Name | Type | Description |
|---|---|---|
session_id |
str |
The checkout to refund |
amount |
float |
Amount to refund, in major units |
currency |
str |
The payment's currency |
Returns
| Key | Description |
|---|---|
refund_id |
The gateway's refund id. Every partial refund's id is kept for reconciliation. |
status |
Informational: the gateway's own refund status, for example succeeded or pending |
amount |
Informational: the amount refunded, in major units. The refunded total is increased by the amount requested, not this value. |
Throw when the gateway refuses the refund. The payment's refund total and status are left unchanged.
handle_webhook
def handle_webhook(self, payload: bytes, headers: dict) -> dict
Verifies a delivery to the webhook endpoint and says what it means.
Parameters
| Name | Type | Description |
|---|---|---|
payload |
bytes |
The raw request body, exactly as received. Verify signatures against this. |
headers |
dict |
Request headers, with title-case names such as X-Acme-Signature |
Returns
| Return | Meaning | HTTP response |
|---|---|---|
| (throws) | Verification failed | 400 |
{} |
Genuine, but not something to act on | 200 |
{"session_id", "status", "event_id"} |
Apply status to the checkout with this session_id |
200 |
event_id must stay the same when the gateway retries a delivery, and differ between events. A repeated event_id is ignored.
cancel_session
def cancel_session(self, session_id: str) -> bool
Optional. The default returns False.
Cancels an open checkout. Called by release_if_unpaid(). Return True only when the gateway confirms the checkout can no longer be paid. On any gateway error, return False instead of throwing.
If this returns False, the payment stays Pending until the gateway settles it, and release_if_unpaid() returns False.
get_gateway_name
def get_gateway_name(self) -> str
Optional. The default is the class name. Returns a human-readable name, for example "Acme Pay". BWH Payments doesn't call it. It's there for your own code.
Customer
The customer argument to create_session:
{
"ref": "CUST-0001", # linked customer id, or ""
"email": "shopper@example.com", # or ""
"phone": "+15550100", # or ""
"name": {"forenames": "Ada", "surname": "Lovelace"},
"address": {"line1": "1 Main St", "city": "Dubai", "country": "United Arab Emirates"}, # or None
}
Guest checkouts often leave fields empty. Drop the empty ones before you send the dict to the gateway.
Currency helpers
from bwh_payments.currency import (
from_minor_units,
get_minor_unit_exponent,
to_minor_units,
validate_transaction_currency,
)
| Helper | Example | Use it to |
|---|---|---|
to_minor_units(amount, currency) |
(49.99, "USD") → 4999, (12.345, "KWD") → 12345 |
Send amounts to gateways that bill in cents |
from_minor_units(amount, currency) |
(4999, "USD") → 49.99 |
Read the gateway's echoed amounts |
get_minor_unit_exponent(currency) |
"KWD" → 3, "JPY" → 0, "USD" → 2 |
Format major-unit strings, e.g. f"{amount:.{exponent}f}" |
validate_transaction_currency(currency) |
Throws for an unknown code | Check the currency exists on the site before calling out |
Exponents follow ISO 4217. They aren't read from the site's Currency settings.
URL helper
from bwh_payments.bwh_payments.utils import get_localised_url
get_localised_url(url) swaps a leading language segment, such as /en/, for the shopper's current language, so they return to the page in their own language. Use it on every return URL you send the gateway.