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.