feat(backend): subscription state columns on users table

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.
This commit is contained in:
David Montero Crespo 2026-05-05 10:21:37 -03:00
parent a5b5e57aae
commit 5f0e7523ed
3 changed files with 23 additions and 0 deletions

View File

@ -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:

View File

@ -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

View File

@ -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}