Part of epic #100.
Functions to ship
1. create-paypal-order (~2h)
- Browser caller:
src/lib/payments/paypal.ts:74
- Request body:
{ payment_intent_id: string }
- Logic:
- CORS + auth
- Load
payment_intents row by id, verify owner
- Get PayPal access token via OAuth (use
PAYPAL_CLIENT_SECRET + browser PAYPAL_CLIENT_ID)
- POST
/v2/checkout/orders with intent: 'CAPTURE' and the intent's amount/currency
- Store PayPal
order.id on payment_intents row (paypal_order_id column)
- Return
{ orderId: order.id }
- PayPal Orders v2 docs: https://developer.paypal.com/docs/api/orders/v2/
2. capture-paypal-order (~2h)
- Browser caller:
src/lib/payments/paypal.ts:101
- Request body:
{ order_id: string }
- Logic:
- CORS + auth
- POST
/v2/checkout/orders/{order_id}/capture
- Map PayPal status →
payment_results row insert (status='completed' or status='failed' with categorized error)
- Return
{ status, capture_id }
Tests
- Deno unit tests mocking PayPal REST calls
- Un-skip relevant
tests/e2e/payment/02-paypal-subscription.spec.ts tests for the one-off flow
Acceptance
🤖 Created from audit on 2026-05-20
Part of epic #100.
Functions to ship
1.
create-paypal-order(~2h)src/lib/payments/paypal.ts:74{ payment_intent_id: string }payment_intentsrow by id, verify ownerPAYPAL_CLIENT_SECRET+ browserPAYPAL_CLIENT_ID)/v2/checkout/orderswithintent: 'CAPTURE'and the intent's amount/currencyorder.idonpayment_intentsrow (paypal_order_idcolumn){ orderId: order.id }2.
capture-paypal-order(~2h)src/lib/payments/paypal.ts:101{ order_id: string }/v2/checkout/orders/{order_id}/capturepayment_resultsrow insert (status='completed'orstatus='failed'with categorized error){ status, capture_id }Tests
tests/e2e/payment/02-paypal-subscription.spec.tstests for the one-off flowAcceptance
/payment-demoPayPal tab → sandbox buyer account → completes,payment_resultsrow createdPAYMENT.CAPTURE.COMPLETEDwebhook) ALSO lands awebhook_eventsrow for idempotency cross-check🤖 Created from audit on 2026-05-20