I asked this question back in 2020: How do I programmatically "place" an order? and provided my own answer there:

Provide an event subscriber:

  public static function getSubscribedEvents() {
    $events['commerce_order.order.paid'][] = ['placeOrder'];
    return $events;

which then applies a transition to the Order:

 public function placeOrder($event) {
    $order = $event->getOrder();

This has worked well for the past 2 years but client now wants to split payment authorize and capture. This requires a later step after accepting payment to "capture" the payment. Although the payment capture seems to work and changes payment state to completed; the order balance is not reduced to $0. Removing my code to fix the Order being placed (on admin authorize and capture payments) allows capture to work correctly.

I can likely trigger my transition by something which distinguishes between the 2 types of payments; but suspect I am likely doing something wrong in how I am placing the Order.


Changed title to better clarify the requirement.

  • And nothing in the order to give a clue as to authorization status. :(
    – liquidcms
    Dec 16, 2022 at 0:17
  • Maybe you have a look in docroot/modules/contrib/commerce/modules/payment/src/EventSubscriber/OrderPaidSubscriber.php Depends on your payment provider there are a few modules that integrate payment gateways such as klarna, gmo etc.
    – DiDebru
    Dec 16, 2022 at 8:15

2 Answers 2


The order of operations here is this:

  1. You create a payment on the backend.
  2. Previously you were capturing immediately, resulting in a completed payment.
  3. When a completed payment is saved, its postSave() function requests an update to the order via the PaymentOrderUpdater service, which recalculates the related order's total_paid field at the end of the page request via its destructor (the destruct() function).
  4. The total_paid is calculated afresh. If it doesn't match the current total_paid value on the order, the order will be saved.
  5. (Note: the first time a draft order is saved, its paid_event_dispatched boolean gets initialized to FALSE in its data array. This is important to the next step.)
  6. When the order is saved by the PaymentOrderUpdater at the end of its updateOrder() function, the OrderStorage checks in the presave process to see if the total_paid is less than order equal to the order total. If so, it dispatches the OrderEvents::ORDER_PAID event you're depending on.
  7. After all order paid subscribers execute, including to apply a state transition, the order is saved.

I don't see why changing from an authorization + capture workflow to an authorization only + capture later workflow would change this process. At the end of the day, steps 3 - 7 only activate when a completed payment is saved, and the process should be no different.

If the PaymentOrderUpdater service is somehow not getting a chance to function, I suppose that would create the instance you're seeing ... but if removing your subscriber solves things, then it sounds more like your subscriber is failing. However, if it is failing, I'd expect you'd see some sort of error. The most common failure I can imagine would be the transition failing to apply because the order is not in a draft state any longer. Since your logic wouldn't get activated until destruction, I suppose the HTTP response has already been streamed, but the save ultimately fails.

You can check your PHP error logs are add debugging to your subscriber, but my hunch is these orders you want to see the balance reduced to $0 on are already placed - i.e. no longer in the draft state.

  • Thanks Ryan, I think through all of that you are suggesting I am probably using the correct event to subscribe to but don't really mention how to "place" the order. Looking through the Payment event subscriber, I see they do the same thing I do - so most likely I am doing this correctly. The question then becomes (as I changed title); how to determine when order.paid is hit from auth/capture or just capture. I think you are suggesting they shouldn't be handled differently. Yet they are. I'll post my answer below.
    – liquidcms
    Dec 16, 2022 at 23:03

Looking at default Event Subscriber from the Payment module, it seems as though I am subscribing to the correct event (commerce_order.order.paid) and I am correctly setting the Order to "placed" with:


The issue is then, that I need to execute this when I auth/capture but not when I capture only (as this partially breaks the capture process). This is my code to determine which of these has triggered the order.paid event and allows me to only "place" the order for the correct one.

  public function placeOrder($event) {
    $order = $event->getOrder();

    // Check Payment to see if we are just Capturing now.
    $payment = $this->getPayment($order_id);
    $authorized = $payment->authorized->value;

    // This only needs to be done when in Admin flow
    // - also, don't do this if just Capturing payment
    if (isStaff() && !$authorized) {

  private function getPayment($order_id) {
    $payments = \Drupal::entityTypeManager()
      ->loadByProperties(['order_id' => $order_id]);
    $payment = current($payments);
    return $payment;

This does seem a bit convoluted; so possibly there is a bug tied up in Commerce here somewhere. Commerce maintainers have suggested in the past that the admin payment flow is less supported than the customer flow - so possibly that explains the requirement for this.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.