3

I currently have an Opayo (SagePay) Server integration where I'm apparently spoilt by an ability to simply respond to a payment notification callback with an ERROR status to reject/void a transaction if there is a problem fulfilling an order on my side. I think, there is some sort of automatic authorise then capture happening behind the scenes, where a payment is only captured if a success/OK status is returned by my notification handler.

I'm looking at how I can achieve the same with a Stripe checkout integration (Pre-built Checkout, PHP with webhooks) and it seems that I need to to use authorisation and manual capturing:

$session = \Stripe\Checkout\Session::create([
  ...
  'payment_intent_data' => [
    'capture_method' => 'manual',
  ],
]);

In the sample code and documentation (which is generally excellent, but I feel falls short here), it shows how to create a webhook to handle the completion of the checkout session:

// Handle the checkout.session.completed event
if ($event->type == 'checkout.session.completed') {
  $session = $event->data->object;

  // Fulfill the purchase...
  fulfill_order($session);
}

Curiously, the payment_status of the transaction is not checked and the example suggests you would simply fulfil your order here. I can only assume, if you are not using a delayed payment method then you can assume the payment is successful at this point, although it seems a dangerous assumption to make.

Further along in the documentation, under "Handle delayed notification payment methods Server-side", we have a fuller example, where the payment status is actually checked before fulfilling the order:

switch ($event->type) {
  case 'checkout.session.completed':
    $session = $event->data->object;

    // Save an order in your database, marked as 'awaiting payment'
    create_order($session);

    // Check if the order is paid (e.g., from a card payment)
    //
    // A delayed notification payment will have an `unpaid` status, as
    // you're still waiting for funds to be transferred from the customer's
    // account.
    if ($session->payment_status == 'paid') {
      // Fulfill the purchase
      fulfill_order($session);
    }
...

If I am using the manual capture method, how exactly should the code above be used/adapted? Presumably the payment_status will not be paid so what do I check for and how do I access the PaymentIntent to be able to capture it? Should this all happen in the checkout.session.completed event?

A code example would be great; unfortunately there is only this footnote in the documentation:

To capture an uncaptured payment, you can use either the Dashboard or the capture endpoint. Programmatically capturing payments requires access to the PaymentIntent created during the Checkout Session, which you can get from the Session object.

I have checked the online code samples but this exact scenario isn't covered.

EDIT: Adding below my understanding of the flow I need with questions:

switch ($event->type) {
  case 'checkout.session.completed':

    $session = $event->data->object;

    if ($session->payment_status == 'unpaid') {

      // We need to capture the payment if we have arrived here?
      // Is this the only scenario where we would end up here?
      // How do we get the PaymentIntent, what status if any should we check on it?

      // Attempt to fulfil the purchase here, if all good then
      // capture the payment

    } else if ($session->payment_status == 'paid') {
      // Assume this was not an auth/capture type payment?
      // There is no other scenario where we would end up here?
    }
...
jamieburchell
  • 761
  • 1
  • 5
  • 18

1 Answers1

0

If you receive the checkout.session.completed event, then the synchronous payment method completed successfully.

For async payment methods, there are other events:

  • checkout.session.async_payment_succeeded
  • checkout.session.async_payment_failed

I'm not sure why you'd use auth and capture here, since the payment is successful either upon receipt of checkout.session.completed or checkout.session.async_payment_succeeded, but if you need to, you'd set the option to do so here and then you'd need to capture the Payment Intent associated with the Checkout Session.

If you need to determine if the Charge was captured or not, you'd need to retrieve the Payment Intent and look a the Charge's captured value: https://stripe.com/docs/api/charges/object#charge_object-captured

floatingLomas
  • 8,553
  • 2
  • 21
  • 27
  • The payment may have been successful, but there may be rare occasions where I am unable to fulfil the order at the notification stage. Think booking time critical slots, checking stock etc. This is where I need to tell the payment provider not to actually take the funds. It's the "you'd need to capture the Payment Intent associated with the Checkout Session." that I'd like an example of, including knowing what object statuses to check for in the context of the "checkout.session.completed" webhook. – jamieburchell Dec 14 '20 at 14:28
  • There is no explicit example of this case; I provided all the relevant links in my answer. This describes the overall process, but starting from a Payment Intent rather than from a Checkout Session with capture set to `manual` as I already described / linked to: https://stripe.com/docs/payments/capture-later – floatingLomas Dec 17 '20 at 05:13
  • I still don't think it's clear what the session payment_status will be or should be checked to be when using the auth and capture flow, or why the examples in the docs check it is "paid" in one place and not in the other. You say that if you receive that event then the payment completed successfully, so why does one of the examples check the payment_status is paid? It's also not clear how to get at and use the PaymentIntent from the session object. – jamieburchell Dec 18 '20 at 00:07
  • Perhaps I'm supposed to capture the payment when the `payment_status` is `unpaid`, then fulfil the order after the function call? – jamieburchell Dec 18 '20 at 00:13
  • I have updated my question with sample commented code at the end which represents my trying to fathom what I need to do – jamieburchell Dec 18 '20 at 00:21