diff --git a/src/scripts/replay-bridge-events.ts b/src/scripts/replay-bridge-events.ts index 915e46bb0..88fe9a3b2 100644 --- a/src/scripts/replay-bridge-events.ts +++ b/src/scripts/replay-bridge-events.ts @@ -131,8 +131,8 @@ const main = async () => { : undefined for await (const event of listAllEvents({ - start_date: args.start, - end_date: args.end, + start: args.start, + end: args.end, event_type: bridgeFilter, })) { const replayEventObject: BridgeReplayEventEnvelope = { diff --git a/src/services/bridge/client.ts b/src/services/bridge/client.ts index d6d07db65..363e535bf 100644 --- a/src/services/bridge/client.ts +++ b/src/services/bridge/client.ts @@ -312,8 +312,6 @@ export interface BridgeWebhookEvent { } export interface ListEventsParams { - start_date?: string - end_date?: string event_type?: string after?: string page_size?: number @@ -563,16 +561,24 @@ export class BridgeClient { export default new BridgeClient() export async function* listAllEvents( - params?: Omit, + params?: Omit & { start?: string; end?: string }, ): AsyncGenerator { const client = new BridgeClient() + const startMs = params?.start ? new Date(params.start).getTime() : -Infinity + const endMs = params?.end ? new Date(params.end).getTime() : Infinity + + // Strip start/end — Bridge /webhook_events only supports cursor params; filter locally. + const { start: _s, end: _e, ...apiParams } = params ?? {} let cursor: string | undefined do { - const page = await client.listEvents({ ...params, after: cursor, page_size: 100 }) + const page = await client.listEvents({ ...apiParams, after: cursor, page_size: 100 }) for (const event of page.data) { - yield event + const eventMs = new Date(event.created_at).getTime() + if (eventMs >= startMs && eventMs <= endMs) { + yield event + } } cursor = page.has_more ? page.cursor : undefined diff --git a/test/flash/unit/services/bridge/client.spec.ts b/test/flash/unit/services/bridge/client.spec.ts index ef9c4c203..fc437762a 100644 --- a/test/flash/unit/services/bridge/client.spec.ts +++ b/test/flash/unit/services/bridge/client.spec.ts @@ -119,30 +119,34 @@ describe("listAllEvents", () => { ) }) - it("forwards start_date, end_date and event_type filters to every page", async () => { - listEventsSpy - .mockResolvedValueOnce({ data: [makeEvent("e1")], has_more: true, cursor: "c1" }) - .mockResolvedValueOnce({ - data: [makeEvent("e2")], - has_more: false, - cursor: undefined, - }) + it("filters events locally by start/end window and does not forward start/end to Bridge API", async () => { + const inWindow = makeEvent("e1") // created_at: "2026-05-01T10:00:00Z" — inside window + const tooEarly = { ...makeEvent("e2"), created_at: "2026-04-30T23:59:59Z" } - const params = { - start_date: "2026-05-01T00:00:00Z", - end_date: "2026-05-02T00:00:00Z", - event_type: "transfer.completed", - } + listEventsSpy + .mockResolvedValueOnce({ data: [inWindow, tooEarly], has_more: true, cursor: "c1" }) + .mockResolvedValueOnce({ data: [makeEvent("e3")], has_more: false, cursor: undefined }) const drained: BridgeWebhookEvent[] = [] - for await (const event of listAllEvents(params)) { + for await (const event of listAllEvents({ + start: "2026-05-01T00:00:00Z", + end: "2026-05-02T00:00:00Z", + event_type: "transfer.completed", + })) { drained.push(event) } - expect(drained).toHaveLength(2) + // e2 is before the window start — only e1 and e3 pass through + expect(drained.map((e) => e.id)).toEqual(["e1", "e3"]) expect(listEventsSpy).toHaveBeenCalledTimes(2) for (const call of listEventsSpy.mock.calls) { - expect(call[0]).toMatchObject(params) + // start/end must NOT be sent to Bridge — it only understands cursor params + expect(call[0]).not.toHaveProperty("start") + expect(call[0]).not.toHaveProperty("end") + expect(call[0]).not.toHaveProperty("start_date") + expect(call[0]).not.toHaveProperty("end_date") + // event_type is still forwarded (mapped to category inside listEvents) + expect(call[0]).toMatchObject({ event_type: "transfer.completed" }) } }) diff --git a/test/flash/unit/services/bridge/webhook-server/replay.spec.ts b/test/flash/unit/services/bridge/webhook-server/replay.spec.ts index 9b9988b64..e74c7898b 100644 --- a/test/flash/unit/services/bridge/webhook-server/replay.spec.ts +++ b/test/flash/unit/services/bridge/webhook-server/replay.spec.ts @@ -12,7 +12,7 @@ jest.mock("@services/logger", () => ({ })) jest.mock("@services/mongoose/bridge-replay-log", () => ({ - createBridgeReplayLog: jest.fn(), + createBridgeReplay: jest.fn(), })) jest.mock("@services/bridge/webhook-server/routes/deposit", () => ({ @@ -173,7 +173,7 @@ describe("replayHandler", () => { ] beforeEach(() => { - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ id: "log-001" }) + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-001" }) }) it("routes outbound withdrawal payment_processed replay to transfer handler", async () => { @@ -182,7 +182,7 @@ describe("replayHandler", () => { ;(res.json as jest.Mock)({ status: "success" }) return Promise.resolve(res) }) - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ id: "log-wd-001" }) + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-wd-001" }) const res = makeRes() await replayHandler( @@ -227,7 +227,7 @@ describe("replayHandler", () => { describe("dry_run mode", () => { it("returns 200 without calling any handler", async () => { - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-dry-001", }) @@ -241,20 +241,20 @@ describe("replayHandler", () => { }) it("persists a dry-run log entry with httpStatus 0", async () => { - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-dry-002", }) const res = makeRes() await replayHandler(makeReq({ ...BASE_BODY, dry_run: true }), res) - expect(ReplayLog.createBridgeReplayLog).toHaveBeenCalledWith( + expect(ReplayLog.createBridgeReplay).toHaveBeenCalledWith( expect.objectContaining({ httpStatus: 0, dryRun: true }), ) }) it("returns 500 when dry-run log creation fails", async () => { - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue( + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue( new Error("db error"), ) @@ -277,7 +277,7 @@ describe("replayHandler", () => { }) it("returns the handler's status code and response body", async () => { - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-live-001", }) @@ -290,14 +290,14 @@ describe("replayHandler", () => { }) it("persists a replay log with triage context (operator + time window)", async () => { - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-live-002", }) const res = makeRes() await replayHandler(makeReq(BASE_BODY), res) - expect(ReplayLog.createBridgeReplayLog).toHaveBeenCalledWith( + expect(ReplayLog.createBridgeReplay).toHaveBeenCalledWith( expect.objectContaining({ operator: "ops@example.com", timeWindowStart: new Date("2026-05-01T00:00:00Z"), @@ -310,7 +310,7 @@ describe("replayHandler", () => { }) it("includes log_id in the response so ops can trace the replay", async () => { - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-trace-007", }) @@ -322,7 +322,7 @@ describe("replayHandler", () => { }) it("returns 500 when log creation fails after a successful handler run", async () => { - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue( + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue( new Error("mongo down"), ) @@ -340,7 +340,7 @@ describe("replayHandler", () => { return Promise.resolve(res) }, ) - ;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ id: "log-4xx" }) + ;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-4xx" }) const res = makeRes() await replayHandler(makeReq(BASE_BODY), res)