bwh_payments.api is the supported import surface for other apps. Import from here, not from doctype modules, which can move.
from bwh_payments.api import (
GatewayOption,
create_payment,
from_minor_units,
get_gateway_options,
get_payment_gateways,
record_external_payment,
record_external_refund,
refund,
resolve_payment_mode,
sync_refunds,
to_minor_units,
validate_currency,
)
create_payment
def create_payment(
ref_doctype: str,
ref_docname: str,
amount: float,
currency: str,
gateway: str,
**fields,
) -> Document
Opens a gateway checkout for a document, and returns its Gateway Payment Request.
| Parameter | Description |
|---|---|
ref_doctype, ref_docname |
The document being paid for. It hears about the payment through on_payment_authorized. |
amount |
In major units: 49.99 |
currency |
ISO 4217 code, for example USD |
gateway |
A Payment Gateway Profile name, for example Stripe |
**fields |
Only company, redirect_to and customer_* fields. Anything else raises. |
Send the shopper to the returned request's order_url.
redirect_to is a path on your site, for example /orders/ORD-0001. When you set it, every gateway sends the shopper back through BWH Payments, which confirms the payment and then redirects to that path. Return pages explains this. A full URL, //host or /\host is refused, so the redirect cannot leave your site.
Raises if the gateway refuses to open a checkout, or the amount cannot be stored exactly on this site (for example KWD without Use Number Format From Currency).
on_payment_authorized
Not a function you call: a method BWH Payments calls on your document.
def on_payment_authorized(self, status: str): ...
When a Gateway Payment Request becomes Paid, BWH Payments runs ref_doc.run_method("on_payment_authorized", "Completed") once. This is the frappe/payments convention. It runs inside the transaction that settled the payment: a webhook job, the shopper's return or a status check. Enqueue heavy work.
on_refund_update
Not a function you call: a method BWH Payments calls on your document.
def on_refund_update(self, refund): ...
BWH Payments calls it each time a Gateway Refund on your document's payment changes status: when the refund is created as Pending, and when it becomes Processed or Failed. refund is the Gateway Refund document:
| Field | Holds |
|---|---|
status |
Pending, Processed or Failed |
amount |
The refunded amount, in major units |
refund_id |
The gateway's refund id. It is empty until the gateway answers. |
reference |
The reference you passed to refund(). It is empty for a refund raised on the gateway's dashboard. |
payment_request |
The Gateway Payment Request |
Use reference to find your own record of the refund. A refund raised on the gateway's dashboard reaches you with no reference, so create a record for it.
get_payment_gateways
def get_payment_gateways() -> list[str]
The names of the enabled Payment Gateway Profiles, for example ["Stripe", "Tabby"]. The list is cached in Redis for one hour, and is cleared when a profile changes.
get_gateway_options
def get_gateway_options(currency: str | None = None) -> list[GatewayOption]
The enabled gateways that can charge currency, with their logos. Without currency, it returns every enabled gateway.
class GatewayOption(BaseModel):
name: str # the Payment Gateway Profile name
title: str
logo: str | None # a site path, for example /assets/bwh_payments/images/gateways/razorpay.svg
logo_dark: str | None # for a dark page
validate_currency
def validate_currency(gateway: str, currency: str) -> None
Throws if the site has no such currency, or if gateway cannot charge in it. For example, Tabby charges only SAR, AED, KWD, BHD and QAR. Call it before you create anything for the payment.
resolve_payment_mode
def resolve_payment_mode(payment_mode: str) -> str | None
Matches a mode sent by the browser, such as "stripe", to an enabled profile name, ignoring case and spaces at either end. Returns None if no enabled gateway matches. Use it to check a shopper's choice before you call create_payment.
refund
def refund(payment_request: str, amount: float | None = None, reference: str | None = None) -> Document
Refunds amount, or the rest of the payment when amount is None, and returns the Gateway Refund. reference is your own id for the refund. It comes back on every on_refund_update.
| The gateway answers | Gateway Refund status |
|---|---|
| The refund went through | Processed |
| The refund is pending | Pending, until a refund webhook or sync_refunds settles it |
| It refuses the refund | Failed, with the error, and refund raises |
A refund larger than what is left is refused. A Pending refund counts against what is left, so two refunds at the same time cannot refund more than the payment.
sync_refunds
def sync_refunds(payment_request: str) -> None
Asks the gateway about each Pending refund on the payment, and applies the answer. Use it when a refund webhook did not arrive. Stripe and Razorpay can report a refund. Telr and Tabby settle refunds at once, so they never leave one Pending.
record_external_payment, record_external_refund
def record_external_payment(
ref_doctype: str,
ref_docname: str,
amount: float,
currency: str,
gateway: str,
status: str,
order_ref: str,
transaction_ref: str | None = None,
) -> Document
def record_external_refund(
payment_request: str, refund_id: str, status: str, amount: float, reference: str | None = None
) -> Document
Record a payment, or a refund, that another app took. Use them to move history into BWH Payments. Both:
- open no checkout and call no gateway
- do not call
on_payment_authorizedoron_refund_update - return the existing record when you call them again with the same
order_reforrefund_id
transaction_ref is the gateway's id for the settled payment. A refund webhook for a refund raised on the gateway's dashboard finds the payment through it. For Razorpay, a payment whose order_ref starts with pay_ is refunded against that payment id, so old payments stay refundable. Move from frappe/payments shows both in a patch.
to_minor_units, from_minor_units
to_minor_units(49.99, "USD") # 4999
to_minor_units(12.345, "KWD") # 12345
from_minor_units(4999, "USD") # 49.99
Exponents follow ISO 4217, not the site's Currency settings. JPY has 0 decimals, KWD has 3.
Gateway Payment Request methods
| Method | Does |
|---|---|
sync_status() |
Reads the real status from the gateway. Whitelisted. |
sync_refunds() |
The same as api.sync_refunds. Whitelisted, and needs write permission on the request. |
create_refund(amount=None, reference=None) |
The same as api.refund, on the document |
refund(amount=None) |
Refunds and returns a summary dict. Kept for older callers. |
release_if_unpaid() |
Cancels an open checkout. Returns True once the payment can no longer be paid. |
refund, create_refund and release_if_unpaid are not whitelisted. Frappe's run_doc_method checks only read permission, so a whitelisted refund would let any reader refund the payment.
Testing helpers
bwh_payments.testing lets your app's tests run the real payment, refund and webhook code against an in-memory gateway. Test with stubs shows how to use it.