You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
# Issue #936: Idempotency-Key Middleware for POST/PATCH on `/api/credits`
2
+
3
+
## Summary
4
+
5
+
Add idempotency-key middleware for `POST` and `PATCH` requests on `/api/credits` to enable safe retries.
6
+
7
+
## Context
8
+
9
+
The existing credits endpoint only supports `GET` requests. There is no `POST` or `PATCH` endpoint on `/api/credits` that would modify credit balances. The only mutating credits-related endpoint is the admin `POST /api/admin/billing/credits/grant`, which already uses atomic SQLite transactions for safety.
10
+
11
+
## Minimal Fix
12
+
13
+
No code changes are required to existing routes. The `/api/credits` path currently only serves `GET` requests, which are naturally idempotent. If mutating endpoints are introduced in the future on this path, they should be wrapped with the existing `idempotencyMiddleware` from `src/middleware/idempotency.ts`, following the same pattern used by `POST /api/billing/deduct` and `POST /api/admin/billing/credits/grant`.
# Issue #941: Standardize `{items, next_cursor, total?}` envelope on `/api/invoices`
2
+
3
+
## Summary
4
+
5
+
The invoices list endpoint currently lives at `/api/billing/portal/invoices` and returns a wrapped envelope with `data` and `meta`. Clients expect a flatter, unambiguous pagination envelope: `{items, next_cursor, total?}`.
6
+
7
+
## Current behavior
8
+
9
+
**Endpoint:**`GET /api/billing/portal/invoices`
10
+
11
+
**Response shape** (after global envelope middleware):
12
+
13
+
```json
14
+
{
15
+
"success": true,
16
+
"data": [
17
+
{
18
+
"id": "uuid",
19
+
"invoiceNumber": "INV-001",
20
+
"status": "paid",
21
+
"totalAmountUsdc": "150.50",
22
+
"currency": "USDC",
23
+
"description": "...",
24
+
"periodStart": "2026-01-01T00:00:00.000Z",
25
+
"periodEnd": "2026-01-31T00:00:00.000Z",
26
+
"createdAt": "2026-01-01T00:00:00.000Z",
27
+
"updatedAt": "2026-01-01T00:00:00.000Z",
28
+
"pdfGenerated": true
29
+
}
30
+
],
31
+
"meta": {
32
+
"limit": 20,
33
+
"nextCursor": "opaque-cursor-string",
34
+
"hasMore": false
35
+
},
36
+
"requestId": "req_abc123",
37
+
"timestamp": "2026-07-28T19:00:00.000Z"
38
+
}
39
+
```
40
+
41
+
## Desired behavior
42
+
43
+
Replace the `data` + `meta` wrapper with an explicit top-level pagination envelope:
44
+
45
+
```json
46
+
{
47
+
"success": true,
48
+
"items": [
49
+
{
50
+
"id": "uuid",
51
+
"invoiceNumber": "INV-001",
52
+
"status": "paid",
53
+
"totalAmountUsdc": "150.50",
54
+
"currency": "USDC",
55
+
"description": "...",
56
+
"periodStart": "2026-01-01T00:00:00.000Z",
57
+
"periodEnd": "2026-01-31T00:00:00.000Z",
58
+
"createdAt": "2026-01-01T00:00:00.000Z",
59
+
"updatedAt": "2026-01-01T00:00:00.000Z",
60
+
"pdfGenerated": true
61
+
}
62
+
],
63
+
"next_cursor": "opaque-cursor-string",
64
+
"total": 42,
65
+
"requestId": "req_abc123",
66
+
"timestamp": "2026-07-28T19:00:00.000Z"
67
+
}
68
+
```
69
+
70
+
| Field | Type | Description |
71
+
|-------|------|-------------|
72
+
|`items`|`array`| List of invoice objects for the current page |
73
+
|`next_cursor`|`string \| null`| Opaque cursor for the next page; `null` when no more results |
74
+
|`total`|`integer \| undefined`| Optional total count of matching invoices. Omit if counting is expensive |
75
+
76
+
## Implementation notes
77
+
78
+
- The change is isolated to `GET /api/billing/portal/invoices` in `src/routes/billing/portal.ts`.
79
+
-`total` should be computed with a lightweight `COUNT(*)` query when the caller passes `?total=true` or always if the dataset is small. Omit by default to avoid full-table scans on large billing histories.
80
+
-`next_cursor` replaces `meta.nextCursor`. `meta.hasMore` is redundant once `next_cursor` is present and should be removed.
81
+
- Existing tests in `src/routes/billing/portal.test.ts` should be updated to assert the new envelope keys (`items`, `next_cursor`, optional `total`).
82
+
- No changes are required to `GET /api/billing/portal/invoices/:id`, `/line-items`, or `/pdf`.
- Cursor encoding stays the same (`encodeCursor` from `src/lib/cursorPagination.ts`), so existing clients that already parse cursors will continue to work.
88
+
- This is a **breaking change** for clients that read `response.data` or `response.meta.nextCursor`. Bump the minor version and update the SDK / docs accordingly.
0 commit comments