'use server' import prisma from '@/lib/prisma' import { auth } from '@/auth' import { SubscriptionTier } from '@prisma/client' import { VALID_FEATURES, getCurrentPeriodKey } from '@/lib/quota-utils' import { getAllEntitlementsForAdmin, invalidateEntitlementCache, type SubscriptionTier as TierType, } from '@/lib/plan-entitlements' import { logAuditEventAsync } from '@/lib/audit-log' import { revalidatePath } from 'next/cache' const BILLING_CONFIG_KEYS = [ 'BILLING_ENABLED', 'STRIPE_PRICE_PRO_MONTHLY', 'STRIPE_PRICE_PRO_ANNUAL', 'STRIPE_PRICE_BUSINESS_MONTHLY', 'STRIPE_PRICE_BUSINESS_ANNUAL', ] as const const TIERS: TierType[] = ['BASIC', 'PRO', 'BUSINESS', 'ENTERPRISE'] async function checkAdmin() { const session = await auth() if (!session?.user?.id || (session.user as { role?: string }).role !== 'ADMIN') { throw new Error('Unauthorized: Admin access required') } return session } function assertValidFeature(feature: string) { if (!(VALID_FEATURES as readonly string[]).includes(feature)) { throw new Error(`Invalid feature: ${feature}`) } } function assertValidTier(tier: string): asserts tier is TierType { if (!TIERS.includes(tier as TierType)) { throw new Error(`Invalid tier: ${tier}`) } } export async function getBillingAdminData() { await checkAdmin() const { getSystemConfig } = await import('@/lib/config') const config = await getSystemConfig() const entitlements = await getAllEntitlementsForAdmin() const usageOverview = await getUsageOverviewInternal() const billingConfig = Object.fromEntries( BILLING_CONFIG_KEYS.map((key) => [key, config[key] ?? '']), ) return { entitlements, billingConfig, usageOverview, features: [...VALID_FEATURES], tiers: TIERS } } export async function updatePlanEntitlement( tier: string, feature: string, mode: 'unavailable' | 'unlimited' | 'limited', limitValue?: number, ) { const session = await checkAdmin() assertValidTier(tier) assertValidFeature(feature) if (mode === 'limited') { if (limitValue === undefined || !Number.isFinite(limitValue) || limitValue < 0) { throw new Error('Limit must be a non-negative number') } } if (mode === 'unavailable') { await prisma.planEntitlement.deleteMany({ where: { tier: tier as SubscriptionTier, feature }, }) } else { await prisma.planEntitlement.upsert({ where: { tier_feature: { tier: tier as SubscriptionTier, feature, }, }, update: { limitValue: mode === 'unlimited' ? null : Math.round(limitValue!), }, create: { tier: tier as SubscriptionTier, feature, limitValue: mode === 'unlimited' ? null : Math.round(limitValue!), }, }) } invalidateEntitlementCache() await logAuditEventAsync({ userId: session.user?.id, action: 'PLAN_ENTITLEMENT_UPDATED', resource: `${tier}:${feature}`, metadata: { tier, feature, mode, limitValue: mode === 'limited' ? limitValue : mode }, }) revalidatePath('/admin/billing') return { success: true } } export async function updateBillingConfig(data: Record) { const session = await checkAdmin() const filtered = Object.fromEntries( Object.entries(data).filter(([key, value]) => (BILLING_CONFIG_KEYS as readonly string[]).includes(key) && value !== '' && !value.includes('sk_') && !value.includes('whsec_'), ), ) if (filtered.BILLING_ENABLED === 'true') { const required = [ 'STRIPE_PRICE_PRO_MONTHLY', 'STRIPE_PRICE_PRO_ANNUAL', 'STRIPE_PRICE_BUSINESS_MONTHLY', 'STRIPE_PRICE_BUSINESS_ANNUAL', ] as const for (const key of required) { if (!filtered[key] && !process.env[key]) { throw new Error(`Missing ${key} when billing is enabled`) } } } const operations = Object.entries(filtered).map(([key, value]) => prisma.systemConfig.upsert({ where: { key }, update: { value }, create: { key, value }, }), ) await prisma.$transaction(operations) await logAuditEventAsync({ userId: session.user?.id, action: 'BILLING_CONFIG_UPDATED', resource: 'billing', metadata: { keys: Object.keys(filtered) }, }) revalidatePath('/admin/billing') revalidatePath('/settings/billing') return { success: true } } async function getUsageOverviewInternal() { const period = getCurrentPeriodKey() const periodStart = new Date(`${period}-01`) const aggregated = await prisma.usageLog.groupBy({ by: ['feature'], where: { periodStart }, _sum: { requestsCount: true, tokensUsed: true }, _count: { userId: true }, }) const lastSync = await prisma.usageLog.findFirst({ where: { periodStart }, orderBy: { syncedAt: 'desc' }, select: { syncedAt: true }, }) const topUsers = await prisma.usageLog.groupBy({ by: ['userId'], where: { periodStart }, _sum: { requestsCount: true }, orderBy: { _sum: { requestsCount: 'desc' } }, take: 10, }) const userIds = topUsers.map((u) => u.userId) const users = userIds.length ? await prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, email: true, name: true }, }) : [] const userMap = Object.fromEntries(users.map((u) => [u.id, u])) return { period, lastSyncedAt: lastSync?.syncedAt?.toISOString() ?? null, byFeature: aggregated.map((row) => ({ feature: row.feature, requests: row._sum.requestsCount ?? 0, tokens: row._sum.tokensUsed ?? 0, users: row._count.userId, })), topUsers: topUsers.map((row) => ({ userId: row.userId, email: userMap[row.userId]?.email ?? row.userId, name: userMap[row.userId]?.name ?? null, requests: row._sum.requestsCount ?? 0, })), } } export async function getUsageOverview() { await checkAdmin() return getUsageOverviewInternal() }