jam-cloud/docs/dev/subscriptions.md

8.3 KiB

Subscriptions and Recurly Integration

Scope

This doc summarizes how subscription and Recurly behavior currently works across web and ruby, and what is currently tested.

Runtime Flow

UI/API entry points (web)

Frontend calls are in web/app/assets/javascripts/jam_rest.js:

  • POST /api/recurly/update_payment (updatePayment)
  • POST /api/recurly/change_subscription (changeSubscription)
  • GET /api/recurly/get_subscription (getSubscription)
  • POST /api/recurly/create_subscription (createSubscription, legacy path)
  • POST /api/recurly/cancel_subscription (cancelSubscription)

Primary subscription UI logic:

  • web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee
    • Plan changes call rest.changeSubscription(...).
  • web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee
    • Payment method updates tokenize in Recurly.js and call rest.updatePayment({recurly_token}).

API controller (web/app/controllers/api_recurly_controller.rb)

Main endpoints and behavior:

  • change_subscription_plan
    • Sets desired_plan_code via RecurlyClient#update_desired_subscription.
    • If plan is blank, intends free tier/cancel behavior.
    • Returns effective and desired plan status payload.
  • update_payment
    • Uses token to ensure/update account billing.
    • Immediately calls RecurlyClient#handle_create_subscription(current_user, current_user.desired_plan_code, account).
    • This is the key bridge that makes payment-first and plan-first converge.
  • get_subscription
    • Returns subscription state + account billing presence + plan metadata.
  • create_subscription
    • Legacy direct purchase path via Sale.purchase_subscription.
  • cancel_subscription
    • Cancels tracked subscription and returns subscription json.

Also in this controller, Recurly-backed commerce exists for non-plan purchases:

  • place_order -> Sale.place_order (JamTracks/gift cards).

Recurly client and business logic (ruby/lib/jam_ruby/recurly_client.rb)

Main responsibilities:

  • Account lifecycle: create_account, get_account, update_account, update_billing_info, find_or_create_account.
  • Subscription lifecycle:
    • update_desired_subscription (records user intent, cancels on free, or calls handle_create_subscription for paid plan).
    • handle_create_subscription (creates/reactivates subscription, sets recurly_subscription_id, effective plan behavior, trial handling, playtime reset).
    • create_subscription (reactivation path first; otherwise new subscription create).
    • find_subscription (repairs missing local recurly_subscription_id, removes expired references).
  • Ongoing sync:
    • sync_subscription(user) reconciles local plan with trial/admin-license/account/subscription/past_due state.
    • sync_transactions imports successful subscription purchases for affiliate distributions and advances GenericState.recurly_transactions_last_sync_at.

Hourly background sync

  • ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb executes hourly and calls:
    • User.hourly_check
  • ruby/lib/jam_ruby/models/user.rb:
    • hourly_check -> subscription_sync + subscription_transaction_sync
    • subscription_sync selects eligible users and calls RecurlyClient#sync_subscription.
    • subscription_transaction_sync calls RecurlyClient#sync_transactions from last sync timestamp.

Other Recurly Usage

  • ruby/lib/jam_ruby/models/sale.rb
    • Recurly invoice/adjustment flow for JamTrack/gift card purchasing (place_order and related methods).
    • purchase_subscription legacy subscription purchase flow.
  • ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb
    • Parses webhook XML and stores transaction records.
  • web/app/controllers/api_recurly_web_hook_controller.rb
    • Receives webhook, validates type via RecurlyTransactionWebHook.is_transaction_web_hook?, persists via create_from_xml.

Current Tests (Recurly/Subscription Focus)

web/spec/requests/api_recurly_web_hook_controller_spec.rb (active)

  • no auth: webhook endpoint requires basic auth (401 without it).
  • succeeds: valid successful-payment webhook returns 200.
  • returns 422 on error: invalid account/user mapping raises and returns 422.
  • returns 200 for unknown hook event: unknown xml root is ignored and still returns 200.

web/spec/controllers/api_recurly_spec.rb (mostly disabled/commented)

  • Contains setup for account CRUD controller tests.
  • All actual examples are commented out; effectively no active ApiRecurlyController coverage here.

ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb (active)

  • deletes jam_track_right when refunded: asserts webhook parse path for refund events and related sale links (current expectation checks right remains present).
  • deletes jam_track_right when voided: same for void (current expectation checks right remains present).
  • successful payment/refund/failed payment/void/not a transaction web hook: validates root-name recognition in is_transaction_web_hook?.
  • create_from_xml successful payment/refund/void: verifies persisted transaction fields map from XML correctly.

ruby/spec/jam_ruby/recurly_client_spec.rb (active)

Examples:

  • can create account
  • can create account with errors
  • with account group:
    • can find account
    • can update account
    • can update billing
  • can remove account

ruby/spec/jam_ruby/models/user_subscriptions_spec.rb (active)

Examples:

  • User sync group:
    • empty results
    • user not in trial
    • revert admin user down
  • Subscription transaction sync (network + mocked):
    • fetches transactions created after GenericState.recurly_transactions_last_sync_at
    • creates AffiliateDistribution records for successful recurring transactions
    • does not create AffiliateDistribution for same transaction previously been created
    • does not create AffiliateDistribution records when there is no affiliate partner
    • does not create AffiliateDistribution if out of affiliate window
    • assigns correct affiliate partner
    • updates affiliate referral fee
    • change affiliate rate and updates referral fee
    • sets subscription product_type
    • sets subscription product_code
    • does not error out if begin_time is nil
    • changes GenericState.recurly_transactions_last_sync_at

Coverage Gaps Right Now

  • No active request/controller coverage for ApiRecurlyController subscription endpoints:
    • update_payment, change_subscription_plan, get_subscription, cancel_subscription.
  • Recurly sync has active coverage, but still needs broader hourly decision-tree cases for cancellations/past-due/time-based lifecycle transitions.
  • No active browser test asserting payment-first and plan-first flows converge to charge/start-subscription behavior.
  • No active automated coverage for first-free-month gold behavior over time progression.

Live Recurly Internet Tests

  • Added active live integration spec (opt-out via env var):
    • ruby/spec/jam_ruby/integration/recurly_live_integration_spec.rb
  • What it verifies against real Recurly API responses:
    • RecurlyClient#find_or_create_account and #get_account create/fetch a real account and parse billing/account fields.
    • RecurlyClient#update_desired_subscription creates a paid subscription (jamsubgold) and returns parsed subscription data.
    • RecurlyClient#payment_history and #invoice_history return parsed hash arrays from live response bodies.
  • How to run:
    • default (runs live): cd ruby && bundle exec rspec spec/jam_ruby/integration/recurly_live_integration_spec.rb
    • opt-out: cd ruby && SKIP_LIVE_RECURLY=1 bundle exec rspec spec/jam_ruby/integration/recurly_live_integration_spec.rb
    • Optional credential override:
      • RECURLY_PRIVATE_API_KEY=... RECURLY_SUBDOMAIN=...
  • Current credential status discovered during validation:
    • Keys configured in web/config/environments/test.rb (jamkazam-test) return HTTP Basic: Access denied.
    • Working sandbox/development combo in-repo for live tests: key 55f2... with subdomain jamkazam-development.

Regression Found While Reviving Tests

  • RecurlyClient#update_account was broken against the current Recurly gem API.
    • Previous code called account.update(...), which no longer exists on Recurly::Account.
    • Fixed code now calls account.update_attributes(...).
    • This was detected by reviving and running ruby/spec/jam_ruby/recurly_client_spec.rb.