2026-03-03 03:03:43 +00:00
# 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.
2026-03-03 03:12:03 +00:00
### `ruby/spec/jam_ruby/recurly_client_spec.rb` (active)
Examples:
2026-03-03 03:03:43 +00:00
- `can create account`
- `can create account with errors`
- `with account` group:
- `can find account`
- `can update account`
- `can update billing`
- `can remove account`
2026-03-03 03:12:03 +00:00
### `ruby/spec/jam_ruby/models/user_subscriptions_spec.rb` (active)
Examples:
2026-03-03 03:03:43 +00:00
- 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` .
2026-03-03 03:12:03 +00:00
- Recurly sync has active coverage, but still needs broader hourly decision-tree cases for cancellations/past-due/time-based lifecycle transitions.
2026-03-03 03:03:43 +00:00
- 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` .
2026-03-03 03:12:03 +00:00
## 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` .