Hyphen User Guides

Subscriptions

Complete technical reference for the reader-facing subscription experience — browsing plans, checkout, payment & billing model, entitlements, gifting, upgrades/downgrades, cancellation, renewal, and how the whole journey flows from the Reader Portal through the Admin Console subscription engine into PostgreSQL and the payment gateways

Version 2.1|Updated 2026-06-23|Readers, Customer Success, Operations, QA, Admin Users

Where this fits: This guide covers the reader-facing (self-service) side of subscriptions — what a reader sees and does on the public site, and exactly what happens behind the scenes when they do it. For the admin side (granting subscriptions, plan editing, the fulfilment list, label templates, the Magazine Schedule), see the Reader Management System guide. For institutional / B2B subscriptions sold offline, see the Sales System guide. For what each plan unlocks (access levels and the paywall), see the Paywall & Access guide.

1. Overview

The Subscription experience lets a reader subscribe to the publication, manage that subscription, and gift one to someone else — all from the public Reader Portal, without contacting support. The Reader Portal is the storefront; the Admin Console owns the subscription engine — the state machine, billing, entitlements, coupons, and fulfilment. Reader-portal API routes proxy to Admin Console routes, which read and write the canonical PostgreSQL tables (subscription_plans, subscriptions, discount_coupons, gift and entitlement tables) via Prisma and orchestrate the payment gateways.

Here is what readers can do:

  • Browse plans — view the plan catalog on /subscribe with pricing, billing intervals (monthly / annual), and what each plan includes.
  • Subscribe — complete a three-step checkout (details + shipping → plan + interval + coupon → payment) and pay with Razorpay, Stripe, or (in test environments) a Dummy gateway.
  • Apply a coupon — enter a discount code at checkout; first-payment, fixed-cycle, and every-renewal discounts are supported.
  • Gift a subscription — buy a subscription for someone else; the recipient receives an activation email and redeems it into their own account.
  • Upgrade or downgrade — switch to a different plan with a live proration preview before confirming.
  • Cancel — cancel from the account page; access continues until the end of the paid period.
  • Renew / reactivate — renew automatically, or resubscribe after expiry (often with a win-back discount).
  • Track everything — see the current subscription, payment history, and per-issue entitlements (including print delivery status) on the account pages.

Implementation status: The reader subscription journey is fully implemented across the Reader Portal, backed by the Admin Console subscription engine.

The end-to-end reader subscription journey, from browsing to ongoing management.
Subscribe page pricing cards
The /subscribe catalog — only Individual, published, publicly-visible plans appear.

2. Who Should Use This Feature

RoleWhat You'll Use
ReadersBrowse plans, subscribe, pay, gift, upgrade/downgrade, cancel, renew, and view subscription + payment history
Customer SuccessWalk readers through checkout, cancellation, and reactivation; explain proration, entitlements, and delivery status
Operations / AdminUnderstand the reader-side flows that mirror admin actions (grants, plan changes, fulfilment) — see Reader Management for the admin tools
QAValidate the end-to-end reader journey across gateways, coupons, gifting, plan changes, cancellation, and renewal

3. How It Works (Behind the Scenes)

Every reader action follows the same shape: Reader Portal UI → reader-portal API route (proxy) → Admin Console API route (engine) → PostgreSQL via Prisma + payment gateway. The Admin Console is the single source of truth. Crucially, subscription state is never set optimistically from the browser — a checkout creates a pending gateway object, and the subscription only becomes ACTIVE when the gateway webhook lands on the Admin Console and the platform writes the state. State flows gateway → webhook → platform, never the reverse; the Hyphen database is the audit source of truth and the gateway dashboards are observability tools.

Data flow: the Reader Portal proxies to the Admin Console engine, which owns Postgres and orchestrates the gateway. Activation is webhook-driven.

