This page shows how your app takes a payment: create the request, send the shopper to the gateway, and react when the money arrives.
Requirements
- BWH Payments installed on your site
- A gateway set up and enabled, for example Stripe
- A document the payment is for, such as an order
1. Create the payment
Call create_payment from your server code:
from bwh_payments.api import create_payment
def start_checkout(order):
payment_request = create_payment(
ref_doctype=order.doctype,
ref_docname=order.name,
amount=order.grand_total, # major units: 49.99, not 4999
currency=order.currency,
gateway="Stripe", # the Payment Gateway Profile name
customer_email=order.email,
customer_forenames=order.first_name,
customer_surname=order.last_name,
redirect_to=f"/orders/{order.name}", # optional: where the shopper lands afterwards
)
return payment_request.order_url
create_payment inserts a Gateway Payment Request. While it saves, BWH Payments opens a checkout session at the gateway and stores:
| Field | Holds |
|---|---|
order_ref |
The gateway's session id |
order_url |
The hosted checkout URL. Send the shopper here. |
status |
Pending |
If the gateway refuses to open a checkout, create_payment raises and nothing is saved.
Extra fields
Apart from the five arguments, create_payment accepts only company, redirect_to and fields that start with customer_. The customer fields are customer_email, customer_phone, customer_forenames, customer_surname, customer_ref (a Customer) and customer_address (an Address). Gateways use them to fill in the checkout form, and leave out any that are empty.
Choose a gateway
List the gateways a shopper can pick from, with their logos:
from bwh_payments.api import get_gateway_options, validate_currency
get_gateway_options("SAR") # only the gateways that can charge SAR
validate_currency("Tabby", order.currency) # throws before any checkout is opened
get_payment_gateways() returns only the names. Both read a list that is cached in Redis and cleared when a profile changes.
2. Send the shopper to the gateway
Redirect the browser to order_url. The shopper pays on the gateway's page, then comes back:
- With
redirect_to, BWH Payments confirms the payment with the gateway, then redirects to your path. - Without it, the gateway sends the shopper to the Success URL or Failure URL in its settings.
Return pages covers both.
3. React when it's paid
When a request becomes Paid, BWH Payments calls on_payment_authorized on the document you passed as ref_doctype and ref_docname:
class SalesOrder(Document):
def on_payment_authorized(self, status: str):
# status is "Completed"
frappe.enqueue(
"your_app.orders.fulfil", order=self.name, enqueue_after_commit=True
)
- It's called once per payment, whether a webhook, a status check or a scheduled sync settled it.
- It's the same convention as the
frappe/paymentsapp, so a document written for that app works unchanged. - If your document has no such method, nothing happens.
Or use a hook
To react to every status, not only Paid, add a doc_events hook instead:
# hooks.py
doc_events = {
"Gateway Payment Request": {
"on_update": "your_app.payments.on_payment_request_update",
}
}
def on_payment_request_update(doc, method=None):
if doc.has_value_changed("status") and doc.status == "Not Paid":
frappe.enqueue(release_stock, order=doc.ref_docname, enqueue_after_commit=True)
Check has_value_changed. The hook runs on every save, not only when the status changes.
4. Settle payments nobody confirmed
Some shoppers close the tab after paying. A webhook usually settles those payments, but Telr has no trusted webhook and Tabby captures only on a status check. Run a scheduled job that calls sync_status() on requests that are still Pending:
def sync_pending_payments():
for name in frappe.get_all(
"Gateway Payment Request",
filters={"status": "Pending", "creation": ("<", frappe.utils.add_to_date(None, minutes=-15))},
pluck="name",
):
try:
frappe.get_doc("Gateway Payment Request", name).sync_status()
frappe.db.commit()
except Exception:
frappe.db.rollback()
frappe.log_error(title=f"Payment sync failed: {name}")
Refund a payment
from bwh_payments.api import refund
refund(payment_request.name) # the rest of the payment
gateway_refund = refund(payment_request.name, amount=10, reference=my_refund.name) # part of it
Each refund is a Gateway Refund with the status Pending, Processed or Failed. A refund that the gateway accepts but has not finished stays Pending until a refund webhook or sync_refunds settles it. Only a Processed refund changes the request to Partially Refunded or Refunded.
BWH Payments refuses a refund larger than what is left, and a Pending refund counts against what is left. To follow a refund, add on_refund_update to your document:
class SalesOrder(Document):
def on_refund_update(self, refund):
# refund.reference is the id you passed, or empty for a refund raised on the gateway's dashboard
if refund.status == "Processed":
mark_refunded(self, refund.amount)
Next steps
- Return pages: confirm the payment when the shopper comes back
- Public API: every function in
bwh_payments.api