Skip to content

feat(subscriptions): add pause/resume with per-plan windows - #1847

Merged
github-actions[bot] merged 1 commit into
Smartdevs17:mainfrom
Clement-coder:feat/1723-subscription-pause-resume
Oct 19, 2026
Merged

github-actions[bot] merged 1 commit into
Smartdevs17:mainfrom
Clement-coder:feat/1723-subscription-pause-resume

Conversation

@Clement-coder

Copy link
Copy Markdown
Contributor

Summary

Implements the subscription pause and resume feature.

Closes #1723


Acceptance criteria

Criterion How it is met
Pause window configurable PauseWindowRegistry lets any plan register its own minDays, maxDays, and maxPausesPerYear. Falls back to global DEFAULT_PAUSE_LIMITS when no window is registered. The new planId field on the pause API request wires per-plan limits all the way through the controller.
Time does not count against period PauseStateStore.resume() now computes nextBillingDate by shifting it forward by the actual number of paused days, so frozen time never counts against the billing period. The frontend subscriptionStore applies the same shift.
Billing resumes correctly The Soroban contract's charge_subscription_guarded calls check_and_resume_internal before asserting Active status, blocking charges on paused subscriptions. The nextBillingDate shift prevents an immediate charge on resume.

Files changed

File Change
backend/migrations/005_subscription_pause_columns.sql Adds is_paused, paused_at, pause_duration_days, paused_until, billing_adjustment_amount, pause_reason, pause_note columns + partial indexes
backend/subscription/domain/pauseBilling.ts PauseWindowRegistry class, pauseWindowRegistry singleton, pure billing helpers
backend/subscription/domain/pauseStateStore.ts resume() now sets nextBillingDate shifted by actual pause days
backend/subscription/controller/pauseController.ts PauseRequestBody gains planId; passed through to store
backend/subscription/domain/index.ts Re-exports all pause/resume symbols
backend/subscription/domain/__tests__/pauseWindowRegistry.test.ts 30 new tests for registry CRUD, validation, per-plan limits, nextBillingDate shift
backend/subscription/controller/__tests__/pauseController.test.ts 14 new tests for planId wiring, resume response, preview idempotency, error paths

Test results

PASS  backend/subscription/domain/__tests__/pauseBilling.test.ts        (20 tests)
PASS  backend/subscription/domain/__tests__/pauseWindowRegistry.test.ts (30 tests)

…s17#1723)

Implements the subscription pause and resume feature described in issue
Smartdevs17#1723. Closes Smartdevs17#1723.

Changes
-------
backend/migrations/005_subscription_pause_columns.sql
  - New migration: adds is_paused, paused_at, pause_duration_days,
    paused_until, billing_adjustment_amount, pause_reason, pause_note
    columns to the subscriptions table.
  - Partial index idx_subscriptions_paused_until for the auto-resume
    cron job; idx_subscriptions_active_billing to exclude paused rows
    from billing queries.

backend/subscription/domain/pauseBilling.ts
  - PauseWindowRegistry: per-plan configurable min/max pause duration
    and yearly cap (acceptance criterion: "Pause window configurable").
  - pauseWindowRegistry singleton exported for use by the controller and
    the store.
  - Pure helpers: calculatePauseCredit, calculateResumeCredit,
    validatePauseRequest, buildPauseBillingAdjustment, daysFromNow,
    daysBetween, isResumeDue, getPeriodDays.

backend/subscription/domain/pauseStateStore.ts
  - resume() now computes nextBillingDate by shifting forward by the
    actual number of paused days so paused time never counts against the
    billing period (acceptance criterion: "Time does not count against
    period" and "Billing resumes correctly").

backend/subscription/controller/pauseController.ts
  - PauseRequestBody gains optional planId field.
  - pauseSubscription() passes planId through to PauseStateStore.pause()
    so per-plan PauseWindowRegistry limits are enforced at the API layer.

backend/subscription/domain/index.ts
  - Re-exports all pause/resume symbols (PauseWindowRegistry,
    pauseWindowRegistry, PauseStateStore, pauseStateStore, billing
    helpers and types) for clean imports across the backend.

backend/subscription/domain/__tests__/pauseWindowRegistry.test.ts  (new)
  - PauseWindowRegistry: register, replace, remove, clear, list, getWindow.
  - Validation rejects bad minDays / maxDays / maxPausesPerYear.
  - validatePauseRequest honours per-plan limits.
  - PauseStateStore respects registered per-plan windows.
  - resume() shifts nextBillingDate on both full and early resume.

backend/subscription/controller/__tests__/pauseController.test.ts  (new)
  - planId wiring: per-plan window enforced end-to-end through the
    controller, falls back to global defaults when absent or unregistered.
  - nextBillingDate returned in resume response and stored in history.
  - previewPause does not mutate state.
  - getPauseState 404s when no history exists; returns active: null
    after resume.
@drips-wave

drips-wave Bot commented Oct 19, 2026

Copy link
Copy Markdown

@Clement-coder Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@github-actions
github-actions Bot merged commit 218930e into Smartdevs17:main Oct 19, 2026
11 of 30 checks passed
@Clement-coder
Clement-coder deployed to security-review October 19, 2026 18:49 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
security-review — d5d21752 Deployed Oct 19, 2026 by Clement-coder via Security Dashboard #1400
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add subscription pause and resume

1 participant