Repository navigation
Conversation
Until now incoming webhooks were only stored in a write-only RemoteEvent table that nothing read. They now drive Sylius order state immediately: - orders/status_update with shipped_percent >= 100 ships the order's Sylius shipment(s), which fulfils the order once payment is complete - order cancellation in Shipmondo (orders/delete, or status_update with order_status=cancelled) cancels the Sylius order Reactions run through a tagged RemoteEventHandlerInterface framework (CompositeRemoteEventHandler + CompositeCompilerPass + the setono_sylius_shipmondo.remote_event_handler autoconfiguration tag), so integrators can add their own handlers the same way they add data mappers. Transitions go through the Sylius state-machine abstraction and are can()-guarded, so re-delivered webhooks are idempotent. An OrderResolver links a webhook back to its Sylius order via shipmondoId (falling back to the order number). The write-only RemoteEvent Sylius resource/entity/factory is removed and the WebhookConsumer now just dispatches to the handlers; see UPGRADE.md.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 2.x #13 +/- ##
======================================
Coverage ? 49.44%
Complexity ? 238
======================================
Files ? 53
Lines ? 904
Branches ? 0
======================================
Hits ? 447
Misses ? 457
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The new webhook handlers use Sylius\Component\Shipping\ShipmentTransitions and Sylius\Abstraction\StateMachine\StateMachineInterface, which were only available transitively. Declare them so the dependency-analysis job (which analyses src against the split Sylius packages) passes.
The webhook handlers obtain the entity manager via the maintained setono/doctrine-orm-trait ORMTrait (getManager()) and a ManagerRegistry, rather than injecting an ObjectManager. Also migrate the existing OrderUploader off the abandoned setono/doctrine-object-manager-trait to the same trait, and drop the abandoned package.
Live-testing the integration against the Shipmondo sandbox surfaced that the
handlers did not actually work against real webhooks — the earlier tests used a
fabricated payload shape and masked it:
- Real webhooks wrap the resource object in a `data` envelope ({webhook, data,
url}); the parser passed the whole envelope through, so handlers read
$payload['id'] as null and silently no-op'd. JWT::decode also returns nested
stdClass. WebhookParser now decodes to associative arrays and unwraps `data`,
so handlers receive the resource object directly.
- Fulfilment: the fully-shipped state (shipped_percent 100, order_status "sent")
arrives via `orders/create_shipment`, not `status_update` (which fires at
shipped_percent 0 when the order is merely packed/fulfilled). FulfillOrderHandler
now triggers on create_shipment (and status_update defensively), still gated on
shipped_percent >= 100.
- Cancellation: Shipmondo has no "cancel" action — cancelling a sales order
deletes/archives it (order_status "archived") and fires `orders/delete`.
CancelOrderHandler now keys purely on the delete action; the speculative
order_status === 'cancelled' branch is removed.
Tests are rewritten onto real payloads captured from the sandbox (fixtures under
tests/Unit/Webhook/fixtures/), and a WebhookParser test guards the envelope
unwrapping. Verified end-to-end through an Expose tunnel: a real orders/delete
cancelled the Sylius order, and a real orders/create_shipment fulfilled it.
Orders are uploaded to Shipmondo while either paid or authorized, so an authorized payment can be captured or voided later in Shipmondo. A new PaymentStateHandler reacts to those events: - orders/payment_captured -> complete the Sylius payment(s); the order's payment state becomes paid - orders/payment_voided -> cancel the order's payment state Capture is applied to the individual payments (so they end up `completed` and consistent). A void is applied to the order payment state machine (sylius_order_payment) directly: cancelling a payment makes Sylius spawn a replacement payment (its "pay again" flow), which is not what a voided authorization means here. Both are can()-guarded, so an already-paid order is a safe no-op. Declares sylius/payment (PaymentTransitions). Verified against real payloads captured from the sandbox: an authorized order -> paid on payment_captured, and -> payment state cancelled on payment_voided.
src/RemoteEvent/RemoteEvent.php -> src/Webhook/RemoteEvent.php (Setono\SyliusShipmondoPlugin\RemoteEvent\RemoteEvent -> Setono\SyliusShipmondoPlugin\Webhook\RemoteEvent); it is only used by the webhook parser/consumer/handlers, so it belongs next to them.
When an order is deleted in Shipmondo (orders/delete), it no longer exists there, so CancelOrderHandler now resets the order's shipmondoState from uploaded_to_shipmondo back to pending in addition to cancelling the Sylius order. Adds a `reset` transition (uploaded_to_shipmondo -> pending) to the OrderWorkflow. The cancel and the reset are applied independently, each can()-guarded, so either one being inapplicable doesn't block the other.
Move incoming-webhook verification and parsing into the Shipmondo SDK (setono/shipmondo-php-sdk ^2.0, firebase/php-jwt ^7.0): - The plugin's WebhookParser is now a thin adapter over the SDK's Setono\Shipmondo\Webhook\WebhookParser, which verifies the HS256 signature, reads the SMD-* headers and unwraps the `data` envelope. - Shipmondo identifies a delivery by its SMD-* headers, so the registered endpoint no longer carries resource/action query parameters; IsShipmondoWebhookRequestMatcher matches on the SMD-Resource-Type/SMD-Action headers and the now-unused HasQueryParameterRequestMatcher is removed. - RemoteEvent::getResource()/getAction(), the handlers, and WebhookRegistrar use the SDK's WebhookResourceName/WebhookAction enums instead of strings. - HS256 requires a >= 32-byte key, so SHIPMONDO_WEBHOOKS_KEY must now be at least 32 bytes (the test-app default is bumped). Documented in UPGRADE.md; consumers must re-run register-webhooks after upgrading. The SDK constraint is temporarily 2.x-dev (the new webhook API is not tagged yet) — composer validate --strict warns on this until it is switched to the tag.
Deleting a sales order in Shipmondo no longer cancels the Sylius order. The old CancelOrderHandler did two contradictory things on `orders/delete` — it cancelled the Sylius order *and* reset the upload state for re-upload — but a cancelled order is never re-upload-eligible (the provider requires state=new), so the reset was dead weight and the order just ended up cancelled. Replace it with ResetUploadStateHandler: on `orders/delete` it resets the order's Shipmondo upload state (shipmondoState -> pending) and clears its shipmondoId, so the next `upload-orders` run re-uploads it. Cancellation is a Sylius-side decision and is intentionally not driven by a Shipmondo deletion. Drops the order state-machine dependency from the handler; updates the service wiring, the test, and UPGRADE.md.
When an order is cancelled in Sylius, delete its Shipmondo sales order (if it was uploaded) and flash a message in the admin so the user knows it happened. The cancellation is caught on both state-machine backends so it survives Sylius's migration from winzou to Symfony Workflow: - a winzou `after` callback on the sylius_order `cancel` transition (the 1.14 default), prepended from the extension and guarded by hasExtension(); - a Symfony Workflow listener on `workflow.sylius_order.completed.cancel`. Both call OrderCancellationListener, which dispatches a new DeleteOrder command on setono_sylius_shipmondo.command_bus (handled synchronously by default; route it to an async transport to keep the Shipmondo call out of the cancel request). The handler deletes via the SDK's new salesOrders()->delete() and clears the order's shipmondoId. Also add the unit tests requested for UploadOrderHandler and DeleteOrderHandler by mocking the PSR-18 HTTP client and constructing a real SDK Client (the final SalesOrdersEndpoint isn't the boundary to mock — the transport is). Drop the (string) casts in their throw messages, which are equivalent mutants once covered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Incoming Shipmondo webhooks were received but nothing reacted to them — they were only written to a write-only
RemoteEventtable that was never read. This PR makes the plugin act on webhooks immediately:orders/status_updatewithshipped_percent >= 100→ ships the order's Sylius shipment(s) (via thesylius_shipmentshiptransition), which fulfils the order once payment is complete.orders/delete, orstatus_updatewithorder_status=cancelled) → cancels the Sylius order.Design
RemoteEventHandlerInterface+CompositeRemoteEventHandler+CompositeCompilerPass+ thesetono_sylius_shipmondo.remote_event_handlerautoconfiguration tag (so integrators can add handlers).Sylius\Abstraction\StateMachine\StateMachineInterfaceand arecan()-guarded, making re-delivered webhooks idempotent (Shipmondo retries on any non-200).OrderResolverlinks a webhook to its Sylius order viashipmondoId(fallback: ordernumber).RemoteEventresource/entity/factory/ORM mapping;WebhookConsumernow just dispatches to the handlers. Thesetono_sylius_shipmondo__remote_eventtable can be dropped — documented inUPGRADE.md.Test plan
All gates green on PHP 8.1: PHPStan (max), PHPUnit (36 tests), ECS, Infection (Covered MSI 100%),
lint:container.Live-verified end-to-end by posting real signed webhooks to the local endpoint against uploaded sandbox orders:
shipped_percent=100)new→fulfilled, shipmentready→shippedfulfilled(no double transition)order_status=cancelledcancelleddeleteactioncancelledNote
The fulfil trigger keys off
shipped_percent(a confirmed field on the sales-order payload;>= 100= fully shipped). The cancelorder_status=cancelledvalue is an assumption (marked@todoinCancelOrderHandler) — theorders/deleteaction is the robust cancel path that doesn't depend on it. Worth confirming the exact value against a real cancellation event.