Skip to content

feat(backend): unified payments for course purchase (fiat and crypto) (#391) - #447

Merged
Penielka merged 3 commits into
AetherEdu:mainfrom
Degentle12:feat/issue-391-unified-payments
Aug 26, 2026
Merged

feat(backend): unified payments for course purchase (fiat and crypto) (#391)#447
Penielka merged 3 commits into
AetherEdu:mainfrom
Degentle12:feat/issue-391-unified-payments

Conversation

@Degentle12

@Degentle12 Degentle12 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

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 / confirmCheckout dispatch 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 without STRIPE_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 — enforces pending → 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/payments and /api/v1/payments; dead paymentRoutes.ts removed)

  • POST /checkout · POST /checkout/:checkoutId/confirm · GET /checkout/:checkoutId
  • POST /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-rates and the existing intent/stellar endpoints the frontend already calls.

Events — backend/src/events/purchaseEvents.ts

  • Typed purchase events (PURCHASE_INITIATED, PURCHASE_CONFIRMED, PURCHASE_FAILED, PURCHASE_REFUNDED, PURCHASE_RECONCILED) dispatched on the in-process bus and published to the Redis purchase:events channel.

Model / config

  • PaymentMethod.STRIPE added; Payment.ts gains unified Checkout types.
  • StellarPaymentService now lazy-initializes the distribution keypair so the server starts without STELLAR_DISTRIBUTION_ACCOUNT configured; STRIPE_SECRET_KEY / STRIPE_PUBLISHABLE_KEY / STRIPE_WEBHOOK_SECRET documented in backend/.env.example.

Frontend (build fix)

  • TourProvider now always mounts its context so next build no longer throws useTour must be used within a TourProvider during 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.

…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.
@Penielka

Copy link
Copy Markdown
Contributor

@Degentle12 kindly resolve conflicts sir

@Penielka
Penielka merged commit 7e3211e into AetherEdu:main Aug 26, 2026
13 of 15 checks 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.

[Backend] Unified payments for course purchase (fiat and crypto)

2 participants