From 0c6120bc17254c55ca9d66c832f156545ecc12fe Mon Sep 17 00:00:00 2001 From: Justin Maier Date: Fri, 7 Feb 2025 10:03:33 -0700 Subject: [PATCH 1/2] Add Pool Estimator --- src/env/schema.ts | 4 + src/pages/user/pool-estimate.tsx | 189 ++++++++++++++++++++++++++++ src/server/redis/client.ts | 5 + src/server/routers/buzz.router.ts | 6 + src/server/services/buzz.service.ts | 86 +++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 src/pages/user/pool-estimate.tsx diff --git a/src/env/schema.ts b/src/env/schema.ts index 236d32db5b..89dd3ff944 100644 --- a/src/env/schema.ts +++ b/src/env/schema.ts @@ -208,6 +208,10 @@ export const serverSchema = z.object({ VIMEO_SECRET: z.string().optional(), VIMEO_CLIENT_ID: z.string().optional(), VIMEO_VIDEO_UPLOAD_URL: z.string().optional(), + + // Creator Program Related: + CREATOR_POOL_TAXES: z.coerce.number().optional(), + CREATOR_POOL_PORTION: z.coerce.number().optional(), }); /** diff --git a/src/pages/user/pool-estimate.tsx b/src/pages/user/pool-estimate.tsx new file mode 100644 index 0000000000..ca63d52cbb --- /dev/null +++ b/src/pages/user/pool-estimate.tsx @@ -0,0 +1,189 @@ +import { Card, Container, Group, NumberInput, Stack, Text, Title } from '@mantine/core'; +import { IconPercentage } from '@tabler/icons-react'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { CurrencyBadge } from '~/components/Currency/CurrencyBadge'; +import { + DescriptionTable, + type Props as DescriptionTableProps, +} from '~/components/DescriptionTable/DescriptionTable'; +import { Meta } from '~/components/Meta/Meta'; +import { useFeatureFlags } from '~/providers/FeatureFlagsProvider'; +import { createServerSideProps } from '~/server/utils/server-side-helpers'; +import { Currency } from '~/shared/utils/prisma/enums'; +import { getLoginLink } from '~/utils/login-helpers'; +import { abbreviateNumber } from '~/utils/number-helpers'; +import { trpc } from '~/utils/trpc'; + +export const getServerSideProps = createServerSideProps({ + useSession: true, + resolver: async ({ features, session, ctx }) => { + if (!features?.buzz) { + return { notFound: true }; + } + + if (!session) + return { + redirect: { + destination: getLoginLink({ returnUrl: ctx.resolvedUrl }), + permanent: false, + }, + }; + }, +}); + + + +export default function EarnPotential() { + const [bankPortion, setBankPortion] = useState(50); + const [creatorBankPortion, setCreatorBankPortion] = useState(100); + const { query } = useRouter(); + const features = useFeatureFlags(); + + const { data: potential, isLoading } = trpc.buzz.getPoolForecast.useQuery( + { username: query.username as string }, + { enabled: features.buzz } + ); + const poolValue = potential?.poolValue ?? 0; + const poolSize = potential?.poolSize ?? 0; + const earned = potential?.earned ?? 0; + + + const bankedBuzz = poolSize * bankPortion/100; + const creatorBankedBuzz = earned * creatorBankPortion/100; + const forecastedEarning = + poolValue / bankedBuzz * creatorBankedBuzz; + + const buzzCurrencyProps = { + currency: Currency.BUZZ, + loading: isLoading, + formatter: abbreviateNumber, + }; + const dollarCurrencyProps = { + currency: Currency.USD, + loading: isLoading, + formatter: (x: number) => abbreviateNumber(x, { decimals: 2 }), + }; + + const poolDetails: DescriptionTableProps['items'] = [ + { + label: 'Pool Value', + info: 'The total value of the creator earning pool', + value: , + }, + { + label: 'Buzz Earned by All Creators', + info: 'The total amount of Buzz earned by all Creators last month', + value: , + }, + { + label: 'Portion of All Earned Buzz Banked', + info: 'The portion of all earned Buzz Creators Bank in the month', + value: ( + setBankPortion(v ?? 50)} + min={10} + max={80} + step={5} + icon={} + /> + ), + }, + { + label: 'Pool Size', + info: 'The total amount of Buzz in the pool', + value: , + }, + { + label: 'Your Buzz Earned', + info: 'The total amount of Buzz you earned last month', + value: , + }, + { + label: 'Your Bank Portion', + info: 'The amount of earned Buzz you plan to Bank', + value: ( + setCreatorBankPortion(v ?? 50)} + min={10} + max={100} + step={5} + icon={} + /> + ), + }, + { + label: 'Your Banked Buzz', + info: 'The total amount of Buzz you\'ve put into the pool', + value: , + }, + ]; + + return ( + <> + + + + + Estimated Creator Compensation Pool Earnings + + This is an estimate of your potential earnings from the Creator Compensation Pool based on your earnings last month as well as the total earnings of all creators on the platform. + + + + + + Pool Earning Factors + + + + + + + Estimated Earnings: + + + + + + + ); +} diff --git a/src/server/redis/client.ts b/src/server/redis/client.ts index d384c998b4..aa5b56504e 100644 --- a/src/server/redis/client.ts +++ b/src/server/redis/client.ts @@ -494,6 +494,11 @@ export const REDIS_KEYS = { BASE: 'packed:home-blocks', }, CACHE_LOCKS: 'cache-lock', + BUZZ: { + POTENTIAL_POOL: 'buzz:potential-pool', + POTENTIAL_POOL_VALUE: 'buzz:potential-pool-value', + EARNED: 'buzz:earned', + }, } as const; // These are used as subkeys after a dynamic key, such as `user:13:stuff` diff --git a/src/server/routers/buzz.router.ts b/src/server/routers/buzz.router.ts index e0d595217b..387a79dea7 100644 --- a/src/server/routers/buzz.router.ts +++ b/src/server/routers/buzz.router.ts @@ -30,6 +30,7 @@ import { claimWatchedAdReward, getClaimStatus, getEarnPotential, + getPoolForecast, } from '~/server/services/buzz.service'; import { isFlagProtected, protectedProcedure, router } from '~/server/trpc'; @@ -72,6 +73,11 @@ export const buzzRouter = router({ if (!input.username && !input.userId) input.userId = ctx.user.id; return getEarnPotential(input); }), + getPoolForecast: buzzProcedure.input(getEarnPotentialSchema).query(({ input, ctx }) => { + if (!ctx.user.isModerator) input.userId = ctx.user.id; + if (!input.username && !input.userId) input.userId = ctx.user.id; + return getPoolForecast(input); + }), getDailyBuzzCompensation: buzzProcedure .input(getDailyBuzzCompensationInput) .query(getDailyCompensationRewardHandler), diff --git a/src/server/services/buzz.service.ts b/src/server/services/buzz.service.ts index 02b7d72928..b22c4bc308 100644 --- a/src/server/services/buzz.service.ts +++ b/src/server/services/buzz.service.ts @@ -37,6 +37,10 @@ import { getServerStripe } from '~/server/utils/get-server-stripe'; import { formatDate, stripTime } from '~/utils/date-helpers'; import { QS } from '~/utils/qs'; import { getUserByUsername, getUsers } from './user.service'; +import { createCachedObject, fetchThroughCache } from '~/server/utils/cache-helpers'; +import { REDIS_KEYS } from '~/server/redis/client'; +import { CacheTTL } from '~/server/common/constants'; +import { number } from 'zod'; // import { adWatchedReward } from '~/server/rewards'; type AccountType = 'User'; @@ -820,6 +824,88 @@ export async function getEarnPotential({ userId, username }: GetEarnPotentialSch return potential; } +const earnedCache = createCachedObject<{ id: number; earned: number }>({ + key: REDIS_KEYS.BUZZ.EARNED, + idKey: 'id', + lookupFn: async (ids) => { + if (ids.length === 0 || !clickhouse) return {}; + + const results = await clickhouse.$query<{ id: number; earned: number }>` + SELECT + toAccountId as id, + SUM(amount) as earned + FROM buzzTransactions + WHERE type = 'compensation' + AND toAccountType = 'user' + AND toAccountId IN (${ids}) + AND toStartOfMonth(date) = toStartOfMonth(subtractMonths(now(), 1)) + GROUP BY toAccountId; + `; + + return Object.fromEntries(results.map((r) => [r.id, { id: r.id, earned: Number(r.earned) }])); + }, + ttl: CacheTTL.day, +}); + +export async function getPoolForecast({ userId, username }: GetEarnPotentialSchema) { + if (!clickhouse) return; + if (!userId && !username) return; + if (!userId && username) { + const user = await getUserByUsername({ username, select: { id: true } }); + if (!user) return; + userId = user.id; + } + if (!userId) return; + + const poolSize = await fetchThroughCache( + REDIS_KEYS.BUZZ.POTENTIAL_POOL, + async () => { + const results = await clickhouse!.$query<{ balance: number }>` + SELECT + SUM(amount) AS balance + FROM buzzTransactions + WHERE toAccountType = 'user' + AND type IN ('compensation', 'tip') + AND toAccountId != 0 + AND toStartOfMonth(date) = toStartOfMonth(subtractMonths(now(), 1)); + `; + if (!results.length) return 135000000; + return results[0].balance; + }, + { ttl: CacheTTL.day } + ); + + const poolValue = await fetchThroughCache( + REDIS_KEYS.BUZZ.POTENTIAL_POOL_VALUE, + async () => { + const results = await clickhouse!.$query<{ balance: number }>` + SELECT + SUM(amount) / 1000 AS balance + FROM buzzTransactions + WHERE toAccountType = 'user' + AND type = 'purchase' + AND fromAccountId = 0 + AND externalTransactionId NOT LIKE 'renewalBonus:%' + AND toStartOfMonth(date) = toStartOfMonth(subtractMonths(now(), 1)); + `; + if (!results.length || !env.CREATOR_POOL_TAXES || !env.CREATOR_POOL_PORTION) return 35000; + const gross = results[0].balance; + const taxesAndFees = gross * (env.CREATOR_POOL_TAXES / 100); + const poolValue = (gross - taxesAndFees) * (env.CREATOR_POOL_PORTION / 100); + return poolValue; + }, + { ttl: CacheTTL.day } + ); + + const results = await earnedCache.fetch(userId); + + return { + poolSize, + poolValue, + earned: results[userId]?.earned ?? 0, + }; +} + type Row = { modelVersionId: number; date: Date; comp: number; tip: number; total: number }; export const getDailyCompensationRewardByUser = async ({ From f66a1a13c5fb0d5bc15bc9c4c1ad62da61b8f1c2 Mon Sep 17 00:00:00 2001 From: Justin Maier Date: Fri, 7 Feb 2025 10:03:53 -0700 Subject: [PATCH 2/2] 5.0.466 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e360fef235..eb74024c09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "model-share", - "version": "5.0.465", + "version": "5.0.466", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "model-share", - "version": "5.0.465", + "version": "5.0.466", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-s3": "^3.490.0", diff --git a/package.json b/package.json index 5d6cab97b9..934b5117c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "model-share", - "version": "5.0.465", + "version": "5.0.466", "private": true, "scripts": { "start": "next start",