Worked path — subscribing:

  1. Reader picks a plan on /subscribe and lands on /subscribe/checkout?plan=<id>&interval=<monthly|annual>.
  2. The portal calls POST /api/subscribe/create-order (one-time / fixed-duration / gift) or POST /api/subscribe/create-subscription (recurring auto-renew), which proxy to the Admin Console /api/payments/create-order or /api/payments/create-subscription.
  3. The Admin Console creates a Razorpay Order or Razorpay Subscription (or a Stripe Checkout Session), records the gateway refs on a pending subscriptions row, and returns the handle to the browser.
  4. The reader pays in the gateway widget. The browser hits POST /api/subscribe/verify-payment / verify-subscription for an immediate optimistic confirmation, but the authoritative transition is the gateway webhook (payment.captured, subscription.charged) landing on /api/webhooks/razorpay (or /stripe).
  5. The webhook handler sets status = ACTIVE, stamps currentPeriodStart / currentPeriodEnd, confirms any reserved coupon, and calls generateEntitlements() — which creates the per-issue print entitlements and makes access resolve.

4. Subscription Concepts & Lifecycle

4.1 Lifecycle states

A reader has at most one Subscription row (readerId is unique). Its status is the SubscriptionStatus enum:

StateMeaning
ACTIVEPaid and current. Entitlements resolve fully.
TRIALINGIn a trial window (trialStart / trialEnd). Treated as active for entitlement resolution.
PAST_DUEA renewal charge failed; the gateway is retrying. Access is at risk pending dunning.
CANCELEDAuto-renew stopped. With cancelAtPeriodEnd = true, access continues until currentPeriodEnd.
EXPIREDThe paid period has ended. No entitlements; eligible for win-back.
PAUSEDTemporarily suspended (admin/gateway action).

A reader with no subscription row resolves to DEFAULT_ENTITLEMENTS (no paid access). The Free plan (a SubscriptionPlan flagged as free) is auto-enrolled — readers get a Free subscriptions row rather than no row, so acquisition flows always have a row to upgrade in place.

Subscription state machine. Cancellation is deferred to the period end; activation and renewal are webhook-driven.

Gift lifecycle is separate (the GiftSubscription row, GiftStatus enum): PENDING_PAYMENT → PAID → SENT → ACTIVATED (or EXPIRED / CANCELLED). A gift can only be created for a recipient who does not already hold a live non-Free subscription (eligibility is checked before payment, §5.5a). Redemption requires the recipient to sign in via OTP and accept Terms / Privacy, then creates a normal subscriptions row of subscriptionType = GIFT on the recipient's account.

Institutional seat lifecycle is yet another track. A reader who belongs to an institution has an InstitutionUser row whose InstitutionUserStatus runs invited → active → deactivated. Bulk-uploaded members start invited; the seat is promoted to active the first time the reader signs in through their institutional magic link (§4.4). An invited (or deactivated) seat does not by itself grant access — see §4.4.

4.2 Entitlement resolution

What a reader can do is computed at read time by resolveEntitlements(), not stored as flat flags. Plans carry a features JSON blob parsed into boolean entitlements (e.g. includesPrint, accessArchive, canGiftSubscription) and numeric limits (e.g. premiumArticlesPerMonth, maxDevices, archiveIssueLimit, printIssueCount). Resolution precedence for each key:

  1. Subscription override (subscription.entitlementOverrides) — per-reader exception, if set.
  2. Plan value (plan.features.entitlements / .limits).
  3. Registry default (BOOLEAN_ENTITLEMENTS / NUMERIC_ENTITLEMENTS).

Only ACTIVE / TRIALING subscriptions resolve entitlements; everything else falls to DEFAULT_ENTITLEMENTS. A queued (pending) plan change is invisible to the resolver until the subscription.charged webhook applies it at the cycle boundary, so a pending downgrade never degrades access early. If the reader has no subscription but an active InstitutionUser seat, entitlements resolve through their institution's plan instead (an invited or deactivated seat does not). See Paywall & Access for how these entitlements gate content.

Read quotas (e.g. premiumArticlesPerMonth) reset at the start of each billing cycle (currentPeriodStart), not on the calendar 1st — so a reader who renews mid-month gets a fresh allowance on their renewal date. The Free plan keeps the calendar-month anchor (its perpetual period never renews). Institutional access anchors to the institution's start date. See Paywall & Access.

