Skip to content

Events

Kessai is the payment mechanism; what a payment means for your domain is your call. The manager records the money outcome and announces it, then a subscriber decides what happens next (confirm an order, grant a membership, send a receipt).

The event names

All events carry a \Drupal\kessai\Event\PaymentEvent and are dispatched under \Drupal\kessai\Event\PaymentEvents:

Constant Fired when
INITIATED The payer is handed off to the provider. Not a money outcome.
AUTHORIZED An amount is authorized (reserved, not yet captured).
CAPTURED A payment is captured (the money is taken).
CANCELLED An authorization is cancelled before capture, or the payer cancelled the checkout.
FAILED A payment fails: the gateway declined it or errored.
EXPIRED A pending payment lapsed at its deadline without anyone resolving it.
REFUNDED A captured payment is refunded.

Five of these are money outcomes: the payment moves to a new state and the money did or did not change hands. Two need a word of their own.

All of them reach the audit trail

With kessai_audit_trail enabled, every event above is written to a tamper-evident chain, INITIATED included. It resolves nothing, but it is the last thing this site knows before the payer leaves for the provider, so a dispute over whether a payer was ever sent to pay is answered there and nowhere else.

The handoff: INITIATED

INITIATED is the odd one out: the payment stays pending and no money has moved. It marks the moment the payer leaves for the provider, so a consumer can bracket the round-trip (a booking that locks its order while the payer is away, and unlocks it if the payment fails, is cancelled or expires).

It is announced by the handoff route, not by PaymentManager::initiate(). Generating a provider URL can happen at render time, before the payer has committed to anything, so firing there would lock an order for a payment page nobody acted on. Send the payer to PaymentHandoffController::handoffUrl($payment, $return_url) instead of straight to the provider, and the deliberate click is what announces the handoff. See Gateways.

The deadline: EXPIRED

A payment carries an optional deadline (its expires timestamp). Each cron run sweeps the pending payments now past theirs, records them expired and announces EXPIRED. It is kept distinct from FAILED and CANCELLED because nobody acted: the payer walked away and the window closed, which a consumer may well want to treat more quietly than a declined card.

Expiry therefore depends on cron running. The deadline itself is set per payment by create(), and defaults to kessai.settings:default_payment_deadline (1800 seconds as shipped). Pass 0 as the $deadline argument for a payment that must never lapse, or set the config key to 0 to switch expiry off site-wide.

Expiry does not call the provider

expire() only records the local state; it never cancels anything at the gateway. If the payer completes the payment at the provider after the sweep has run, the money is taken while the payment reads expired, and no CAPTURED event fires. Keep the deadline comfortably longer than the provider's own checkout session, and reconcile from the provider rather than trusting expiry to mean "no money moved".

Reacting to a captured payment

use Drupal\kessai\Event\PaymentEvent;
use Drupal\kessai\Event\PaymentEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class ConfirmOnPayment implements EventSubscriberInterface {

  public static function getSubscribedEvents(): array {
    return [PaymentEvents::CAPTURED => 'onCaptured'];
  }

  public function onCaptured(PaymentEvent $event): void {
    $subject = $event->payment->getSubject();
    // Do whatever a captured payment means for your domain.
  }

}

Handing off to a workflow

PaymentEvent carries a handled flag. A subscriber that drives the outcome itself (for example, resuming a workflow that was waiting on the payment) calls $event->setHandled(). Any shipped default reaction should check $event->isHandled() first and stand down when another subscriber has taken ownership, so the outcome is driven exactly once.