Subscriptions & Plans — QA Plan
Smoke
| # | Scenario | Expected |
|---|---|---|
| S1 | Create monthly subscription plan | Row created with interval='monthly', currency inherits from org. |
| S2 | Create class-pack plan without classCredits | 400 Class pack plans require classCredits. |
| S3 | Create subscription plan without interval | 400 Subscription plans require an interval. |
| S4 | Create class-pack on Lite tier | 403 — class_packs feature requires Pro. |
| S5 | Soft-delete plan | isActive=false. Existing subs continue to charge; new purchases blocked. |
| S6 | Member buys free plan | Sub created active immediately; no paymentPageUrl; no payment_transactions row. |
| S7 | Member buys paid plan | Sub pending + paymentPageUrl returned; on webhook, sub flips active. |
| S8 | Member retries abandoned checkout | Same pending sub row reused; fresh paymentPageUrl. No duplicate. |
| S9 | Admin enrolls member with skipPayment | Sub active; no payment provider call. |
Lifecycle
| # | Scenario | Expected |
|---|---|---|
| L1 | Freeze active sub | status='paused', pausedAt=now. Renewal cron skips it. Bookings reject. |
| L2 | Resume paused sub after 7 days | status='active', pausedAt=null, currentPeriodEnd += 7d. |
| L3 | Cancel active sub (admin) | status='cancelled', cancelledAt=now. If providerSubscriptionId was set, cancelRecurring is called. |
| L4 | Cancel already-cancelled sub | 400 Subscription is already cancelled. |
| L5 | Staff record a member’s notice (initiator: 'member_request') | cancelAtPeriodEnd=true, sub stays active. Email + low-priority task. |
| L6 | Member looks for a cancel or resume action on web / mobile | None offered; POST /subscriptions/my/:id/cancel-at-period-end and POST /subscriptions/my/:id/resume answer 404. |
| L8 | Cancellation cron sweeps | At 02:30 UTC daily, all subs with cancelAtPeriodEnd=true and currentPeriodEnd ≤ now flip to cancelled; member email fires. |
Renewal failures (FIT-136 surface)
See payments/qa-plan.md for the same scenarios. Subscription-side check:
| # | Scenario | Expected |
|---|---|---|
| RN1 | Past_due sub renews on retry | status='active', attempts=0, period advanced. Membership flips to payment_status='current'. |
| RN2 | Sub enters debt | status='debt', debt_since=now, debt_amount_in_cents=plan.price. Renewal cron will NOT pick it up (selector excludes debt). |
| RN3 | Clear debt | Owner triggers DebtService.clearDebt. Charge fires for debt_amount_in_cents; on success, sub returns to active, debt zeroed, nextChargeDate restored to max(now, currentPeriodEnd) (A6 — previously stayed NULL forever). |
| RN3b | Clear debt double-collection race (A6) | Two concurrent clearDebt calls on the same debt sub both charge; the second’s atomic post-charge claim matches 0 rows → that txn flips to refund_pending + a manual_refund task opens, no exception surfaced to that caller. |
Cancellation request workflow (mid-cycle refund)
| # | Scenario | Expected |
|---|---|---|
| CR1 | Member submits request with refund | Row created, owner email, member email, urgent cancellation_review task. |
| CR2 | Duplicate request | 400 A cancellation request is already pending. |
| CR3 | Owner approves with refund | Sub canceled, refund flow runs (capability-aware), request → approved, refundTaskId filled if manual. Member email. |
| CR4 | Owner rejects | Request → rejected. Sub stays active. Member email. |
| CR5 | Member cancels someone else’s request | Service guard (cancellation-requests.service.ts:76): 403 You can only cancel your own subscription. |
| CR6 | Approve request whose sub has no completed charge | Sub still cancels; refund skipped silently. |
| CR7 | Approve request whose linked refund task already exists | Refused upstream by payment.service.ts:508 (Refund already in progress). |
Credits
| # | Scenario | Expected |
|---|---|---|
| C1 | Booking deducts credit from class-pack sub | remaining_credits--. |
| C2 | Cancel booking refunds credit | remaining_credits++. |
| C3 | Booking when credits = 0 | Service throws No credits remaining. |
| C4 | Booking on unlimited sub | No-op deduct. |
| C5 | Admin adjusts credits by +5 | remaining_credits += 5. |
| C6 | Admin adjusts credits by −100 (more than balance) | Clamped to 0. |
| C7 | Adjust credits on cancelled sub | 400 Subscription must be active or paused. |
| C8 | Adjust credits on unlimited plan | 400 This plan has unlimited credits. |
Permissions
| # | Scenario | Expected |
|---|---|---|
| P1 | Member creates plan | 403 |
| P2 | Coach creates plan | 403 |
| P3 | Owner creates class_pack on Lite | 403 (feature-gated) |
| P4 | Member force-cancels another member’s sub | 403 |
| P5 | Coach reads member’s subscriptions | 200 (allowed) |
| P6 | Owner reads cross-org sub | 404 (filtered out) |
E2E
apps/web/e2e/specs/plans-purchase.spec.ts— full member purchase including Cardcom test payment.apps/web/e2e/specs/cancellation-flow.spec.ts— member submit → owner approve → refund task.
Renew fix (C1) + status gate (A1)
| # | Scenario | Expected |
|---|---|---|
| RN4 | Renew with no card on file | 409 NO_ACTIVE_PAYMENT_METHOD before any charge attempt. |
| RN5 | Renew charge declines | Sub → past_due only; currentPeriodEnd/remainingCredits untouched (behavior change from pre-fix — old code advanced the period even on failure); 402 RENEWAL_CHARGE_FAILED. |
| RN6 | Renew charge succeeds (from past_due) | Period advances, credits refill, sub → active, and now also: nextChargeDate = currentPeriodEnd, failedChargeAttempts = 0 (A1 — previously left stale, causing the very next cron tick to double-charge). |
| RN6b | Renew charge succeeds (from cancelled) | Same as RN6 — a fully-ended subscription can be renewed with a fresh period. |
| RN7 | Renew on Cardcom/iCredit/Morning with the token now attached | Charge actually reaches the provider (pre-fix: silently no-token, provider-dependent failure or accidental success). |
| RN8 | Renew an active subscription (A1) | 409 SUBSCRIPTION_ALREADY_ACTIVE, no charge attempted. |
| RN9 | Renew a debt subscription (A1) | 409 DEBT_CLEARANCE_REQUIRED, no charge attempted — clear-debt is the only path back from debt. |
| RN10 | Renew a paused subscription (A1) | 409 RESUME_FIRST, no charge attempted. |
| RN11 | Renew a pending subscription (A1) | 400, no charge attempted. |
Plan change — eligibility matrix (FIT-254 §4.1)
Every row of the eligibility matrix in behavior.md’s Plan change section as an explicit api-e2e assertion — both immediate and scheduled timing, both org-initiated and member self-serve where applicable:
| # | Old sub state | Timing | Expected |
|---|---|---|---|
| PC1 | active | immediate | Allowed. |
| PC2 | active | scheduled | Allowed. |
| PC3 | active + cancel_at_period_end=true | immediate | Allowed; flag implicitly cleared on the new sub (n/a — old sub is terminal; verify no stray flag survives). |
| PC4 | active + cancel_at_period_end=true | scheduled | 409 pending_action_conflict. |
| PC5 | active + pending cancellation request | immediate or scheduled | 409 pending_action_conflict. |
| PC6 | active + existing scheduled change | immediate or scheduled | Allowed; overwrites the prior schedule (verify old scheduled_plan_id is replaced, not stacked). |
| PC7 | past_due | immediate or scheduled | 409 outstanding_balance. |
| PC8 | debt | immediate or scheduled | 409 outstanding_balance. |
| PC9 | paused | immediate or scheduled | 409 resume_first. |
| PC10 | pending | immediate or scheduled | 400 “checkout still pending”. |
| PC11 | cancelled | immediate or scheduled | 400 “already cancelled”. |
| PC12 | Target plan not isActive | any | 400 “Target plan is not available”. |
| PC13 | Target plan = current plan | any | 400 “Already on this plan”. |
| PC14 | Target plan already held (non-cancelled) on the same membership | any | 409 “Already subscribed to this plan”. |
| PC15 | Target plan is class_pack/drop_in/course | any | 400 “must be a subscription-type plan”. |
| PC16 | Source plan is class_pack/drop_in/course | any | 400 “Only subscription-type plans support plan change”. |
Plan change — money & mechanics
| # | Scenario | Expected |
|---|---|---|
| PC20 | computeDueNow property test | unusedCredit always in [0, oldPriceInCents]; dueNow never produces a charge below 0 (callers clamp); integer cents throughout, no floating point. |
| PC21 | Upgrade via org saved-card charge | Pending-row-first; charge fails → 402 PLAN_CHANGE_CHARGE_FAILED, DB diff is zero (old sub untouched, no new row). |
| PC22 | Upgrade via org saved-card charge, success | Old sub cancelled (plan_change), new sub active with fresh period from now, changedFromSubscriptionId set, sweep ran, receipt email sent. |
| PC23 | Comp change | No charge; new sub inherits old sub’s currentPeriodEnd/nextChargeDate exactly; dueNowInCents: 0 in the emitted event. |
| PC24 | Upgrade via member checkout, webhook activates | Old sub cancelled only on the true pending→active transition (activated flag); webhook replay does not re-cancel or re-sweep. |
| PC25 | Webhook replay / verify-return race on a replaces-checkout | Old-sub cancel + sweep run at most once (gated on activated). |
| PC26 | Concurrent change vs. organic booking | Both run in SERIALIZABLE transactions with retry on 40001 — no lost updates. |
| PC27 | Concurrent double change-checkout on the same sub | Creating a second replaces-checkout deletes the prior pending replaces-sub — at most one in-flight change. |
| PC28 | Scheduled change fires at the boundary, charge succeeds | plan_id swapped, credits reset, sweep ran (applyInPlace), then charged at the new plan’s live price; SUBSCRIPTION_RENEWED emitted. |
| PC29 | Scheduled change fires at the boundary, charge fails | Sub → past_due on the new plan (already swapped) — no rollback of the swap, no parallel billing path. |
| PC30 | Provider not on CHARGE_VERIFIED_PROVIDERS | 409 provider_charge_unverified (org saved-card charge variant only — checkout/comp are unaffected). |
| PC31 | dueNow < ₪1 submitted with billing=charge_saved_card | 400 — “use period_end or skip_payment instead”. |
Plan change — Wave B hardening (FIT-254 review Wave B)
| # | Scenario | Expected |
|---|---|---|
| PC32 | Double-click org saved-card charge on the same sub | Second call can’t acquire the per-sub advisory lock → 409 plan_change_in_progress, adapter never called on the second call. |
| PC33 | Sub changes status between dispatch and the lock being acquired (e.g. a concurrent staff cancel) | Re-verify-inside-the-lock catches it → 409 plan_change_conflict before any charge attempt. |
| PC34 | Old-sub cancel’s guarded UPDATE matches zero rows (raced past the lock — e.g. the renewal cron) after a successful charge | No new sub row; txn flagged for manual review (pending → refund_pending); 409 plan_change_conflict. |
| PC35 | Swap transaction itself throws after a successful charge (serialization exhaustion/crash-adjacent) | Same recovery as PC34 — flagged, 409, txn never marked completed. |
| PC36 | Happy-path org saved-card charge | Ordering: charge → swap tx (cancel+insert) → txn completed → receipt/events — never completed before the swap commits. |
| PC37 | Cross-currency org/member/checkout plan change (old plan currency ≠ target plan currency) | 409 plan_change_currency_mismatch at every computeDueNow call site (preview, member, org charge, purchase-replaces), before the math runs. |
| PC38 | Leftover pending replaces-sub already on the target plan | Does NOT conflict (exclusivity check now excludes pending) — retry via memberChangePlan or purchase reuses the row instead of 409ing. |
| PC39 | scheduleChange’s guarded write races a concurrent memberCancelAtPeriodEnd (or vice versa) | Whichever write loses the race gets 409 pending_action_conflict — the mutual-exclusion invariant can never be violated, not just checked-then-raced. |
| PC40 | purchase({ replacesSubscriptionId }) with member-plan-change OFF | 403, before any read of the replaced subscription. |
| PC41 | purchase({ replacesSubscriptionId }) computing dueNow < ₪1 | 400 plan_change_use_schedule — the ≥0 clamp is no longer the only guard. |
| PC42 | A second in-flight replaces-checkout supersedes the first (different target plan) | The superseded sub’s own pending payment_transactions rows flip to cancelled — a stale hosted-page payment later no-ops with a clear trace. |
| PC43 | Non-owner, non-staff caller previews/changes a sub whose plan is NOT subscription-type | Gets the same 403/404 as for any other sub — never a 400 revealing the plan-type (ownership/permission resolved before the target-plan lookup). |
| PC44 | Non-member of the org cancels a scheduled change | 403 before the subscription lookup — no existence oracle via 404. |
| PC45 | Garbage (non-UUID) :orgId/:subscriptionId/:membershipId on any plan-change/manual-charge/clear-debt/card-registration-list route, or garbage newPlanId query on preview | 400, never a Postgres-level 500. |
| PC46 | Coach previews a plan change (any matrix version) | 403 — subscriptions:manage required; the legacy view carve-out is gone. |
| PC47 | Waitlist promotion inside an immediate charge/comp sweep | sweepEngine.sendPendingPromotionEmails fires once, after the swap transaction commits — not before, not at all if the swap transaction throws. |
| PC48 | Booking-entitlement sweep throws after an admin cancel / cron flip / webhook plan-change activation already committed | The triggering action still “succeeds” (cancel returns normally; webhook returns 200); a high-priority general task is opened naming the affected subscription(s); payment.sweep_failed (subscriptions path) or payment.activation_failed (webhook path) observability emitted. |
| PC49 | Webhook replay on an already-active, plan-change-REPLACED subscription | remainingCredits is left alone — a plain (non-replaces) renewal replay still resets it to the plan’s allotment as before. |
Plan change — Wave C hardening (FIT-254 review Wave C)
| # | Scenario | Expected |
|---|---|---|
| PC50 | Scheduled-change confirmation email, recipient locale = he/ru | Effective date rendered in he-IL/ru-RU format, not hardcoded en-US; no scheduled boundary (defensive) renders the localized “next billing date” fallback. |
| PC51 | Entitlement-sweep/plan-change-scheduled email with a malicious session title / plan name / user name (<script>…) | Rendered HTML-escaped (<script>), never executable markup. |
| PC52 | Same as PC51 but the value contains a literal $& | Output preserved intact — no $-pattern corruption from a bare .replace(token, str). |
| PC53 | Subscription enters DEBT (3rd consecutive failed charge), revoked bookings > 0 | Consolidated email uses the payment_issue variant — never subscription_ended’s “membership has ended” copy. |
| PC54 | Subscription ends for real (immediate cancel / period-end cron), revoked bookings > 0 | Consolidated email uses the subscription_ended variant, unchanged. |
| PC55 | Any subscription-ended/payment_issue sweep with ZERO revoked bookings | Email is skipped entirely — no empty-handed email stacked on the cancellation/debt-warning email already sent for the same event. |
| PC56 | Plan-change confirmation sweep with zero kept/revoked | Still sends — it’s the change confirmation, not conditional on the sweep’s outcome. |
| PC57 | Debt-clearance with no active payment method on file | 409 no_active_payment_method (structured code — the web collect-debt dialog can now map it, previously a plain uncoded 400). |
| PC58 | Change-plan dialog, member has no active card | Billing selection defaults to skip_payment (comp), not the disabled charge_saved_card radio (previously selected-but-disabled by default). |
| PC59 | Change-plan dialog receives plan_change_in_progress / plan_change_conflict / plan_change_currency_mismatch / no_active_payment_method | Localized inline copy shown — previously fell through to the generic message. |
| PC60 | Member books a session beyond a subscription’s pending end, sees the resulting error | Localized, locale-date-formatted toast (“your subscription ends on ‹date›…”) — not the raw English fallback. Every other booking error (full session, daily/weekly limit, etc.) is unaffected, still shows the raw message. |
Booking-entitlement sweep (FIT-254 §4.6/§5)
| # | Scenario | Expected |
|---|---|---|
| SW1 | Upgrade to an unlimited/looser plan | Everything re-attributes; zero revocations; preview matches apply. |
| SW2 | New plan disallows overlap where old plan allowed it (or vice versa) | New plan’s overlap rule governs re-attribution. |
| SW3 | Two same-day sessions, new plan maxBookingsPerDay=1 | Soonest kept, second revoked (deterministic soonest-first ordering). |
| SW4 | Waitlisted booking, new sub’s credits exhausted by kept confirmed bookings | Revoked — a member who can never get a credit shouldn’t squat a waitlist slot. |
| SW5 | Booking’s session gets cancelled between preview and apply | Simply absent from the sweep set at apply time; no error. |
| SW6 | Concurrent member booking during the change transaction | SERIALIZABLE + retry; no lost updates, sweep and booking converge. |
| SW7 | Member books between preview and confirm | Preview is advisory; apply recomputes from live data — the confirmation email is the source of truth, not the earlier preview. |
| SW8 | Revocation | status='cancelled' + cancelledAt, no cancellation-window check (admin-cancel semantics), no credit refund to the old sub, one waitlist-promotion attempt per freed confirmed seat. |
| SW9 | Untracked (subscription_id IS NULL) or staff booking | Never touched by the sweep, regardless of plan/entitlement changes. |
| SW10 | Preview vs. apply on identical (frozen) fixtures | Byte-identical kept/revoked classification — both share computeSweep/classifyForNewSub. |
| SW11 | Consolidated email content | Kept/revoked lists in the email match the sweep result exactly; no per-booking cancellation email sent for swept revocations. |
| SW12 | Waitlist promotion during a sweep | Promoted member gets the existing “You’re in!” email — without a notification-prefs opt-out check (documented gap, W7 audit item). |
| SW13 | C3 pending-cancellation horizon, flag on | Booking beyond currentPeriodEnd on a cancelAtPeriodEnd=true sub → 409 booking_beyond_subscription_end. |
| SW14 | C3 scheduled-change horizon, flag on | Booking beyond currentPeriodEnd on a sub with scheduledPlanId set → allowed; day/week caps evaluated against the target plan; overlap + credits still evaluated against the current sub. |
| SW15 | C3, flag off | Both checks are skipped entirely — pre-FIT-254 behavior (unlimited horizon booking). |
| SW16 | C4 immediate admin-cancel, flag on | Future bookings swept (newSubscriptionId: null, effectiveAt=now); consolidated “membership ended” email sent. |
| SW17 | C4 period-end cancellation cron, flag on | Sweep effectiveAt = the period boundary that just passed, not the cron’s wall-clock time. |
| SW18 | C4 debt entry via renewal cron (3rd failure), flag on | Sweep runs after the debt transition commits. |
| SW19 | C4 debt entry via provider webhook, flag on | Same sweep as SW18, triggered by WebhookProcessingService.handlePaymentFailed. |
| SW20 | C4, freeze | No sweep — freeze is explicitly exempt in v1, regardless of flag state. |
| SW21 | C4, flag off | No-op on every trigger — today’s behavior (bookings outlive the ended subscription), unchanged. |
| SW22 | sweepEnded automation, plan-changed old sub | Excluded (cancellation_reason='plan_change') — no win-back automation misfires. |
| SW23 | sweepEnded automation, member independently bought a new plan | Excluded (membership holds another non-terminal subscription-type sub) — fixes the pre-existing false positive too. |
| SW24 | sweepEnded automation, genuinely churned member | Still enrolled — the exclusions are additive, not a blanket suppression. |
| SW25 | C3-guard: booking a post-boundary session on a sub with a scheduled plan change, straddling week/day window contains a PRE-boundary booking | Pre-boundary booking does NOT consume the target plan’s day/week quota — post-boundary counting mirrors the sweep’s own post-boundary-only counting. |
| SW26 | C4: candidate overlaps another booking elsewhere on the SAME membership (different subscription, or untracked), target plan disallows overlap | Revoked — the overlap baseline is membership-wide, matching book()’s own overlap check, not scoped to the target subscription’s own existing bookings. |
| SW27 | C4: untracked booking elsewhere on the membership, same UTC day as a kept candidate | Day/week caps unaffected — those stay scoped to the target subscription only. |
| SW28 | C5: subscription cancelled/swept between book()’s pre-tx resolve and its write transaction | Booking rejected inside the transaction (re-verified status), not silently written against a terminal subscription. |
Manual charge / plan change flags
See payments/qa-plan.md for the manual-charge/debt-collection scenarios (MCH1-14). Flag-off behavior for every plan-change endpoint (preview, org change, member change, cancel-scheduled): 403 before any DB/provider work; web UI renders nothing (no button, no menu entry). i18n: en/he/ru parity required for every new key, including RTL rendering of the preview sheet and the new sweep/plan-change emails.
Presale withdraw + member action array (unflagged)
Covered by apps/api/src/subscriptions/presale-withdraw.int.spec.ts,
member-action.unit.spec.ts and subscription-display-status.unit.spec.ts.
| # | Scenario | Expected |
|---|---|---|
| PW1 | Member withdraws from a scheduled presale | Row → cancelled / presale_withdrawn, cancelAtPeriodEnd=false, cancellationEffectiveAt=null, filer = the member. Pending (₪1 tokenisation) transactions → cancelled. Nothing charged, nothing refunded. |
| PW2 | Seat release | The plan’s seatsTaken drops immediately (seatStatusCondition stops counting cancelled), so a capped presale reopens the seat for someone else. |
| PW3 | Member buys the plan again after withdrawing | Allowed — purchase only treats active|pending|paused|past_due|debt as blocking. |
| PW4 | No cancellation artifacts | No membership_cancellation form issued, no cancellation_review task, no “membership ended” email. Every one of them would assert something untrue about a membership that never started. |
| PW5 | Withdraw after a REAL charge settled | 409 withdraw_requires_cancellation — the member’s path is the notice flow with its refund math. |
| PW5b | Withdraw when the only settled transaction is the ₪1 presale tokenisation (metadata.tokenOnly) | Succeeds. Nothing was charged. Regression watch: without the marker this refuses every presale withdrawal, since the tokenisation row is completed/type: 'charge' like any sale. |
| PW6 | Withdraw while a charge is in flight | 409 withdraw_charge_in_flight; the row is untouched. |
| PW7 | Withdraw an active/paused/past_due/debt subscription | 409 withdraw_requires_cancellation, body carries the current status. |
| PW8 | Withdraw a pending or cancelled row | 400 — a different kind of row; the lever is cancel-pending / resume-checkout. |
| PW9 | Withdraw somebody else’s subscription | 404, not 403 — a 403 confirms the id exists. Same for checkout-return and resume-checkout. |
| PW9b | Staff cancel a scheduled presale outright | Works: cancelled, no refund transaction, seat released. This is what the use_withdraw 409 points staff at when they try to record a member’s notice on a presale. |
| PW10 | Staff record a member’s notice on a scheduled subscription | 409 use_withdraw. Regression watch: it used to succeed and strand the row forever. Verify no cancelAtPeriodEnd / cancellationEffectiveAt is written and no form is issued. |
| PW11 | Cancellation cron repair pass | Every pre-existing scheduled + cancelAtPeriodEnd=true row is withdrawn (source: 'repair_stuck_scheduled'), regardless of its fabricated effective date; the member’s ORIGINAL cancellation_requested_at/by is preserved; seats are released. |
| PW11b | Repair pass meets a stranded row that HAS since been charged, or has a charge in flight | Left untouched (scheduled, flag intact, in-flight transaction not cancelled) and payment.presale_withdraw_skipped emitted. A background job must not close a membership somebody paid for because a stale flag says so. |
| PW12 | Repair pass throws | Logged, and the matured-cancellation sweep still runs — a stranded presale must never block a real cancellation from taking effect. |
| PW13 | Healthy scheduled presale during the cron | Untouched, seat still held. |
| PW14 | Withdraw races the opening-day promotion | The FOR UPDATE re-check refuses (409) rather than closing a membership that just started. |
| PW15 | Display status | A withdrawn row reads withdrawn — never cancelled (implies a refund conversation) and never checkout_abandoned (the sale did complete). It stays VISIBLE in the member’s own history. |
| PW16 | Staff view of a withdrawn row | Visible on the member page with the withdrawn label; no refund affordance. |
| PW17 | memberActions array | pending → ['complete_checkout','cancel_pending']; scheduled → ['withdraw_scheduled']; past_due → ['renew']; debt → ['update_card']; active/paused/externally-billed → []. Never contains an action the endpoint would refuse. |
| PW18 | Legacy memberAction | scheduled → 'none' (deployed mobile builds are unaffected by withdraw); pending → 'cancel_pending' for every org, now that the cancel-pending flag is gone. |
| PW19 | Cancel-pending with no PostHog reachable (e2e, local dev) | Works. The flag gate is deleted, so there is nothing left to fail closed — this is what the e2e “cancelled checkout → Cancel purchase” journey exercises. |