FLUX-SE / PayumStripe

Payum Stripe gateways (with SCA support)
MIT License
28 stars 15 forks source link

What the best way to use webhook for subscription ? #24

Open lwillems opened 2 years ago

lwillems commented 2 years ago

Hi

Currently using stripe and this package (symfony bundle) to build as subscription on stripe : According stripe documentation Webhooks should be used to provision and renewal (checkout.session.completed and invoice.paid), invoice.paid is not implement but i can register an extra action. I have implemented webhook with dedicated (unsafe) route provided, i am new to payum and i am a bit confused on how to hook my own logical. As far as i understand, when webhook is received, payment is retrieved and detail is updated (stored in doctrine in my case), but i need to perform extra steps on my customer account (another entity linked to payment), such as update subscription date.

  1. Should i need to listen on payum event (execute/postExecute ?) if so on which action can i retrieve needed info and rely on ?
  2. Should i extend an action ?

Thanks for your help Regards

Prometee commented 2 years ago

Hello @lwillems,

I'm also using this library with the Sylius plugin to provide subscriptions on our shop.

  1. create a Payum ConvertSubscriptionAction (it will be nearly the same action as this one), it will add subscription_data to the array used to create a Stripe Session object. Tips : your payment object is available on the $request object : $request->getSource() allowing you to know what is the products you will have to add to the subscription_data array.
  2. checkout.session.completed, checkout.session.async_payment_failed and checkout.session.async_payment_succeeded are already setup into this library, you just have to setup the webhook into Stripe dashboard (the webhook url is always the notify unsafe one describe in the doc). Async events are required for sepa_debit or other async payment methods.
  3. listen to invoice.payment_failed and invoice.payment_succeeded by creating two new Payum actions : InvoicePaymentFailedWebhookEventAction and InvoicePaymentSucceededWebhookEventAction extending \FluxSE\PayumStripe\Action\Api\WebhookEvent\AbstractWebhookEventAction. Example :
<?php

declare(strict_types=1);

namespace App\PayumStripe\Action\StripeCheckoutSession\Api\WebhookEvent;

use FluxSE\PayumStripe\Wrapper\EventWrapperInterface;
use Payum\Core\Exception\RequestNotSupportedException;
use FluxSE\PayumStripe\Action\Api\WebhookEvent\AbstractWebhookEventAction;
use Stripe\Event;

final class InvoicePaymentSucceededWebhookEventAction extends AbstractWebhookEventAction
{

    private foo $handler;

    public function __construct(foo $handler)
    {
        $this->handler = $handler;
    }

    protected function getSupportedEventTypes(): array
    {
        return [
            Event::INVOICE_PAYMENT_SUCCEEDED,
        ];
    }

    public function execute($request): void
    {
        RequestNotSupportedException::assertSupports($this, $request);

        /** @var EventWrapperInterface $eventWrapper */
        $eventWrapper = $request->getModel();
        $event = $eventWrapper->getEvent();

        $this->handler->handle($event);
    }
}

The service $this->handler will be in charge of making what you need to do with you own doctrine entities. Tips: you can also execute other Payum requests by implementing the Payum\Core\GatewayAwareInterface using the Payum\Core\GatewayAwareTrait and give the $this->gateway to $this->handler->handle($this->gateway, $event).

  1. I also listen for customer.subscription.updated event to update my database when the subscription is created in Stripe, when it expires or status changes.

Hope it helps you 😉

lwillems commented 2 years ago

Hi @Prometee

It was so helpful for my understanding of payum architecture. You're awesome as your bundle thanks. About subscription auto-renewal, i am not able to test, can you confirm that when it's occurs, invoice.payment_succeeded event is sent with invoice billing_reason to subscription_cycle and with initial metadata from subscription creation (token_hash) ?

Regards,

Prometee commented 2 years ago

@lwillems Yes those events are triggered, to test them I made a 2 days cycle sub.

You can store some info into the metadata of the created subscription :

# ConvertSubscriptionAction.php (#1 of my previous comment)
$details->offsetSet('subscription_data', [
    // ... other attributes like items etc
    'metadata' => [
        'my_metadata_id' => $something,
        // 'token_hash' will be set automatically by this library, so you don't have to worry about
        // it and this token hash won't be consumed and deleted after the first use because the
        // notify unsafe which will receive it is not aware of it, only a notify safe is using a token
        // and delete it after use.
    ],
]);

// dont forget to `setResult` after adding what you needed to the `$details` array
$request->setResult($details);

About the billing reason, I have several checks like this into my InvoicePaymentSucceededHandler to avoid processing invoice of the first cycle for example:

use Stripe\Invoice as StripeInvoice;

if (
    // skip the first created invoice
    $stripeInvoice->billing_reason === StripeInvoice::BILLING_REASON_SUBSCRIPTION_CREATE
) {
    return null;
}
lwillems commented 2 years ago

@Prometee Nice tips for 2 days cycle and metadata, will give it a try. So as far as i understand, you are using InvoicePaymentSucceededWebhookEventAction only for renewal and CheckoutSessionCompletedAction for 1st created subscription ? I guess you have overrided native CheckoutSessionCompletedAction ? because bundle native one is only updating payum storage entity details and you probably need to update your own entity at this step too ?

Regards

Prometee commented 2 years ago

So as far as i understand, you are using InvoicePaymentSucceededWebhookEventAction only for renewal and CheckoutSessionCompletedAction for 1st created subscription ?

Yes, because the first order is already created by Sylius in my case, then each new Stripe invoices are synced to Sylius Order for accounting reasons (we are not using Stripe invoices for our accounting).

I guess you have overrided native CheckoutSessionCompletedAction ? because bundle native one is only updating payum storage entity details and you probably need to update your own entity at this step too ?

If you are using doctrine then Payum is triggering the update of the Payment entity if needed. If your entity is linked to this entity then Doctrine will also save any update of it. If not you will have to use your doctrine manager carefully, to update your subscription entity.

oulfr commented 8 months ago

@Prometee : What is the most effective method for tracking all payments associated with subscriptions? The initial payment entries are created when a user subscribes, but how can we generate additional payment entries when the subscription is renewed? Is there a more efficient approach to handle this process?

Prometee commented 8 months ago

@oulfr if you use Checkout Session with subscription type, the initial payment will create a Stripe Subscription and it will be saved in the Payment details field. Then Stripe will handle the subscription payments on each cycles (month for ex).

You can handle subscriptions with very different maners, you can ask for a Stripe SetupIntent for the first initialization of the sub and then handle each payments by yourself. You can even let Stripe manage the subscription but it will require to sync all new Orders and Payments if you already have an accounting software of want your shop to deliver the invoice.

On my side, since my company is only selling software licences, I let Stripe handle all the cycles and sync the invoices to orders, payments into my shop. And I also manage the subscription from within the shop to pause, cancel, change the default payment etc.

oulfr commented 8 months ago

Thank you for your response. Do you have an example of how you have synced invoices with orders and payments in your shop? or what do you have implemented ? In my side I use Checkout Session to handle the subscription

Prometee commented 8 months ago

@oulfr unfortunately this part is on closed sources, I simply make some transformer services which create an order and a payment when the webhook event Stripe\Event::INVOICE_PAYMENT_SUCCEEDED is received. You will also listen for :

to be able to cover all the differents flow happening during a Subscription life cycle.

oulfr commented 8 months ago

@Prometee: It's exactly what i planned to do, Thank you for your support