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

View file

@ -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=<code>`. 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(

View file

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

View file

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

View file

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