From 1be0c5a43674807b0ec7d13dd612565cbd6ad3db Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Tue, 26 May 2026 23:09:39 +0200 Subject: [PATCH] docs: drop Phase D.x markers now that the referral loop is closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Phase D.1/D.2/D.3" comment scaffolding and the "Paddle webhook will fill this in" references became actively misleading after D.3 landed — anyone reading the code would think referral conversion was still pending. Also corrects a stale "Paddle" reference to "Stripe" (we never shipped Paddle; ended up on Stripe after the Paddle → Polar → Stripe MoR onboarding pivot). Pure docstring sweep, no behaviour change. Co-Authored-By: Claude Opus 4.7 --- app/cli.py | 5 +++-- app/models.py | 18 ++++++++++-------- app/routers/pages.py | 7 ++++--- app/routers/universe.py | 2 +- app/services/access.py | 11 ++++++----- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/app/cli.py b/app/cli.py index 6e6f2a3..c780f0b 100644 --- a/app/cli.py +++ b/app/cli.py @@ -11,8 +11,9 @@ Usage from the host:: two months, not one (avoids accidental erosion of an existing grant when re-running the command). -This is the manual lever for Phase D.2. In D.3 the Paddle webhook will -call the same helper for both sides of a referral conversion. +This is the manual lever for admin grants. The Stripe webhook applies +the same stacking rule via ``referral_service.convert_referral`` for +both sides of a referral conversion. """ from __future__ import annotations diff --git a/app/models.py b/app/models.py index 32b04ad..c0d321d 100644 --- a/app/models.py +++ b/app/models.py @@ -169,16 +169,17 @@ class User(Base): settings_json: Mapped[dict | None] = mapped_column(JSON) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) - # Referrals (Phase D.1). The code is unique + URL-safe; generated on - # first need rather than at row creation so existing accounts get one - # the next time they hit /settings. + # Referral code is unique + URL-safe; generated on first need rather + # than at row creation so existing accounts get one the next time + # they hit /settings. referral_code: Mapped[str | None] = mapped_column(String(16), nullable=True) referred_by_user_id: Mapped[int | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) - # Paid-tier credit window (Phase D.2). Null = no credit. When set and - # > now(), the user gets paid-tier features regardless of `tier`. - # Populated by admin CLI (manual grants) or Paddle webhook (D.3). + # Paid-tier credit window. Null = no credit. When set and > now(), + # the user gets paid-tier features regardless of `tier`. Populated + # by admin CLI (manual grants) and by referral conversion (45 days + # per converted referral, both parties). credit_until: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) @@ -248,8 +249,9 @@ class Referral(Base): """One row per captured (referrer, referred) pair. Created at signup when the new user supplied a valid `?ref=`. The conversion fields (`converted_at`, `credited_at`) stay null until the referred - user makes their first paid subscription — Phase D.3 fills them in - via the Paddle webhook.""" + user makes their first paid subscription — the Stripe webhook calls + ``referral_service.convert_referral`` to fill them in and extend + both parties' ``credit_until``.""" __tablename__ = "referrals" id: Mapped[int] = mapped_column(_PK, primary_key=True, autoincrement=True) referrer_user_id: Mapped[int] = mapped_column( diff --git a/app/routers/pages.py b/app/routers/pages.py index db6f5a3..f7ef42b 100644 --- a/app/routers/pages.py +++ b/app/routers/pages.py @@ -117,9 +117,10 @@ async def settings_page( session: AsyncSession = Depends(get_session), principal: CurrentUser = Depends(require_auth), ): - """Per-user settings. Currently shows email, tier, and the referral - block (own code + invite link + counts of pending/converted - referrals). The Credit / Paddle pieces land in D.3.""" + """Per-user settings. Shows email, tier, Stripe subscription + management, email-digest preferences, cloud-sync status, portfolio + import, and the referral block (own code + invite link + counts of + pending / converted / actively-credited referrals).""" user = principal.user if user is None: # Bearer-token admin path — no per-user settings to show. diff --git a/app/routers/universe.py b/app/routers/universe.py index 163e99d..561f419 100644 --- a/app/routers/universe.py +++ b/app/routers/universe.py @@ -321,7 +321,7 @@ async def analyze_portfolio( is persisted. The ai_calls ledger row records tokens + cost, never holdings. - Gated behind ``require_paid`` (Phase D.2): free-tier users get 402. + Gated behind ``require_paid``: free-tier users get 402. Admin bearer-token bypasses the gate for testing.""" # Read JSON body manually so we can enforce a hard size cap. FastAPI's # default body limit is generous; we want tighter control here. diff --git a/app/services/access.py b/app/services/access.py index da388f6..2f91f7a 100644 --- a/app/services/access.py +++ b/app/services/access.py @@ -2,11 +2,12 @@ Two sources can grant paid access: -1. ``user.tier in {"paid", "enterprise"}`` — set by Paddle webhook in - Phase D.3 once a subscription is active. -2. ``user.credit_until > now()`` — non-subscription credit. Currently - populated by the admin CLI (`python -m app.cli grant-credit`) and, in - D.3, by the referral-conversion path (3 months at 50% off). +1. ``user.tier in {"paid", "enterprise"}`` — set by the Stripe webhook + once a subscription is active. +2. ``user.credit_until > now()`` — non-subscription credit. Populated + by the admin CLI (``python -m app.cli grant-credit``) and by the + referral-conversion path (45 days per converted referral, both + parties). Either is sufficient. We use a single ``paid_status`` function so the Settings page can show *why* a user has paid access ("paid subscription"