4.3 The billing / payment model

  • Razorpay is the billing engine in production. Recurring auto-renew plans create a Razorpay Subscription object (one active subscription per reader); fixed-duration and gift purchases create a one-time Razorpay Order. The platform owns subscription state — Razorpay drives charges via webhooks (subscription.charged, payment.captured), and the platform records each charge in its own payments ledger (PaymentStatus: PENDING / SUCCEEDED / FAILED / REFUNDED).
  • Stripe is used where configured (often international): a hosted Checkout Session, verified on the success page via /api/subscribe/stripe-verify, with /api/webhooks/stripe for the authoritative transition.
  • Dummy is a test-only gateway (Simulate Success / Simulate Failure) that never contacts a real provider — see §6.
  • Coupons / split payment — a coupon discount is applied at the gateway by billing against a discounted Razorpay Plan object (razorpayDiscountedPlanIdMonthly / ...Annual) for N_CYCLES / ALL_RENEWALS coupons, so the recurring charge runs at the discounted amount; FIRST_PAYMENT coupons only reduce the initial charge.
  • Proration / upgrades — on a mid-cycle plan change, unused time on the current plan is credited (floored by day count) and the new plan is charged the difference (ceiled). Some changes apply immediately; others queue to the next cycle boundary as a pending change.

4.4 Institutional seats (invited → active)

Institutional (B2B) subscriptions are sold offline (see the Sales System guide). Each entitled reader is represented by an InstitutionUser seat, not a self-service Subscription row, and the seat carries its own status:

InstitutionUserStatusMeaning
invitedThe seat was provisioned (e.g. by bulk upload) but the reader has not yet signed in. It does not grant access on its own.
activeThe reader has signed in through their institutional magic link at least once; access resolves through the institution's plan.
deactivatedThe seat has been revoked; no access.

A reader activates their seat by opening their institutional magic link (/institutional-login/[token]). Like gift activation, this requires an OTP sign-in plus Terms / Privacy acceptance through the shared ActivationAuthPanel: the email is locked to the invited reader, the session must be authenticated, the magic-link token is single-use, and the institution must still be active and unexpired. On a successful first login the seat is promoted invited → active (idempotent for an already-active member) and lastLoginAt is stamped. Access then resolves through the institution's plan as described in §4.2.


5. Step-by-Step Guide

5.1 Browsing plans (/subscribe)

The /subscribe page is template-driven (admins arrange its sections in the template editor). By default it shows a hero, the pricing cards, a "Why subscribe" value-prop grid, a comparison table, testimonials, and an FAQ.

  • Each plan card shows the display name, price, billing interval, and key inclusions.
  • A monthly / annual toggle switches pricing where the plan supports both intervals (supportedBillingIntervals; empty = both allowed).
  • Only plans with audience = INDIVIDUAL, isActive = true, and isPubliclyVisible = true appear, and a non-expired validUntil. Institutional plans never show here — B2B readers see an institutional call-to-action routing to /subscribe/institutional. (Internal/comp/gift-seed plans can still be subscribed via a direct ?planId=<id> link even when hidden from the listing.)
  • For print plans, a live current-issue stock badge reflects Shopify inventory sourced from the Issue model (isCurrent + inventoryAvailable).

Choosing a plan opens checkout pre-selected: /subscribe/checkout?plan=<id>&interval=<monthly|annual>.

5.2 Checkout — the three steps (/subscribe/checkout)

Checkout is a guided three-step flow. The reader must be signed in; unauthenticated visitors are redirected to login with a callback back to checkout. Sign-in is itself gated by a required Terms of Service / Privacy Policy checkbox — the "Continue with Google" / OTP sign-in button stays disabled until the reader ticks "I agree to the Terms of Service and Privacy Policy" (AuthFlow). So consent is captured before any plan or payment step.

Three-step checkout
Checkout: details + shipping, then plan + interval + coupon, then payment.

Step 1 — Your details & shipping address

  • The reader confirms their name (pre-filled from their profile or session where available), written to shippingName.
  • For print (PRINT) and bundle (BUNDLE) plans, a structured shipping address is captured: Address Line 1, Address Line 2, Landmark, City, State, Country (shippingCountry, default IN), and PIN code, plus optional phone.
  • A Save to my profile checkbox (on by default) writes the address back to the reader's profile for next time.
  • Print activations are server-validated — a missing required field is rejected with a clear SHIPPING_ADDRESS_INCOMPLETE error rather than silently accepted.

Step 2 — Plan & billing

  • The reader reviews the selected plan and billing interval (MONTHLY / ANNUAL).
  • Where a plan allows buying multiple cycles upfront (maxCycles > 1), a cycles stepper appears (1..maxCycles, recorded as cyclesPurchased); hidden when maxCycles = 1.
  • A coupon code field validates the code live and shows the resulting price. See §5.4.

Step 3 — Payment

  • The available payment method depends on the environment: Razorpay (default), Stripe, or Dummy (test only — see §6). When Dummy mode is on, it is the only option shown.
  • On success the reader lands on /subscribe/success (which also verifies Stripe sessions where applicable) and, after the webhook confirms, the subscription is activated and issue entitlements are generated for print/bundle plans.
  • New readers who signed up as part of subscribing may pass through /subscribe/welcome to confirm their plan choice.

5.3 Payment methods & duration units

GatewayWhen usedNotes
RazorpayDefault in productionRecurring Subscription for auto-renew plans; one-time Order for fixed-duration / gift purchases. Webhooks drive activation and renewal.
StripeWhere configuredHosted Checkout Session; success page verifies via /api/subscribe/stripe-verify; /api/webhooks/stripe confirms.
DummyTest environments onlySimulate Success / Simulate Failure; never contacts a real gateway. Gated by env + DB flag (see §6).

A plan's length is expressed in one of three duration units, with precedence durationIssues > durationDays > durationMonths:

  • Issues (durationIssues) — print, counted by magazine issues delivered.
  • Days (durationDays) — explicit day count (min 1).
  • Months (durationMonths, default 12) — most common.

Server-side validation enforces that exactly one driver is active. The reader sees the human-readable duration ("12 months", "6 issues") on the card and in their account.

5.4 Applying a coupon

Enter a code in Step 2. The CouponAppliesTo enum defines three discount models:

appliesToBehavior
FIRST_PAYMENTDiscount applies to the first charge only.
N_CYCLESDiscount applies to discountCycles billing cycles, then full price resumes (billed via the discounted Razorpay Plan).
ALL_RENEWALSDiscount applies to every charge for the life of the subscription.

A coupon carries discountPercent or discountAmount (paise), optional plan/interval restrictions (planId, billingInterval), usage limits (maxUses, usedCount, maxUsesPerUser), validity window (validFrom / validTo), and eligibility (newSubscribersOnly, and allowGift / allowBulk / allowInstitutional).

Validation (POST /api/promo/validate) checks the code against the selected plan and interval: a code tied to a specific planId used on a different plan is rejected with PLAN_MISMATCH, and a code tied to a billingInterval used on the other interval is rejected with INTERVAL_MISMATCH (e.g. "only valid for annual billing"). This catches a wrong-plan or wrong-interval code at Step 2 instead of at payment.

Once a code validates, the applied promo is carried through every checkout path — the recurring-subscription order, the fixed-duration order, and the test-only Dummy order all forward couponCode, so the discounted amount is identical in production and in test mode. The code is reserved at checkout and confirmed on payment success (tracked on the subscription via couponId, couponAppliedCycles, couponTotalCycles); abandoned checkouts release the reservation automatically. Win-back codes (§5.8) auto-apply when the reader follows a reactivation link.

5.5 Gifting a subscription (/gift)

A reader (or any buyer) can purchase a subscription for someone else.

