147 lines
8.3 KiB
Markdown
147 lines
8.3 KiB
Markdown
# 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`.
|