Skip to content

feat(webhooks): add delivery detail endpoint and list filtering#35

Open
Jaydbrown wants to merge 3 commits into
OrbitStream:mainfrom
Jaydbrown:fix/issue-17-webhook-delivery-log
Open

feat(webhooks): add delivery detail endpoint and list filtering#35
Jaydbrown wants to merge 3 commits into
OrbitStream:mainfrom
Jaydbrown:fix/issue-17-webhook-delivery-log

Conversation

@Jaydbrown

Copy link
Copy Markdown

Closes #17

Summary

The webhook delivery log endpoint only supported a bare limit query param and had no way to fetch a single delivery's full attempt history. This PR adds GET /v1/webhooks/deliveries/:id for per-delivery inspection and extends GET /v1/webhooks/deliveries with filtering by event type, status, and date range — matching the requirements in the issue.

Changes

src/webhook/webhook.service.ts

ListDeliveriesOptions interface — named options bag replaces the single limit parameter:

export interface ListDeliveriesOptions {
  limit?: number;
  event?: string;
  status?: string;
  after?: Date;
  before?: Date;
}

listDeliveries — builds a dynamic and(...) condition from whichever filters are provided:

async listDeliveries(merchantId: string, options: ListDeliveriesOptions = {}) {
  const { limit = 50, event, status, after, before } = options;

  const conditions = [eq(webhookDeliveries.merchantId, merchantId)];
  if (event)  conditions.push(eq(webhookDeliveries.event, event));
  if (status) conditions.push(eq(webhookDeliveries.status, status as any));
  if (after)  conditions.push(gte(webhookDeliveries.createdAt, after));
  if (before) conditions.push(lte(webhookDeliveries.createdAt, before));

  return db.query.webhookDeliveries.findMany({
    where: and(...conditions),
    orderBy: [desc(webhookDeliveries.createdAt)],
    limit: Math.min(limit, 100),
  });
}

getDelivery — new method, scoped to the merchant to prevent cross-tenant access:

async getDelivery(merchantId: string, deliveryId: string) {
  return db.query.webhookDeliveries.findFirst({
    where: and(
      eq(webhookDeliveries.merchantId, merchantId),
      eq(webhookDeliveries.deliveryId, deliveryId),
    ),
  });
}

The response includes attemptLog (all attempt timestamps, HTTP statuses, and errors), deliveredAt, nextRetryAt, and every other column on webhook_deliveries.

src/webhook/webhook.controller.ts

GET /v1/webhooks/deliveries — accepts the new filter query params:

@Get('deliveries')
async listDeliveries(
  @Request() req: any,
  @Query('limit') limit?: string,
  @Query('event') event?: string,
  @Query('status') status?: string,
  @Query('after') after?: string,
  @Query('before') before?: string,
)

Example queries:

  • GET /v1/webhooks/deliveries?status=failed — all failed deliveries
  • GET /v1/webhooks/deliveries?event=payment.confirmed — only payment confirmations
  • GET /v1/webhooks/deliveries?after=2024-01-01T00:00:00Z&before=2024-02-01T00:00:00Z — date range

GET /v1/webhooks/deliveries/:id — new endpoint, returns 404 if the delivery doesn't belong to the requesting merchant:

@Get('deliveries/:id')
async getDelivery(@Request() req: any, @Param('id', ParseUUIDPipe) id: string) {
  const merchantId = await this.merchantId(req);
  const delivery = await this.webhooks.getDelivery(merchantId, id);
  if (!delivery) throw new NotFoundException('Delivery not found');
  return delivery;
}

Example response for GET /v1/webhooks/deliveries/:id

{
  "id": "...",
  "deliveryId": "018e9f2a-...",
  "merchantId": "...",
  "event": "payment.confirmed",
  "status": "failed",
  "attempts": 2,
  "sequence": 3,
  "attemptLog": [
    { "attempt": 1, "timestamp": "2024-01-15T10:00:00Z", "status": 503, "error": "HTTP 503" },
    { "attempt": 2, "timestamp": "2024-01-15T10:05:00Z", "status": null, "error": "ECONNRESET: read ECONNRESET" }
  ],
  "nextRetryAt": "2024-01-15T10:35:00Z",
  "deliveredAt": null,
  "createdAt": "2024-01-15T09:59:58Z"
}

Existing behaviour unchanged

The DLQ endpoints (GET /dead-letter, POST /dead-letter/:id/retry, DELETE /dead-letter/:id), the idempotency headers, and the ordering/retry/dead-letter logic in the queue service are untouched. The limit cap of 100 is preserved.

Test plan

  • GET /deliveries (no params) → returns up to 50 most recent deliveries
  • GET /deliveries?limit=10 → returns at most 10
  • GET /deliveries?event=payment.confirmed → only payment.confirmed events
  • GET /deliveries?status=delivered → only successfully delivered webhooks
  • GET /deliveries?after=2024-01-01T00:00:00Z → only deliveries after the given date
  • GET /deliveries/:id with a valid delivery ID belonging to the merchant → full record with attemptLog
  • GET /deliveries/:id with a non-existent ID → 404
  • GET /deliveries/:id with a delivery ID belonging to a different merchant → 404 (tenant isolation)

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.

[FEAT] Ordered Webhook Delivery with Dead Letter Queue

2 participants