Problem/Motivation

We currently expose a fulfilment webhook, but it'd be great to have Webhook routes automatically generated for each available transition.

That way an order can be "canceled" via the webhook (I'm not sure the "placed" transition makes sense to have but... well...

Maybe we should have a whitelist of transitions that we'd like to expose?

Additionally, I think we should change our webhooks paths, from /webhooks/fulfillment/{commerce_order}
to /webhooks/order/<transition>/{commerce_order}. that would help preventing colisions in case we expose webhooks for other entity types in the future.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

jsacksick created an issue. See original summary.

jsacksick’s picture

Issue summary: View changes
mglaman’s picture

Maybe we should add a settings form that exposes what routes are generated? 🤔 would it go on the Order Type form: "you have this workflow selected, what webhooks?" Or just a generic form that allows checking what endpoints are allowed for each order type? So a config entry that is a sequence of enabled webhooks based on each order type. On route generation, we create our routes which says "order must be this type and we handle this transition"

jsacksick’s picture

Status: Active » Needs review
FileSize
10.57 KB

The attached patch implements a different approach.

I'm not convinced it's worth exposing settings in the UI for determining which transitions are made available (perhaps in a future iteration if there's really a need for that?).

At first, I've started by dynamically generating routes based on all the existing transitions but then realized we could just reuse the same route with a transition parameter.

The attached patch introduces BC, but it's probably ok since we're in early beta stage.

First of all, currently, when an exception occurs, or when a transition is not allowed, we respond with a 400 (but a regular HTML response), not actually a JSON response, which is weird since we return a JSON response after a transition was applied successfully.

Also, I changed the route to be "/webhooks/order/{commerce_order}/{transition}" which translates to /webhooks/order//fulfill".

We use the transition ID in the route, instead of the "to" state, which makes more sense to me.

One thing we should probably try improving, is returning a 404, when a transition doesn't actually exist. Could this be done via a param converter? Although we technically don't "convert" the transition parameter?

I also added a Kernel test for the controller, but wondering if a functional doesn't make more sense.

Also, for the actual event name, should we ensure the previous fulfilment event is fired with the same name?

@mglaman: thoughts?

mglaman’s picture

I'm not convinced it's worth exposing settings in the UI for determining which transitions are made available (perhaps in a future iteration if there's really a need for that?).

+1.

The attached patch introduces BC, but it's probably ok since we're in early beta stage.

Is there a way to provide BC for our one existing route? At least for one release for anyone that is not us using this?

We use the transition ID in the route, instead of the "to" state, which makes more sense to me.

That's a good idea. We should add a follow up with somewhere to show these available webhook URLs.

One thing we should probably try improving, is returning a 404, when a transition doesn't actually exist. Could this be done via a param converter? Although we technically don't "convert" the transition parameter?

There wouldn't be much to convert. It would be more like a param validator of sorts.

I also added a Kernel test for the controller, but wondering if a functional doesn't make more sense.

It can be, but they're slower. But I guess it helps properly cover the endpoint.

Also, for the actual event name, should we ensure the previous fulfilment event is fired with the same name?

See last comment about BC. Keep it around for one release, maybe?


  1. +++ b/commerce_api.routing.yml
    @@ -19,10 +19,10 @@ commerce_api.current_store:
    -commerce_api.webhook_order_fulfillment:
    -  path: '/webhooks/fulfillment/{commerce_order}'
    +commerce_api.webhook_order_transition:
    +  path: '/webhooks/order/{commerce_order}/{transition}'
    

    This makes a lot more sense, because it allows us to open shipment workflow transitions

  2. +++ b/commerce_api.routing.yml
    @@ -30,3 +30,4 @@ commerce_api.webhook_order_fulfillment:
    +    transition: '[a-z]+'
    

    😎nice.

  3. +++ b/src/Controller/WebhookController.php
    @@ -65,39 +49,26 @@ final class WebhookController implements ContainerInjectionInterface {
    +    if (!isset($transitions[$transition])) {
    +      $message = sprintf('Cannot apply the "%s" transition to the order %s.', $transition, $commerce_order->id());
    +      return new JsonResponse(['message' => $message], 400);
         }
    

    I think this is the best we can do. It'd be interesting to check how JSONRPC returns 404s (since this is basically like making a JSONRPC endpoint)

    (I don't think it is worth making that a dependency and leveraging it over our own controller)

  4. +++ b/src/Controller/WebhookController.php
    @@ -65,39 +49,26 @@ final class WebhookController implements ContainerInjectionInterface {
    +      // Special case for the "fulfill" transition for BC reason.
    

    Let's add a "@todo remove before full release"

    Also, this prevents folks from using the proper fulfill event 😱.

    Maybe handling BC will be hard, unless we dispatch twice for that event?


We don't want to return the order object? I guess it's not required, as the backend system could then fetch it.

mglaman’s picture

jsacksick’s picture

FileSize
11.02 KB

I added the old route back. And I'm now dispatching 2 events to ensure we don't introduce a breaking change.

One thing we could do prevent dispatching 2 events for the "fulfill" transition, is using the "to" state from the transition, which matches "fulfillment".

So events would be named this way:

"commerce_api.webhook_order_canceled" instead of "commerce_api.webhook_order_cancel"
"commerce_api.webhook_order_fulfillment" instead of "commerce_api.webhook_order_fulfill".

Which is probably fine...

We don't want to return the order object? I guess it's not required, as the backend system could then fetch it.

Well, maybe we could, but we can't keep the "Location" HTTP header as it isn't supposed to be there when returning a 200. and that can cause some HTTP client to follow the redirect (See https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Location).

Do you think we should return the entiy via the entity to json thing we have?

mglaman’s picture

Agreed, we need to drop the Location header. I'm torn on returning the entity. What we return isn't even valid JSON:API, technically. We're returning just the data object not the entire document. So I'm fine returning a minimal response.

I think using the transition name for the event name makes sense. It's the same as the route naming path variable then.

So:

  • Okay with response (drop Location header and body)
  • Use transition name in the event name, which matches route parameter
  • Remove old event after beta2, which we need to tag soon
jsacksick’s picture

So? Is that RTBC?

mglaman’s picture

Assigned: Unassigned » mglaman
Status: Needs review » Reviewed & tested by the community

RTBC, pending docs updates. I'll take care of this.

mglaman’s picture

  • mglaman committed ccfe6ff on 8.x-1.x authored by jsacksick
    Issue #3137825 by jsacksick: Expose webhooks for all order transitions
    
mglaman’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Needs documentation updates

Thanks, @jsacksick!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.