PayPal Server SDK migration: the 1.0.0 renames and the 2.0.0 options object
paypal sdk migration 6958 tracked change records
The PayPal Server SDK (@paypal/paypal-server-sdk) shipped two breaking majors back to back. The 1.0.0 release renamed every controller method: the ordersCreate and authorizationsCapture style names became createOrder and captureAuthorizedPayment, fifteen renames across the orders and payments controllers. Then 2.0.0 kept the new names but changed the calling convention itself: every method that took positional parameters now takes a single options object. Code migrated by hand for 1.0.0 broke again a release later. Both halves are mechanical, both touch every call site, and that is the profile of a codemod job.
The 1.0.0 controller renames
This list is read directly from the mendapi migration pack registry (paypal-server-sdk-v1-controller-renames) at page build time, so it matches what the codemod actually does:
- ordersController: rename legacy method calls to their 1.0.0 names
- paymentsController: rename legacy method calls to their 1.0.0 names
The full fifteen-pair rename map was verified token by token against the SDK source at tags 0.6.1 and 1.0.0: ordersCreate to createOrder, ordersCapture to captureOrder, authorizationsVoid to voidPayment, capturesRefund to refundCapturedPayment, and the rest of both controller families. Each rename applies from a whitelisted map and only on calls anchored to an ordersController. or paymentsController. member chain, so a same-named method on an unrelated object is never touched. One honest wrinkle from the release history: the rename actually landed in 0.7.0 and 1.0.0 kept it, so the pack mends pre-0.7.0 call sites no matter which release you jump to.
The 2.0.0 calling-convention change
The second pack (paypal-server-sdk-v2-options-object) handles the structural half:
- Wrap positional controller-method arguments into the 2.0.0 options object (requestOptions stays positional)
In 1.0.0 a capture call was positional: paymentsController.captureAuthorizedPayment(authId, undefined, requestId). In 2.0.0 the same call wraps every former positional parameter into one object with named keys, while requestOptions stays behind as a second positional argument. A find-and-replace cannot do this rewrite, because the correct key for each position depends on which method you are calling. The pack carries the per-method parameter order extracted mechanically from the 1.0.0 signatures, so the position-to-key mapping is deterministic, and arguments are split with a string-aware bracket-depth scanner rather than a naive comma split, so nested calls and string literals containing commas are safe.
What the codemod refuses to touch
A migration tool earns trust by what it leaves alone. Multi-line call sites do not match the options-object rule at all: the rewrite simply skips them, which is the conservative direction. A call whose first argument is already an object literal is treated as migrated and left untouched, so running the pack twice is a no-op. Template literals and comments inside an argument list also stop the rewrite. Every rule in both packs is locked by golden-fixture regression tests with negative controls, so a rule that starts over-firing fails our build before it reaches your repo.
Preview each migration as one diff
Both packs run locally as a dry run by default. On the bundled fixture repos, each pack rewrites two files and writes a unified patch you can read before anything is applied:
npx mendapi sync # -> one network call: pulls the change feed into a local database npx mendapi scan # -> which files touch the PayPal SDK, with line numbers mendapi fix --migration paypal-server-sdk-v1-controller-renames # -> dry run: changes.patch + fix-report.json, nothing applied mendapi fix --migration paypal-server-sdk-v2-options-object # -> the calling-convention half, same dry-run discipline mendapi fix --migration paypal-server-sdk-v2-options-object --apply # -> applies the reviewed diff
Nothing leaves your machine at any step; zero network calls is the default and only mode. The report records which rule fired where, and every rewritten file carries a syntax-check verdict, so review is a read of the patch, not an audit of the tool.
The diff itself, byte for byte
This is the actual changes.patch the 2.0.0 options-object pack's dry run produces on the bundled 1.0.0 fixture — embedded here from the golden regression evidence at page build time, not retyped. Two files change: positional calls like createOrder(orderRequest, undefined, requestId) collapse into a single named-key object with the paypalRequestId inferred from position, skipped undefined slots disappear instead of becoming junk keys, and body payloads like finalCapture and noteToPayer land under a body key. Note what the codemod leaves alone: the comment lines describing each case (Positional with skipped optionals, the bracket-depth splitter test) survive byte-for-byte, and the one call that already uses the 2.0.0 shape — getRefund with an object literal — is treated as migrated and not touched, which is why running the pack twice is a no-op.
--- a/index.js +++ b/index.js @@ -16,31 +16,31 @@ async function createCheckoutOrder(orderRequest, requestId) { // Positional with skipped optionals: body, mockResponse, requestId. - const { result } = await ordersController.createOrder(orderRequest, undefined, requestId); + const { result } = await ordersController.createOrder({ body: orderRequest, paypalRequestId: requestId }); return result; } async function captureWithPrefer(orderId) { // id, mock, requestId, prefer — a string literal containing a comma-free // value but exercising the arg-name inference (id !== orderId). - const { result } = await ordersController.captureOrder(orderId, undefined, undefined, 'return=representation'); + const { result } = await ordersController.captureOrder({ id: orderId, prefer: 'return=representation' }); return result; } async function inspectOrder(id) { // Single positional arg whose name matches the parameter: shorthand form. - const { result } = await ordersController.getOrder(id); + const { result } = await ordersController.getOrder({ id }); return result.status; } async function amendOrder(id, patchBody, requestOptions) { // Full-arity call: trailing requestOptions must stay positional. - await ordersController.patchOrder(id, undefined, undefined, patchBody, requestOptions); + await ordersController.patchOrder({ id, body: patchBody }, requestOptions); } async function addTracking(id, tracker) { // Nested object literal argument with commas — bracket-depth splitter test. - await ordersController.createOrderTracking(id, { carrier: tracker.carrier, trackingNumber: tracker.number }); + await ordersController.createOrderTracking({ id, body: { carrier: tracker.carrier, trackingNumber: tracker.number } }); } module.exports = { createCheckoutOrder, captureWithPrefer, inspectOrder, amendOrder, addTracking }; --- a/lib/payments.js +++ b/lib/payments.js @@ -7,21 +7,21 @@ const { paymentsController } = client; async function settle(authorizationId) { - const { result: auth } = await paymentsController.getAuthorizedPayment(authorizationId); + const { result: auth } = await paymentsController.getAuthorizedPayment({ authorizationId }); if (auth.status === 'CREATED') { - await paymentsController.reauthorizePayment(authorizationId, undefined, 'return=minimal', undefined, { amount: auth.amount }); + await paymentsController.reauthorizePayment({ authorizationId, prefer: 'return=minimal', body: { amount: auth.amount } }); } - const { result } = await paymentsController.captureAuthorizedPayment(authorizationId, undefined, undefined, undefined, undefined, { finalCapture: true }); + const { result } = await paymentsController.captureAuthorizedPayment({ authorizationId, body: { finalCapture: true } }); return result; } async function cancel(authId) { - await paymentsController.voidPayment(authId); + await paymentsController.voidPayment({ authorizationId: authId }); } async function refundFlow(captureId, note) { - const { result: capture } = await paymentsController.getCapturedPayment(captureId); - const { result: refund } = await paymentsController.refundCapturedPayment(captureId, undefined, undefined, undefined, undefined, { noteToPayer: note }); + const { result: capture } = await paymentsController.getCapturedPayment({ captureId }); + const { result: refund } = await paymentsController.refundCapturedPayment({ captureId, body: { noteToPayer: note } }); // Already-migrated call site (2.0.0 shape): must stay untouched. const { result: check } = await paymentsController.getRefund({ refundId: refund.id }); return { capture, refund, check };
The part no codemod fixes
Honest scope: the packs handle the mechanical rewrites. They do not decide how your code should adopt new SDK behavior: response typing differences, error-class changes, or code that picks a controller method dynamically at runtime. The tracked PayPal history counts 1198 breaking or deprecation entries, and the platform-behavior ones stay not code-fixable by verdict, because pretending a codemod covers them is how migrations go wrong.
Related
- PayPal API breaking changes: complete tracker — every tracked entry with fixability verdicts.
- Stripe Payment Records migration — the other fintech provider where breaking changes land in payment paths.
- Migration pack catalog — every deterministic fix mendapi ships.