8.3 KiB
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(...).
- Plan changes call
web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee- Payment method updates tokenize in Recurly.js and call
rest.updatePayment({recurly_token}).
- Payment method updates tokenize in Recurly.js and call
API controller (web/app/controllers/api_recurly_controller.rb)
Main endpoints and behavior:
change_subscription_plan- Sets
desired_plan_codeviaRecurlyClient#update_desired_subscription. - If plan is blank, intends free tier/cancel behavior.
- Returns effective and desired plan status payload.
- Sets
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.
- Legacy direct purchase path via
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 callshandle_create_subscriptionfor paid plan).handle_create_subscription(creates/reactivates subscription, setsrecurly_subscription_id, effective plan behavior, trial handling, playtime reset).create_subscription(reactivation path first; otherwise new subscription create).find_subscription(repairs missing localrecurly_subscription_id, removes expired references).
- Ongoing sync:
sync_subscription(user)reconciles local plan with trial/admin-license/account/subscription/past_due state.sync_transactionsimports successful subscription purchases for affiliate distributions and advancesGenericState.recurly_transactions_last_sync_at.
Hourly background sync
ruby/lib/jam_ruby/resque/scheduled/hourly_job.rbexecutes hourly and calls:User.hourly_check
ruby/lib/jam_ruby/models/user.rb:hourly_check->subscription_sync+subscription_transaction_syncsubscription_syncselects eligible users and callsRecurlyClient#sync_subscription.subscription_transaction_synccallsRecurlyClient#sync_transactionsfrom last sync timestamp.
Other Recurly Usage
ruby/lib/jam_ruby/models/sale.rb- Recurly invoice/adjustment flow for JamTrack/gift card purchasing (
place_orderand related methods). purchase_subscriptionlegacy subscription purchase flow.
- Recurly invoice/adjustment flow for JamTrack/gift card purchasing (
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 viacreate_from_xml.
- Receives webhook, validates type via
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
ApiRecurlyControllercoverage 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 inis_transaction_web_hook?.create_from_xmlsuccessful payment/refund/void: verifies persisted transaction fields map from XML correctly.
ruby/spec/jam_ruby/recurly_client_spec.rb (active)
Examples:
can create accountcan create account with errorswith accountgroup:can find accountcan update accountcan update billing
can remove account
ruby/spec/jam_ruby/models/user_subscriptions_spec.rb (active)
Examples:
- User sync group:
empty resultsuser not in trialrevert admin user down
- Subscription transaction sync (network + mocked):
fetches transactions created after GenericState.recurly_transactions_last_sync_atcreates AffiliateDistribution records for successful recurring transactionsdoes not create AffiliateDistribution for same transaction previously been createddoes not create AffiliateDistribution records when there is no affiliate partnerdoes not create AffiliateDistribution if out of affiliate windowassigns correct affiliate partnerupdates affiliate referral feechange affiliate rate and updates referral feesets subscription product_typesets subscription product_codedoes not error out if begin_time is nilchanges GenericState.recurly_transactions_last_sync_at
Coverage Gaps Right Now
- No active request/controller coverage for
ApiRecurlyControllersubscription 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_accountand#get_accountcreate/fetch a real account and parse billing/account fields.RecurlyClient#update_desired_subscriptioncreates a paid subscription (jamsubgold) and returns parsed subscription data.RecurlyClient#payment_historyand#invoice_historyreturn 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=...
- default (runs live):
- Current credential status discovered during validation:
- Keys configured in
web/config/environments/test.rb(jamkazam-test) returnHTTP Basic: Access denied. - Working sandbox/development combo in-repo for live tests: key
55f2...with subdomainjamkazam-development.
- Keys configured in
Regression Found While Reviving Tests
RecurlyClient#update_accountwas broken against the current Recurly gem API.- Previous code called
account.update(...), which no longer exists onRecurly::Account. - Fixed code now calls
account.update_attributes(...). - This was detected by reviving and running
ruby/spec/jam_ruby/recurly_client_spec.rb.
- Previous code called