Skip to content

fix: let PayPal product and price lists page past the first page - #211

Merged
thomasfosterau merged 2 commits into
trunkfrom
claude/issue-202-paypal
Oct 22, 2026
Merged

thomasfosterau merged 2 commits into
trunkfrom
claude/issue-202-paypal

Conversation

@thomasfosterau

@thomasfosterau thomasfosterau commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

This is the PayPal half of #202, built to the owner's decision there: option A, an opaque cursor (comment). The Airwallex half is the companion PR; both carry the same first commit, so whichever merges second drops it on rebase.

Commit 1, shared (179f187): core and Stripe

  • The core Page gains an optional nextCursor, present when hasMore is set. A caller passes it back as startingAfter, with the same limit and filters. The cursor is opaque, and only the provider that returned it reads it. Each ListParams.startingAfter doc comment, the core README and the core CLAUDE.md say so. This is a core minor: an additive field.
  • Stripe sets nextCursor to the last item's id, which is its own starting_after. Passing any item id keeps working, so nothing changes for existing Stripe callers.
  • Core ./internal (semver-exempt) gains pageNumberedList. It lists one page of a page-numbered vendor list and encodes the next page number, plus the explicit limit it was made for, as page=N or page=N&size=S. On the way back it refuses three things with PaymentInvalidRequestError (param: "startingAfter") before the list runs: an item id, a malformed cursor, and a cursor made for another limit, since a page number read with another page size would skip or repeat items. It is tested in tests/internal.test.ts, and its shape is asserted in tests/dist-consumer. Both providers' list helpers call it. A first version had each provider carry the paging loop itself, which fallow dupes reported as a 20-line clone across the two PRs, so the loop moved into the core, as the core's ./internal rule asks.

Commit 2: PayPal

  • Product.list and Price.list go through pageNumberedList and request the 1-based PayPal page the cursor names. The first page sends no page, so the wire is unchanged for it. Each page carries the next page's cursor while PayPal links a next page.
  • The active filter, which PayPal's list requests cannot apply (Distilled's ListProductsRequest and ListPlansRequest have no status parameter), now fails with PaymentInvalidRequestError instead of being ignored.
  • The README's "Lists return one page" note becomes "Lists page by page number", and the layer doc comments say the same.

Refs #202. The Airwallex PR closes it.

Verification

Run in a worktree after pnpm install --frozen-lockfile, on trunk at e4ed006, after the pageNumberedList rework:

  1. vp check: pass (0 errors; 119 warnings, the same set as on trunk, file by file)
  2. vp run check: pass
  3. vp run -r test:unit: pass (core 152, Airwallex 121, PayPal 167, Stripe 1369)
  4. vp run -r build: pass
  5. vp run docgen: pass ("docgen: every example compiled and ran")
  6. vp run test:coverage: pass, then fallow audit --base origin/trunk: pass ("No issues in 19 changed files")

I also ran fallow dupes on a scratch branch combining this PR and the Airwallex PR. It reports no new clone group.

Changeset

  • .changeset/page-next-cursor.md: core minor (Page.nextCursor) and Stripe patch (it sets the cursor).
  • .changeset/paypal-list-paging.md: PayPal patch (paging, and the refused active filter).

Notes

  • Page-number drift: an item created or removed between two calls can shift a later page by one. That's inherent to PayPal's page numbers, and the README says so.
  • Cursor format: page=N[&size=S] is internal to the providers and semver-exempt. Callers treat it as opaque.
  • The branch was force-pushed once, to replace the shared commit with the pageNumberedList version.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SwiB327vtGxPf5T8PzcC25

`Page` gains an optional `nextCursor`, which the caller passes back as
`startingAfter` to list the next page. Stripe sets it to the last item's
id. The core's ./internal gains `pageNumberedList`, which lists one page of
a page-numbered vendor list and encodes (and reads back) its page number
and limit as that cursor.

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwiB327vtGxPf5T8PzcC25
`Product.list` / `Price.list` now read `startingAfter` as a previous page's
`nextCursor`, request that PayPal `page`, and return the next page's cursor
while PayPal links one. An item id, a cursor for another `limit`, or the
`active` filter (which PayPal cannot apply, and which was ignored) fail with
`PaymentInvalidRequestError`.

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwiB327vtGxPf5T8PzcC25
@thomasfosterau
thomasfosterau force-pushed the claude/issue-202-paypal branch from 3b9e60e to 6fa35b3 Compare October 22, 2026 10:42
@thomasfosterau
thomasfosterau merged commit 9f1fb99 into trunk Oct 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants