Skip to content

QA: credentialed end-to-end sandbox testing for PayPal & Binance Pay (follow-up to #466 / PR #478) #479

Description

@guillermoscript

Follow-up to #466 / PR #478 (implements PayPal in full + Binance Pay). That PR shipped with code + build + unit-test verification only — no live sandbox run, because no PayPal sandbox or Binance Pay merchant credentials were available in the dev environment. This issue tracks the credentialed end-to-end QA that must happen after the PR merges, before either provider is enabled for a real school.

Prerequisites (setup)

  • Apply the migration (supabase db push) — adds binance to the products/plans payment_provider CHECK constraints (20260719180000_add_binance_payment_provider.sql).
  • PayPal sandbox app (developer.paypal.com): set PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_ENVIRONMENT=sandbox.
  • PayPal webhook: register https://<host>/api/payments/webhook/paypal with events PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.REFUNDED, BILLING.SUBSCRIPTION.ACTIVATED, BILLING.SUBSCRIPTION.CANCELLED, BILLING.SUBSCRIPTION.EXPIRED, BILLING.SUBSCRIPTION.SUSPENDED, BILLING.SUBSCRIPTION.PAYMENT.FAILED, PAYMENT.SALE.COMPLETED; set the webhook id as PAYPAL_WEBHOOK_ID. Local testing needs a public tunnel (ngrok/cloudflared) since PayPal must reach the endpoint.
  • Binance Pay merchant account: set BINANCE_PAY_API_KEY, BINANCE_PAY_API_SECRET; configure the merchant webhook URL to https://<host>/api/payments/webhook/binance.
  • Admin → Settings → Payment: enable PayPal and Binance Pay for the test tenant.

PayPal — one-time (product)

  • Create a paid product with provider PayPal → a Catalog Product id (PROD-…) is stored in provider_product_id and a synthetic PAYPAL-ONETIME:… id in provider_price_id.
  • Student checkout → redirected to PayPal sandbox approve page → after approval, returned via /api/payments/paypal/capture to the success page.
  • Transaction is successful, enrollment exists (enroll_user ran), provider_subscription_id holds the PayPal order id.
  • Refresh the capture return URL → no double processing (transaction stays successful, single enrollment).
  • PAYMENT.CAPTURE.COMPLETED webhook delivery shows 200 in the PayPal dashboard; webhook_events row exists with processed_at set.
  • Refund the capture from the PayPal sandbox dashboard → PAYMENT.CAPTURE.REFUNDED flips the transaction to refunded and revokes the product entitlements.

PayPal — subscription (plan)

  • Create a monthly plan with provider PayPal → Billing Plan id (P-…) stored in provider_price_id.
  • Student subscribes → PayPal approve → BILLING.SUBSCRIPTION.ACTIVATED webhook flips the pending transaction, creates the subscription row with provider_subscription_id = I-… and period end = PayPal's next_billing_time.
  • Owner-binding: confirm the webhook only activates when the custom_id userId/tenantId match the transaction (tamper test below covers the negative).
  • Renewal (sandbox billing cycles are slow — optionally use PayPal's webhook simulator for PAYMENT.SALE.COMPLETED with a real billing_agreement_id) → extend_subscription_period extends current_period_end.
  • Student self-cancel (billing page) → PayPal subscription cancelled; CANCELLED webhook marks the row canceled and enrollments are disabled.
  • Plan change (Student plan-change flow: updateSubscription in provider contract + switch UX #463 supersession path): switching plans cancels the old PayPal subscription and starts a new checkout — no parallel billing.

Binance Pay

  • Create a paid product with provider Binance Pay (no provider ids required) → student checkout redirects to Binance checkout (checkoutUrl), pay in USDT.
  • PAY_SUCCESS webhook enrolls the student; delivery log shows our ack {"returnCode":"SUCCESS"} and Binance marks the notification delivered (no retry storm).
  • Create a monthly plan with provider Binance Pay → after payment, subscription row exists with the plan duration as period and the Binance order id as provider_subscription_id.
  • Re-purchase the same plan before expiry → period extends (renewal path); after expiry with no payment, the expiry cron lapses the subscription (selfManagedPeriod).
  • Abandon a checkout (close the Binance page) → transaction stays pending; PAY_CLOSED (order expiry) arrives → normalized payment.failed, no state corruption, retry purchase works (partial unique index allows it).
  • Refund via merchant tooling → PAY_REFUND/REFUND_SUCCESS flips the transaction to refunded and revokes entitlements.

Webhook security (both providers)

  • Replay a captured webhook body with a tampered signature → 400, no state change.
  • Replay a valid delivery unchanged → duplicate: true ack, no double processing (webhook_events idempotency).
  • Send a valid-signature event whose custom_id/passThroughInfo references another user's pending transaction id → dispatcher throws owner-mismatch, transaction untouched (fail-closed guard).
  • POST to /api/payments/webhook/binance with missing signature headers → 400.

UI polish check

  • Admin payment settings shows the Binance Pay toggle (en + es); product/plan forms and the product-creation wizard offer PayPal/Binance only when enabled.
  • Checkout page for a PayPal/Binance product shows the hosted-redirect flow (no dead "card unavailable" state); PayPal label no longer says "Coming Soon".
  • Capture screenshots/GIF of one full PayPal flow and one Binance flow for the record (PR feat(payments): implement PayPal (full) and Binance Pay providers (#466) #478 shipped without visual evidence).

Acceptance

  • Every checklist item above passes, or a bug issue is filed referencing the failing step.
  • Both providers verified end-to-end on at least one tenant before being documented as production-ready.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

paymentsPayment provider / billing related

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions