From 5f0e7523ed1509f2f320c88438cf92a618487256 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Tue, 5 May 2026 10:21:37 -0300 Subject: [PATCH] feat(backend): subscription state columns on users table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 4 nullable columns to the users table so deployments that wire an external billing system (e.g. velxio.dev with Odoo) have a place to cache subscription status. Self-hosters never write these — defaults are safe (no paid features unlock). - models/user.py: is_paid_subscriber (bool, default false), subscription_status (str|None), subscription_period_end (datetime|None), odoo_partner_id (int|None, indexed). - main.py: 4 ALTER TABLE statements appended to the legacy_migrations list so existing deployments auto-migrate on next boot. - schemas/auth.py: extend UserResponse with the 3 user-visible fields (is_paid_subscriber, subscription_status, subscription_period_end). The frontend useAuthStore already persists the whole UserResponse, so these surface automatically without any frontend changes upstream. odoo_partner_id stays internal — clients don't need it. Zero behavioural change for existing OSS deployments. --- backend/app/main.py | 7 +++++++ backend/app/models/user.py | 8 ++++++++ backend/app/schemas/auth.py | 8 ++++++++ 3 files changed, 23 insertions(+) diff --git a/backend/app/main.py b/backend/app/main.py index e871d0bc..30296edb 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -74,6 +74,13 @@ async def lifespan(_app: FastAPI): "ALTER TABLE usage_events ADD COLUMN country VARCHAR(2)", # Multi-board persistence (replaces single board_type as the source of truth) "ALTER TABLE projects ADD COLUMN boards_json TEXT NOT NULL DEFAULT '[]'", + # Subscription state. Self-hosters never write these; deployments + # that wire an external billing system (e.g. velxio.dev → Odoo) + # populate them via webhooks + periodic resync. + "ALTER TABLE users ADD COLUMN is_paid_subscriber BOOLEAN NOT NULL DEFAULT 0", + "ALTER TABLE users ADD COLUMN subscription_status VARCHAR(20)", + "ALTER TABLE users ADD COLUMN subscription_period_end DATETIME", + "ALTER TABLE users ADD COLUMN odoo_partner_id INTEGER", ] for stmt in legacy_migrations: try: diff --git a/backend/app/models/user.py b/backend/app/models/user.py index b75bf236..66185993 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -31,4 +31,12 @@ class User(Base): signup_country: Mapped[str | None] = mapped_column(String(2), nullable=True, index=True) last_country: Mapped[str | None] = mapped_column(String(2), nullable=True, index=True) + # Subscription state. Self-hosters never set these — defaults are safe + # (no paid features unlock). Production velxio.dev syncs them from an + # external billing system (Odoo) via webhook + periodic resync. + is_paid_subscriber: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + subscription_status: Mapped[str | None] = mapped_column(String(20), nullable=True) + subscription_period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + odoo_partner_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True) + projects: Mapped[list["Project"]] = relationship("Project", back_populates="owner", lazy="select") # noqa: F821 diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index 7f121350..66cfafe6 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -41,4 +41,12 @@ class UserResponse(BaseModel): is_admin: bool = False created_at: datetime + # Subscription state. Always present in the response shape so the frontend + # gating logic doesn't have to deal with `undefined`. Self-hosters get + # the safe defaults (no paid features); production overlays sync these + # from an external billing system. + is_paid_subscriber: bool = False + subscription_status: str | None = None + subscription_period_end: datetime | None = None + model_config = {"from_attributes": True}