diff --git a/packages/app/control/src/app/(app)/admin/_components/overview/AppEarnings.tsx b/packages/app/control/src/app/(app)/admin/_components/overview/AppEarnings.tsx new file mode 100644 index 000000000..ed4796381 --- /dev/null +++ b/packages/app/control/src/app/(app)/admin/_components/overview/AppEarnings.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { OverviewPanel } from '@/components/overview-panel/OverviewPanel'; +import { api } from '@/trpc/client'; + +export function AppEarningsOverview() { + const { data, isLoading, error } = + api.admin.earnings.getAppEarningsOverviewMetrics.useQuery(); + + return ( + + ); +} + diff --git a/packages/app/control/src/app/(app)/admin/_components/overview/PaymentHistory.tsx b/packages/app/control/src/app/(app)/admin/_components/overview/PaymentHistory.tsx new file mode 100644 index 000000000..0b8d5f540 --- /dev/null +++ b/packages/app/control/src/app/(app)/admin/_components/overview/PaymentHistory.tsx @@ -0,0 +1,23 @@ +"use client"; + +import React from 'react'; +import { OverviewPanel } from '@/components/overview-panel/OverviewPanel'; +import { api } from '@/trpc/client'; + +export function PaymentsOverview() { + const { data, isLoading, error } = + api.admin.payments.getPaymentsOverviewMetrics.useQuery(); + + return ( + + ); +} + diff --git a/packages/app/control/src/app/(app)/admin/_components/overview/UserEarnings.tsx b/packages/app/control/src/app/(app)/admin/_components/overview/UserEarnings.tsx new file mode 100644 index 000000000..e995e63e8 --- /dev/null +++ b/packages/app/control/src/app/(app)/admin/_components/overview/UserEarnings.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { OverviewPanel } from '@/components/overview-panel/OverviewPanel'; +import { api } from '@/trpc/client'; + +export function UserEarningsOverview() { + const { data, isLoading, error } = + api.admin.earnings.getUserEarningsOverviewMetrics.useQuery(); + + return ( + + ); +} diff --git a/packages/app/control/src/app/(app)/admin/_components/overview/UserSpending.tsx b/packages/app/control/src/app/(app)/admin/_components/overview/UserSpending.tsx new file mode 100644 index 000000000..3d4789255 --- /dev/null +++ b/packages/app/control/src/app/(app)/admin/_components/overview/UserSpending.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { OverviewPanel } from '@/components/overview-panel/OverviewPanel'; +import { api } from '@/trpc/client'; + +export function UserSpendingOverview() { + const { data, isLoading, error } = + api.admin.spending.getUserSpendingOverviewMetrics.useQuery(); + + return ( + + ); +} + + diff --git a/packages/app/control/src/app/(app)/admin/dashboard/app-earnings/page.tsx b/packages/app/control/src/app/(app)/admin/dashboard/app-earnings/page.tsx index 607ea6660..c6bc303e1 100644 --- a/packages/app/control/src/app/(app)/admin/dashboard/app-earnings/page.tsx +++ b/packages/app/control/src/app/(app)/admin/dashboard/app-earnings/page.tsx @@ -1,5 +1,6 @@ import AppEarningsTable from '@/app/(app)/admin/_components/table/AppEarnings'; import { TableLayout } from '@/app/(app)/admin/_components'; +import { AppEarningsOverview } from '@/app/(app)/admin/_components/overview/AppEarnings'; export default function AppEarningsPage() { return ( @@ -7,6 +8,7 @@ export default function AppEarningsPage() { title="App Earnings" description="Monitor app performance and revenue generation metrics" > + ); diff --git a/packages/app/control/src/app/(app)/admin/dashboard/payments/page.tsx b/packages/app/control/src/app/(app)/admin/dashboard/payments/page.tsx index f6809f697..6e0468569 100644 --- a/packages/app/control/src/app/(app)/admin/dashboard/payments/page.tsx +++ b/packages/app/control/src/app/(app)/admin/dashboard/payments/page.tsx @@ -1,4 +1,5 @@ import PaymentHistoryTable from '@/app/(app)/admin/_components/table/PaymentHistory'; +import { PaymentsOverview } from '@/app/(app)/admin/_components/overview/PaymentHistory'; import { TableLayout } from '@/app/(app)/admin/_components'; export default function PaymentHistoryPage() { @@ -7,6 +8,7 @@ export default function PaymentHistoryPage() { title="Payment History" description="Review payment transactions and payout history" > + ); diff --git a/packages/app/control/src/app/(app)/admin/dashboard/user-earnings/page.tsx b/packages/app/control/src/app/(app)/admin/dashboard/user-earnings/page.tsx index 7b92d8fdd..67f4fcd1a 100644 --- a/packages/app/control/src/app/(app)/admin/dashboard/user-earnings/page.tsx +++ b/packages/app/control/src/app/(app)/admin/dashboard/user-earnings/page.tsx @@ -1,5 +1,6 @@ import UserEarningsTable from '@/app/(app)/admin/_components/table/UserEarnings'; import { TableLayout } from '@/app/(app)/admin/_components'; +import { UserEarningsOverview } from '@/app/(app)/admin/_components/overview/UserEarnings'; export default function UserEarningsPage() { return ( @@ -7,6 +8,7 @@ export default function UserEarningsPage() { title="User Earnings" description="Comprehensive view of user earnings and transaction aggregates across all apps" > + ); diff --git a/packages/app/control/src/app/(app)/admin/dashboard/user-spending/page.tsx b/packages/app/control/src/app/(app)/admin/dashboard/user-spending/page.tsx index 5165bc130..ad717de64 100644 --- a/packages/app/control/src/app/(app)/admin/dashboard/user-spending/page.tsx +++ b/packages/app/control/src/app/(app)/admin/dashboard/user-spending/page.tsx @@ -1,6 +1,7 @@ import React from 'react'; import UserSpendingTable from '@/app/(app)/admin/_components/table/UserSpending'; import { TableLayout } from '@/app/(app)/admin/_components'; +import { UserSpendingOverview } from '@/app/(app)/admin/_components/overview/UserSpending'; export default function UserSpendingPage() { return ( @@ -8,6 +9,7 @@ export default function UserSpendingPage() { title="User Spending" description="Analyze user spending patterns and credit usage across the platform" > + ); diff --git a/packages/app/control/src/services/admin/app-earnings-summary.ts b/packages/app/control/src/services/admin/app-earnings-summary.ts new file mode 100644 index 000000000..13a1579a3 --- /dev/null +++ b/packages/app/control/src/services/admin/app-earnings-summary.ts @@ -0,0 +1,187 @@ +import { db } from '@/lib/db'; +import { OverviewMetricConfig } from './type/overview-metric'; + +function percentChange(currentValue: number | bigint, previousValue: number | bigint): number { + const currentNumber = Number(currentValue) || 0; + const previousNumber = Number(previousValue) || 0; + if (previousNumber === 0) return currentNumber ? 100 : 0; + const delta = currentNumber - previousNumber; + return (delta / previousNumber) * 100; +} + +type AppEarningsOverviewRow = { + totalRevenue: number; + totalTransactions: number; + activeApps: number; + avgRevenuePerActiveApp: number; + avgTransactionValue: number; + freeTierUsage: number; +}; + +type AppEarningsTrendRow = { + spend_current: number; + spend_prev: number; + tx_count_current: number; + tx_count_prev: number; + active_apps_current: number; + active_apps_prev: number; + free_tier_current: number; + free_tier_prev: number; +}; + +export async function getAppEarningsOverviewMetrics(): Promise { + const summaryQuery = ` + WITH t AS ( + SELECT + COALESCE(SUM(t."totalCost"), 0)::double precision AS total_revenue, + COALESCE(COUNT(t.id), 0)::double precision AS total_transactions, + COALESCE(SUM(CASE WHEN t."spendPoolId" IS NOT NULL THEN t."totalCost" END), 0)::double precision AS free_tier_usage + FROM "transactions" t + ), a AS ( + SELECT COALESCE(COUNT(DISTINCT t2."echoAppId"), 0)::double precision AS active_apps + FROM "transactions" t2 + ), avg_tx AS ( + SELECT COALESCE(AVG(t3."totalCost"), 0)::double precision AS avg_txn_value + FROM "transactions" t3 + ) + SELECT + t.total_revenue AS "totalRevenue", + t.total_transactions AS "totalTransactions", + a.active_apps AS "activeApps", + CASE WHEN a.active_apps > 0 THEN t.total_revenue / a.active_apps ELSE 0 END AS "avgRevenuePerActiveApp", + avg_tx.avg_txn_value AS "avgTransactionValue", + t.free_tier_usage AS "freeTierUsage" + FROM t, a, avg_tx; + `; + + const summary = (await db.$queryRawUnsafe(summaryQuery)) as Array; + const s = summary[0] || { + totalRevenue: 0, + totalTransactions: 0, + activeApps: 0, + avgRevenuePerActiveApp: 0, + avgTransactionValue: 0, + freeTierUsage: 0, + }; + + const trendQuery = ` + WITH ranges AS ( + SELECT + (NOW()::date - INTERVAL '7 days') AS start_current, + NOW()::date AS end_current, + (NOW()::date - INTERVAL '14 days') AS start_prev, + (NOW()::date - INTERVAL '7 days') AS end_prev + ), + txn AS ( + SELECT + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."totalCost" END), 0)::double precision AS spend_current, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."totalCost" END), 0)::double precision AS spend_prev, + COALESCE(COUNT(CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN 1 END), 0)::double precision AS tx_count_current, + COALESCE(COUNT(CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN 1 END), 0)::double precision AS tx_count_prev, + COALESCE(SUM(CASE WHEN t."spendPoolId" IS NOT NULL AND t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."totalCost" END), 0)::double precision AS free_tier_current, + COALESCE(SUM(CASE WHEN t."spendPoolId" IS NOT NULL AND t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."totalCost" END), 0)::double precision AS free_tier_prev + FROM "transactions" t, ranges r + ), + apps AS ( + SELECT + COALESCE(COUNT(DISTINCT CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."echoAppId" END), 0)::double precision AS active_apps_current, + COALESCE(COUNT(DISTINCT CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."echoAppId" END), 0)::double precision AS active_apps_prev + FROM "transactions" t, ranges r + ) + SELECT + txn.spend_current, + txn.spend_prev, + txn.tx_count_current, + txn.tx_count_prev, + apps.active_apps_current, + apps.active_apps_prev, + txn.free_tier_current, + txn.free_tier_prev + FROM txn, apps; + `; + + const trendRows = (await db.$queryRawUnsafe(trendQuery)) as Array; + const t = trendRows[0] || { + spend_current: 0, + spend_prev: 0, + tx_count_current: 0, + tx_count_prev: 0, + active_apps_current: 0, + active_apps_prev: 0, + free_tier_current: 0, + free_tier_prev: 0, + }; + + const trendLabel = 'vs previous 7d'; + + const avgRevCurrent = t.active_apps_current ? t.spend_current / t.active_apps_current : 0; + const avgRevPrev = t.active_apps_prev ? t.spend_prev / t.active_apps_prev : 0; + const avgTxnCurrent = t.tx_count_current ? t.spend_current / t.tx_count_current : 0; + const avgTxnPrev = t.tx_count_prev ? t.spend_prev / t.tx_count_prev : 0; + + const metrics: OverviewMetricConfig[] = [ + { + id: 'totalRevenue', + title: 'Total Revenue', + description: 'Total revenue generated across all apps', + displayType: 'currency', + value: s.totalRevenue, + trendValue: percentChange(t.spend_current, t.spend_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'activeApps', + title: 'Active Apps', + description: 'Apps with at least one transaction', + displayType: 'number', + value: s.activeApps, + trendValue: percentChange(t.active_apps_current, t.active_apps_prev), + size: 'sm', + format: { showTrend: true, trendLabel }, + }, + { + id: 'avgRevenuePerActiveApp', + title: 'Avg Revenue / Active App', + description: 'Average revenue per active app', + displayType: 'currency', + value: s.avgRevenuePerActiveApp, + trendValue: percentChange(avgRevCurrent, avgRevPrev), + size: 'sm', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'totalTransactions', + title: 'Total Transactions', + description: 'Number of transactions across all apps', + displayType: 'number', + value: s.totalTransactions, + trendValue: percentChange(t.tx_count_current, t.tx_count_prev), + size: 'sm', + format: { showTrend: true, trendLabel }, + }, + { + id: 'avgTransactionValue', + title: 'Avg Transaction Value', + description: 'Average spend per transaction', + displayType: 'currency', + value: s.avgTransactionValue, + trendValue: percentChange(avgTxnCurrent, avgTxnPrev), + size: 'sm', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'freeTierUsage', + title: 'Free Tier Usage', + description: 'Total consumption covered by spend pools', + displayType: 'currency', + value: s.freeTierUsage, + trendValue: percentChange(t.free_tier_current, t.free_tier_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + ]; + + return metrics; +} + diff --git a/packages/app/control/src/services/admin/app-earnings.ts b/packages/app/control/src/services/admin/app-earnings.ts index 907b16fd5..bd683c1ae 100644 --- a/packages/app/control/src/services/admin/app-earnings.ts +++ b/packages/app/control/src/services/admin/app-earnings.ts @@ -42,13 +42,14 @@ const COLUMN_MAPPINGS: Record = { creatorUserId: 'creator.id', creatorUserName: 'creator.name', creatorUserEmail: 'creator.email', - totalTransactions: 'COUNT(t.id)', - totalRevenue: 'COALESCE(SUM(t."totalCost"), 0)', - totalAppProfit: 'COALESCE(SUM(t."appProfit"), 0)', - totalMarkupProfit: 'COALESCE(SUM(t."markUpProfit"), 0)', - totalReferralProfit: 'COALESCE(SUM(t."referralProfit"), 0)', - totalReferralCodes: 'COUNT(DISTINCT am_users."referrerId")', - totalUsers: 'COUNT(DISTINCT am_users.id)', + // Use aggregated CTE outputs to avoid fan-out multiplication + totalTransactions: 'COALESCE(txn.total_transactions, 0)', + totalRevenue: 'COALESCE(txn.total_revenue, 0)', + totalAppProfit: 'COALESCE(txn.total_app_profit, 0)', + totalMarkupProfit: 'COALESCE(txn.total_markup_profit, 0)', + totalReferralProfit: 'COALESCE(txn.total_referral_profit, 0)', + totalReferralCodes: 'COALESCE(membership.total_referral_codes, 0)', + totalUsers: 'COALESCE(membership.total_users, 0)', }; // Main TRPC procedure function for app earnings @@ -89,6 +90,30 @@ export const getAppEarningsWithPagination = async ( // Build the main query with dynamic clauses const baseQuery = ` + WITH txn AS ( + SELECT + t."echoAppId" AS app_id, + COUNT(t.id)::double precision AS total_transactions, + COALESCE(SUM(t."totalCost"), 0)::double precision AS total_revenue, + COALESCE(SUM(t."appProfit"), 0)::double precision AS total_app_profit, + COALESCE(SUM(t."markUpProfit"), 0)::double precision AS total_markup_profit, + COALESCE(SUM(t."referralProfit"), 0)::double precision AS total_referral_profit + FROM "transactions" t + GROUP BY t."echoAppId" + ), membership AS ( + SELECT + am."echoAppId" AS app_id, + COALESCE(COUNT(DISTINCT am."userId"), 0)::double precision AS total_users, + COALESCE(COUNT(DISTINCT am."referrerId") FILTER (WHERE am."referrerId" IS NOT NULL), 0)::double precision AS total_referral_codes + FROM "app_memberships" am + GROUP BY am."echoAppId" + ), app_emails AS ( + SELECT + oes."echoAppId" AS app_id, + COALESCE(array_agg(DISTINCT oes."emailCampaignId") FILTER (WHERE oes."emailCampaignId" IS NOT NULL), '{}'::text[]) AS app_email_campaigns + FROM "outbound_emails_sent" oes + GROUP BY oes."echoAppId" + ) SELECT a.id, a.name, @@ -98,31 +123,27 @@ export const getAppEarningsWithPagination = async ( creator.id as "creatorUserId", creator.name as "creatorUserName", creator.email as "creatorUserEmail", - COALESCE( - array_agg(DISTINCT app_oes."emailCampaignId") FILTER (WHERE app_oes."emailCampaignId" IS NOT NULL), - '{}'::text[] - ) as "appEmailCampaigns", - COALESCE( - array_agg(DISTINCT owner_oes."emailCampaignId") FILTER (WHERE owner_oes."emailCampaignId" IS NOT NULL), - '{}'::text[] - ) as "ownerEmailCampaigns", - COUNT(t.id) as "totalTransactions", - COALESCE(SUM(t."totalCost"), 0) as "totalRevenue", - COALESCE(SUM(t."appProfit"), 0) as "totalAppProfit", - COALESCE(SUM(t."markUpProfit"), 0) as "totalMarkupProfit", - COALESCE(SUM(t."referralProfit"), 0) as "totalReferralProfit", - COUNT(DISTINCT am_users."referrerId") as "totalReferralCodes", - COUNT(DISTINCT am_users.id) as "totalUsers" + COALESCE(app_emails.app_email_campaigns, '{}'::text[]) as "appEmailCampaigns", + COALESCE(( + SELECT COALESCE(array_agg(DISTINCT owner_oes."emailCampaignId") FILTER (WHERE owner_oes."emailCampaignId" IS NOT NULL), '{}'::text[]) + FROM "outbound_emails_sent" owner_oes + WHERE owner_oes."userId" = creator.id + ), '{}'::text[]) as "ownerEmailCampaigns", + ${COLUMN_MAPPINGS.totalTransactions} as "totalTransactions", + ${COLUMN_MAPPINGS.totalRevenue} as "totalRevenue", + ${COLUMN_MAPPINGS.totalAppProfit} as "totalAppProfit", + ${COLUMN_MAPPINGS.totalMarkupProfit} as "totalMarkupProfit", + ${COLUMN_MAPPINGS.totalReferralProfit} as "totalReferralProfit", + ${COLUMN_MAPPINGS.totalReferralCodes} as "totalReferralCodes", + ${COLUMN_MAPPINGS.totalUsers} as "totalUsers" FROM "echo_apps" a INNER JOIN "app_memberships" owner_am ON a.id = owner_am."echoAppId" INNER JOIN "users" creator ON owner_am."userId" = creator.id - LEFT JOIN "app_memberships" am_users ON a.id = am_users."echoAppId" - LEFT JOIN "transactions" t ON a.id = t."echoAppId" - LEFT JOIN "outbound_emails_sent" app_oes ON a.id = app_oes."echoAppId" - LEFT JOIN "outbound_emails_sent" owner_oes ON creator.id = owner_oes."userId" - -- Referral codes are linked via app_memberships.referrerId in the new schema + LEFT JOIN txn ON txn.app_id = a.id + LEFT JOIN membership ON membership.app_id = a.id + LEFT JOIN app_emails ON app_emails.app_id = a.id ${whereClause} - GROUP BY a.id, a.name, a.description, a."createdAt", a."updatedAt", creator.id, creator.name, creator.email + GROUP BY a.id, a.name, a.description, a."createdAt", a."updatedAt", creator.id, creator.name, creator.email, app_emails.app_email_campaigns, txn.total_transactions, txn.total_revenue, txn.total_app_profit, txn.total_markup_profit, txn.total_referral_profit, membership.total_referral_codes, membership.total_users ${havingClause} ${orderByClause} LIMIT $${parameters.length + 1} @@ -162,16 +183,35 @@ export const getAppEarningsWithPagination = async ( // Build count query with same filters const countQuery = ` - SELECT COUNT(DISTINCT a.id) as count - FROM "echo_apps" a - INNER JOIN "app_memberships" owner_am ON a.id = owner_am."echoAppId" - INNER JOIN "users" creator ON owner_am."userId" = creator.id - LEFT JOIN "app_memberships" am_users ON a.id = am_users."echoAppId" - LEFT JOIN "transactions" t ON a.id = t."echoAppId" - LEFT JOIN "outbound_emails_sent" app_oes ON a.id = app_oes."echoAppId" - LEFT JOIN "outbound_emails_sent" owner_oes ON creator.id = owner_oes."userId" - ${whereClause} - ${havingClause ? `GROUP BY a.id ${havingClause}` : ''} + WITH txn AS ( + SELECT + t."echoAppId" AS app_id, + COUNT(t.id)::double precision AS total_transactions, + COALESCE(SUM(t."totalCost"), 0)::double precision AS total_revenue, + COALESCE(SUM(t."appProfit"), 0)::double precision AS total_app_profit, + COALESCE(SUM(t."markUpProfit"), 0)::double precision AS total_markup_profit, + COALESCE(SUM(t."referralProfit"), 0)::double precision AS total_referral_profit + FROM "transactions" t + GROUP BY t."echoAppId" + ), membership AS ( + SELECT + am."echoAppId" AS app_id, + COALESCE(COUNT(DISTINCT am."userId"), 0)::double precision AS total_users, + COALESCE(COUNT(DISTINCT am."referrerId") FILTER (WHERE am."referrerId" IS NOT NULL), 0)::double precision AS total_referral_codes + FROM "app_memberships" am + GROUP BY am."echoAppId" + ) + SELECT COUNT(*) as count FROM ( + SELECT a.id + FROM "echo_apps" a + INNER JOIN "app_memberships" owner_am ON a.id = owner_am."echoAppId" + INNER JOIN "users" creator ON owner_am."userId" = creator.id + LEFT JOIN txn ON txn.app_id = a.id + LEFT JOIN membership ON membership.app_id = a.id + ${whereClause} + GROUP BY a.id, a.name, a.description, a."createdAt", a."updatedAt", creator.id, creator.name, creator.email, txn.total_transactions, txn.total_revenue, txn.total_app_profit, txn.total_markup_profit, txn.total_referral_profit, membership.total_referral_codes, membership.total_users + ${havingClause} + ) grouped `; // If we have HAVING clauses, we need to count the grouped results diff --git a/packages/app/control/src/services/admin/payments-summary.ts b/packages/app/control/src/services/admin/payments-summary.ts new file mode 100644 index 000000000..43d8bd788 --- /dev/null +++ b/packages/app/control/src/services/admin/payments-summary.ts @@ -0,0 +1,189 @@ +import { db } from '@/lib/db'; +import { OverviewMetricConfig } from './type/overview-metric'; + +function percentChange(currentValue: number | bigint, previousValue: number | bigint): number { + const currentNumber = Number(currentValue) || 0; + const previousNumber = Number(previousValue) || 0; + if (previousNumber === 0) return currentNumber ? 100 : 0; + const delta = currentNumber - previousNumber; + return (delta / previousNumber) * 100; +} + +type PaymentsOverviewRow = { + totalAmount: number; + stripeAmount: number; + adminAmount: number; + signUpGiftAmount: number; + uniquePayers: number; +}; + +type PaymentsTrendRow = { + total_current: number; + total_prev: number; + stripe_current: number; + stripe_prev: number; + admin_current: number; + admin_prev: number; + signup_current: number; + signup_prev: number; + unique_payers_current: number; + unique_payers_prev: number; +}; + +export async function getPaymentsOverviewMetrics(): Promise { + const summaryQuery = ` + WITH p AS ( + SELECT + COALESCE(SUM(p.amount), 0)::double precision AS total_amount, + COALESCE(SUM(CASE WHEN p.source = 'stripe' and p.status = 'completed' THEN p.amount END), 0)::double precision AS stripe_amount, + COALESCE(SUM(CASE WHEN p.source = 'admin' and p.status = 'completed' THEN p.amount END), 0)::double precision AS admin_amount, + COALESCE(SUM(CASE WHEN p.source = 'signUpGift' and p.status = 'completed' THEN p.amount END), 0)::double precision AS signup_amount, + COALESCE(COUNT(DISTINCT CASE WHEN p.status = 'completed' AND p.source = 'stripe' THEN p."userId" END), 0)::double precision AS unique_payers + FROM "payments" p + WHERE p."isArchived" = false + ) + SELECT + p.total_amount AS "totalAmount", + p.stripe_amount AS "stripeAmount", + p.admin_amount AS "adminAmount", + p.signup_amount AS "signUpGiftAmount", + p.unique_payers AS "uniquePayers" + FROM p; + `; + + const summary = (await db.$queryRawUnsafe(summaryQuery)) as Array; + const s = summary[0] || { + totalAmount: 0, + stripeAmount: 0, + adminAmount: 0, + signUpGiftAmount: 0, + uniquePayers: 0, + }; + + const trendQuery = ` + WITH ranges AS ( + SELECT + (NOW()::date - INTERVAL '7 days') AS start_current, + NOW()::date AS end_current, + (NOW()::date - INTERVAL '14 days') AS start_prev, + (NOW()::date - INTERVAL '7 days') AS end_prev + ), t AS ( + SELECT + COALESCE(SUM(CASE WHEN p."createdAt" >= r.start_current AND p."createdAt" < r.end_current THEN p.amount END), 0)::double precision AS total_current, + COALESCE(SUM(CASE WHEN p."createdAt" >= r.start_prev AND p."createdAt" < r.end_prev THEN p.amount END), 0)::double precision AS total_prev, + COALESCE(SUM(CASE WHEN p.source = 'stripe' AND p."createdAt" >= r.start_current AND p."createdAt" < r.end_current THEN p.amount END), 0)::double precision AS stripe_current, + COALESCE(SUM(CASE WHEN p.source = 'stripe' AND p."createdAt" >= r.start_prev AND p."createdAt" < r.end_prev THEN p.amount END), 0)::double precision AS stripe_prev, + COALESCE(SUM(CASE WHEN p.source = 'admin' AND p."createdAt" >= r.start_current AND p."createdAt" < r.end_current THEN p.amount END), 0)::double precision AS admin_current, + COALESCE(SUM(CASE WHEN p.source = 'admin' AND p."createdAt" >= r.start_prev AND p."createdAt" < r.end_prev THEN p.amount END), 0)::double precision AS admin_prev, + COALESCE(SUM(CASE WHEN p.source = 'signUpGift' AND p."createdAt" >= r.start_current AND p."createdAt" < r.end_current THEN p.amount END), 0)::double precision AS signup_current, + COALESCE(SUM(CASE WHEN p.source = 'signUpGift' AND p."createdAt" >= r.start_prev AND p."createdAt" < r.end_prev THEN p.amount END), 0)::double precision AS signup_prev + FROM "payments" p, ranges r + WHERE p."isArchived" = false + ), u AS ( + SELECT + COALESCE(COUNT(DISTINCT CASE WHEN p.source = 'stripe' AND p.status = 'completed' AND p."createdAt" >= r.start_current AND p."createdAt" < r.end_current THEN p."userId" END), 0)::double precision AS unique_payers_current, + COALESCE(COUNT(DISTINCT CASE WHEN p.source = 'stripe' AND p.status = 'completed' AND p."createdAt" >= r.start_prev AND p."createdAt" < r.end_prev THEN p."userId" END), 0)::double precision AS unique_payers_prev + FROM "payments" p, ranges r + WHERE p."isArchived" = false + ) + SELECT + t.total_current, + t.total_prev, + t.stripe_current, + t.stripe_prev, + t.admin_current, + t.admin_prev, + t.signup_current, + t.signup_prev, + u.unique_payers_current, + u.unique_payers_prev + FROM t, u; + `; + + const trendRows = (await db.$queryRawUnsafe(trendQuery)) as Array; + const t = trendRows[0] || { + total_current: 0, + total_prev: 0, + stripe_current: 0, + stripe_prev: 0, + admin_current: 0, + admin_prev: 0, + signup_current: 0, + signup_prev: 0, + unique_payers_current: 0, + unique_payers_prev: 0, + }; + + const trendLabel = 'vs previous 7d'; + + const stripeFees = s.stripeAmount * 0.029; + const stripeFeesCurrent = t.stripe_current * 0.029; + const stripeFeesPrev = t.stripe_prev * 0.029; + + const metrics: OverviewMetricConfig[] = [ + { + id: 'totalPaymentVolume', + title: 'Total Payment Volume', + description: 'Total amount paid into the platform (all sources)', + displayType: 'currency', + value: s.totalAmount, + trendValue: percentChange(t.total_current, t.total_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'stripePayments', + title: 'Stripe Payments', + description: 'Total amount of payments from Stripe', + displayType: 'currency', + value: s.stripeAmount, + trendValue: percentChange(t.stripe_current, t.stripe_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'adminCredits', + title: 'Admin Credits', + description: 'Total amount of payments from Admin', + displayType: 'currency', + value: s.adminAmount, + trendValue: percentChange(t.admin_current, t.admin_prev), + size: 'sm', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'signupGifts', + title: 'Sign-up Gifts', + description: 'Total amount of payments from Sign-up Gift', + displayType: 'currency', + value: s.signUpGiftAmount, + trendValue: percentChange(t.signup_current, t.signup_prev), + size: 'sm', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'stripeFees', + title: 'Stripe Fees (2.9%)', + description: 'Estimated Stripe processing fees on Stripe payments', + displayType: 'currency', + value: stripeFees, + trendValue: percentChange(stripeFeesCurrent, stripeFeesPrev), + size: 'sm', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'uniquePayers', + title: 'Unique Payers', + description: 'Distinct users who made at least one payment', + displayType: 'number', + value: s.uniquePayers, + trendValue: percentChange(t.unique_payers_current, t.unique_payers_prev), + size: 'sm', + format: { showTrend: true, trendLabel }, + }, + ]; + + return metrics; +} + + diff --git a/packages/app/control/src/services/admin/user-earning-summary.ts b/packages/app/control/src/services/admin/user-earning-summary.ts new file mode 100644 index 000000000..d036a3a01 --- /dev/null +++ b/packages/app/control/src/services/admin/user-earning-summary.ts @@ -0,0 +1,209 @@ +import { db } from '@/lib/db'; +import { OverviewMetricConfig } from './type/overview-metric'; + +function percentChange(currentValue: number | bigint, previousValue: number | bigint): number { + const currentNumber = Number(currentValue) || 0; + const previousNumber = Number(previousValue) || 0; + if (previousNumber === 0) return currentNumber ? 100 : 0; + const delta = currentNumber - previousNumber; + return (delta / previousNumber) * 100; +} + +type EarningsOverviewRow = { + totalRevenue: number; + totalAppProfit: number; + totalMarkupProfit: number; + totalReferralProfit: number; + totalCompletedPayouts: number; + earningUsers: number; +}; + +type EarningsTrendRow = { + revenue_current: number; + revenue_prev: number; + app_profit_current: number; + app_profit_prev: number; + markup_profit_current: number; + markup_profit_prev: number; + referral_profit_current: number; + referral_profit_prev: number; + payouts_current: number; + payouts_prev: number; + earning_users_current: number; + earning_users_prev: number; +}; + +export async function getUserEarningsOverviewMetrics(): Promise { + const summaryQuery = ` + WITH t AS ( + SELECT + COALESCE(SUM(t."totalCost"), 0)::double precision AS total_revenue, + COALESCE(SUM(t."appProfit"), 0)::double precision AS total_app_profit, + COALESCE(SUM(t."markUpProfit"), 0)::double precision AS total_markup_profit, + COALESCE(SUM(t."referralProfit"), 0)::double precision AS total_referral_profit, + 0 AS earning_users -- placeholder; real value in eu CTE + FROM "transactions" t + ), p AS ( + SELECT COALESCE(SUM(p."amount") FILTER (WHERE p."status" = 'completed'), 0)::double precision AS total_completed_payouts + FROM "payouts" p + ), eu AS ( + SELECT COUNT(DISTINCT am."userId") AS earning_users + FROM "app_memberships" am + JOIN "transactions" t2 ON t2."echoAppId" = am."echoAppId" + WHERE am.role = 'owner' + ) + SELECT + t.total_revenue AS "totalRevenue", + t.total_app_profit AS "totalAppProfit", + t.total_markup_profit AS "totalMarkupProfit", + t.total_referral_profit AS "totalReferralProfit", + p.total_completed_payouts AS "totalCompletedPayouts", + eu.earning_users AS "earningUsers" + FROM t, p, eu; + `; + + const summary = (await db.$queryRawUnsafe(summaryQuery)) as Array; + const s = summary[0] || { + totalRevenue: 0, + totalAppProfit: 0, + totalMarkupProfit: 0, + totalReferralProfit: 0, + totalCompletedPayouts: 0, + earningUsers: 0, + }; + + const trendQuery = ` + WITH ranges AS ( + SELECT + (NOW()::date - INTERVAL '7 days') AS start_current, + NOW()::date AS end_current, + (NOW()::date - INTERVAL '14 days') AS start_prev, + (NOW()::date - INTERVAL '7 days') AS end_prev + ), + txn AS ( + SELECT + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."totalCost" END), 0)::double precision AS revenue_current, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."totalCost" END), 0)::double precision AS revenue_prev, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."appProfit" END), 0)::double precision AS app_profit_current, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."appProfit" END), 0)::double precision AS app_profit_prev, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."markUpProfit" END), 0)::double precision AS markup_profit_current, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."markUpProfit" END), 0)::double precision AS markup_profit_prev, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."referralProfit" END), 0)::double precision AS referral_profit_current, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."referralProfit" END), 0)::double precision AS referral_profit_prev + FROM "transactions" t, ranges r + ), + payouts AS ( + SELECT + COALESCE(SUM(CASE WHEN p."status" = 'completed' AND p."createdAt" >= r.start_current AND p."createdAt" < r.end_current THEN p."amount" END), 0)::double precision AS payouts_current, + COALESCE(SUM(CASE WHEN p."status" = 'completed' AND p."createdAt" >= r.start_prev AND p."createdAt" < r.end_prev THEN p."amount" END), 0)::double precision AS payouts_prev + FROM "payouts" p, ranges r + ), + earning_users AS ( + SELECT + COALESCE(COUNT(DISTINCT CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN am."userId" END), 0)::double precision AS earning_users_current, + COALESCE(COUNT(DISTINCT CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN am."userId" END), 0)::double precision AS earning_users_prev + FROM "transactions" t + JOIN "app_memberships" am ON am."echoAppId" = t."echoAppId" AND am.role = 'owner', + ranges r + ) + SELECT + txn.revenue_current, + txn.revenue_prev, + txn.app_profit_current, + txn.app_profit_prev, + txn.markup_profit_current, + txn.markup_profit_prev, + txn.referral_profit_current, + txn.referral_profit_prev, + payouts.payouts_current, + payouts.payouts_prev, + earning_users.earning_users_current, + earning_users.earning_users_prev + FROM txn, payouts, earning_users; + `; + + const trendRows = (await db.$queryRawUnsafe(trendQuery)) as Array; + const t = trendRows[0] || { + revenue_current: 0, + revenue_prev: 0, + app_profit_current: 0, + app_profit_prev: 0, + markup_profit_current: 0, + markup_profit_prev: 0, + referral_profit_current: 0, + referral_profit_prev: 0, + payouts_current: 0, + payouts_prev: 0, + earning_users_current: 0, + earning_users_prev: 0, + }; + + const trendLabel = 'vs previous 7d'; + + const metrics: OverviewMetricConfig[] = [ + { + id: 'totalRevenue', + title: 'Total Revenue', + description: 'Total revenue generated by users', + displayType: 'currency', + value: s.totalRevenue, + trendValue: percentChange(t.revenue_current, t.revenue_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'totalAppProfit', + title: 'App Profit', + description: 'Total profit from app usage', + displayType: 'currency', + value: s.totalAppProfit, + trendValue: percentChange(t.app_profit_current, t.app_profit_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'totalMarkupProfit', + title: 'Markup Profit', + description: 'Profit from markup fees', + displayType: 'currency', + value: s.totalMarkupProfit, + trendValue: percentChange(t.markup_profit_current, t.markup_profit_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'totalReferralProfit', + title: 'Referral Profit', + description: 'Profit from referrals', + displayType: 'currency', + value: s.totalReferralProfit, + trendValue: percentChange(t.referral_profit_current, t.referral_profit_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'totalCompletedPayouts', + title: 'Completed Payouts', + description: 'Total amount paid out to users', + displayType: 'currency', + value: s.totalCompletedPayouts, + trendValue: percentChange(t.payouts_current, t.payouts_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'earningUsers', + title: 'Earning Users', + description: 'Distinct owners with at least one app that has a transaction', + displayType: 'number', + value: s.earningUsers, + trendValue: percentChange(t.earning_users_current, t.earning_users_prev), + size: 'sm', + format: { showTrend: true, trendLabel }, + }, + ]; + + return metrics; +} + + diff --git a/packages/app/control/src/services/admin/user-earnings.ts b/packages/app/control/src/services/admin/user-earnings.ts index a56eae715..69a31ffcd 100644 --- a/packages/app/control/src/services/admin/user-earnings.ts +++ b/packages/app/control/src/services/admin/user-earnings.ts @@ -187,4 +187,4 @@ export const getUserEarningsWithPagination = async ( page_size: params.page_size, total_count: Number(totalCount[0].count), }); -}; +}; \ No newline at end of file diff --git a/packages/app/control/src/services/admin/user-spending-summary.ts b/packages/app/control/src/services/admin/user-spending-summary.ts new file mode 100644 index 000000000..78aa58288 --- /dev/null +++ b/packages/app/control/src/services/admin/user-spending-summary.ts @@ -0,0 +1,197 @@ +import { db } from '@/lib/db'; +import { OverviewMetricConfig } from './type/overview-metric'; + +function percentChange(currentValue: number | bigint, previousValue: number | bigint): number { + const currentNumber = Number(currentValue) || 0; + const previousNumber = Number(previousValue) || 0; + if (previousNumber === 0) return currentNumber ? 100 : 0; + const delta = currentNumber - previousNumber; + return (delta / previousNumber) * 100; +} + +type SpendingOverviewRow = { + totalSpend: number; + totalPayments: number; + totalFreeTierUsage: number; + totalOutstandingBalance: number; + spendingUsers: number; +}; + +type SpendingTrendRow = { + spend_current: number; + spend_prev: number; + payments_current: number; + payments_prev: number; + free_tier_current: number; + free_tier_prev: number; + spending_users_current: number; + spending_users_prev: number; +}; + +export async function getUserSpendingOverviewMetrics(): Promise { + const summaryQuery = ` + WITH t AS ( + SELECT + COALESCE(SUM(t."totalCost"), 0)::double precision AS total_spend, + COALESCE(SUM(CASE WHEN t."spendPoolId" IS NOT NULL THEN t."totalCost" END), 0)::double precision AS total_free_tier_usage + FROM "transactions" t + ), p AS ( + SELECT COALESCE(SUM(p."amount"), 0)::double precision AS total_payments + FROM "payments" p + WHERE p."isArchived" = false + AND p."status" = 'completed' + AND p."source" = 'stripe' + ), b AS ( + SELECT COALESCE(SUM(u."totalPaid" - u."totalSpent"), 0)::double precision AS total_outstanding_balance + FROM "users" u + ), su AS ( + SELECT COUNT(DISTINCT t2."userId")::double precision AS spending_users + FROM "transactions" t2 + ) + SELECT + t.total_spend AS "totalSpend", + p.total_payments AS "totalPayments", + t.total_free_tier_usage AS "totalFreeTierUsage", + b.total_outstanding_balance AS "totalOutstandingBalance", + su.spending_users AS "spendingUsers" + FROM t, p, b, su; + `; + + const summary = (await db.$queryRawUnsafe(summaryQuery)) as Array; + const s = summary[0] || { + totalSpend: 0, + totalPayments: 0, + totalFreeTierUsage: 0, + totalOutstandingBalance: 0, + spendingUsers: 0, + }; + + const trendQuery = ` + WITH ranges AS ( + SELECT + (NOW()::date - INTERVAL '7 days') AS start_current, + NOW()::date AS end_current, + (NOW()::date - INTERVAL '14 days') AS start_prev, + (NOW()::date - INTERVAL '7 days') AS end_prev + ), + txn AS ( + SELECT + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."totalCost" END), 0)::double precision AS spend_current, + COALESCE(SUM(CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."totalCost" END), 0)::double precision AS spend_prev, + COALESCE(SUM(CASE WHEN t."spendPoolId" IS NOT NULL AND t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."totalCost" END), 0)::double precision AS free_tier_current, + COALESCE(SUM(CASE WHEN t."spendPoolId" IS NOT NULL AND t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."totalCost" END), 0)::double precision AS free_tier_prev + FROM "transactions" t, ranges r + ), + pay AS ( + SELECT + COALESCE(SUM(CASE WHEN p."createdAt" >= r.start_current AND p."createdAt" < r.end_current THEN p."amount" END), 0)::double precision AS payments_current, + COALESCE(SUM(CASE WHEN p."createdAt" >= r.start_prev AND p."createdAt" < r.end_prev THEN p."amount" END), 0)::double precision AS payments_prev + FROM "payments" p, ranges r + WHERE p."isArchived" = false + AND p."status" = 'completed' + AND p."source" = 'stripe' + ), + su AS ( + SELECT + COALESCE(COUNT(DISTINCT CASE WHEN t."createdAt" >= r.start_current AND t."createdAt" < r.end_current THEN t."userId" END), 0)::double precision AS spending_users_current, + COALESCE(COUNT(DISTINCT CASE WHEN t."createdAt" >= r.start_prev AND t."createdAt" < r.end_prev THEN t."userId" END), 0)::double precision AS spending_users_prev + FROM "transactions" t, ranges r + ) + SELECT + txn.spend_current, + txn.spend_prev, + pay.payments_current, + pay.payments_prev, + txn.free_tier_current, + txn.free_tier_prev, + su.spending_users_current, + su.spending_users_prev + FROM txn, pay, su; + `; + + const trendRows = (await db.$queryRawUnsafe(trendQuery)) as Array; + const t = trendRows[0] || { + spend_current: 0, + spend_prev: 0, + payments_current: 0, + payments_prev: 0, + free_tier_current: 0, + free_tier_prev: 0, + spending_users_current: 0, + spending_users_prev: 0, + }; + + const trendLabel = 'vs previous 7d'; + + const avgCurrent = t.spending_users_current ? t.spend_current / t.spending_users_current : 0; + const avgPrev = t.spending_users_prev ? t.spend_prev / t.spending_users_prev : 0; + const netBalanceDeltaCurrent = t.payments_current - t.spend_current; + const netBalanceDeltaPrev = t.payments_prev - t.spend_prev; + + const metrics: OverviewMetricConfig[] = [ + { + id: 'totalSpend', + title: 'Total User Spend', + description: 'Total amount users have spent via transactions', + displayType: 'currency', + value: s.totalSpend, + trendValue: percentChange(t.spend_current, t.spend_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'totalPayments', + title: 'Total Payments', + description: 'Total payments received from users', + displayType: 'currency', + value: s.totalPayments, + trendValue: percentChange(t.payments_current, t.payments_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'totalFreeTierUsage', + title: 'Free Tier Usage', + description: 'Total consumption covered by spend pools', + displayType: 'currency', + value: s.totalFreeTierUsage, + trendValue: percentChange(t.free_tier_current, t.free_tier_prev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'spendingUsers', + title: 'Spending Users', + description: 'Distinct users with at least one transaction', + displayType: 'number', + value: s.spendingUsers, + trendValue: percentChange(t.spending_users_current, t.spending_users_prev), + size: 'sm', + format: { showTrend: true, trendLabel }, + }, + { + id: 'avgSpendPerUser', + title: 'Avg Spend per User', + description: 'Average spend per spending user', + displayType: 'currency', + value: s.spendingUsers ? s.totalSpend / s.spendingUsers : 0, + trendValue: percentChange(avgCurrent, avgPrev), + size: 'sm', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + { + id: 'totalOutstandingBalance', + title: 'Outstanding Balance', + description: 'Sum of user balances (paid minus spent)', + displayType: 'currency', + value: s.totalOutstandingBalance, + trendValue: percentChange(netBalanceDeltaCurrent, netBalanceDeltaPrev), + size: 'md', + format: { showTrend: true, decimals: 2, trendLabel }, + }, + ]; + + return metrics; +} + + diff --git a/packages/app/control/src/services/email/emailer.ts b/packages/app/control/src/services/email/emailer.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/app/control/src/trpc/routers/admin/earnings.ts b/packages/app/control/src/trpc/routers/admin/earnings.ts index 061f2bb4e..b63d25d64 100644 --- a/packages/app/control/src/trpc/routers/admin/earnings.ts +++ b/packages/app/control/src/trpc/routers/admin/earnings.ts @@ -2,6 +2,8 @@ import { adminProcedure, createTRPCRouter } from '../../trpc'; import { getUserEarningsWithPagination } from '@/services/admin/user-earnings'; import { getAppEarningsWithPagination } from '@/services/admin/app-earnings'; +import { getUserEarningsOverviewMetrics } from '@/services/admin/user-earning-summary'; +import { getAppEarningsOverviewMetrics } from '@/services/admin/app-earnings-summary'; import { paginationParamsSchema } from '@/services/lib/pagination'; import { multiSortParamsSchema } from '@/services/lib/sorting'; import { filterParamsSchema } from '@/services/lib/filtering'; @@ -26,4 +28,12 @@ export const adminEarningsRouter = createTRPCRouter({ .query(async ({ input }) => { return await getAppEarningsWithPagination(input); }), + + getUserEarningsOverviewMetrics: adminProcedure.query(async () => { + return await getUserEarningsOverviewMetrics(); + }), + + getAppEarningsOverviewMetrics: adminProcedure.query(async () => { + return await getAppEarningsOverviewMetrics(); + }), }); diff --git a/packages/app/control/src/trpc/routers/admin/payments.ts b/packages/app/control/src/trpc/routers/admin/payments.ts index 2e3c46c4b..d682a5625 100644 --- a/packages/app/control/src/trpc/routers/admin/payments.ts +++ b/packages/app/control/src/trpc/routers/admin/payments.ts @@ -1,6 +1,7 @@ import { adminProcedure, createTRPCRouter } from '../../trpc'; import { getPaymentsWithPagination } from '@/services/admin/payments'; +import { getPaymentsOverviewMetrics } from '@/services/admin/payments-summary'; import { paginationParamsSchema } from '@/services/lib/pagination'; import { multiSortParamsSchema } from '@/services/lib/sorting'; import { filterParamsSchema } from '@/services/lib/filtering'; @@ -15,4 +16,8 @@ export const adminPaymentsRouter = createTRPCRouter({ .query(async ({ input }) => { return await getPaymentsWithPagination(input); }), + + getPaymentsOverviewMetrics: adminProcedure.query(async () => { + return await getPaymentsOverviewMetrics(); + }), }); diff --git a/packages/app/control/src/trpc/routers/admin/spending.ts b/packages/app/control/src/trpc/routers/admin/spending.ts index 72464433b..e959299b8 100644 --- a/packages/app/control/src/trpc/routers/admin/spending.ts +++ b/packages/app/control/src/trpc/routers/admin/spending.ts @@ -1,6 +1,7 @@ import { adminProcedure, createTRPCRouter } from '../../trpc'; import { getUserSpendingWithPagination } from '@/services/admin/user-spending'; +import { getUserSpendingOverviewMetrics } from '@/services/admin/user-spending-summary'; import { paginationParamsSchema } from '@/services/lib/pagination'; import { multiSortParamsSchema } from '@/services/lib/sorting'; import { filterParamsSchema } from '@/services/lib/filtering'; @@ -15,4 +16,8 @@ export const adminSpendingRouter = createTRPCRouter({ .query(async ({ input }) => { return await getUserSpendingWithPagination(input); }), + + getUserSpendingOverviewMetrics: adminProcedure.query(async () => { + return await getUserSpendingOverviewMetrics(); + }), });