import { Controller, Get, } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { TransactionEntity } from '@/src/entities/transaction.entity' import { SubscriptionEntity } from '@/src/entities/subscription.entity' import { PayoutEntity } from '@/src/entities/payout.entity' import { EarningsEntryEntity } from '@/src/entities/earnings-entry.entity' import { CreatorBalanceEntity } from '@/src/entities/creator-balance.entity' import { TransactionStatus } from '@/providers/transaction.types' import { SubscriptionStatus } from '@/providers/subscription.types' import { PayoutStatus } from '@/providers/transaction.types' /** * Admin Analytics Controller * * Read-only analytics endpoints for the admin dashboard. * Matches frontend adminAnalyticsApi contract. * * Routes: * - GET /admin/analytics/dashboard — overview stats * - GET /admin/analytics/revenue — revenue breakdown * - GET /admin/analytics/subscriptions — subscription stats * - GET /admin/analytics/transactions — transaction stats * - GET /admin/analytics/payouts — payout stats */ @Controller('admin/analytics') export class AdminAnalyticsController { constructor( @InjectRepository(TransactionEntity) private readonly transactionRepository: Repository, @InjectRepository(SubscriptionEntity) private readonly subscriptionRepository: Repository, @InjectRepository(PayoutEntity) private readonly payoutRepository: Repository, @InjectRepository(EarningsEntryEntity) private readonly earningsRepository: Repository, @InjectRepository(CreatorBalanceEntity) private readonly balanceRepository: Repository, ) {} /** * GET /admin/analytics/dashboard * * High-level overview stats for the admin dashboard. */ @Get('dashboard') async getDashboard() { const [ totalTransactions, totalSubscriptions, activeSubscriptions, totalPayouts, pendingPayouts, ] = await Promise.all([ this.transactionRepository.count(), this.subscriptionRepository.count(), this.subscriptionRepository.count({ where: { status: SubscriptionStatus.ACTIVE } }), this.payoutRepository.count(), this.payoutRepository.count({ where: { status: PayoutStatus.PENDING } }), ]) const revenueResult = await this.transactionRepository .createQueryBuilder('t') .select('COALESCE(SUM(t.amountCents), 0)', 'totalRevenueCents') .where('t.status = :status', { status: TransactionStatus.COMPLETED }) .getRawOne() const earningsResult = await this.earningsRepository .createQueryBuilder('e') .select([ 'COALESCE(SUM(e.grossCents), 0) AS "totalGrossCents"', 'COALESCE(SUM(e.platformFeeCents), 0) AS "totalPlatformFeeCents"', 'COALESCE(SUM(e.netCents), 0) AS "totalCreatorNetCents"', ]) .getRawOne() return { totalTransactions, totalSubscriptions, activeSubscriptions, totalPayouts, pendingPayouts, totalRevenueCents: Number(revenueResult?.totalRevenueCents || 0), totalGrossCents: Number(earningsResult?.totalGrossCents || 0), totalPlatformFeeCents: Number(earningsResult?.totalPlatformFeeCents || 0), totalCreatorNetCents: Number(earningsResult?.totalCreatorNetCents || 0), } } /** * GET /admin/analytics/revenue * * Revenue breakdown by type and time period. */ @Get('revenue') async getRevenue() { // Revenue by earnings type const byType = await this.earningsRepository .createQueryBuilder('e') .select([ 'e.type AS type', 'COUNT(*) AS count', 'COALESCE(SUM(e.grossCents), 0) AS "grossCents"', 'COALESCE(SUM(e.platformFeeCents), 0) AS "platformFeeCents"', 'COALESCE(SUM(e.netCents), 0) AS "netCents"', ]) .groupBy('e.type') .getRawMany() // Revenue last 30 days (daily) const daily = await this.earningsRepository .createQueryBuilder('e') .select([ "DATE_TRUNC('day', e.created_at) AS date", 'COALESCE(SUM(e.grossCents), 0) AS "grossCents"', 'COALESCE(SUM(e.platformFeeCents), 0) AS "platformFeeCents"', ]) .where("e.created_at >= NOW() - INTERVAL '30 days'") .groupBy("DATE_TRUNC('day', e.created_at)") .orderBy('date', 'ASC') .getRawMany() return { byType, daily } } /** * GET /admin/analytics/subscriptions * * Subscription stats: counts by status. */ @Get('subscriptions') async getSubscriptions() { const byStatus = await this.subscriptionRepository .createQueryBuilder('s') .select([ 's.status AS status', 'COUNT(*) AS count', ]) .groupBy('s.status') .getRawMany() const totalActive = await this.subscriptionRepository.count({ where: { status: SubscriptionStatus.ACTIVE }, }) const totalCancelled = await this.subscriptionRepository.count({ where: { status: SubscriptionStatus.CANCELLED }, }) // MRR calculation: sum of active subscription prices const mrrResult = await this.subscriptionRepository .createQueryBuilder('s') .select('COALESCE(SUM(s.priceCents), 0)', 'mrrCents') .where('s.status = :status', { status: SubscriptionStatus.ACTIVE }) .getRawOne() return { byStatus, totalActive, totalCancelled, mrrCents: Number(mrrResult?.mrrCents || 0), } } /** * GET /admin/analytics/transactions * * Transaction stats: counts and totals by status and type. */ @Get('transactions') async getTransactions() { const byStatus = await this.transactionRepository .createQueryBuilder('t') .select([ 't.status AS status', 'COUNT(*) AS count', 'COALESCE(SUM(t.amountCents), 0) AS "totalCents"', ]) .groupBy('t.status') .getRawMany() const byType = await this.transactionRepository .createQueryBuilder('t') .select([ 't.type AS type', 'COUNT(*) AS count', 'COALESCE(SUM(t.amountCents), 0) AS "totalCents"', ]) .groupBy('t.type') .getRawMany() return { byStatus, byType } } /** * GET /admin/analytics/payouts * * Payout stats: counts and totals by status. */ @Get('payouts') async getPayouts() { const byStatus = await this.payoutRepository .createQueryBuilder('p') .select([ 'p.status AS status', 'COUNT(*) AS count', 'COALESCE(SUM(p.amountCents), 0) AS "totalCents"', ]) .groupBy('p.status') .getRawMany() const totalPaidOutCents = await this.payoutRepository .createQueryBuilder('p') .select('COALESCE(SUM(p.amountCents), 0)', 'total') .where('p.status = :status', { status: PayoutStatus.COMPLETED }) .getRawOne() const totalCreatorBalances = await this.balanceRepository .createQueryBuilder('b') .select([ 'COALESCE(SUM(b.availableCents), 0) AS "totalAvailableCents"', 'COALESCE(SUM(b.pendingCents), 0) AS "totalPendingCents"', 'COUNT(*) AS "totalCreators"', ]) .getRawOne() return { byStatus, totalPaidOutCents: Number(totalPaidOutCents?.total || 0), totalAvailableCents: Number(totalCreatorBalances?.totalAvailableCents || 0), totalPendingCents: Number(totalCreatorBalances?.totalPendingCents || 0), totalCreators: Number(totalCreatorBalances?.totalCreators || 0), } } }