Skip to content

fix: emit a consistent webhook payload from both dispatch paths#1

Open
2ro wants to merge 1 commit into
Such-Software:masterfrom
2ro:fix/consistent-webhook-payload
Open

fix: emit a consistent webhook payload from both dispatch paths#1
2ro wants to merge 1 commit into
Such-Software:masterfrom
2ro:fix/consistent-webhook-payload

Conversation

@2ro

@2ro 2ro commented Jun 7, 2026

Copy link
Copy Markdown

What

The plugin has two webhook dispatch paths and they had drifted in payload shape:

  • GrinService.DispatchWebhook (checkout / broadcast → InvoiceProcessing) sent top-level invoiceId + storeId, invoice.confirmations, and metadata.order_id.
  • GrinPaymentMonitorService.DispatchWebhookAsync (settled / invalid / expired) omitted invoiceId, storeId, and confirmations, and used metadata.medusa_cart_id instead of metadata.order_id.

Why it matters

A webhook consumer that handles both broadcast and settlement events sees two different shapes. Any consumer keyed on metadata.order_id silently drops every settlement/expiry event — the broadcast arrives, but the order never advances or cancels. (The in-code comments claiming the two paths are kept "bit-for-bit identical" only ever applied to the signature encoding, not the payload.)

Fix

Extract a single GrinWebhookPayload.Build() used by both paths so they can no longer drift.

To stay backward compatible, the order reference is emitted under both order_id and medusa_cart_id (same value), and the monitor path now also includes invoiceId, storeId, and confirmations.

No signature-format change — WebhookSignatureTests still pass.

Changes

  • New BTCPayServer.Plugins.Grin/Services/GrinWebhookPayload.cs — single source of truth for the payload.
  • GrinService.cs and GrinPaymentMonitorService.cs now both call GrinWebhookPayload.Build(invoice, eventType).

The two webhook dispatch paths drifted in payload shape:

- GrinService.DispatchWebhook (checkout / broadcast -> InvoiceProcessing)
  sent top-level `invoiceId` + `storeId`, `invoice.confirmations`, and
  `metadata.order_id`.
- GrinPaymentMonitorService.DispatchWebhookAsync (settled / invalid /
  expired) omitted `invoiceId`, `storeId` and `confirmations`, and used
  `metadata.medusa_cart_id` instead of `metadata.order_id`.

So a webhook consumer that handles both broadcast and settlement events
sees two different shapes, and any consumer keyed on `metadata.order_id`
silently drops every settlement/expiry (the broadcast arrives, but the
order never advances or cancels). The in-code comments claiming the two
paths are kept "bit-for-bit identical" only ever applied to the signature
encoding, not the payload.

Extract a single GrinWebhookPayload.Build() used by both paths so they
can't drift. To stay backward compatible the order reference is emitted
under BOTH `order_id` and `medusa_cart_id` (same value), and the monitor
path now also includes `invoiceId`, `storeId` and `confirmations`.

No signature-format change (WebhookSignatureTests still pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant