feat(backend): unified payments for course purchase (fiat and crypto) (#391) - #447
Merged
Penielka merged 3 commits intoAug 26, 2026
Merged
Conversation
…AetherEdu#391) Implement a unified checkout that abstracts the payment rail so learners can pay with Stripe (fiat) or Stellar (crypto): - New checkout orchestration (services/payments/): CheckoutService drives create/confirm across both rails, StripePaymentService wraps the Stripe SDK (payment intents, refunds, signature-verified webhooks), and PaymentReconciliationService matches pending crypto payments against on-chain Stellar transactions via the checkout memo. - Payment state machine enforces pending -> processing -> completed/failed and completed -> refunded transitions so stale webhooks and duplicate confirms are rejected instead of corrupting state. - Refunds supported on both rails (Stripe refunds API, Stellar refund XDR). - Typed purchase events (events/purchaseEvents.ts) emitted on every lifecycle transition over the in-process bus and the Redis purchase channel; Stripe and Stellar webhook endpoints consume them. - Payments routes (routes/payments.ts) mounted at /api/payments and /api/v1/payments with checkout, confirm, reconcile, refund, receipt, and frontend-compatible endpoints; the dead paymentRoutes.ts is removed. - StellarPaymentService now boots without STELLAR_DISTRIBUTION_ACCOUNT (lazy keypair) so the server starts in dev/CI; STRIPE_* env vars added to backend/.env.example.
…t throw TourProvider returned children without the context provider until the client-side effect ran, so any page rendered during SSR/static generation (where useTour is called by TourGuide, OnboardingModal, and profile pages) threw "useTour must be used within a TourProvider" and broke `next build`. The provider is now always mounted; onboarding state still defaults to "completed" until localStorage resolves on the client, preserving the no-flash behavior.
Contributor
|
@Degentle12 kindly resolve conflicts sir |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #391
Overview
Learners can now purchase courses and credentials with either Stripe (fiat) or Stellar (crypto) through a single unified checkout. The checkout abstracts the payment rail, reconciles crypto payments against on-chain transactions, supports refunds on both rails, and emits purchase events at every lifecycle transition.
What changed
Payment orchestration —
backend/src/services/payments/CheckoutService— unified checkout lifecycle:createCheckout/confirmCheckoutdispatch to the Stripe or Stellar rail and own the checkout state machine.StripePaymentService— Stripe SDK wrapper: payment intents, confirm, refunds, and signature-verified webhook parsing. Client is created lazily so the server boots withoutSTRIPE_SECRET_KEY(dev/CI); missing keys surface a clear error at call time.PaymentReconciliationService— sweeps pending crypto payments and matches them to on-chain Stellar transactions via the checkout memo, verifying destination/amount/asset before finalizing.paymentStateMachine— enforcespending → processing → completed/failed,completed → refunded/partially_refunded. Stale webhooks and duplicate confirms are rejected instead of corrupting state.Endpoints —
backend/src/routes/payments.ts(mounted at/api/paymentsand/api/v1/payments; deadpaymentRoutes.tsremoved)POST /checkout·POST /checkout/:checkoutId/confirm·GET /checkout/:checkoutIdPOST /webhook/stripe(raw body + signature verification) ·POST /webhook/stellar(triggers reconciliation) ·POST /reconcile(admin sweep)POST /:paymentId/refund·GET /receipt/:paymentId·GET /methods·GET /exchange-ratesand the existing intent/stellar endpoints the frontend already calls.Events —
backend/src/events/purchaseEvents.tsPURCHASE_INITIATED,PURCHASE_CONFIRMED,PURCHASE_FAILED,PURCHASE_REFUNDED,PURCHASE_RECONCILED) dispatched on the in-process bus and published to the Redispurchase:eventschannel.Model / config
PaymentMethod.STRIPEadded;Payment.tsgains unifiedCheckouttypes.StellarPaymentServicenow lazy-initializes the distribution keypair so the server starts withoutSTELLAR_DISTRIBUTION_ACCOUNTconfigured;STRIPE_SECRET_KEY/STRIPE_PUBLISHABLE_KEY/STRIPE_WEBHOOK_SECRETdocumented inbackend/.env.example.Frontend (build fix)
TourProvidernow always mounts its context sonext buildno longer throwsuseTour must be used within a TourProviderduring static generation (pre-existing break surfaced once the root lockfile change triggered the frontend build job).Tests
backend/src/__tests__/payments.test.ts(19 tests) covers the state machine, both checkout rails (with in-memory Stellar/Stripe doubles), Stripe webhook finalization + idempotency + refunds, on-chain reconciliation, and event emission.CI
All checks pass: backend build/lint, backend tests, frontend build, OpenAPI spec validation, security scan, bundle budget, and Docker image builds.