Gift a subscription form
The /gift flow — buyer details, recipient details, message, and a giftable plan.
  1. On /gift, the buyer enters their name/email, the recipient's name/email, an optional personal message, and an optional delivery date, then selects a giftable plan. The plan list is fetched live from /api/gift/plans (with a built-in fallback set if the request fails), and the first plan is pre-selected. A gift-card preview shows what the recipient will see.
  2. Before any payment, the server runs a recipient eligibility check (checkGiftRecipientEligibility). If the recipient's email already belongs to a reader with a live, non-Free subscription (status ACTIVE / TRIALING / PAUSED / PAST_DUE), the request is rejected with RECIPIENT_ALREADY_SUBSCRIBED and no gift row or payment order is created — so the buyer is never charged for a gift the recipient cannot use. A recipient who has only the auto-enrolled Free plan, or no account yet, is eligible. See §5.5a.
  3. The buyer pays via a Razorpay Order (not a recurring Subscription — gifts do not auto-renew). The GiftSubscription row moves PENDING_PAYMENT → PAID.
  4. On payment success the recipient receives an activation email with a redemption link; status becomes SENT.
  5. The recipient opens /gift/activate and must sign in via OTP and accept Terms / Privacy before redeeming — see §5.5a. Redemption creates a subscriptions row of subscriptionType = GIFT. The gift status becomes ACTIVATED.

Giftable plans are resolved from the gift catalog (/api/gift/plans), which ignores normal plan visibility, so a plan can be gift-eligible without being publicly listed. Buyers can review and resend activation from Account → Gifts.

5.5a Activating a gift (OTP + Terms gate)

Gift redemption requires sign-in rather than a one-click link. /gift/activate renders the shared ActivationAuthPanel (the same dark sign-in surface used by institutional activation), which wraps AuthFlow with a locked recipient email and forced Terms/Privacy acceptance:

  • The recipient must complete an OTP login for the exact email the gift was sent to. The activation API requires an authenticated hyphen_access_token session and verifies the signed-in email matches the recipient — it does not auto-create an account or session from the bare link.
  • The activation GET returns needsTermsAcceptance; when true, the panel shows the Terms of Service / Privacy Policy checkbox even for an existing reader who has no recorded acceptance (e.g. a bulk-created account). The reader must accept before the redeem call fires.
  • Activating an already-activated gift is idempotent (the page shows the active subscription); an expired link returns a clear error; a gift that has not been paid for cannot be activated.

5.6 Managing your subscription (/account/subscription)

The My Subscription page (backed by /api/account/subscription/api/subscribe/my-subscription) shows the current plan: name, price, billing cycle, next renewal date (currentPeriodEnd), payment method, and a billing-history table. From here the reader can upgrade/downgrade, cancel, resume, or (after expiry) resubscribe.

5.7 Upgrading or downgrading (proration)

From My Subscription, an Upgrade / Change plan action (/api/account/change-plan → Admin Console /api/reader/subscription/change-plan) lets an active subscriber switch plans:

  • The reader picks a target plan and interval and sees a live proration preview — unused time on the current plan is credited (floored by day count), and the new plan is charged the difference (ceiled).
  • Confirming records the change. Immediate changes update planId now; others are scheduled for the next cycle boundary via pendingPlanId / pendingChangeEffectiveAt (shown as a pending plan change) and applied by the subscription.charged webhook.
  • A pending change can be undone with Cancel pending change before it takes effect.
  • Plan changes are blocked for institutional subscribers and for cancelled/expired subscriptions. A plan with an expired validUntil is rejected as a target.

5.8 Cancelling, renewing, and reactivating

Cancel — From My Subscription, Cancel Subscription sets cancelAtPeriodEnd = true and schedules cancellation at the end of the current paid period. The card shows "Cancels on <date>" and access continues until then (auto-renew is stopped at the gateway). A Resume action (/api/account/resume) clears the flag before the period ends.

Renew — Recurring plans auto-renew via the gateway (subscription.charged). Readers receive renewal reminder emails ahead of expiry (per-plan renewalReminders, up to 3 entries with triggerDaysBefore).

