mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
aa1922b4ba
* feat(referral): add spend-triggered referral program v2 Referrers earn Tokens (1/2/3 per Bronze/Silver/Gold membership month paid, capped at 3 months per referee) + 10% Blue Buzz kickback on referee yellow Buzz purchases. Referees get 25% of tier monthlyBuzz as Blue Buzz on first paid membership. 7-day settlement window with chargeback revoke. Tokens redeemable in shop for perk-only CustomerSubscription grants (no buzz stipend, no tier badge). Gated by Flipt flag referral-program-v2. - Prisma: ReferralReward, ReferralMilestone, ReferralRedemption models - UserReferral.firstPaidAt + paidMonthCount for cap tracking - referral.service.ts handles earning, settlement, clawback, milestones - Stripe webhook hooks manageInvoicePaid + completeStripeBuzzTransaction - charge.refunded + charge.dispute.created webhooks for clawback - Cookie TTL 5d -> 30d, single auto code per user - referral:* signals (pending, settled, milestone, tier-granted, etc.) - /user/referrals dashboard with shop modal + activity feed - Checkout banner with manual code entry + bonus preview - Terms at src/static-content/referrals/terms.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(referral): address review feedback from code + gap analysis - Add @@unique([kind, sourceEventId]) + @@index([status, settledAt]) on ReferralReward. Replaces fetch-then-insert dedupe with unique-violation catch; closes idempotency race on webhook retries. - Make sourceEventId NOT NULL (always set in practice) - Fix settleRewardRow: move buzz grant before status flip and revert claim on buzz transaction failure. Prior version left Settled status after a post-commit rollback attempt. - revokeForChargeback now handles Settled rewards via negative (ChargeBack) createBuzzTransaction, not just Pending. - Chargeback webhook looks up the linked invoice for the PI so membership rewards (sourceEventId = invoice.id) are also revoked. - Enforce minReferrerAccountAgeDays in resolveReferrerForReferee. - getReferrerBalance: single groupBy query replaces 4 aggregates. - redeemTokens: FOR UPDATE lock on settled token rows; grant actual CustomerSubscription via findReferralProductForTier. Requires admin setup of Civitai-provider products flagged referralGrantable:true. - awardMilestones: deterministic sourceEventId (userId:threshold) and P2002 catch prevent double-award under concurrent settlement. - Referee bonus consolidated into the membership payment transaction. - ReferralCheckoutBanner now self-fetches per-tier monthly buzz via trpc.referral.getTierBonuses and displays the computed bonus. - trackCheckoutView moved to publicProcedure (anonymous guests can hit it). - logFraudEvent moved after dedupe checks so only real events log. - Constants: REWARD_DESCRIPTIONS table, promoted REFERRAL_SYSTEM_ACCOUNT_ID. Known gaps deferred to follow-ups: Paddle buzz kickback hook, referral:click emitter + dashboard stat, share buttons (Twitter/Reddit/ Discord), expiring-soon UI card, redemption-vs-paid-tier UI check, Top Affiliate cosmetic on 1M milestone. See review compile for full list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): address Justin's second-pass feedback Key changes following review: - Schema source of truth: moved all referral additions from the generated schema.prisma to schema.full.prisma so db:generate regenerates correctly. - Referral grants use CustomerSubscription.buzzType='referral' (new distinct value) so they stack with paid yellow/green/blue subs via session-user's highest-tier-across-all aggregation. @@unique([userId, buzzType]) no longer blocks a user from holding paid + referral at the same time. On re-redeem, existing referral sub is extended (same/lower tier) or upgraded (higher tier). - Attribution logging moved from Axiom to Postgres. New ReferralAttribution table links each attribution event to UserReferralCode + payment metadata (Stripe PI id, invoice id, charge id, card fingerprint, IP). Indexes support "all events for code/card/IP" queries for mod review. - Stripe invoice + PI webhooks forward payment identifiers + card fingerprint into the attribution record. - Paddle webhook gets a clarifying deprecation comment; no new referral paths touch Paddle. - Notifications (persistent + email) added alongside signals for: - referral-reward-settled - referral-milestone-hit - referral-token-expiring (daily cron, deduped per-user-per-expiry-date) - referral-welcome-bonus (referee gets in-app + email thank-you) - /user/referrals dashboard gains Share-on-X, Share-on-Reddit, and Share-on-Discord buttons in the hero. - 1M-milestone Top Affiliate badge grants the most-recent cosmetic as a placeholder until a bespoke one is authored. - Design doc (referral-program.md) extended with overlap stacking approach, attribution + fraud detection plan, and notification matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): tier queue redemptions, no exploit path Mirrors the prepaid-membership pattern: each redemption becomes a tier-time chunk stored on CustomerSubscription.metadata.referralQueue. Chunks sort by tier DESC so Gold always activates before Bronze. Same-tier chunks collapse into a single entry so the metadata stays small. On redemption: pool the currently-active chunk's remaining days + existing queue + new chunk, sort/collapse, promote the top chunk to active, persist the rest as queue. Cheap-Bronze-stacked-to-Gold exploit is impossible because chunks never change tier — a Bronze chunk grants Bronze for its own days, then the next chunk runs on its own terms. New hourly cron advance-referral-subs: for referral subs past currentPeriodEnd with a non-empty queue, promote the next chunk; otherwise cancel the sub. Static modal copy updated to reflect the new behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(referral): unit tests for queue, earning, chargeback, milestones 24 vitest cases covering: - collapseTierQueue (pure): sort order, same-tier collapse, zero-duration drops, and the "cheap Bronze stacked with one Gold" exploit is impossible - recordMembershipPaymentReward: no-bound-referrer skip, 3-month cap, first-payment referee bonus creation, subsequent-month no-bonus path, P2002 idempotency, min-referrer-age rejection - recordBuzzPurchaseKickback: skip when referee never paid, 10% rate calculation - revokeForChargeback: pending (no buzz clawback) vs settled (negative buzz txn with referral-clawback:<id> externalTransactionId) - awardMilestones: no-op on zero lifetime, awards only qualifying thresholds, swallows P2002 on duplicate milestone - advanceReferralSubscriptions: empty-queue cancels, non-empty promotes highest tier, missing referralGrantable product skips (no partial progress) Collapse exports collapseTierQueue for direct testing. DB clients mocked with deep shape for each model used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): timeline progress bar on dashboard Adds a segmented progress bar to /user/referrals showing the user's queued tier-time chunks. Mirrors the prepaid membership timeline pattern: each chunk is colored by tier (gold/silver/bronze), annotated with tooltips showing its date range and days-in-tier, and the active chunk is visually highlighted. getDashboard now returns referralGrant (activeTier, period bounds, queue). New ReferralTimelineProgress component renders the segmented bar only when referralGrant is present. Nothing shown if user has no active referral sub. Answers Justin's visibility concern — users can now see "Gold for 14 days, then Bronze for 42" rather than guessing at metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(referral): add review walkthrough checklist Section 0 lists pre-review setup: migrations, Civitai referral product inserts, Flipt flag, terms doc placeholders. Sections 1-6 walk the design docs, code-by-area, manual smoke tests, known gaps, design questions, and sign-off checklist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(referral): lock effective date + governing law Effective Date: April 21, 2026. Governing Law: Delaware (matches main TOS Section 19.2). Flipt flag referral-program-v2 added upstream in civitai/flipt-state and enabled by default (commit f09d584 there). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referral): generic ReferralRedemption + flatten migrations Collapses three migrations into one (safe — none applied yet): - 20260420153258_add_referral_program_v2 (original tables) - 20260420170000_add_referral_attribution - 20260421120000_referral_redemption_generic (redemption refactor) New consolidated migration 20260421120000_add_referral_program_v2 captures the final target schema in one file. ReferralRedemption drops tier/durationDays/subscriptionId in favor of: - rewardType: ReferralRedemptionType enum ('MembershipPerks' only today) - metadata: JSONB with shape { tier, durationDays, subscriptionId } Leaves room for future redemption types (BuzzGrant, cosmetic, etc.) without another migration. Service + dashboard UI updated to read metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(prepaid): exclude referral buzzType from prepaid cron Both the daily tier-transition sweep and the expired-cleanup pass filter by product.provider='Civitai'. Referral-granted subs (buzzType='referral') point to Civitai-provider referral products, so without this guard the prepaid cron would try to transition them using its prepaid-token logic and cancel them for having no tokens. advanceReferralSubscriptions owns those subs instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): link dashboard from buzz + membership pages ReferralCallout component with two variants: - full: gradient hero card for the Buzz Dashboard top-of-page - compact: slim row for the Membership page header Both flag-gated on features.referralProgramV2 so the card vanishes when the program is off. Links to /user/referrals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): hidden debug endpoint for UI experimentation POST /api/testing/referrals guarded by the WEBHOOK_TOKEN header (same pattern as /api/testing/strikes). Exposes a controlled surface for driving the referrals dashboard without paying real money, instead of reaching into the DB directly. Actions: dump, bind-code, grant-tokens, grant-blue-buzz, enqueue-chunk, simulate-membership-payment, simulate-buzz-purchase, simulate-chargeback, settle-all, advance-subs, expire-tokens, reset. settleImmediately=true on grant-* skips the 7-day wait. settle-all / advance-subs / expire-tokens fast-forward the relevant cron for one user. reset wipes a user back to a clean slate. Typical flow + curl examples documented in docs/features/referral-program.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(referral): document debug endpoint usage * docs(testing): establish debug-endpoint convention + header doc Referrals debug endpoint now leads with a block comment enumerating each action, required params, and a one-line description. That header is the documentation — agents read the file directly instead of consulting a wrapper skill. CLAUDE.md gets a new section describing the convention so the same pattern propagates to future features. Rule is: drop <feature>.ts under src/pages/api/testing/, guard with WebhookEndpoint, lead with a block comment, scope destructive actions to a single userId per call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(referral): 8 staged experiment scenarios + auth fix Review doc section 7 adds a phase-by-phase experiment plan Justin can walk through as the feature matures in review. Phase 1 seeded directly via DB (6 settled Bronze tokens + 500 Blue Buzz display row on user 1) so the dashboard walk-through works before this branch deploys. Testing endpoint header updated: auth is ?token= query param (reading from TokenSecuredEndpoint), not Authorization: Bearer. Doc + curl examples corrected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): A/B/C/D dashboard variants with switcher Page now delegates rendering to one of four variants, picked via ?v= query param. Parent owns data fetch, redeem mutation, shop modal; variants are dumb components taking a shared ReferralDashboardVariantProps. Variants: - A · Current — baseline minimal (extracted from original referrals.tsx) - B · Explainer — Buzz-dashboard-style narrative, how-it-works callouts, 4-step earn/spend breakdown - C · Gamified — level/rank badge by conversion count, milestone ladder as centerpiece, achievement feed framing for recent rewards - D · Funnel — share → click → signup → first-paid → loyal stages, stages we can't track yet labeled "coming soon" instead of fabricated metrics SegmentedControl at the top lets Justin flip between them side-by-side on the real dashboard to pick a direction. Switcher removes itself once a final variant is chosen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): refined gamified dashboard Picked variant C as the baseline per Justin's feedback and applied the rework in one pass. All four variants + switcher removed. Key changes: - Recruiter rank driven by a unified score (conversions + lifetimeBlueBuzz/1000), mapped to named ranks Rookie / Recruit / Advocate / Champion / Legend at 0 / 1 / 10 / 50 / 200 points. Separates cleanly from the Blue Buzz milestone ladder (milestone bonuses still trigger purely on buzz thresholds). - Rank card: big name, progress bar to next rank, three supporting stats (paid referrals, lifetime Blue Buzz, recruiter score). - Milestone ladder: full formatted numbers, named milestones, Blue Buzz bolt icons throughout, per-row Unlocked/Up next/Locked badge, progress bar on the next-up row. Dismissible explainer alert (persists in localStorage). - How-it-works card: 4-step explainer pulled from variant B (Share → Earn Tokens → Earn Blue Buzz → Spend Tokens). - Token Bank: inline 6-offer grid replaces the modal. Redeem happens in place. Shop modal removed from the parent page. - Recent referrals (renamed from Recent activity): kickbacks and recruits with amount on the right under status badge, cap 10 with Load more. - Palette pared back to neutral dark accents + tier colors on the shop tiles only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): dashboard polish pass Addresses feedback from the first C rework: - Bring color back: tier palette on shop headers (bronze/silver/gold gradients), accent-colored rank card (Rookie gray / Recruit teal / Advocate blue / Champion grape / Legend yellow), blue for Blue Buzz, yellow for milestone trophies, green for Settled badges. - "Recruiter rank" -> "Your rank". Removed the redundant top-right rank badge. Rank name is colored by accent and paired with a trophy icon in a filled ThemeIcon. - How it works is now dismissible (localStorage), uses FeatureCard-style layout with gradient icon headers (Share blue, Earn Tokens teal, Earn Blue Buzz yellow, Spend grape). - Milestone explainer wording clarified: "Once a friend pays for any Membership with your code, every Buzz purchase they make earns you 10% back as Blue Buzz" — reflects the actual gate (firstPaidAt, no time window). Lifetime Blue Buzz promoted to a standalone pill with a large bold number and bolt icon, visible alongside the section title. - Milestone rows: trophy icon + yellow filled ThemeIcon when unlocked, blue light ThemeIcon when Up next, muted when locked. Bonus amount now called out in yellow with a bolt icon. - Token Bank: Spendable / Pending promoted to prominent tiles with icons (IconCoin / IconClock) and hint text ("Ready to redeem" / "Settles in 7 days"). Shop regrouped by tier — 3 tier cards, each containing the 14d and 30d options as rows instead of 6 flat tiles. - Recent referrals: big reward number with tier-colored icon per row. - A few iconography upgrades across stats (IconSparkles for Recruiter Score, IconRocket for "Next rank" label). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(referral): blue-buzz bonus color, filled bolt, rank stats as cards - Milestone bonus amounts + unlocked trophy/badge now blue (Blue Buzz color), not yellow. Yellow was misleading since bonuses pay in Blue Buzz, not yellow Buzz. - Swapped IconBolt -> IconBoltFilled across the dashboard. Civitai never uses the outline variant elsewhere. - Rank stats converted from a divider-separated left-aligned Group to a 3-column Grid of RankStatCard tiles (icon block + label + value), matching the Token Bank / milestone pill styling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referrals): rescale recruiter score for granular progress - 1 point per Blue Buzz, 1,000 per paid referral month - Thresholds: Rookie 0, Recruit 1k, Advocate 10k, Champion 50k, Legend 200k - 500 BB now shows halfway to Recruit instead of stuck at 0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): restructure dashboard per feedback - Move How It Works above Rank card - Move referral timeline under Token Bank - Token Bank: tier-colored accent header (no dark text on gradient), drop meaningless sparkles icon, add divider between duration rows, keep button filled when affordable / outlined default when not - Token Bank subtitle: "Spend referral tokens on Membership perks." - Add dismissible info alert describing token earning mechanics - Migrate kickback + how-it-works + token-bank dismissals from localStorage to user-level dismissedAlerts (trpc.user.dismissAlert) - Remove icon on Recent referrals header (only card with one) - Simplify HowStep #4 copy (drop tier-queue phrasing) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): round 2 UI polish per feedback - Rank card: progress bar uses next-rank color so it's visible against the subtle backdrop (previously gray on gray) - Token Bank alert: switch color from orange to blue to match Blue Buzz milestone alert - Tier cards: full-width dividers between duration rows - Spendable/Pending tiles: drop hint text, add info tooltip (IconInfoCircle) next to label - Recent referrals: - Add "Settles {date}" for Pending rows - Blue Buzz amounts in blue + bolt icon (drop "Blue Buzz" text) - Token amounts show coin icon (drop "tokens" text) - Icon backgrounds match tier color (bronze/silver/gold) - Milestone bonus: bolt icon before amount (+[bolt]{num} bonus) - How It Works cards: FeatureCards-style gradient header + circular icon to match Buzz dashboard style - Timeline: move tier-queue note to bottom, style as crypto-deposit info row (IconInfoCircle + dimmed text) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): round 3 polish — premium code block + stats + spotlight - Code block redesigned crypto-deposit style: gradient accent bar, spotlight hover, monospace code in a chip with ActionIcon copy, gradient "Copy share link" button, subtle background wash, socials under a divider with uppercase section label - Blue Buzz milestones: replace lifetime-earned chip with Earned + Pending stat blocks matching rank card / token bank pattern - Unify RankStatCard + TokenTile into single StatBlock (optional tooltip) - Add spotlight hover effect to How It Works cards - Rewrite How It Works copy: shorter, less dense, especially step 2 which was number-heavy - Move Program Terms link to footer of the page Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): scope spotlight, relocate copy-link, add skeletons - HowStep cards: spotlight now confined to the colored header (was spanning the whole card) - Referral code block: drop the out-of-place top-right "Copy share link" button; move it into the Share It row as the first action (still gradient-styled, compact-sm to match siblings) - Rank card: remove the radial glow accent — not used elsewhere in the dashboard, looked out of place above the other cards - Replace loading spinner on /user/referrals with a full-dashboard skeleton (matches section structure: code, how-it-works, rank, milestones, token bank, recent referrals) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): tier perks popover, yellow tokens, premium rank card - Shop tier cards: spotlight hover on the Paper, info bobble next to the tier title that opens a Popover with PlanBenefitList (monthly Buzz + default perks filtered by tier/buzzType). On green servers the popover also links out to /pricing for full details. - Subscriptions/PlanBenefitList: rename default benefit "Generate mature content with Blue Buzz" → "Unrestricted generation with Blue Buzz" (appears on red/yellow servers only). Affects the referrals popover and the pricing page. - Unify token color to yellow across the dashboard: - Token Bank Spendable stat: green → yellow - Recent referrals token icon: orange-5 → yellow-5 - Shop card cost icon: default → yellow-5 - Redemption history reward format: "−1 tok" → "−[coin] 1" - Rank card: redesigned premium style with spotlight, rank-colored gradient accent bar, subtle tinted background wash (matches the referral code block treatment) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): round 6 — inline value icons, bg fixes, popover portal - StatBlock gains optional valueIcon prop rendered inline before the number; outer icon is now decorative only - Blue Buzz milestones: - Earned: green accent, IconCircleCheck outer, IconBoltFilled inline - Pending: IconBoltFilled inline (outer IconClock unchanged) - Lifetime Blue Buzz (rank card): outer icon → IconHistory, inline IconBoltFilled - Token Bank: - Spendable: green accent, IconCircleCheck outer, IconCoin inline - Pending: IconCoin inline (outer IconClock unchanged) - TierPerksPopover: `withinPortal` so the Gold popover is no longer clipped by the card's `overflow: hidden` - Recent referrals: the leading "+" stays white regardless of reward color - ReferralCodeBlock + RankCard: apply light-dark background so they stop blending with the page body in dark mode Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referrals): points-driven milestones + auto-backfill Replace blue-buzz-driven milestone progression with a unified Referral Points metric: 1 pt per Blue Buzz earned + 1k/2.5k/5k per paid Bronze/ Silver/Gold referral month (≈10% of each tier's monthly Buzz value). - constants.referrals.pointsPerTierMonth defines the tier weights - computeLifetimeReferralPoints sums settled buzz + tier-weighted months - awardMilestones now drives off lifetime points (same thresholds: 1k/10k/50k/200k/1M) - getReferrerBalance returns lifetimePoints + pendingPoints alongside the existing token/buzz fields (plus nextTokenExpiresAt and expiringSoonTokens for the use-them-or-lose-them UI) - getDashboard fires awardMilestones(userId) fire-and-forget so a user whose lifetime points crossed a threshold under the new scoring gets the bonus written + payed out on next page load (idempotent via the existing unique (userId, threshold) constraint) - getDashboard exposes activeMembership so the redeem confirm modal can warn before stacking on top of an existing paid plan - getTierBonuses now returns rewardsMultiplierByTier and purchasesMultiplierByTier (skipping referral-grantable placeholder products so we reflect what real subscribers get); coerces the loose JSON multiplier values to number to avoid string surprises - Testing endpoint grant-tokens defaults tokenAmount to the tier's canonical 1/2/3 when omitted so seeded data matches real flow Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): dashboard polish + flag gate Big consolidated UI pass after Justin's feedback rounds 7-10. Page + content - pages/user/referrals.tsx: gated server-side on features.referralProgramV2 -> notFound; meta title aligned with buzz-dashboard pattern (Civitai | Refer & earn); skeleton fallback while data loads - static-content/referrals/terms.md: frontmatter moved above the HTML placement comment so gray-matter parses the title correctly (terms page now shows the right title + description) Dashboard - "Refer & earn" hero with IconGift ThemeIcon; subtitle tightened - Premium ReferralCodeBlock: plain left side (gray-50/dark white-3%), spotlight + subtle gradient on the right Share It panel, gradient divider between, share buttons natural compact-sm - Friends-get blurb under the code: "Friends get 25% bonus Blue Buzz on their first Membership month" - HowStep cards spotlight scoped to the colored header; rewritten copy; "Token Bank" -> "Token Shop" everywhere (title, step 4, alert key) - Rank card: premium style w/ rank-colored gradient bar + spotlight, fallback default text color when rank is gray (rookie) so it reads in light mode - Milestones renamed -> "Milestones"; driven by lifetimePoints with green Earned (IconCircleCheck) + gray Pending (IconClock) stat blocks; inline blue bolt next to BB amounts, violet star next to point counts; unlocked thresholds show a fully filled progress bar - Token Shop: spendable (green) + pending (gray) stats with inline yellow coin icons; per-tier ShopTierCard with spotlight, tier- colored accent header, info popover that opens PlanBenefitList (tier-filtered), full-width dividers between duration rows; redeem triggers an openConfirmModal when the user has an active paid membership - Spendable stat shows an IconAlertTriangle to the right of the number when tokens expire within 30 days; popover explains how many and earliest expiry - Recent referrals: rows with bg-gray-50/dark white-3%, tier-colored icon backgrounds (bronze/silver/gold), settles-on date for pending, blue bolt + amount layout for BB, yellow coin + amount for tokens - Redemption history: "−1 tok" replaced with "− [coin] 1" - Tooltips switched to color="dark" so they stop reading white in light mode - Scoring details popover (with table) replaces the inline scoring text; same popover reused on the Points stat block via an UnstyledButton info bobble that matches the rest of the dashboard - Recruiter Score renamed -> Referral Score, IconAward outer + IconStarFilled inline - Skeleton mirrors all of the above so the loading state matches the new layout - ReferralCallout (Buzz dashboard entry): tileCard-style background matching neighboring cards; gift icon; copy without slashed tier list - ReferralTimelineProgress: dark-color tooltips; tier-queue note moved below the bars in crypto-deposit info-row style - dashboard.types.ts: computeRecruiterScore now passes through lifetimePoints (rank thresholds unchanged: 0/1k/10k/50k/200k) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(stripe): inline referral code + percent/multiplier perks copy Move the referral-code entry to Stripe's hosted checkout via custom_fields so it lives "in the same modal as checkout" (per feedback: the previous pre-checkout banner felt awkward). - createSubscribeSession adds a single optional text custom_field ("Referral code (optional)", key=ref_code). Cookie-driven refCode still flows through subscription metadata as before. - checkout.session.completed handler (subscription branch) reads session.custom_fields[].text.value for ref_code, uppercases/trims, and patches the new Subscription's metadata. The existing manageInvoicePaid -> bindReferralCodeForUser / recordMembershipPaymentReward flow then picks it up unchanged. - pages/pricing: drop the standalone ReferralCheckoutBanner since the field now lives on Stripe's checkout itself. Cross-cutting perks copy: - PlanBenefitList: "Generate mature content with Blue Buzz" -> "Unrestricted generation with Blue Buzz"; remove negative mx on the section divider so it stops blowing out of popovers. - getPlanDetails: shared formatBoostCopy helper renders perk multipliers as "{pct}% bonus..." below 2x and "{n}x ..." at or above 2x; applied to both rewards and purchases multipliers; drop the isGreen-only gate so red shows the purchase multiplier when the tier metadata defines one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(referrals): correctness + abuse hardening from external review External Gemini review of the reward distribution paths surfaced a batch of correctness, race, and abuse issues. All addressed in this commit, with vitest coverage. The dashboard copy + token-earning popover from the same round are bundled in. Backend fixes - recordMembershipPaymentReward: open a $transaction and SELECT … FOR UPDATE the UserReferral row before reading paidMonthCount / firstPaidAt. Two concurrent invoice.paid webhooks for the same referee can no longer both create a RefereeBonus or both increment past the cap. - manageInvoicePaid (stripe webhook): if subscription_details.metadata is empty, fall back to fetching the parent Checkout Session and reading custom_fields[].text.value for ref_code. Stripe doesn't guarantee checkout.session.completed lands before invoice.paid; this catches the "first invoice arrives first" race. - ReferralReward.points column (new) snapshots the tier's point value at write time. computeLifetimeReferralPoints + getReferrerBalance now sum that column instead of dynamically multiplying historical membership token counts by the current pointsPerTierMonth. Re-tuning the constants no longer retroactively unlocks milestones. Migration backfills existing rows with their tier-canonical points. - payment_intent.succeeded: drop the swallowed .catch(() => null) on recordBuzzPurchaseKickback so a transient DB error propagates and Stripe retries the webhook. Idempotency is already guaranteed by the @@unique(kind, sourceEventId) constraint. - revokeForChargeback: per-referee tx that locks UserReferral via $queryRaw FOR UPDATE, decrements paidMonthCount by the count of revoked MembershipToken rows, and clears firstPaidAt to null when the count drops to 0. Closes the refund-and-keep-buzz-kickbacks abuse path. Lock prevents two concurrent chargebacks for the same referee from racing the read-modify-write. - grantReferralSubscription: lock the CustomerSubscription row at the start of the tx so a concurrent advance-cron run can't overwrite a newly-appended queue entry. - advanceReferralSubscriptions: run each sub through its own $transaction with FOR UPDATE on the row, re-check currentPeriodEnd inside the lock so a redemption that bumped the period out doesn't get clobbered. - grantReferralSubscription enforces constants.referrals.maxQueuedDays (365). A hot referrer can't queue years of perks (Stripe's date max is 2038). Returns a user-facing error so redeemTokens rolls back and the spent tokens are not consumed. - computeLifetimeReferralPoints + getReferrerBalance.lifetimePoints include Expired status — a token expiring shouldn't pull the user's lifetime points down (or boot them out of a milestone). Dashboard copy - Milestones info alert reworked to match how kickbacks actually work: earn points per paid Membership month + 10% of any Buzz the friend buys after joining. Cross a milestone for a bonus. - Token Shop info alert simplified: "Earn tokens per Bronze (1 token), Silver (2 tokens), or Gold (3 tokens) month, up to three months per friend." - New TokenEarningsPopover wired to the Spendable stat info bobble (mirrors the ScoringDetailsPopover pattern). Permanent reference for per-tier token amounts + the 3-month-per-friend cap + 90-day expiry, available even after the dismissable alert is closed. Tests - 35 vitest cases (was 24); all green. - New: race-protected first-payment via locked paidMonthCount, cap enforcement after lock, inline RefereeBonus settle (success + buzz-grant-failure revert), revokeForChargeback decrement + firstPaidAt clear, RefereeBonus revoke does not touch UserReferral, paid months remain → firstPaidAt stays, FOR UPDATE used in revokeForChargeback (no findUnique fallback), advance bails when currentPeriodEnd is bumped out by a parallel redeem, points snapshot is immune to config changes, lifetime aggregate filter includes Expired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>