Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Preparing Ring Platform content
Preparing Ring Platform content
Preparing Ring Platform content
Filter this page with Founder / Developer in the docs sidebar. Founders learn provider costs, the manage page, PayPal, and Stars; developers get ledger fields, provider modules, and webhook branches.
SubscriptionConductor is Ring Platform's multi-provider membership billing facade. One SSOT ledger (subscription_ledger) tracks subscriber state for every rail. Provider modules implement SubscriptionProviderModule; PaymentConductor still owns one-shot Orders (store / wallet / one-shot membership).
Implementation: lib/payments/subscription/. Manage UI: /membership/manage.
Recurring membership revenue with clear gateway fees. Internal rails (credit points, native token, NFT gate) charge 0% platform PSP fee. Members manage cancel / renew / history on Manage membership (/membership/manage) — not only the upgrade funnel at /membership.
Own file manager (/file-cabinet) and gallery manage (/profile/gallery) are member+ (hasMemberPrivileges + MemberUpgradeGate). Shared-with-me (/profile/shared) is subscriber+ for trustee shares; subscriber-only users see an upgrade CTA for an own manager — see Ring File Cabinet.
| Provider | Details | Gateway | Status |
|---|---|---|---|
| Credit Balance | Auto-deduct credit units (points) | Credit points | ✅ Live |
| WayForPay | Card HPP + recToken regularApi | WayForPay | ✅ Live |
| Stripe | Stripe Subscriptions API | Stripe | ✅ Live |
| Native Token | On-chain Solana / RingMembership | Native RING | ✅ Live |
| NFT Gate | Metaplex Core + GateEscrow stake | NFT Gate | ✅ Live (MVP-A) |
| PayPal | Subscriptions v1 (recurring) + Orders one-shot | PayPal | ✅ Live (flags + credentials) |
TELEGRAM_MINI_APP_BOT_TOKEN (same helper as Mini App auth — see Authentication).metadata.telegramUserId (the payer’s Telegram numeric id). Optional: metadata.invoiceTitle / invoiceDescription.pending ledger row and returns a Telegram invoice URL (redirect.mode: navigate). Member pays in Telegram (currency XTR).successful_payment on POST /api/telegram/stars-bot/webhook activates it (same pattern as PayPal BILLING.SUBSCRIPTION.ACTIVATED). Do not treat invoice creation alone as paid membership.payment.gateways.paypal.enabled: true in ring-config.json.PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_ID, PAYPAL_MODE.NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true (also gates store PayPal + membership PaymentModal PayPal tab).BILLING.SUBSCRIPTION.*, PAYMENT.SALE.COMPLETED).PAYPAL_MEMBERSHIP_PRODUCT_ID / PAYPAL_MEMBERSHIP_PLAN_ID so checkout does not recreate catalog objects every time.Members land on /membership/manage after upgrade (members hitting /membership redirect there). Cancel uses the PayPal subscription id (I-…) stored on the ledger.
| Provider | Fee % | Fixed Fee | Currency |
|---|---|---|---|
| WayForPay | 2.5% | — | UAH (typical) |
| Stripe | 2.9% | $0.30 | USD |
| Credit Balance | 0% | — | Credit units |
| Native Token | 0% | — | RING |
| NFT Gate | 0% | — | RING |
| PayPal |
Net: net = gross - (gross × feePercent / 100) - feeFixed via calculateNetRevenue() in subscription-config.ts. Stars fee percent in admin stats defaults to 0 until you configure gateway fees for telegram_stars.
Prod public flags must also be baked as Docker NEXT_PUBLIC_* build-args and applied via k8s ConfigMap/Secret — see ENV-PROD-WIRING in the clone (ring-platform.org/k8s/ENV-PROD-WIRING.md).
Superadmins: /admin/subscriptions — list, filters, net revenue, /api/admin/subscriptions/stats.
Prerequisite: one-shot Orders vs this recurring ledger facade.
Same-workflow: card HPP + recToken pattern mirrored by PayPal I-….
Deep-dive: webhook dispatcher and API route table.
See-also: GateEscrow stakes shown on the manage page.
Filter this page with Founder / Developer in the docs sidebar. Founders learn provider costs, the manage page, PayPal, and Stars; developers get ledger fields, provider modules, and webhook branches.
SubscriptionConductor is Ring Platform's multi-provider membership billing facade. One SSOT ledger (subscription_ledger) tracks subscriber state for every rail. Provider modules implement SubscriptionProviderModule; PaymentConductor still owns one-shot Orders (store / wallet / one-shot membership).
Implementation: lib/payments/subscription/. Manage UI: /membership/manage.
Recurring membership revenue with clear gateway fees. Internal rails (credit points, native token, NFT gate) charge 0% platform PSP fee. Members manage cancel / renew / history on Manage membership (/membership/manage) — not only the upgrade funnel at /membership.
Own file manager (/file-cabinet) and gallery manage (/profile/gallery) are member+ (hasMemberPrivileges + MemberUpgradeGate). Shared-with-me (/profile/shared) is subscriber+ for trustee shares; subscriber-only users see an upgrade CTA for an own manager — see Ring File Cabinet.
| Provider | Details | Gateway | Status |
|---|---|---|---|
| Credit Balance | Auto-deduct credit units (points) | Credit points | ✅ Live |
| WayForPay | Card HPP + recToken regularApi | WayForPay | ✅ Live |
| Stripe | Stripe Subscriptions API | Stripe | ✅ Live |
| Native Token | On-chain Solana / RingMembership | Native RING | ✅ Live |
| NFT Gate | Metaplex Core + GateEscrow stake | NFT Gate | ✅ Live (MVP-A) |
| PayPal | Subscriptions v1 (recurring) + Orders one-shot | PayPal | ✅ Live (flags + credentials) |
TELEGRAM_MINI_APP_BOT_TOKEN (same helper as Mini App auth — see Authentication).metadata.telegramUserId (the payer’s Telegram numeric id). Optional: metadata.invoiceTitle / invoiceDescription.pending ledger row and returns a Telegram invoice URL (redirect.mode: navigate). Member pays in Telegram (currency XTR).successful_payment on POST /api/telegram/stars-bot/webhook activates it (same pattern as PayPal BILLING.SUBSCRIPTION.ACTIVATED). Do not treat invoice creation alone as paid membership.payment.gateways.paypal.enabled: true in ring-config.json.PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_ID, PAYPAL_MODE.NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true (also gates store PayPal + membership PaymentModal PayPal tab).BILLING.SUBSCRIPTION.*, PAYMENT.SALE.COMPLETED).PAYPAL_MEMBERSHIP_PRODUCT_ID / PAYPAL_MEMBERSHIP_PLAN_ID so checkout does not recreate catalog objects every time.Members land on /membership/manage after upgrade (members hitting /membership redirect there). Cancel uses the PayPal subscription id (I-…) stored on the ledger.
| Provider | Fee % | Fixed Fee | Currency |
|---|---|---|---|
| WayForPay | 2.5% | — | UAH (typical) |
| Stripe | 2.9% | $0.30 | USD |
| Credit Balance | 0% | — | Credit units |
| Native Token | 0% | — | RING |
| NFT Gate | 0% | — | RING |
| PayPal |
Net: net = gross - (gross × feePercent / 100) - feeFixed via calculateNetRevenue() in subscription-config.ts. Stars fee percent in admin stats defaults to 0 until you configure gateway fees for telegram_stars.
Prod public flags must also be baked as Docker NEXT_PUBLIC_* build-args and applied via k8s ConfigMap/Secret — see ENV-PROD-WIRING in the clone (ring-platform.org/k8s/ENV-PROD-WIRING.md).
Superadmins: /admin/subscriptions — list, filters, net revenue, /api/admin/subscriptions/stats.
Prerequisite: one-shot Orders vs this recurring ledger facade.
Same-workflow: card HPP + recToken pattern mirrored by PayPal I-….
Deep-dive: webhook dispatcher and API route table.
See-also: GateEscrow stakes shown on the manage page.
Filter this page with Founder / Developer in the docs sidebar. Founders learn provider costs, the manage page, PayPal, and Stars; developers get ledger fields, provider modules, and webhook branches.
SubscriptionConductor is Ring Platform's multi-provider membership billing facade. One SSOT ledger (subscription_ledger) tracks subscriber state for every rail. Provider modules implement SubscriptionProviderModule; PaymentConductor still owns one-shot Orders (store / wallet / one-shot membership).
Implementation: lib/payments/subscription/. Manage UI: /membership/manage.
Recurring membership revenue with clear gateway fees. Internal rails (credit points, native token, NFT gate) charge 0% platform PSP fee. Members manage cancel / renew / history on Manage membership (/membership/manage) — not only the upgrade funnel at /membership.
Own file manager (/file-cabinet) and gallery manage (/profile/gallery) are member+ (hasMemberPrivileges + MemberUpgradeGate). Shared-with-me (/profile/shared) is subscriber+ for trustee shares; subscriber-only users see an upgrade CTA for an own manager — see Ring File Cabinet.
| Provider | Details | Gateway | Status |
|---|---|---|---|
| Credit Balance | Auto-deduct credit units (points) | Credit points | ✅ Live |
| WayForPay | Card HPP + recToken regularApi | WayForPay | ✅ Live |
| Stripe | Stripe Subscriptions API | Stripe | ✅ Live |
| Native Token | On-chain Solana / RingMembership | Native RING | ✅ Live |
| NFT Gate | Metaplex Core + GateEscrow stake | NFT Gate | ✅ Live (MVP-A) |
| PayPal | Subscriptions v1 (recurring) + Orders one-shot | PayPal | ✅ Live (flags + credentials) |
TELEGRAM_MINI_APP_BOT_TOKEN (same helper as Mini App auth — see Authentication).metadata.telegramUserId (the payer’s Telegram numeric id). Optional: metadata.invoiceTitle / invoiceDescription.pending ledger row and returns a Telegram invoice URL (redirect.mode: navigate). Member pays in Telegram (currency XTR).successful_payment on POST /api/telegram/stars-bot/webhook activates it (same pattern as PayPal BILLING.SUBSCRIPTION.ACTIVATED). Do not treat invoice creation alone as paid membership.payment.gateways.paypal.enabled: true in ring-config.json.PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_ID, PAYPAL_MODE.NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true (also gates store PayPal + membership PaymentModal PayPal tab).BILLING.SUBSCRIPTION.*, PAYMENT.SALE.COMPLETED).PAYPAL_MEMBERSHIP_PRODUCT_ID / PAYPAL_MEMBERSHIP_PLAN_ID so checkout does not recreate catalog objects every time.Members land on /membership/manage after upgrade (members hitting /membership redirect there). Cancel uses the PayPal subscription id (I-…) stored on the ledger.
| Provider | Fee % | Fixed Fee | Currency |
|---|---|---|---|
| WayForPay | 2.5% | — | UAH (typical) |
| Stripe | 2.9% | $0.30 | USD |
| Credit Balance | 0% | — | Credit units |
| Native Token | 0% | — | RING |
| NFT Gate | 0% | — | RING |
| PayPal |
Net: net = gross - (gross × feePercent / 100) - feeFixed via calculateNetRevenue() in subscription-config.ts. Stars fee percent in admin stats defaults to 0 until you configure gateway fees for telegram_stars.
Prod public flags must also be baked as Docker NEXT_PUBLIC_* build-args and applied via k8s ConfigMap/Secret — see ENV-PROD-WIRING in the clone (ring-platform.org/k8s/ENV-PROD-WIRING.md).
Superadmins: /admin/subscriptions — list, filters, net revenue, /api/admin/subscriptions/stats.
Prerequisite: one-shot Orders vs this recurring ledger facade.
Same-workflow: card HPP + recToken pattern mirrored by PayPal I-….
Deep-dive: webhook dispatcher and API route table.
See-also: GateEscrow stakes shown on the manage page.
| Telegram Stars |
XTR invoice link via Bot API createInvoiceLink |
| Telegram |
| ✅ Create pending — webhook activation TBD |
| 2.9% |
| $0.30 |
| USD |
| Telegram Stars | Telegram commission (operator-side) | — | XTR |
ringCreditSubscriptionProvider |
providers/ring-credit-subscription.ts |
native_token | nativeTokenSubscriptionProvider | providers/native-token-subscription.ts |
stripe | stripeSubscriptionProvider | providers/stripe-subscription.ts |
wayforpay | wayforpaySubscriptionProvider | providers/wayforpay-subscription.ts |
nft_gate | nftGateSubscriptionProvider | providers/nft-gate-subscription.ts |
paypal | paypalSubscriptionProvider | providers/paypal-subscription.ts |
telegram_stars | telegramStarsSubscriptionProvider | providers/telegram-stars-subscription.ts |
Client helpers: createPayPalBillingSubscription, cancelPayPalBillingSubscription, getPayPalBillingSubscription in lib/payments/processors/paypal-client.ts.
Stars env helper: getTelegramMiniAppBotToken() from lib/auth/telegram-miniapp-initdata.ts.
Schema: lib/payments/subscription/subscription-ledger-schema.ts.
| Field | Notes |
|---|---|
status | pending | active | expired | cancelled | suspended | grace_period |
method | card | credit_balance | crypto | nft | paypal | stars |
provider | includes telegram_stars |
paypal_subscription_id | PayPal Subscriptions I-… (not Orders orderReference) |
telegram_stars_payload / telegram_stars_invoice_link | Payload = ledger id; invoice URL from createInvoiceLink |
telegram_payment_charge_id | Set on successful_payment (refundStarPayment) |
stripe_subscription_id / wayforpay_rec_token / solana_tx_signature / nft_mint_address | Other rails |
gatewayRefToField('paypal') → { paypal_subscription_id }. Cancel API + app/_actions/membership.ts pass that id into SubscriptionConductor.cancelSubscription.
| Operation | Role |
|---|---|
createSubscription | Honors ledgerStatus: 'pending' — skips MEMBER upgrade until active |
recordPaidSubscription | Insert active row without calling provider (Orders capture) |
findByPaypalSubscriptionId | Webhook lookup by I-… |
updateSubscriptionStatus | Can set paypal_subscription_id, payments aggregates |
dispatchPayPalWebhook (webhook-dispatcher.ts):
| Event | Handler |
|---|---|
PAYMENT.CAPTURE.COMPLETED | Orders purpose handlers (store / membership capture / wallet / news) |
BILLING.SUBSCRIPTION.ACTIVATED | membership-paypal-subscription.ts → activate ledger |
PAYMENT.SALE.COMPLETED | Advance next_payment_due / payments_count |
BILLING.SUBSCRIPTION.CANCELLED / SUSPENDED / EXPIRED / PAYMENT.FAILED | Terminal / suspended status |
Canonical route: POST /api/payments/paypal/webhook.
| Method | Path | Role |
|---|---|---|
POST | /api/membership/payment/paypal | Create recurring (default) or renew status-sync |
POST | /api/membership/subscription/cancel | Cancel; extracts paypal_subscription_id |
GET | /api/membership/subscription/status | can_cancel for active | pending |
| Page | /[locale]/(protected)/membership/manage | SubscriptionManagement + role + NFT stakes + payment_transactions |
| Route helper | ROUTES.MEMBERSHIP_MANAGE(locale) | /membership/manage |
UI: components/membership/payment-modal.tsx PayPal tab when NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true; components/membership/subscription-management.tsx on the manage page.
Secrets stay runtime (K8s Secret). Public flags also need Dockerfile / prod.js build-args for client bundles.
Unchanged five membership crons in lib/processes/registry.ts (expiry, credit-balance monthly, subscription-payment, solana-batch, nft-gate-expiry). PayPal renewals are primarily webhook-driven (PAYMENT.SALE.COMPLETED); manual renew is a Subscriptions GET status sync. Stars renew currently returns a synthetic success + 30-day nextPaymentDue — production renew should follow Telegram payment events once the webhook is live.
SubscriptionConductor.createSubscription from PayPal webhooks (re-opens checkout). Use recordPaidSubscription / updateSubscriptionStatus.orderReference as paypal_subscription_id.returnUrl alone as paid — wait for webhook events.createInvoiceLink success as membership — wait for successful_payment on /api/telegram/stars-bot/webhook → activate pending ledger.metadata.telegramUserId on Stars create (provider returns an error).See-also: purpose map across the clone.
{
"payment": {
"cardPaymentProcessor": "wayforpay",
"gateways": {
"paypal": { "enabled": true, "feePercent": 2.9, "feeFixedCents": 30, "currency": "USD" }
}
}
}
Membership upgrade / PaymentModal / manage cancel
↓
SubscriptionConductor (create | cancel | renew | recordPaidSubscription)
↓
Providers: stripe | wayforpay | credit_balance | native_token | nft_gate | paypal | telegram_stars
↓
subscription_ledger (paypal_subscription_id = PayPal I-…; Stars stores invoice link pending)
↓
Cron: expiry · credit-balance · payment · solana-batch · nft-gate| Telegram Stars |
XTR invoice link via Bot API createInvoiceLink |
| Telegram |
| ✅ Create pending — webhook activation TBD |
| 2.9% |
| $0.30 |
| USD |
| Telegram Stars | Telegram commission (operator-side) | — | XTR |
ringCreditSubscriptionProvider |
providers/ring-credit-subscription.ts |
native_token | nativeTokenSubscriptionProvider | providers/native-token-subscription.ts |
stripe | stripeSubscriptionProvider | providers/stripe-subscription.ts |
wayforpay | wayforpaySubscriptionProvider | providers/wayforpay-subscription.ts |
nft_gate | nftGateSubscriptionProvider | providers/nft-gate-subscription.ts |
paypal | paypalSubscriptionProvider | providers/paypal-subscription.ts |
telegram_stars | telegramStarsSubscriptionProvider | providers/telegram-stars-subscription.ts |
Client helpers: createPayPalBillingSubscription, cancelPayPalBillingSubscription, getPayPalBillingSubscription in lib/payments/processors/paypal-client.ts.
Stars env helper: getTelegramMiniAppBotToken() from lib/auth/telegram-miniapp-initdata.ts.
Schema: lib/payments/subscription/subscription-ledger-schema.ts.
| Field | Notes |
|---|---|
status | pending | active | expired | cancelled | suspended | grace_period |
method | card | credit_balance | crypto | nft | paypal | stars |
provider | includes telegram_stars |
paypal_subscription_id | PayPal Subscriptions I-… (not Orders orderReference) |
telegram_stars_payload / telegram_stars_invoice_link | Payload = ledger id; invoice URL from createInvoiceLink |
telegram_payment_charge_id | Set on successful_payment (refundStarPayment) |
stripe_subscription_id / wayforpay_rec_token / solana_tx_signature / nft_mint_address | Other rails |
gatewayRefToField('paypal') → { paypal_subscription_id }. Cancel API + app/_actions/membership.ts pass that id into SubscriptionConductor.cancelSubscription.
| Operation | Role |
|---|---|
createSubscription | Honors ledgerStatus: 'pending' — skips MEMBER upgrade until active |
recordPaidSubscription | Insert active row without calling provider (Orders capture) |
findByPaypalSubscriptionId | Webhook lookup by I-… |
updateSubscriptionStatus | Can set paypal_subscription_id, payments aggregates |
dispatchPayPalWebhook (webhook-dispatcher.ts):
| Event | Handler |
|---|---|
PAYMENT.CAPTURE.COMPLETED | Orders purpose handlers (store / membership capture / wallet / news) |
BILLING.SUBSCRIPTION.ACTIVATED | membership-paypal-subscription.ts → activate ledger |
PAYMENT.SALE.COMPLETED | Advance next_payment_due / payments_count |
BILLING.SUBSCRIPTION.CANCELLED / SUSPENDED / EXPIRED / PAYMENT.FAILED | Terminal / suspended status |
Canonical route: POST /api/payments/paypal/webhook.
| Method | Path | Role |
|---|---|---|
POST | /api/membership/payment/paypal | Create recurring (default) or renew status-sync |
POST | /api/membership/subscription/cancel | Cancel; extracts paypal_subscription_id |
GET | /api/membership/subscription/status | can_cancel for active | pending |
| Page | /[locale]/(protected)/membership/manage | SubscriptionManagement + role + NFT stakes + payment_transactions |
| Route helper | ROUTES.MEMBERSHIP_MANAGE(locale) | /membership/manage |
UI: components/membership/payment-modal.tsx PayPal tab when NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true; components/membership/subscription-management.tsx on the manage page.
Secrets stay runtime (K8s Secret). Public flags also need Dockerfile / prod.js build-args for client bundles.
Unchanged five membership crons in lib/processes/registry.ts (expiry, credit-balance monthly, subscription-payment, solana-batch, nft-gate-expiry). PayPal renewals are primarily webhook-driven (PAYMENT.SALE.COMPLETED); manual renew is a Subscriptions GET status sync. Stars renew currently returns a synthetic success + 30-day nextPaymentDue — production renew should follow Telegram payment events once the webhook is live.
SubscriptionConductor.createSubscription from PayPal webhooks (re-opens checkout). Use recordPaidSubscription / updateSubscriptionStatus.orderReference as paypal_subscription_id.returnUrl alone as paid — wait for webhook events.createInvoiceLink success as membership — wait for successful_payment on /api/telegram/stars-bot/webhook → activate pending ledger.metadata.telegramUserId on Stars create (provider returns an error).See-also: purpose map across the clone.
{
"payment": {
"cardPaymentProcessor": "wayforpay",
"gateways": {
"paypal": { "enabled": true, "feePercent": 2.9, "feeFixedCents": 30, "currency": "USD" }
}
}
}
Membership upgrade / PaymentModal / manage cancel
↓
SubscriptionConductor (create | cancel | renew | recordPaidSubscription)
↓
Providers: stripe | wayforpay | credit_balance | native_token | nft_gate | paypal | telegram_stars
↓
subscription_ledger (paypal_subscription_id = PayPal I-…; Stars stores invoice link pending)
↓
Cron: expiry · credit-balance · payment · solana-batch · nft-gate| Telegram Stars |
XTR invoice link via Bot API createInvoiceLink |
| Telegram |
| ✅ Create pending — webhook activation TBD |
| 2.9% |
| $0.30 |
| USD |
| Telegram Stars | Telegram commission (operator-side) | — | XTR |
ringCreditSubscriptionProvider |
providers/ring-credit-subscription.ts |
native_token | nativeTokenSubscriptionProvider | providers/native-token-subscription.ts |
stripe | stripeSubscriptionProvider | providers/stripe-subscription.ts |
wayforpay | wayforpaySubscriptionProvider | providers/wayforpay-subscription.ts |
nft_gate | nftGateSubscriptionProvider | providers/nft-gate-subscription.ts |
paypal | paypalSubscriptionProvider | providers/paypal-subscription.ts |
telegram_stars | telegramStarsSubscriptionProvider | providers/telegram-stars-subscription.ts |
Client helpers: createPayPalBillingSubscription, cancelPayPalBillingSubscription, getPayPalBillingSubscription in lib/payments/processors/paypal-client.ts.
Stars env helper: getTelegramMiniAppBotToken() from lib/auth/telegram-miniapp-initdata.ts.
Schema: lib/payments/subscription/subscription-ledger-schema.ts.
| Field | Notes |
|---|---|
status | pending | active | expired | cancelled | suspended | grace_period |
method | card | credit_balance | crypto | nft | paypal | stars |
provider | includes telegram_stars |
paypal_subscription_id | PayPal Subscriptions I-… (not Orders orderReference) |
telegram_stars_payload / telegram_stars_invoice_link | Payload = ledger id; invoice URL from createInvoiceLink |
telegram_payment_charge_id | Set on successful_payment (refundStarPayment) |
stripe_subscription_id / wayforpay_rec_token / solana_tx_signature / nft_mint_address | Other rails |
gatewayRefToField('paypal') → { paypal_subscription_id }. Cancel API + app/_actions/membership.ts pass that id into SubscriptionConductor.cancelSubscription.
| Operation | Role |
|---|---|
createSubscription | Honors ledgerStatus: 'pending' — skips MEMBER upgrade until active |
recordPaidSubscription | Insert active row without calling provider (Orders capture) |
findByPaypalSubscriptionId | Webhook lookup by I-… |
updateSubscriptionStatus | Can set paypal_subscription_id, payments aggregates |
dispatchPayPalWebhook (webhook-dispatcher.ts):
| Event | Handler |
|---|---|
PAYMENT.CAPTURE.COMPLETED | Orders purpose handlers (store / membership capture / wallet / news) |
BILLING.SUBSCRIPTION.ACTIVATED | membership-paypal-subscription.ts → activate ledger |
PAYMENT.SALE.COMPLETED | Advance next_payment_due / payments_count |
BILLING.SUBSCRIPTION.CANCELLED / SUSPENDED / EXPIRED / PAYMENT.FAILED | Terminal / suspended status |
Canonical route: POST /api/payments/paypal/webhook.
| Method | Path | Role |
|---|---|---|
POST | /api/membership/payment/paypal | Create recurring (default) or renew status-sync |
POST | /api/membership/subscription/cancel | Cancel; extracts paypal_subscription_id |
GET | /api/membership/subscription/status | can_cancel for active | pending |
| Page | /[locale]/(protected)/membership/manage | SubscriptionManagement + role + NFT stakes + payment_transactions |
| Route helper | ROUTES.MEMBERSHIP_MANAGE(locale) | /membership/manage |
UI: components/membership/payment-modal.tsx PayPal tab when NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true; components/membership/subscription-management.tsx on the manage page.
Secrets stay runtime (K8s Secret). Public flags also need Dockerfile / prod.js build-args for client bundles.
Unchanged five membership crons in lib/processes/registry.ts (expiry, credit-balance monthly, subscription-payment, solana-batch, nft-gate-expiry). PayPal renewals are primarily webhook-driven (PAYMENT.SALE.COMPLETED); manual renew is a Subscriptions GET status sync. Stars renew currently returns a synthetic success + 30-day nextPaymentDue — production renew should follow Telegram payment events once the webhook is live.
SubscriptionConductor.createSubscription from PayPal webhooks (re-opens checkout). Use recordPaidSubscription / updateSubscriptionStatus.orderReference as paypal_subscription_id.returnUrl alone as paid — wait for webhook events.createInvoiceLink success as membership — wait for successful_payment on /api/telegram/stars-bot/webhook → activate pending ledger.metadata.telegramUserId on Stars create (provider returns an error).See-also: purpose map across the clone.
{
"payment": {
"cardPaymentProcessor": "wayforpay",
"gateways": {
"paypal": { "enabled": true, "feePercent": 2.9, "feeFixedCents": 30, "currency": "USD" }
}
}
}
Membership upgrade / PaymentModal / manage cancel
↓
SubscriptionConductor (create | cancel | renew | recordPaidSubscription)
↓
Providers: stripe | wayforpay | credit_balance | native_token | nft_gate | paypal | telegram_stars
↓
subscription_ledger (paypal_subscription_id = PayPal I-…; Stars stores invoice link pending)
↓
Cron: expiry · credit-balance · payment · solana-batch · nft-gate