Reactivate after expiry (win-back) — When status = EXPIRED, My Subscription shows a win-back offer. If a win-back coupon is active, the reader gets a "Claim offer & resubscribe" button pre-filling checkout with the previous plan, interval, and discount code; otherwise a "View plans & resubscribe" link.

5.9 Subscription history & entitlements (/account/subscription/history)

The Subscription History page consolidates:

  • Current subscription — plan, status, and key dates.
  • Payments — each charge with amount, date, and (in test environments) a Dummy badge; invoice/receipt links (/account/subscription/history/invoice/[id]).
  • Issue entitlements — every magazine issue the subscription is owed, each with a fulfilment status (FulfilmentStatus): DIGITAL_AVAILABLE for digital, and for print PRINT_PENDING → PRINT_DISPATCHED → PRINT_DELIVERED.

The admin side of fulfilment (the dispatch list, address-completeness highlighting, bulk Mark Dispatched, and label printing) lives in Reader Management §5.4a / §5.13a.


6. Dummy Payment Mode (test environments)

Dummy mode lets test builds simulate payment without a real gateway, gated by a two-key control so it can never accidentally run in production:

GateDefaultEffect
DUMMY_PAYMENT_MODE_ENABLED (env floor)on in non-prodSafety net. When on, checkout shows only the Dummy gateway and hides Razorpay/Stripe. Set to false on the production process to opt out.
dummy_payment_mode_enabled (DB feature flag)off (fail-closed)Runtime kill-switch toggled from Settings → Features without a redeploy. Dummy mode is active only when both the env floor is on and this flag is on; a missing flag row is treated as off.

The reader portal asks the Admin Console at checkout-mount time whether Dummy mode is on, so the gateway choice always matches the server.


7. Configuration Reference

7.1 Plan attributes (Admin Console → Subscriptions → Plans)

Field / OptionWhat it doesTypeMandatory?DefaultWhere configuredRole
displayNamePublic plan name on the cardStringYesUI: Plans editorAdmin
audienceINDIVIDUAL (self-service) vs INSTITUTIONAL (offline)enum PlanAudienceYesINDIVIDUALUIAdmin
deliveryFormatDIGITAL / PRINT / BUNDLE — drives address captureenum DeliveryFormatYesDIGITALUIAdmin
supportedBillingIntervalsWhitelist of MONTHLY / ANNUAL; empty = bothenum[]No[] (both)UIAdmin
durationMonths / durationDays / durationIssuesPlan length; exactly one drives (issues > days > months)IntOne requiredmonths=12UIAdmin
priceMonthly / priceAnnual / priceOneTimePrices in paise (smallest unit)IntYes (relevant)0UIAdmin
currencyBilling currencyStringYesINRUIAdmin
featuresBoolean entitlements + numeric limits (JSON)JSONYes[]UI (entitlement editor)Admin
maxCyclesMax cycles buyable upfront; 1 hides the stepperIntNo1UIAdmin
isPubliclyVisibleShow on /subscribe (direct link still works)BoolNotrueUIAdmin
isActivePlan is sellableBoolNotrueUIAdmin
validUntilSell-by date; past = excluded as new/target planDateTime?NonullUIAdmin
renewalRemindersUp to 3 pre-expiry reminder entriesJSONNo[]UIAdmin
allowsRenewalWhether the plan can renewBoolNotrueUIAdmin
razorpayPlanId* / stripePriceId*Gateway plan/price IDsString?If recurringnullUI / setupAdmin
hsnCode / gstRatePercentTax configString / DecimalNonullUIAdmin

7.2 Checkout fields (reader)

Field / OptionWhat it doesTypeMandatory?DefaultWhereRole
NameSubscriber / shipping nameStringYesfrom profileCheckout Step 1Reader
Shipping address (Line1/2, Landmark, City, State, Country, PIN, Phone)Print/bundle delivery; server-validatedString fieldsYes for PRINT/BUNDLECountry INStep 1Reader
Save to my profilePersist address for next timeBoolNocheckedStep 1Reader
Billing intervalMONTHLY / ANNUALenumYesper planStep 2Reader
CyclesUpfront cycles (when maxCycles > 1)IntNo1Step 2Reader
Coupon codeDiscount at checkoutStringNoStep 2Reader
Payment methodRazorpay / Stripe / DummychoiceYesenv-drivenStep 3Reader

