# 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`.