feat(subscriptions): add subscription change management with upgrade/downgrade scheduling

- Add subscription_change_pending_tier and subscription_change_pending_date fields to UserProfile
- Create migration 019_add_subscription_change_pending
- Add apply_pending_subscription_changes(), request_subscription_change(), cancel_pending_subscription_change() utilities
- Add POST /api/subscriptions/change and DELETE /api/subscriptions/change endpoints
- Update GET /api/subscriptions/my to apply pending changes and return pending change info
- Update subscription view to apply pending changes and pass period_start + pending info
- Update subscription.html: per-tier action buttons (upgrade/downgrade/cancel), pending-change banner, period start date
- Add Celery daily task apply_pending_subscription_changes_all at 00:05 UTC
- Add 19 new tests covering all new utility functions and API endpoints

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-07 20:14:12 +00:00
parent 0599cf86b3
commit 231f983429
9 changed files with 1074 additions and 59 deletions
@@ -0,0 +1,33 @@
"""Add pending subscription-change columns to user_profiles
Revision ID: 019_add_subscription_change_pending
Revises: 018_add_local_users_and_billing
Create Date: 2026-03-07
"""
from typing import Union
import sqlalchemy as sa
from alembic import op
revision: str = "019_add_subscription_change_pending"
down_revision: Union[str, None] = "018_add_local_users_and_billing"
depends_on: Union[str, None] = None
def upgrade() -> None:
"""Add subscription_change_pending_tier and subscription_change_pending_date columns."""
op.add_column(
"user_profiles",
sa.Column("subscription_change_pending_tier", sa.String(50), nullable=True),
)
op.add_column(
"user_profiles",
sa.Column("subscription_change_pending_date", sa.DateTime(timezone=True), nullable=True),
)
def downgrade() -> None:
"""Remove pending subscription-change columns."""
op.drop_column("user_profiles", "subscription_change_pending_date")
op.drop_column("user_profiles", "subscription_change_pending_tier")