7.3 Gift fields (/gift)

FieldWhat it doesMandatory?
Buyer name / emailWho is payingYes
Recipient name / emailWho receives the gift + activation emailYes
Personal messageShown on the gift cardNo
Delivery dateWhen the activation email is sentNo
PlanA giftable plan (from gift catalog)Yes

7.4 Account-management actions

ActionRouteEffect
Upgrade / Change plan/api/account/change-planProration preview, immediate or queued change
Cancelsets cancelAtPeriodEndAccess until period end
Resume/api/account/resumeClears pending cancellation
Resubscribe (win-back)/subscribe/checkout pre-filledAfter expiry

7.5 Payment env vars (backend / operators)

VariablePurpose
RAZORPAY_KEY_ID / RAZORPAY_KEY_SECRETRazorpay API credentials
RAZORPAY_WEBHOOK_SECRETVerifies inbound Razorpay webhooks
STRIPE_PUBLISHABLE_KEY / STRIPE_SECRET_KEYStripe API credentials
STRIPE_WEBHOOK_SECRETVerifies inbound Stripe webhooks
DUMMY_PAYMENT_MODE_ENABLEDEnv floor for Dummy mode (see §6)

8. How to Verify It Worked

  • After subscribing: redirected to the success page; /account/subscription shows the plan with a next renewal date; print/bundle plans list issue entitlements as PRINT_PENDING; a receipt email arrives via the subscription_lifecycle purpose.
  • After a coupon: Step 2 shows the discounted price; the receipt and history reflect the discounted amount.
  • After gifting: the buyer sees the gift under Account → Gifts; the recipient gets an activation email and, after redeeming, has a GIFT-type subscription.
  • After changing plans: an immediate change updates My Subscription; a scheduled change shows a pending-plan-change banner with the effective date.
  • After cancelling: My Subscription shows "Cancels on <date>"; access remains until then.
  • After expiry: My Subscription shows a win-back offer; following it pre-fills checkout.

9. Worked Examples (End-to-End Scenarios)

Example 1: Annual digital plan with a coupon

  1. On /subscribe, toggle Annual and choose a digital plan → checkout opens pre-selected.
  2. Step 1: confirm name (no address for digital). Step 2: enter coupon WELCOME20; price updates. Step 3: pay with Razorpay (or Simulate Success in test).
  3. Verify: /account/subscription shows the plan + renewal date; a discounted receipt arrives.

Example 2: Print plan and track delivery

  1. Choose a print plan → Step 1: fill the structured shipping address; leave Save to my profile checked.
  2. Complete payment. On success, history lists entitled issues as PRINT_PENDING.
  3. After the operator dispatches (admin side), the issues flip to PRINT_DISPATCHED, then PRINT_DELIVERED.

Example 3: Gift a subscription

  1. On /gift, enter buyer + recipient details and a message; pick a giftable plan; pay (Razorpay Order).
  2. Recipient gets an activation email, opens /gift/activate, signs in, and redeems.
  3. Verify: recipient's account shows a GIFT subscription; buyer can resend activation if needed.

Example 4: Upgrade mid-cycle

  1. On /account/subscription, choose Change plan, pick a higher plan, review the proration preview.
  2. Confirm. If scheduled, a pending-plan-change banner appears; undo with Cancel pending change.
  3. Verify the new plan and prorated charge in payment history.

Example 5: Cancel and reactivate

  1. On /account/subscription, Cancel Subscription → card shows "Cancels on <date>".
  2. After expiry, return → a win-back offer appears.
  3. Click Claim offer & resubscribe → checkout opens pre-filled with the prior plan and discount code.

