docs: drop Phase D.x markers now that the referral loop is closed

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 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-26 23:09:39 +02:00
parent ce36ce36fd
commit 1be0c5a436
5 changed files with 24 additions and 19 deletions

View file

@ -11,8 +11,9 @@ Usage from the host::
two months, not one (avoids accidental erosion of an existing grant two months, not one (avoids accidental erosion of an existing grant
when re-running the command). when re-running the command).
This is the manual lever for Phase D.2. In D.3 the Paddle webhook will This is the manual lever for admin grants. The Stripe webhook applies
call the same helper for both sides of a referral conversion. the same stacking rule via ``referral_service.convert_referral`` for
both sides of a referral conversion.
""" """
from __future__ import annotations from __future__ import annotations

View file

@ -169,16 +169,17 @@ class User(Base):
settings_json: Mapped[dict | None] = mapped_column(JSON) settings_json: Mapped[dict | None] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
# Referrals (Phase D.1). The code is unique + URL-safe; generated on # Referral code is unique + URL-safe; generated on first need rather
# first need rather than at row creation so existing accounts get one # than at row creation so existing accounts get one the next time
# the next time they hit /settings. # they hit /settings.
referral_code: Mapped[str | None] = mapped_column(String(16), nullable=True) referral_code: Mapped[str | None] = mapped_column(String(16), nullable=True)
referred_by_user_id: Mapped[int | None] = mapped_column( referred_by_user_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ForeignKey("users.id", ondelete="SET NULL"), nullable=True,
) )
# Paid-tier credit window (Phase D.2). Null = no credit. When set and # Paid-tier credit window. Null = no credit. When set and > now(),
# > now(), the user gets paid-tier features regardless of `tier`. # the user gets paid-tier features regardless of `tier`. Populated
# Populated by admin CLI (manual grants) or Paddle webhook (D.3). # by admin CLI (manual grants) and by referral conversion (45 days
# per converted referral, both parties).
credit_until: Mapped[datetime | None] = mapped_column( credit_until: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True, DateTime(timezone=True), nullable=True,
) )
@ -248,8 +249,9 @@ class Referral(Base):
"""One row per captured (referrer, referred) pair. Created at signup """One row per captured (referrer, referred) pair. Created at signup
when the new user supplied a valid `?ref=<code>`. The conversion when the new user supplied a valid `?ref=<code>`. The conversion
fields (`converted_at`, `credited_at`) stay null until the referred fields (`converted_at`, `credited_at`) stay null until the referred
user makes their first paid subscription Phase D.3 fills them in user makes their first paid subscription the Stripe webhook calls
via the Paddle webhook.""" ``referral_service.convert_referral`` to fill them in and extend
both parties' ``credit_until``."""
__tablename__ = "referrals" __tablename__ = "referrals"
id: Mapped[int] = mapped_column(_PK, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(_PK, primary_key=True, autoincrement=True)
referrer_user_id: Mapped[int] = mapped_column( referrer_user_id: Mapped[int] = mapped_column(

View file

@ -117,9 +117,10 @@ async def settings_page(
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
principal: CurrentUser = Depends(require_auth), principal: CurrentUser = Depends(require_auth),
): ):
"""Per-user settings. Currently shows email, tier, and the referral """Per-user settings. Shows email, tier, Stripe subscription
block (own code + invite link + counts of pending/converted management, email-digest preferences, cloud-sync status, portfolio
referrals). The Credit / Paddle pieces land in D.3.""" import, and the referral block (own code + invite link + counts of
pending / converted / actively-credited referrals)."""
user = principal.user user = principal.user
if user is None: if user is None:
# Bearer-token admin path — no per-user settings to show. # Bearer-token admin path — no per-user settings to show.

View file

@ -321,7 +321,7 @@ async def analyze_portfolio(
is persisted. The ai_calls ledger row records tokens + cost, never is persisted. The ai_calls ledger row records tokens + cost, never
holdings. 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.""" Admin bearer-token bypasses the gate for testing."""
# Read JSON body manually so we can enforce a hard size cap. FastAPI's # Read JSON body manually so we can enforce a hard size cap. FastAPI's
# default body limit is generous; we want tighter control here. # default body limit is generous; we want tighter control here.

View file

@ -2,11 +2,12 @@
Two sources can grant paid access: Two sources can grant paid access:
1. ``user.tier in {"paid", "enterprise"}`` set by Paddle webhook in 1. ``user.tier in {"paid", "enterprise"}`` set by the Stripe webhook
Phase D.3 once a subscription is active. once a subscription is active.
2. ``user.credit_until > now()`` non-subscription credit. Currently 2. ``user.credit_until > now()`` non-subscription credit. Populated
populated by the admin CLI (`python -m app.cli grant-credit`) and, in by the admin CLI (``python -m app.cli grant-credit``) and by the
D.3, by the referral-conversion path (3 months at 50% off). referral-conversion path (45 days per converted referral, both
parties).
Either is sufficient. We use a single ``paid_status`` function so the Either is sufficient. We use a single ``paid_status`` function so the
Settings page can show *why* a user has paid access ("paid subscription" Settings page can show *why* a user has paid access ("paid subscription"