Commera

Commera

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

Hooks

Hooks run inside the shopper's request, before Commera commits. Today every hook runs at checkout. They can refuse a cart, add a fee, hide or reprice a delivery option, or hide a payment method.

Register them in one commera_hooks dict in hooks.py. Each key is a hook name, and each value is a list of handlers.

commera_hooks = {
	"validate_cart": ["my_app.cart.get_cart_refusal"],
	"cart_fees": ["my_app.cart.get_cart_fees"],
}

bench migrate prints a warning and writes an Error Log for a key that is not a hook name, for example cart_fee.

Key Arguments Returns Effect
validate_cart the cart Quotation str or None A string refuses checkout and shows the string to the shopper
cart_fees the cart Quotation list[dict] Adds one charge row to the cart for each fee
delivery_options the cart Quotation, the delivery options list[dict] Hides, reprices or renames delivery options
payment_methods the cart Quotation, the payment method names list[str] Hides payment methods

Rules for every hook

  • Be fast. Commera calls these hooks on cart changes, and the shopper waits. Do not call a remote API.
  • Do not change data. Do not change or save the Quotation. The hooks can run many times for one cart, and the transaction can roll back. Do side effects in an event handler.
  • Translate messages. The shopper sees the string that you return. Wrap it in frappe._().
  • Do not raise to refuse. An exception is a crash, not a refusal. See When a hook raises.

When more than one plugin has a hook, Commera calls them in the order of frappe.get_hooks. For validate_cart, the first string wins. For the other two list hooks, each handler gets the list that the handler before it returned.

validate_cart

commera_hooks = {"validate_cart": ["my_app.cart.get_cart_refusal"]}
import frappe
from frappe import _


def get_cart_refusal(quotation) -> str | None:
	item_codes = [row.item_code for row in quotation.items]
	missing = frappe.get_all(
		"Item", filters={"name": ["in", item_codes], "my_app_sku": ["is", "not set"]}, pluck="name"
	)
	if missing:
		return _("{0} can't be ordered right now. Please remove it from your cart to check out.").format(
			missing[0]
		)
	return None

Commera calls the hook when the shopper starts checkout, for every payment mode, after it applies the delivery charge and the plugin fees. It calls it again when the shopper confirms a cash on delivery order. When the hook returns a string, no payment session opens and no order is made. Commera writes no Error Log for a refusal.

Return None to allow the cart. An empty value such as "" or False also allows it. Any other value that is not a string counts as a crash.

cart_fees

commera_hooks = {"cart_fees": ["my_app.cart.get_cart_fees"]}
from frappe import _


def get_cart_fees(quotation) -> list[dict]:
	if not quotation.get("my_app_gift_wrap"):
		return []
	return [{"description": _("Gift wrap"), "amount": 50}]
Key Required Value
description Yes The label the shopper sees
amount Yes An int or a float above zero, in the cart currency
account_head No The account for the fee. The default is Commera Settings > Charge Account Head

Commera calls the hook each time it prices the cart: a cart change, an address change, a delivery option change, and the start of checkout. Each time, it removes every plugin fee row and asks every plugin again, so a fee never adds up across calls.

Each fee becomes a row in the cart's taxes table with Type Actual, after all other rows, so no tax is calculated on it. The row has commera_plugin_fee set to 1, and it goes into the Sales Order with the rest of the cart.

  • An amount of 0 adds no row.
  • A negative amount, a missing description or an amount that is not a number counts as a crash. A fee cannot be a discount.
  • If you give no account_head and Charge Account Head is empty, checkout fails.
  • Use an account that no rate-based tax row uses.

delivery_options

commera_hooks = {"delivery_options": ["my_app.delivery.get_delivery_options"]}
def get_delivery_options(quotation, options: list[dict]) -> list[dict]:
	if not any(row.get("my_app_fragile") for row in quotation.items):
		return options
	return [option for option in options if option["title"] != "Economy"]

Each option is a copy with the keys title, amount, description, provider, service_code, is_free and is_live_rate. Return the options that the shopper can choose. Commera reads only these keys:

Key Value
title Required. The title of an option that Commera gave you. You cannot change it
amount Optional. A new price of 0 or more
label Optional. The name that the shopper sees
description Optional. A new description

You cannot add an option or change the order. An added or renamed option, a duplicate title or a negative amount counts as a crash.

The hook runs only when bwh_shipping is installed, the cart has a shipping address and the order is not a store pickup. Commera calls it when it lists the options, when the shopper selects one, and at the start of checkout. If the hook hides the selected option at checkout, the shopper sees "Your delivery option is no longer available. Please choose another one."

payment_methods

commera_hooks = {"payment_methods": ["my_app.payments.get_payment_methods"]}
from frappe.utils.data import flt


def get_payment_methods(quotation, methods: list[str]) -> list[str]:
	if flt(quotation.grand_total) <= 5000:
		return methods
	return [method for method in methods if method != "COD"]

methods holds the names of the enabled payment gateways, and COD when cash on delivery is on. You can only remove methods. Commera calls the hook on the checkout page of a signed-in shopper, at the start of checkout, and when the shopper confirms a COD order. A guest's checkout page shows every method, but the start of checkout still refuses a hidden one.

When a hook raises

Commera writes an Error Log with the title commera_hooks["<hook>"] hook failed: <handler> and removes any message that your hook printed. Then:

  • On a cart edit, the options list or the checkout page: Commera leaves out that handler's fees, options change or methods change. The cart still works.
  • At the start of checkout or a COD confirmation: the shopper sees "Something went wrong, please try again." and cannot check out.

The Error Log is written with a deferred insert, so it can appear a few minutes later.

Refuse a cancel

There is no Commera hook to refuse a cancel. Use a Sales Order before_cancel doc event, and return early for orders that are not store orders:

doc_events = {"Sales Order": {"before_cancel": "my_app.orders.before_order_cancel"}}
import frappe
from frappe import _
from commera.sdk import STORE_ORDER_TYPE


def before_order_cancel(doc, method=None):
	if doc.order_type != STORE_ORDER_TYPE:
		return
	if frappe.db.exists("My Job", {"sales_order": doc.name, "status": "In Production"}):
		frappe.throw(_("This order is already in production, so it can't be cancelled."))

The refusal applies to the shopper, the dashboard and Desk. No order_cancelled event fires.

Next steps

Last updated 3 days ago
Was this helpful?
Thanks!