BWH Payments (Next)

BWH Payments (Next)

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Public API

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_authorized or on_refund_update
  • return the existing record when you call them again with the same order_ref or refund_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.

Last updated 17 hours ago
Was this helpful?
Thanks!