Documentation

    Concepts, value, and typical clone scenarios — less code.

    Welcome to Ring
    Quick Reference
    Getting Started
    Prerequisites
    Installation
    First Success Validation
    Next Steps
    Features
    Multi-Vendor Store
    SubscriptionConductor
    PaymentConductor
    Payments Overview
    WayForPay Payment Integration
    Wallet & Credit System
    WalletConductor
    Affiliate & Referral Enablement
    Referral Codes (Refcodes)
    NFT Exhibition Marketplace
    Solana NFT Gates
    Token Staking System
    Owner Project Lab
    Entities
    Opportunities
    Real-Time Messaging
    Ring Tasks
    WebRTC Calls & STUNner TURN
    News Module
    Member Blogs
    Public Profile Pages
    Username Reservation System
    Scientific Editor
    Notifications
    Push Notifications with FCM (Ring-Powered)
    Email AI-CRM
    Ring Mailer & RingdomX Mail
    Tunnel Protocol
    VideoConductor
    MediaConductor
    Generative Gallery
    Authentication
    Security & Compliance
    Admin console
    Admin Wiki
    Manage via Telegram
    Locale System
    Mobile Experience
    Performance Optimization Patterns
    Examples
    Quick Start
    Basic Setup
    White Label
    Custom Branding
    Web3 Integration
    Real World
    Advanced Features
    Customization
    Quick Start — Your First Ring Clone
    Customization Guide
    Branding
    Themes
    Features
    Localization
    Token Economics Setup
    Payment Gateway Integration
    Reference Ring deployments
    Web3
    Token launch jurisdictions
    Wallet
    Wallet Security Tips
    Integrations
    Ethereum wallets (Wagmi v3)
    RingFileBase (object storage API)
    Ring CDN (RingFileBase edge)
    Deployment
    Self-hosted deployment
    Vercel
    Docker
    Environment Configuration
    Monitoring & Analytics
    Performance Optimization
    Backup & Recovery
    Architecture
    Data Model
    Security
    Real Time
    Discovery Mutation Sync
    PaymentConductor architecture
    WalletConductor architecture
    Development
    Ring MCP Server

    Quick entry (CTOs · auditors · agents)

    Welcome — mission & audiences
    Quick Reference
    Getting started
    Architecture & Auth.js
    Backend modes & databases (DB_BACKEND_MODE)
    Self-hosted
    Ring MCP Tools
    Ring MCP Server
    Token economics
    Token launch jurisdictions
    Deploy (Docker · k8s)
    Security & compliance reads
    ringdom.org — LegioX homebase
    Source — MIT license (GitHub)

    Documentation

    Concepts, value, and typical clone scenarios — less code.

    Welcome to Ring
    Quick Reference
    Getting Started
    Prerequisites
    Installation
    First Success Validation
    Next Steps
    Features
    Multi-Vendor Store
    SubscriptionConductor
    PaymentConductor
    Payments Overview
    WayForPay Payment Integration
    Wallet & Credit System
    WalletConductor
    Affiliate & Referral Enablement
    Referral Codes (Refcodes)
    NFT Exhibition Marketplace
    Solana NFT Gates
    Token Staking System
    Owner Project Lab
    Entities
    Opportunities
    Real-Time Messaging
    Ring Tasks
    WebRTC Calls & STUNner TURN
    News Module
    Member Blogs
    Public Profile Pages
    Username Reservation System
    Scientific Editor
    Notifications
    Push Notifications with FCM (Ring-Powered)
    Email AI-CRM
    Ring Mailer & RingdomX Mail
    Tunnel Protocol
    VideoConductor
    MediaConductor
    Generative Gallery
    Authentication
    Security & Compliance
    Admin console
    Admin Wiki
    Manage via Telegram
    Locale System
    Mobile Experience
    Performance Optimization Patterns
    Examples
    Quick Start
    Basic Setup
    White Label
    Custom Branding
    Web3 Integration
    Real World
    Advanced Features
    Customization
    Quick Start — Your First Ring Clone
    Customization Guide
    Branding
    Themes
    Features
    Localization
    Token Economics Setup
    Payment Gateway Integration
    Reference Ring deployments
    Web3
    Token launch jurisdictions
    Wallet
    Wallet Security Tips
    Integrations
    Ethereum wallets (Wagmi v3)
    RingFileBase (object storage API)
    Ring CDN (RingFileBase edge)
    Deployment
    Self-hosted deployment
    Vercel
    Docker
    Environment Configuration
    Monitoring & Analytics
    Performance Optimization
    Backup & Recovery
    Architecture
    Data Model
    Security
    Real Time
    Discovery Mutation Sync
    PaymentConductor architecture
    WalletConductor architecture
    Development
    Ring MCP Server

    Quick entry (CTOs · auditors · agents)

    Welcome — mission & audiences
    Quick Reference
    Getting started
    Architecture & Auth.js
    Backend modes & databases (DB_BACKEND_MODE)
    Self-hosted
    Ring MCP Tools
    Ring MCP Server
    Token economics
    Token launch jurisdictions
    Deploy (Docker · k8s)
    Security & compliance reads
    ringdom.org — LegioX homebase
    Source — MIT license (GitHub)

    Documentation

    Concepts, value, and typical clone scenarios — less code.

    Welcome to Ring
    Quick Reference
    Getting Started
    Prerequisites
    Installation
    First Success Validation
    Next Steps
    Features
    Multi-Vendor Store
    SubscriptionConductor
    PaymentConductor
    Payments Overview
    WayForPay Payment Integration
    Wallet & Credit System
    WalletConductor
    Affiliate & Referral Enablement
    Referral Codes (Refcodes)
    NFT Exhibition Marketplace
    Solana NFT Gates
    Token Staking System
    Owner Project Lab
    Entities
    Opportunities
    Real-Time Messaging
    Ring Tasks
    WebRTC Calls & STUNner TURN
    News Module
    Member Blogs
    Public Profile Pages
    Username Reservation System
    Scientific Editor
    Notifications
    Push Notifications with FCM (Ring-Powered)
    Email AI-CRM
    Ring Mailer & RingdomX Mail
    Tunnel Protocol
    VideoConductor
    MediaConductor
    Generative Gallery
    Authentication
    Security & Compliance
    Admin console
    Admin Wiki
    Manage via Telegram
    Locale System
    Mobile Experience
    Performance Optimization Patterns
    Examples
    Quick Start
    Basic Setup
    White Label
    Custom Branding
    Web3 Integration
    Real World
    Advanced Features
    Customization
    Quick Start — Your First Ring Clone
    Customization Guide
    Branding
    Themes
    Features
    Localization
    Token Economics Setup
    Payment Gateway Integration
    Reference Ring deployments
    Web3
    Token launch jurisdictions
    Wallet
    Wallet Security Tips
    Integrations
    Ethereum wallets (Wagmi v3)
    RingFileBase (object storage API)
    Ring CDN (RingFileBase edge)
    Deployment
    Self-hosted deployment
    Vercel
    Docker
    Environment Configuration
    Monitoring & Analytics
    Performance Optimization
    Backup & Recovery
    Architecture
    Data Model
    Security
    Real Time
    Discovery Mutation Sync
    PaymentConductor architecture
    WalletConductor architecture
    Development
    Ring MCP Server

    Quick entry (CTOs · auditors · agents)

    Welcome — mission & audiences
    Quick Reference
    Getting started
    Architecture & Auth.js
    Backend modes & databases (DB_BACKEND_MODE)
    Self-hosted
    Ring MCP Tools
    Ring MCP Server
    Token economics
    Token launch jurisdictions
    Deploy (Docker · k8s)
    Security & compliance reads
    ringdom.org — LegioX homebase
    Source — MIT license (GitHub)
    1. Docs
    2. /Architecture
    3. /PaymentConductor architecture

    Updated Jul 16, 20266 min listen

    Ring Platform Logo

    Завантаження документації...

    Підготовка контенту платформи Ring

    1. Docs
    2. /Architecture
    3. /PaymentConductor architecture

    Updated Jul 16, 20266 min listen

    Ring Platform Logo

    Завантаження документації...

    Підготовка контенту платформи Ring

    1. Docs
    2. /Architecture
    3. /PaymentConductor architecture

    Updated Jul 16, 20266 min listen

    Ring Platform Logo

    Завантаження документації...

    Підготовка контенту платформи Ring

    PaymentConductor architecture

    PaymentConductor is Ring Platform's config-driven payment layer. One PostgreSQL ledger (payment_transactions) and one webhook dispatcher serve store checkout, membership upgrades, news promotion, and wallet credit top-up.

    Operator setup: Payment integration · WayForPay

    Use Founder / Developer tabs in the docs sidebar to filter this page.

    Operator guide

    PSP dashboards and env vars

    Feature overview

    Purposes and rails

    Wallet top-up

    Live wallet_topup via Card / PayPal

    WayForPay HPP

    form_post vs navigate; returnUrl vs serviceUrl

    Why this architecture matters

    • Single ledger — every card redirect and credit settlement writes payment_transactions before the PSP page opens.
    • Idempotent callbacks — duplicate WayForPay / Stripe / PayPal webhooks do not double-fulfill; order_reference is unique.
    • Processor-agnostic UI — the browser only follows a Conductor redirect object. Switching a clone to Stripe is an env flip (PAYMENT_*_PROCESSOR=stripe), not a new top-up screen.
    • Webhook = money — returning to the site after pay is not confirmation; Approved (or Stripe/PayPal capture) webhooks are.
    • Admin audit — Payments tab loads membership + wallet top-up rows for a user without scraping PSP dashboards.

    Request flow

    Browser handoff (CheckoutRedirect)

    Conductor-owned DTO on CreateCheckoutResult — clients must not import WayForPay-branded helpers.

    ModeWhenClient
    form_postWayForPay HPP (secure.wayforpay.com/pay)Hidden form POST via followCheckoutRedirect
    navigateStripe Checkout Session, PayPal approve URL, WFP invoiceUrlwindow.location.href

    Helpers:

    • navigateCheckoutRedirect(url) / formPostCheckoutRedirect(url, fields)

    Related

    WalletConductor architecture

    Custodial native + credit facade (top-up / desk / spend)

    Payment integration

    Operator PSP setup

    Payments feature

    User-facing flows

    Admin API

    User Payments tab endpoint

    PaymentConductor architecture

    PaymentConductor is Ring Platform's config-driven payment layer. One PostgreSQL ledger (payment_transactions) and one webhook dispatcher serve store checkout, membership upgrades, news promotion, and wallet credit top-up.

    Operator setup: Payment integration · WayForPay

    Use Founder / Developer tabs in the docs sidebar to filter this page.

    Operator guide

    PSP dashboards and env vars

    Feature overview

    Purposes and rails

    Wallet top-up

    Live wallet_topup via Card / PayPal

    WayForPay HPP

    form_post vs navigate; returnUrl vs serviceUrl

    Why this architecture matters

    • Single ledger — every card redirect and credit settlement writes payment_transactions before the PSP page opens.
    • Idempotent callbacks — duplicate WayForPay / Stripe / PayPal webhooks do not double-fulfill; order_reference is unique.
    • Processor-agnostic UI — the browser only follows a Conductor redirect object. Switching a clone to Stripe is an env flip (PAYMENT_*_PROCESSOR=stripe), not a new top-up screen.
    • Webhook = money — returning to the site after pay is not confirmation; Approved (or Stripe/PayPal capture) webhooks are.
    • Admin audit — Payments tab loads membership + wallet top-up rows for a user without scraping PSP dashboards.

    Request flow

    Browser handoff (CheckoutRedirect)

    Conductor-owned DTO on CreateCheckoutResult — clients must not import WayForPay-branded helpers.

    ModeWhenClient
    form_postWayForPay HPP (secure.wayforpay.com/pay)Hidden form POST via followCheckoutRedirect
    navigateStripe Checkout Session, PayPal approve URL, WFP invoiceUrlwindow.location.href

    Helpers:

    • navigateCheckoutRedirect(url) / formPostCheckoutRedirect(url, fields)

    Related

    WalletConductor architecture

    Custodial native + credit facade (top-up / desk / spend)

    Payment integration

    Operator PSP setup

    Payments feature

    User-facing flows

    Admin API

    User Payments tab endpoint

    PaymentConductor architecture

    PaymentConductor is Ring Platform's config-driven payment layer. One PostgreSQL ledger (payment_transactions) and one webhook dispatcher serve store checkout, membership upgrades, news promotion, and wallet credit top-up.

    Operator setup: Payment integration · WayForPay

    Use Founder / Developer tabs in the docs sidebar to filter this page.

    Operator guide

    PSP dashboards and env vars

    Feature overview

    Purposes and rails

    Wallet top-up

    Live wallet_topup via Card / PayPal

    WayForPay HPP

    form_post vs navigate; returnUrl vs serviceUrl

    Why this architecture matters

    • Single ledger — every card redirect and credit settlement writes payment_transactions before the PSP page opens.
    • Idempotent callbacks — duplicate WayForPay / Stripe / PayPal webhooks do not double-fulfill; order_reference is unique.
    • Processor-agnostic UI — the browser only follows a Conductor redirect object. Switching a clone to Stripe is an env flip (PAYMENT_*_PROCESSOR=stripe), not a new top-up screen.
    • Webhook = money — returning to the site after pay is not confirmation; Approved (or Stripe/PayPal capture) webhooks are.
    • Admin audit — Payments tab loads membership + wallet top-up rows for a user without scraping PSP dashboards.

    Request flow

    Browser handoff (CheckoutRedirect)

    Conductor-owned DTO on CreateCheckoutResult — clients must not import WayForPay-branded helpers.

    ModeWhenClient
    form_postWayForPay HPP (secure.wayforpay.com/pay)Hidden form POST via followCheckoutRedirect
    navigateStripe Checkout Session, PayPal approve URL, WFP invoiceUrlwindow.location.href

    Helpers:

    • navigateCheckoutRedirect(url) / formPostCheckoutRedirect(url, fields)

    Related

    WalletConductor architecture

    Custodial native + credit facade (top-up / desk / spend)

    Payment integration

    Operator PSP setup

    Payments feature

    User-facing flows

    Admin API

    User Payments tab endpoint

  1. normalizeCheckoutResult(result) — always applied at the end of PaymentConductor.createCheckout
  2. Browser: lib/payments/checkout-redirect.ts → followCheckoutRedirect / followCheckoutResult
  3. WayForPay HPP rejects GET query-string “payment URLs” (Bad Request / This page requires only POST data). Build fields in lib/payments/wayforpay-hpp.ts.

    Core modules

    ModulePathResponsibility
    Conductorlib/payments/conductor/payment-conductor.tscreateCheckout (+ normalize), handleWebhook, transaction lookup
    Typeslib/payments/conductor/types.tsPurposes, rails, CheckoutRedirect, normalizeCheckoutResult
    Client handofflib/payments/checkout-redirect.tsUnbranded followCheckoutResult
    WFP HPPlib/payments/wayforpay-hpp.tsSign + form_post fields
    Dispatcherlib/payments/conductor/webhook-dispatcher.tsVerify PSP payloads; route to purpose handlers
    Order referenceslib/payments/order-reference.tsBuild / parse idempotent orderReference strings
    Ledger servicelib/payments/payment-transaction-service.tsCRUD + listByUserId on payment_transactions
    Configlib/payments/payment.config.tsgetPaymentProvider, isRailEnabled, webhook URL helpers

    Purpose handlers

    PurposeHandler file
    store_orderconductor/handlers/store-order.ts (+ Stripe: store-order-stripe.ts)
    membership_upgradeconductor/handlers/membership-upgrade.ts (+ Stripe: membership-upgrade-stripe.ts)
    news_promotionconductor/handlers/news-promotion.ts
    wallet_topupconductor/handlers/wallet-topup.ts (+ Stripe / PayPal capture handlers)
    native_token_onrampconductor/handlers/native-token-onramp.ts (+ Stripe: native-token-onramp-stripe.ts)

    Processors

    ProcessorFileBrowser mode
    WayForPayprocessors/wayforpay.processor.tsHPP form_post (or invoice navigate)
    Stripeprocessors/stripe.processor.tsnavigate (Checkout Session URL)
    PayPalprocessors/paypal.processor.tsnavigate (Orders v2 approve URL)
    Internal creditprocessors/internal-credit.processor.tsSync settle (no redirect)
    Native tokenprocessors/native-token.processor.tsSync settle (no redirect)

    Processor resolution in createCheckout:

    1. rail === 'internal_credit' / 'native_token' → that processor
    2. Else metadata.processor if paypal | stripe | wayforpay
    3. Else getPaymentProvider(purpose) (PAYMENT_*_PROCESSOR / PAYMENT_DEFAULT_PROCESSOR)

    Legacy helpers (wayforpay-service.ts, wayforpay-store-service.ts) remain for specialized form building; new route entry points go through the conductor.

    Types

    CreateCheckoutContext carries purpose, optional rail, userId, amount, currency, returnUrl, metadata, and purpose-specific fields (orderId, articleId, targetRole, etc.).

    Configuration surface

    lib/payments/payment.config.ts:

    • getPaymentProvider(purpose) — WayForPay vs Stripe from env overrides
    • isRailEnabled(purpose, rail) — credit / token gates
    • getDefaultStoreCurrencySymbol() — PAYMENT_FIAT_CURRENCY (default USD)
    • getWebhookUrl(provider) — ${site}/api/payments/${provider}/webhook

    Env keys per purpose: PAYMENT_STORE_PROCESSOR, PAYMENT_MEMBERSHIP_PROCESSOR, PAYMENT_NEWS_PROCESSOR, PAYMENT_WALLET_TOPUP_PROCESSOR, PAYMENT_NATIVE_TOKEN_ONRAMP_PROCESSOR. Default: PAYMENT_DEFAULT_PROCESSOR (wayforpay | stripe | paypal).

    WayForPay env SSOT (no WAYFORPAY_MERCHANT_ID):

    Order reference prefixes

    PurposePatternExample
    store_orderstore_{orderId}_{timestamp}store_ord_abc_1717000000000
    membership_upgrademembership_{userId}_{timestamp}membership_user123_1717000000000
    membership_upgrade (legacy)ring_{userId}_{timestamp}still accepted
    news_promotionnews-promo-{base64url(articleId)}-{timestamp}news-promo-YWJj-1717000000000
    wallet_topupwallettopup_{userId}_{timestamp}wallettopup_user123_1717000000000

    buildOrderReference / parseOrderReference in order-reference.ts. Duplicate webhook delivery is safe: ledger enforces unique order_reference.

    Ledger (payment_transactions)

    Migration: data/migrations/004_payment_transactions.sql

    JSON data fields (via payment-transaction-service.ts): purpose, processor, rail, order_reference, entity_type, entity_id, user_id, amount_minor, currency, status, status_history, processor_payload, paid_at.

    createPending returns existing row if order_reference already exists. Admin list: paymentTransactionService.listByUserId (default purposes: membership_upgrade, wallet_topup).

    Webhook dispatcher

    1. 1

      WayForPay — dispatchWayForPayWebhook: parse orderReference → verify signature → purpose handler. Wallet: transactionStatus === 'Approved' before addFiatUsd.

    2. 2

      Stripe — dispatchStripeWebhook: verify stripe-signature → read metadata.purpose → Stripe purpose handlers.

    3. 3

      PayPal — dispatchPayPalWebhook with transmission headers → Orders capture by purpose or Subscriptions lifecycle (BILLING.SUBSCRIPTION.*, PAYMENT.SALE.COMPLETED) via membership-paypal-subscription.ts.

    4. 4

      Canonical routes: app/api/payments/wayforpay/webhook/route.ts, app/api/payments/stripe/webhook/route.ts, app/api/payments/paypal/webhook/route.ts (when enabled).

    serviceUrl / Stripe endpoint must be public HTTPS. Localhost cannot receive PSP callbacks — use a tunnel or staging for settlement tests.

    API routes

    MethodPathRole
    POST/api/payments/wayforpay/webhookUnified WayForPay callback
    POST/api/payments/stripe/webhookStripe signed events
    POST/api/store/payments/wayforpayStore redirect via conductor (store_order)
    POST/api/store/payments/stripeStore Stripe Checkout via conductor
    POST/api/store/payments/tokenStore native-token rail (PAYMENT_STORE_ALLOW_TOKEN=true)
    POST/api/store/payments/paypalStore PayPal Orders v2 (NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL)
    POST/api/store/payments/cardStore card alias → WayForPay
    GET/api/store/payments/[orderId]/statusPoll status
    POST/api/store/payments/creditInternal credit
    POST/api/membership/payment/paypalMembership PayPal via SubscriptionConductor (Subscriptions v1 / Orders)
    POST/api/payments/paypal/webhookPayPal Orders + Subscriptions webhooks
    GET/api/admin/users/[id]/paymentsAdmin Payments tab
    POST/api/membership/payment/tokenRING membership
    POST/api/news/promotion/submitNews promotion checkout

    Deprecated aliases (delegate to canonical webhook):

    • app/api/store/payments/wayforpay/webhook/route.ts
    • app/api/news/promotion/wayforpay-webhook/route.ts

    Wallet top-up entry

    WalletConductor.initiateTopUp / initiateCreditTopupPayment → PaymentConductor.createCheckout({ purpose: 'wallet_topup' }) → processor from env or metadata.processor → UI followCheckoutResult(result.redirect) → webhook credits via creditBalanceService.addFiatUsd. Amount gate: 25–2000. Card tab omits processor (env SSOT); PayPal tab sets processor=paypal.

    Membership card entry

    initiateMembershipPayment card path → PaymentConductor.createCheckout({ purpose: 'membership_upgrade' }). PaymentModal tabs: native-token, card, PayPal (flag-gated). Recurring PayPal: SubscriptionConductor. Manage UI: /membership/manage.

    PaymentConductor API

    Security notes

    • Never store raw card numbers — hosted PSP checkout only
    • WayForPay: HMAC / merchant signature verification per flow; HPP via form POST only
    • Stripe: stripe-signature + STRIPE_WEBHOOK_SECRET
    • PayPal: webhook transmission signature verification
    • WAYFORPAY_MERCHANT_PASSWORD for regularApi only — never expose to client
    • Internal credit routes require authenticated session + ownership checks
    • Do not trust returnUrl query params for fulfillment
    bash
    
    WAYFORPAY_MERCHANT_ACCOUNT=...
    WAYFORPAY_SECRET_KEY=...
    WAYFORPAY_MERCHANT_PASSWORD=...   # regularApi / recurring
    WAYFORPAY_DOMAIN=ring-platform.org
    WAYFORPAY_API_URL=https://api.wayforpay.com/api
  4. normalizeCheckoutResult(result) — always applied at the end of PaymentConductor.createCheckout
  5. Browser: lib/payments/checkout-redirect.ts → followCheckoutRedirect / followCheckoutResult
  6. WayForPay HPP rejects GET query-string “payment URLs” (Bad Request / This page requires only POST data). Build fields in lib/payments/wayforpay-hpp.ts.

    Core modules

    ModulePathResponsibility
    Conductorlib/payments/conductor/payment-conductor.tscreateCheckout (+ normalize), handleWebhook, transaction lookup
    Typeslib/payments/conductor/types.tsPurposes, rails, CheckoutRedirect, normalizeCheckoutResult
    Client handofflib/payments/checkout-redirect.tsUnbranded followCheckoutResult
    WFP HPPlib/payments/wayforpay-hpp.tsSign + form_post fields
    Dispatcherlib/payments/conductor/webhook-dispatcher.tsVerify PSP payloads; route to purpose handlers
    Order referenceslib/payments/order-reference.tsBuild / parse idempotent orderReference strings
    Ledger servicelib/payments/payment-transaction-service.tsCRUD + listByUserId on payment_transactions
    Configlib/payments/payment.config.tsgetPaymentProvider, isRailEnabled, webhook URL helpers

    Purpose handlers

    PurposeHandler file
    store_orderconductor/handlers/store-order.ts (+ Stripe: store-order-stripe.ts)
    membership_upgradeconductor/handlers/membership-upgrade.ts (+ Stripe: membership-upgrade-stripe.ts)
    news_promotionconductor/handlers/news-promotion.ts
    wallet_topupconductor/handlers/wallet-topup.ts (+ Stripe / PayPal capture handlers)
    native_token_onrampconductor/handlers/native-token-onramp.ts (+ Stripe: native-token-onramp-stripe.ts)

    Processors

    ProcessorFileBrowser mode
    WayForPayprocessors/wayforpay.processor.tsHPP form_post (or invoice navigate)
    Stripeprocessors/stripe.processor.tsnavigate (Checkout Session URL)
    PayPalprocessors/paypal.processor.tsnavigate (Orders v2 approve URL)
    Internal creditprocessors/internal-credit.processor.tsSync settle (no redirect)
    Native tokenprocessors/native-token.processor.tsSync settle (no redirect)

    Processor resolution in createCheckout:

    1. rail === 'internal_credit' / 'native_token' → that processor
    2. Else metadata.processor if paypal | stripe | wayforpay
    3. Else getPaymentProvider(purpose) (PAYMENT_*_PROCESSOR / PAYMENT_DEFAULT_PROCESSOR)

    Legacy helpers (wayforpay-service.ts, wayforpay-store-service.ts) remain for specialized form building; new route entry points go through the conductor.

    Types

    CreateCheckoutContext carries purpose, optional rail, userId, amount, currency, returnUrl, metadata, and purpose-specific fields (orderId, articleId, targetRole, etc.).

    Configuration surface

    lib/payments/payment.config.ts:

    • getPaymentProvider(purpose) — WayForPay vs Stripe from env overrides
    • isRailEnabled(purpose, rail) — credit / token gates
    • getDefaultStoreCurrencySymbol() — PAYMENT_FIAT_CURRENCY (default USD)
    • getWebhookUrl(provider) — ${site}/api/payments/${provider}/webhook

    Env keys per purpose: PAYMENT_STORE_PROCESSOR, PAYMENT_MEMBERSHIP_PROCESSOR, PAYMENT_NEWS_PROCESSOR, PAYMENT_WALLET_TOPUP_PROCESSOR, PAYMENT_NATIVE_TOKEN_ONRAMP_PROCESSOR. Default: PAYMENT_DEFAULT_PROCESSOR (wayforpay | stripe | paypal).

    WayForPay env SSOT (no WAYFORPAY_MERCHANT_ID):

    Order reference prefixes

    PurposePatternExample
    store_orderstore_{orderId}_{timestamp}store_ord_abc_1717000000000
    membership_upgrademembership_{userId}_{timestamp}membership_user123_1717000000000
    membership_upgrade (legacy)ring_{userId}_{timestamp}still accepted
    news_promotionnews-promo-{base64url(articleId)}-{timestamp}news-promo-YWJj-1717000000000
    wallet_topupwallettopup_{userId}_{timestamp}wallettopup_user123_1717000000000

    buildOrderReference / parseOrderReference in order-reference.ts. Duplicate webhook delivery is safe: ledger enforces unique order_reference.

    Ledger (payment_transactions)

    Migration: data/migrations/004_payment_transactions.sql

    JSON data fields (via payment-transaction-service.ts): purpose, processor, rail, order_reference, entity_type, entity_id, user_id, amount_minor, currency, status, status_history, processor_payload, paid_at.

    createPending returns existing row if order_reference already exists. Admin list: paymentTransactionService.listByUserId (default purposes: membership_upgrade, wallet_topup).

    Webhook dispatcher

    1. 1

      WayForPay — dispatchWayForPayWebhook: parse orderReference → verify signature → purpose handler. Wallet: transactionStatus === 'Approved' before addFiatUsd.

    2. 2

      Stripe — dispatchStripeWebhook: verify stripe-signature → read metadata.purpose → Stripe purpose handlers.

    3. 3

      PayPal — dispatchPayPalWebhook with transmission headers → Orders capture by purpose or Subscriptions lifecycle (BILLING.SUBSCRIPTION.*, PAYMENT.SALE.COMPLETED) via membership-paypal-subscription.ts.

    4. 4

      Canonical routes: app/api/payments/wayforpay/webhook/route.ts, app/api/payments/stripe/webhook/route.ts, app/api/payments/paypal/webhook/route.ts (when enabled).

    serviceUrl / Stripe endpoint must be public HTTPS. Localhost cannot receive PSP callbacks — use a tunnel or staging for settlement tests.

    API routes

    MethodPathRole
    POST/api/payments/wayforpay/webhookUnified WayForPay callback
    POST/api/payments/stripe/webhookStripe signed events
    POST/api/store/payments/wayforpayStore redirect via conductor (store_order)
    POST/api/store/payments/stripeStore Stripe Checkout via conductor
    POST/api/store/payments/tokenStore native-token rail (PAYMENT_STORE_ALLOW_TOKEN=true)
    POST/api/store/payments/paypalStore PayPal Orders v2 (NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL)
    POST/api/store/payments/cardStore card alias → WayForPay
    GET/api/store/payments/[orderId]/statusPoll status
    POST/api/store/payments/creditInternal credit
    POST/api/membership/payment/paypalMembership PayPal via SubscriptionConductor (Subscriptions v1 / Orders)
    POST/api/payments/paypal/webhookPayPal Orders + Subscriptions webhooks
    GET/api/admin/users/[id]/paymentsAdmin Payments tab
    POST/api/membership/payment/tokenRING membership
    POST/api/news/promotion/submitNews promotion checkout

    Deprecated aliases (delegate to canonical webhook):

    • app/api/store/payments/wayforpay/webhook/route.ts
    • app/api/news/promotion/wayforpay-webhook/route.ts

    Wallet top-up entry

    WalletConductor.initiateTopUp / initiateCreditTopupPayment → PaymentConductor.createCheckout({ purpose: 'wallet_topup' }) → processor from env or metadata.processor → UI followCheckoutResult(result.redirect) → webhook credits via creditBalanceService.addFiatUsd. Amount gate: 25–2000. Card tab omits processor (env SSOT); PayPal tab sets processor=paypal.

    Membership card entry

    initiateMembershipPayment card path → PaymentConductor.createCheckout({ purpose: 'membership_upgrade' }). PaymentModal tabs: native-token, card, PayPal (flag-gated). Recurring PayPal: SubscriptionConductor. Manage UI: /membership/manage.

    PaymentConductor API

    Security notes

    • Never store raw card numbers — hosted PSP checkout only
    • WayForPay: HMAC / merchant signature verification per flow; HPP via form POST only
    • Stripe: stripe-signature + STRIPE_WEBHOOK_SECRET
    • PayPal: webhook transmission signature verification
    • WAYFORPAY_MERCHANT_PASSWORD for regularApi only — never expose to client
    • Internal credit routes require authenticated session + ownership checks
    • Do not trust returnUrl query params for fulfillment
    bash
    
    WAYFORPAY_MERCHANT_ACCOUNT=...
    WAYFORPAY_SECRET_KEY=...
    WAYFORPAY_MERCHANT_PASSWORD=...   # regularApi / recurring
    WAYFORPAY_DOMAIN=ring-platform.org
    WAYFORPAY_API_URL=https://api.wayforpay.com/api
  7. normalizeCheckoutResult(result) — always applied at the end of PaymentConductor.createCheckout
  8. Browser: lib/payments/checkout-redirect.ts → followCheckoutRedirect / followCheckoutResult
  9. WayForPay HPP rejects GET query-string “payment URLs” (Bad Request / This page requires only POST data). Build fields in lib/payments/wayforpay-hpp.ts.

    Core modules

    ModulePathResponsibility
    Conductorlib/payments/conductor/payment-conductor.tscreateCheckout (+ normalize), handleWebhook, transaction lookup
    Typeslib/payments/conductor/types.tsPurposes, rails, CheckoutRedirect, normalizeCheckoutResult
    Client handofflib/payments/checkout-redirect.tsUnbranded followCheckoutResult
    WFP HPPlib/payments/wayforpay-hpp.tsSign + form_post fields
    Dispatcherlib/payments/conductor/webhook-dispatcher.tsVerify PSP payloads; route to purpose handlers
    Order referenceslib/payments/order-reference.tsBuild / parse idempotent orderReference strings
    Ledger servicelib/payments/payment-transaction-service.tsCRUD + listByUserId on payment_transactions
    Configlib/payments/payment.config.tsgetPaymentProvider, isRailEnabled, webhook URL helpers

    Purpose handlers

    PurposeHandler file
    store_orderconductor/handlers/store-order.ts (+ Stripe: store-order-stripe.ts)
    membership_upgradeconductor/handlers/membership-upgrade.ts (+ Stripe: membership-upgrade-stripe.ts)
    news_promotionconductor/handlers/news-promotion.ts
    wallet_topupconductor/handlers/wallet-topup.ts (+ Stripe / PayPal capture handlers)
    native_token_onrampconductor/handlers/native-token-onramp.ts (+ Stripe: native-token-onramp-stripe.ts)

    Processors

    ProcessorFileBrowser mode
    WayForPayprocessors/wayforpay.processor.tsHPP form_post (or invoice navigate)
    Stripeprocessors/stripe.processor.tsnavigate (Checkout Session URL)
    PayPalprocessors/paypal.processor.tsnavigate (Orders v2 approve URL)
    Internal creditprocessors/internal-credit.processor.tsSync settle (no redirect)
    Native tokenprocessors/native-token.processor.tsSync settle (no redirect)

    Processor resolution in createCheckout:

    1. rail === 'internal_credit' / 'native_token' → that processor
    2. Else metadata.processor if paypal | stripe | wayforpay
    3. Else getPaymentProvider(purpose) (PAYMENT_*_PROCESSOR / PAYMENT_DEFAULT_PROCESSOR)

    Legacy helpers (wayforpay-service.ts, wayforpay-store-service.ts) remain for specialized form building; new route entry points go through the conductor.

    Types

    CreateCheckoutContext carries purpose, optional rail, userId, amount, currency, returnUrl, metadata, and purpose-specific fields (orderId, articleId, targetRole, etc.).

    Configuration surface

    lib/payments/payment.config.ts:

    • getPaymentProvider(purpose) — WayForPay vs Stripe from env overrides
    • isRailEnabled(purpose, rail) — credit / token gates
    • getDefaultStoreCurrencySymbol() — PAYMENT_FIAT_CURRENCY (default USD)
    • getWebhookUrl(provider) — ${site}/api/payments/${provider}/webhook

    Env keys per purpose: PAYMENT_STORE_PROCESSOR, PAYMENT_MEMBERSHIP_PROCESSOR, PAYMENT_NEWS_PROCESSOR, PAYMENT_WALLET_TOPUP_PROCESSOR, PAYMENT_NATIVE_TOKEN_ONRAMP_PROCESSOR. Default: PAYMENT_DEFAULT_PROCESSOR (wayforpay | stripe | paypal).

    WayForPay env SSOT (no WAYFORPAY_MERCHANT_ID):

    Order reference prefixes

    PurposePatternExample
    store_orderstore_{orderId}_{timestamp}store_ord_abc_1717000000000
    membership_upgrademembership_{userId}_{timestamp}membership_user123_1717000000000
    membership_upgrade (legacy)ring_{userId}_{timestamp}still accepted
    news_promotionnews-promo-{base64url(articleId)}-{timestamp}news-promo-YWJj-1717000000000
    wallet_topupwallettopup_{userId}_{timestamp}wallettopup_user123_1717000000000

    buildOrderReference / parseOrderReference in order-reference.ts. Duplicate webhook delivery is safe: ledger enforces unique order_reference.

    Ledger (payment_transactions)

    Migration: data/migrations/004_payment_transactions.sql

    JSON data fields (via payment-transaction-service.ts): purpose, processor, rail, order_reference, entity_type, entity_id, user_id, amount_minor, currency, status, status_history, processor_payload, paid_at.

    createPending returns existing row if order_reference already exists. Admin list: paymentTransactionService.listByUserId (default purposes: membership_upgrade, wallet_topup).

    Webhook dispatcher

    1. 1

      WayForPay — dispatchWayForPayWebhook: parse orderReference → verify signature → purpose handler. Wallet: transactionStatus === 'Approved' before addFiatUsd.

    2. 2

      Stripe — dispatchStripeWebhook: verify stripe-signature → read metadata.purpose → Stripe purpose handlers.

    3. 3

      PayPal — dispatchPayPalWebhook with transmission headers → Orders capture by purpose or Subscriptions lifecycle (BILLING.SUBSCRIPTION.*, PAYMENT.SALE.COMPLETED) via membership-paypal-subscription.ts.

    4. 4

      Canonical routes: app/api/payments/wayforpay/webhook/route.ts, app/api/payments/stripe/webhook/route.ts, app/api/payments/paypal/webhook/route.ts (when enabled).

    serviceUrl / Stripe endpoint must be public HTTPS. Localhost cannot receive PSP callbacks — use a tunnel or staging for settlement tests.

    API routes

    MethodPathRole
    POST/api/payments/wayforpay/webhookUnified WayForPay callback
    POST/api/payments/stripe/webhookStripe signed events
    POST/api/store/payments/wayforpayStore redirect via conductor (store_order)
    POST/api/store/payments/stripeStore Stripe Checkout via conductor
    POST/api/store/payments/tokenStore native-token rail (PAYMENT_STORE_ALLOW_TOKEN=true)
    POST/api/store/payments/paypalStore PayPal Orders v2 (NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL)
    POST/api/store/payments/cardStore card alias → WayForPay
    GET/api/store/payments/[orderId]/statusPoll status
    POST/api/store/payments/creditInternal credit
    POST/api/membership/payment/paypalMembership PayPal via SubscriptionConductor (Subscriptions v1 / Orders)
    POST/api/payments/paypal/webhookPayPal Orders + Subscriptions webhooks
    GET/api/admin/users/[id]/paymentsAdmin Payments tab
    POST/api/membership/payment/tokenRING membership
    POST/api/news/promotion/submitNews promotion checkout

    Deprecated aliases (delegate to canonical webhook):

    • app/api/store/payments/wayforpay/webhook/route.ts
    • app/api/news/promotion/wayforpay-webhook/route.ts

    Wallet top-up entry

    WalletConductor.initiateTopUp / initiateCreditTopupPayment → PaymentConductor.createCheckout({ purpose: 'wallet_topup' }) → processor from env or metadata.processor → UI followCheckoutResult(result.redirect) → webhook credits via creditBalanceService.addFiatUsd. Amount gate: 25–2000. Card tab omits processor (env SSOT); PayPal tab sets processor=paypal.

    Membership card entry

    initiateMembershipPayment card path → PaymentConductor.createCheckout({ purpose: 'membership_upgrade' }). PaymentModal tabs: native-token, card, PayPal (flag-gated). Recurring PayPal: SubscriptionConductor. Manage UI: /membership/manage.

    PaymentConductor API

    Security notes

    • Never store raw card numbers — hosted PSP checkout only
    • WayForPay: HMAC / merchant signature verification per flow; HPP via form POST only
    • Stripe: stripe-signature + STRIPE_WEBHOOK_SECRET
    • PayPal: webhook transmission signature verification
    • WAYFORPAY_MERCHANT_PASSWORD for regularApi only — never expose to client
    • Internal credit routes require authenticated session + ownership checks
    • Do not trust returnUrl query params for fulfillment
    bash
    
    WAYFORPAY_MERCHANT_ACCOUNT=...
    WAYFORPAY_SECRET_KEY=...
    WAYFORPAY_MERCHANT_PASSWORD=...   # regularApi / recurring
    WAYFORPAY_DOMAIN=ring-platform.org
    WAYFORPAY_API_URL=https://api.wayforpay.com/api