BWH Payments

BWH Payments

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

Overview

BWH Payments is a Frappe app for taking payments through hosted checkout pages. Your app asks for a payment, the shopper pays on the gateway's own page, and BWH Payments tells you when the money has arrived. You can also refund all or part of a payment later.

It ships with four gateways, and you can add your own by writing one Python class.

Built-in gateways

Gateway Best for Page
Stripe Cards and wallets, worldwide Stripe
Razorpay Cards, UPI, netbanking and wallets in India Razorpay
Telr Cards and local methods across the GCC Telr
Tabby Buy now, pay later in the Middle East Tabby

How a payment works

Every payment is a Gateway Payment Request document. Its status moves the same way whichever gateway is behind it:

stateDiagram-v2
    [*] --> Pending: checkout opened
    Pending --> Paid
    Pending --> NotPaid
    Pending --> Cancelled
    Pending --> Expired
    Paid --> PartiallyRefunded: refund
    Paid --> Refunded: full refund
    PartiallyRefunded --> Refunded: refund the rest
    NotPaid: Not Paid
    PartiallyRefunded: Partially Refunded
  1. Your app creates a Gateway Payment Request. BWH Payments asks the gateway for a checkout session and stores the checkout URL on the request.
  2. You redirect the shopper to that URL. They pay on the gateway's own page.
  3. The request moves out of Pending. This happens when the gateway sends a signed webhook, or when your app asks for the latest status.
  4. You react to the status change, for example by creating an order when the request becomes Paid.
payment_request = frappe.get_doc(
	{
		"doctype": "Gateway Payment Request",
		"gateway": "Stripe",  # a Payment Gateway Profile
		"amount": 49.99,
		"currency_code": "USD",
		"ref_doctype": order.doctype,  # any DocType: the thing being paid for
		"ref_docname": order.name,
		"customer_email": "shopper@example.com",
	}
).insert(ignore_permissions=True)

# Send the shopper here:
payment_request.order_url

Only System Manager can create Gateway Payment Requests. Accounts User can read and edit them. A website checkout therefore inserts with ignore_permissions=True, as above. Check what the shopper is allowed to buy before you do that.

To react when the payment settles, add a doc_events hook on Gateway Payment Request → on_update:

def on_payment_request_update(doc, method=None):
	if doc.has_value_changed("status") and doc.status == "Paid":
		frappe.enqueue(create_order, payment_request=doc.name, enqueue_after_commit=True)

Check has_value_changed, because the hook fires on every save, not only when the status changes. Enqueue heavy work. The hook runs inside the webhook request, and if it throws, the status change is rolled back.

When the shopper comes back to your site, call payment_request.sync_status() from a POST request. That reads the real status from the gateway. Never trust the redirect itself, because anyone can load a success URL. Return pages shows how to build the pages shoppers come back to.

Build your own gateway

A gateway is a Frappe Single DocType that holds the gateway's credentials. Its controller class extends PaymentGatewayBase and implements four methods:

Method What it does
create_session Opens a hosted checkout and returns its URL
get_payment_status Asks the gateway whether the shopper paid
refund_payment Refunds all or part of a payment
handle_webhook Verifies a gateway callback and reports what happened

The status rules, webhook endpoint, replay protection and refund bookkeeping are already done. Your class only talks to the gateway.

→ Build a payment gateway

Install

You need a site on Frappe 16 or later.

bench get-app https://github.com/bwhtech/bwh_payments
bench --site your.site install-app bwh_payments

Next steps

Last updated 3 weeks ago
Was this helpful?
Thanks!