BWH Payments (Next)

BWH Payments (Next)

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

Accept a payment

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/payments app, 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
Last updated 17 hours ago
Was this helpful?
Thanks!