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:
parent
ce36ce36fd
commit
1be0c5a436
5 changed files with 24 additions and 19 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue