|
| 1 | +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' |
| 2 | +import { createLogger } from '@sim/logger' |
| 3 | +import { toError } from '@sim/utils/errors' |
| 4 | +import { NextResponse } from 'next/server' |
| 5 | +import { adminV1ResolveOutboxEventContract } from '@/lib/api/contracts/v1/admin' |
| 6 | +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' |
| 7 | +import { OUTBOX_EVENT_TYPES } from '@/lib/billing/webhooks/outbox-events' |
| 8 | +import { resolveDeadLetteredOutboxEvent } from '@/lib/core/outbox/service' |
| 9 | +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' |
| 10 | +import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' |
| 11 | + |
| 12 | +const logger = createLogger('AdminOutboxResolveAPI') |
| 13 | + |
| 14 | +export const dynamic = 'force-dynamic' |
| 15 | + |
| 16 | +/** |
| 17 | + * Billing Stripe events only: their readers tell a resolved row from delivered work (the admin |
| 18 | + * dashboard reports a completed cancellation with a `lastError` as `resolved`). Other domains |
| 19 | + * count every `completed` row as delivered and retry through their own admin actions. |
| 20 | + */ |
| 21 | +const RESOLVABLE_EVENT_TYPES = Object.values(OUTBOX_EVENT_TYPES) |
| 22 | + |
| 23 | +const invalidOutboxEventResponse = (message: string) => |
| 24 | + NextResponse.json({ success: false, error: message }, { status: 400 }) |
| 25 | + |
| 26 | +/** |
| 27 | + * POST /api/v1/admin/outbox/[id]/resolve |
| 28 | + * |
| 29 | + * Close a dead-lettered billing Stripe event an operator has remediated by hand or decided needs |
| 30 | + * nothing (see `resolveDeadLetteredOutboxEvent`). Use `/requeue` to retry one instead. |
| 31 | + * |
| 32 | + * Body: `{ "reason": string, "resolvedBy": string }` |
| 33 | + */ |
| 34 | +export const POST = withRouteHandler( |
| 35 | + withAdminAuthParams<{ id: string }>(async (request, context) => { |
| 36 | + const parsed = await parseRequest(adminV1ResolveOutboxEventContract, request, context, { |
| 37 | + validationErrorResponse: (error) => |
| 38 | + invalidOutboxEventResponse(getValidationErrorMessage(error, 'Invalid resolve request')), |
| 39 | + invalidJsonResponse: () => invalidOutboxEventResponse('Request body must be valid JSON'), |
| 40 | + }) |
| 41 | + if (!parsed.success) return parsed.response |
| 42 | + |
| 43 | + const { id } = parsed.data.params |
| 44 | + |
| 45 | + try { |
| 46 | + const resolved = await resolveDeadLetteredOutboxEvent( |
| 47 | + id, |
| 48 | + RESOLVABLE_EVENT_TYPES, |
| 49 | + parsed.data.body |
| 50 | + ) |
| 51 | + if (!resolved) { |
| 52 | + return NextResponse.json( |
| 53 | + { |
| 54 | + success: false, |
| 55 | + error: |
| 56 | + 'Event not found, not in dead_letter status, or not a billing Stripe event. Only dead-lettered billing Stripe events can be resolved.', |
| 57 | + }, |
| 58 | + { status: 404 } |
| 59 | + ) |
| 60 | + } |
| 61 | + |
| 62 | + recordAudit({ |
| 63 | + workspaceId: null, |
| 64 | + actorId: null, |
| 65 | + actorName: `Admin API (${parsed.data.body.resolvedBy})`, |
| 66 | + action: AuditAction.BILLING_SYNC_RESOLVED, |
| 67 | + resourceType: AuditResourceType.BILLING, |
| 68 | + resourceId: resolved.id, |
| 69 | + description: `Admin API resolved a dead-lettered ${resolved.eventType} event`, |
| 70 | + metadata: { eventType: resolved.eventType, ...parsed.data.body }, |
| 71 | + request, |
| 72 | + }) |
| 73 | + |
| 74 | + return NextResponse.json({ success: true, resolved }) |
| 75 | + } catch (error) { |
| 76 | + logger.error('Failed to resolve outbox event', { eventId: id, error: toError(error).message }) |
| 77 | + return NextResponse.json( |
| 78 | + { success: false, error: 'Failed to resolve outbox event' }, |
| 79 | + { status: 500 } |
| 80 | + ) |
| 81 | + } |
| 82 | + }) |
| 83 | +) |
0 commit comments