All files / src/app/api/user/affiliate/clicks route.ts

0% Statements 0/134
100% Branches 0/0
0% Functions 0/1
0% Lines 0/134

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135                                                                                                                                                                                                                                                                             
export const dynamic = "force-dynamic";

import { NextRequest, NextResponse } from 'next/server';
import { Session } from "next-auth";
import { prisma } from "@/lib/prisma";
import {
  withUser,
  withErrorHandling,
  successResponse,
  ApiError,
  ApiSuccessResponse,
  ApiErrorResponse } from "@/lib/api";
import { RouteContext } from "@/lib/api/middleware";
import type { AuthenticatedUser } from '@/lib/api/middleware/types';

/**
 * GET /api/user/affiliate/clicks
 * Get current user's affiliate click statistics
 */
async function handleGet(
  request: NextRequest,
  _context: RouteContext | undefined,
  _session: Session,
  user: AuthenticatedUser
): Promise<NextResponse<ApiSuccessResponse<unknown> | ApiErrorResponse>> {
  // Get user's affiliate account
  const affiliate = await prisma.affiliate.findUnique({
    where: { userId: user.id }});

  if (!affiliate) {
    throw ApiError.notFound("Affiliate account");
  }

  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get("page") || "1");
  const limit = parseInt(searchParams.get("limit") || "50");
  const period = searchParams.get("period") || "30d";

  const skip = (page - 1) * limit;

  // Calculate date range
  let startDate: Date;
  const endDate = new Date();

  switch (period) {
    case "7d":
      startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
      break;
    case "30d":
      startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
      break;
    case "90d":
      startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
      break;
    default:
      startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
  }

  const where = {
    affiliateId: affiliate.id,
    clickedAt: { gte: startDate, lte: endDate }};

  const [clicks, total, clicksByDay] = await Promise.all([
    // Recent clicks
    prisma.affiliateClick.findMany({
      where,
      skip,
      take: limit,
      orderBy: { clickedAt: "desc" },
      select: {
        id: true,
        visitorId: true,
        ipAddress: true,
        userAgent: true,
        referer: true,
        landingPage: true,
        convertedToSale: true,
        clickedAt: true}}),
    prisma.affiliateClick.count({ where }),
    // Daily click aggregation using raw query
    prisma.$queryRaw<{ date: string; count: bigint }[]>`
      SELECT
        DATE(clicked_at) as date,
        COUNT(*) as count
      FROM affiliate_clicks
      WHERE affiliate_id = ${affiliate.id}
        AND clicked_at >= ${startDate}
        AND clicked_at <= ${endDate}
      GROUP BY DATE(clicked_at)
      ORDER BY date DESC
    `,
  ]);

  // Get conversion stats
  const conversions = await prisma.affiliateClick.count({
    where: {
      ...where,
      convertedToSale: true}});

  return successResponse({
    clicks: clicks.map((click) => ({
      ...click,
      ipAddress: click.ipAddress ? maskIpAddress(click.ipAddress) : null})),
    stats: {
      totalClicks: total,
      conversions,
      conversionRate: total > 0 ? (conversions / total) * 100 : 0},
    dailyStats: clicksByDay.map((d) => ({
      date: d.date,
      count: Number(d.count)})),
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)}});
}

/**
 * Mask IP address for privacy (show only first two octets)
 */
function maskIpAddress(ip: string): string {
  const parts = ip.split(".");
  if (parts.length === 4) {
    return `${parts[0]}.${parts[1]}.*.*`;
  }
  // For IPv6, just show first part
  if (ip.includes(":")) {
    const v6parts = ip.split(":");
    return `${v6parts[0]}:${v6parts[1]}:****`;
  }
  return ip;
}

export const GET = withErrorHandling(withUser(handleGet));