Cart Hooks

2 Aufrufe Markdown

Eight hooks from a customer filling a cart to the order being placed. The add gates, the pricing chain and the checkout step.

Overview

Cart pricing is a three-stage chain and each stage has its own filter. The raw inputs come first, then the priced lines, then the summary. Knowing the order puts your rule on the right link.

Picking the wrong link fails quietly. Change a total on the item filter and the summary works it out again, writing over you.

Reference

Stopping something going in the cart

gateorder.cart_add
Cart the product is validated

Runs before the product is written into the cart, with the product and cycle already validated.

Parameters 3
$productarrayThe product record: id, type, name, stock, add-ons, options. Already confirmed to be on sale.
$cyclestringThe billing cycle chosen. Confirmed to be one the product actually offers.
$itemarrayThe line about to be written: kind, product id, cycle, quantity, the configured mark, the options.
Return 1
stringA non-empty string stops the action; the text is shown to the customer as the error.
Listener
Hook::add('gate:order.cart_add', 10, function ($product, $cycle, $item) {
    // The core checks stock; add your own business rule.
    if (Acme::limitReached($product['id'] ?? 0))
        return 'The daily order limit for this product is spent.';

    return null;
});

Changing the raw inputs

filterorder.cart_inputs
Orders::buildCart() the first link

Runs on the raw order inputs, before pricing starts.

Parameters 2
$inputsarrayrefThe raw inputs, each with its own group (domain, product, add-on). This is the head of the chain: adding a line here means the next two stages price it for you.
$ucidintThe currency the pricing runs in.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:order.cart_inputs', 10, function (&$inputs, $ucid) {
    // The HEAD of the chain: a line added here gets priced by the core.
    if (Acme::freeSslCampaign())
        $inputs[] = ['group' => 'product', 'product_id' => Acme::SSL_ID, 'cycle' => 'annually'];
});

Changing the priced lines

filterorder.cart_items
Orders::buildCart() the second link

Runs after the lines were priced, before the summary is built.

Parameters 4
$itemsarrayrefThe priced lines: type, product or extension, unit price, total, taxable amount, add-ons, reseller discount. You can change a line total, and yet the grand total is worked out again in the next stage.
$user_idintThe order's owner.
$user_currencyintThe order currency.
$dealershiparrayThe customer's reseller settings, resolved.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:order.cart_items', 10,
    function (&$items, $user_id, $user_currency, $dealership) {
        // Change a line price here; the GRAND total is built in the next stage.
        foreach ($items as &$i)
            if (Acme::bundleEligible($i)) $i['price'] = Acme::bundlePrice($i);
    });

Changing the cart summary

filterorder.cart_totals
Orders::buildCart() the last link

Runs after the price summary was built — the final figures the customer sees are here.

Parameters 5
$summaryarrayrefThe price summary: the subtotal, the display subtotal, the reseller discount, the coupon discount, tax. The end of the chain: these figures reach the customer and the invoice.
$itemsarrayThe priced lines as the previous filter left them. Context you cannot change.
$subtotalfloatThe subtotal before discounts and tax.
$selectedCouponsarrayThe coupons in play. Fixed amounts are already converted into the customer's currency.
$ctxarrayThe pricing context: currency, taxation type, tax rates, exemption.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:order.cart_totals', 10,
    function (&$summary, $items, $subtotal, $selectedCoupons, $ctx) {
        // The END of the chain: what you write here is what the customer sees.
        if (Acme::minimumOrder() > (float) $summary['subtotal'])
            $summary['acme_notice'] = Acme::minimumNotice();
    });

Stopping the checkout step

gateorder.checkout
ClientCheckout the lines are unpriced

Runs before the order is placed. A guest never reaches here: the sign-in check runs earlier.

Parameters 2
$memberarrayThe signed-in member's record.
$cartItemsarrayThe raw cart lines. Not priced yet: for a rule that depends on an amount, work the prices out yourself or use the summary filter.
Return 1
stringA non-empty string stops the action; the text is shown to the customer as the error.
Listener
Hook::add('gate:order.checkout', 10, function ($member, $cartItems) {
    // The lines are NOT priced yet: keep amount-based rules out of here.
    if (Acme::fraudScore((int) ($member['id'] ?? 0)) > 80)
        return 'Your order wants a review; our team will be in touch.';

    return null;
});

Learning that an order was placed

actionorder.checkout_completed
ClientCheckout the invoice is unpaid

Runs after the order was placed. Its invoice is unpaid at this point.

Parameters 4
$order_idintThe id of the placed order.
$invoice_idintThe order's invoice. Unpaid right now; a zero-total order or one settled from the balance closes after this hook in the same request.
$pmethodstringThe resolved payment method. On a zero-total order Free; on a stored card the real gateway module's name.
$memberarrayThe record of the member who ordered.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:order.checkout_completed', 10,
    function ($order_id, $invoice_id, $pmethod, $member) {
        // The invoice is NOT paid yet: leave opening services to the payment hook.
        Crm::orderPlaced($order_id, $pmethod, (string) ($member['email'] ?? ''));
    });

Changing the coupon discount

filterorder.coupon_discount
Orders the computed discount

Runs after the coupon discount was worked out.

Parameters 1
$discountarrayrefThe computed discount. It feeds the cart summary and runs before the summary filter.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:order.coupon_discount', 10, function (&$discount) {
    Acme::capCouponDiscount($discount);
});

Changing the order number

filterorder.number
Orders it has to be unique

Runs after the order number was produced.

Parameters 1
$numberstringrefThe number produced. Changing it, guarantee uniqueness yourself: the core does not check what you produced.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:order.number', 10, function (&$number) {
    // Uniqueness is YOURS to keep: a clash breaks the order.
    $number = Acme::prefix() . '-' . $number;
});

Pitfalls

The wrong link in the chain is quietly overwritten

Pricing runs in three stages: inputs, lines, summary. Touching the grand total at the line stage is wasted — the summary stage works it out again. A line price belongs on the item filter, a final figure on the summary filter, and a new product on the input filter.

The lines are unpriced at the checkout gate

The checkout gate hands you the raw cart lines: no amounts, discounts or tax yet. Writing "stop orders above this amount" here means reading a field that is not there.

Placing an order is not paying for it

On the order-completed hook the invoice is unpaid. Opening a service, switching an add-on on or delivering to the customer here means an order that never pays is delivered anyway.

Change the number and its uniqueness is yours

The order number filter hands you a number already produced. Once you change it the core does not check it again: produce a clashing one and the order record breaks. Adding your own prefix is safe; producing the number from scratch is not.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.