diff --git a/src/webhook/webhook.controller.ts b/src/webhook/webhook.controller.ts index 25a777b..f4fe643 100644 --- a/src/webhook/webhook.controller.ts +++ b/src/webhook/webhook.controller.ts @@ -30,9 +30,31 @@ export class WebhookController { /** List recent webhook deliveries for the authenticated merchant. */ @Get('deliveries') - async listDeliveries(@Request() req: any, @Query('limit') limit?: string) { + 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, + ) { const merchantId = await this.merchantId(req); - return this.webhooks.listDeliveries(merchantId, this.clampLimit(limit)); + return this.webhooks.listDeliveries(merchantId, { + limit: this.clampLimit(limit), + event, + status, + after: after ? new Date(after) : undefined, + before: before ? new Date(before) : undefined, + }); + } + + /** Get a single delivery record by its delivery ID, including full attempt log. */ + @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; } /** Bound a client-supplied limit to a sane [1, 100] range (default 50). */ diff --git a/src/webhook/webhook.service.ts b/src/webhook/webhook.service.ts index 147cfd6..2a3a238 100644 --- a/src/webhook/webhook.service.ts +++ b/src/webhook/webhook.service.ts @@ -1,9 +1,17 @@ import { Injectable, Logger } from '@nestjs/common'; import { db } from '../db/index'; import { webhookDeadLetters, webhookDeliveries, merchants } from '../db/schema'; -import { and, desc, eq } from 'drizzle-orm'; +import { and, desc, eq, gte, lte } from 'drizzle-orm'; import { WebhookQueueService } from './webhook-queue.service'; +export interface ListDeliveriesOptions { + limit?: number; + event?: string; + status?: string; + after?: Date; + before?: Date; +} + @Injectable() export class WebhookService { private readonly logger = new Logger(WebhookService.name); @@ -27,12 +35,30 @@ export class WebhookService { await this.queue.enqueue({ merchantId, event, body: data, sessionId }); } - /** Recent delivery records for a merchant (most recent first). */ - async listDeliveries(merchantId: string, limit = 50) { + /** Recent delivery records for a merchant with optional filtering. */ + 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: eq(webhookDeliveries.merchantId, merchantId), + where: and(...conditions), orderBy: [desc(webhookDeliveries.createdAt)], - limit, + limit: Math.min(limit, 100), + }); + } + + /** Single delivery record with full attempt log. */ + async getDelivery(merchantId: string, deliveryId: string) { + return db.query.webhookDeliveries.findFirst({ + where: and( + eq(webhookDeliveries.merchantId, merchantId), + eq(webhookDeliveries.deliveryId, deliveryId), + ), }); }