10. Common Mistakes and How to Fix Them

  • "The plan I want isn't on the subscribe page" — Only INDIVIDUAL, active, publicly-visible, non-expired plans appear. Check audience, isActive, isPubliclyVisible, and validUntil in Admin Console → Subscriptions → Plans. Institutional buyers use /subscribe/institutional.
  • "Checkout won't continue past the address step" — Print/bundle addresses are server-validated; complete Line 1, City, State, Country, and PIN. A missing field returns SHIPPING_ADDRESS_INCOMPLETE.
  • "I only see the Dummy payment option" — The environment has Dummy mode on, which hides real gateways by design. See §6.
  • "My coupon didn't stick" — Coupons are reserved at checkout and confirmed on payment; abandoned checkouts release the reservation. Re-enter the code and confirm its appliesTo model and eligibility (newSubscribersOnly, usage limits).
  • "My coupon says it's not valid for this plan / interval" — The code is restricted to a specific plan or billing interval. PLAN_MISMATCH means it only works on another plan; INTERVAL_MISMATCH means it only works on monthly or only on annual. Switch to the eligible plan/interval or use a different code.
  • "I can't gift to this person"RECIPIENT_ALREADY_SUBSCRIBED means the recipient already holds a live, paid subscription. Gifts can only go to readers with no subscription or only the Free plan; you are not charged when this check fails.
  • "The gift recipient can't activate — it asks them to sign in" — Expected: activation requires the recipient to OTP-sign-in with the exact gift email and accept Terms/Privacy. The link does not auto-create an account. Confirm they are using the same email the gift was sent to.
  • "An institutional reader can't read anything yet" — Their seat is probably still invited. They must open their institutional magic link and complete OTP sign-in once to promote the seat to active. See §4.4.
  • "The gift recipient never got their email" — Confirm the subscription_lifecycle purpose maps to a verified email account (Settings → Email → Purposes) and review email logs. The buyer can resend from Account → Gifts.
  • "I can't change my plan" — Blocked for institutional subscribers and cancelled/expired subscriptions. Reactivate first, or contact the institution admin.
  • "I cancelled but I can still read everything" — Expected: cancellation takes effect at the end of the paid period (cancelAtPeriodEnd).
  • "My print issues still say Pending"PRINT_PENDING means not yet dispatched. Dispatch happens on the admin Fulfilment List. See Reader Management §5.4a.

11. Dependencies & Impact

The reader subscription journey is the hub of several systems:

  • Reader Management — the admin side of the same subscriptions / subscription_plans tables: plan editing, manual grants, the dispatch/fulfilment list, label templates, and the Magazine Schedule that maps issue entitlements. Retiring a plan is handled by the admin DeletePlanDialog: a plan with no subscribers is a simple confirm-and-delete; a plan with subscribers offers either Deactivate (hides it from /subscribe, existing subscribers keep access, no new enrolments) or Migrate subscribers to another active plan (then the source plan is deactivated, or for an already-inactive "zombie" plan, hard-deleted). Migrations affecting more than 10 subscribers require the admin to type the plan name to confirm.
  • Paywall & Access — consumes resolveEntitlements(); subscription status and plan entitlements directly determine what content a reader can open.
  • Email System — receipts, gift activation, renewal reminders, and cancellation notices all route through the subscription_lifecycle purpose.
  • Future Readers — a coupon source of future_readers; acquisition campaigns issue coupons consumed by this checkout.
  • Magazine — print issue entitlements and fulfilment statuses are anchored to the Issue model (current-issue inventory, dispatch tracking).

Changing plan attributes, entitlement registries, gateway credentials, or the Magazine Schedule can therefore affect catalog visibility, what readers can access, billing, and print fulfilment simultaneously.


FAQ


  • Reader Management System — admin-side subscriptions, grants, Magazine Schedule, print fulfilment, label templates
  • Sales System — institutional (B2B) subscriptions, seats, renewals, offline payment
  • Paywall & Access — what each plan unlocks; access levels and content gating
  • Email System Guide — lifecycle emails (receipts, gift activation, renewal reminders, cancellation)
  • Future Readers — acquisition campaigns and coupons
  • Magazine — issues, current-issue inventory, and print fulfilment