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}