import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { db } from '@/lib/db'; import { z } from 'zod'; // Validation schema for creating a membership const createMembershipSchema = z.object({ clientId: z.string().cuid('Invalid client ID'), planId: z.string().cuid('Invalid plan ID'), startDate: z.string().datetime({ message: 'Invalid start date format' }), endDate: z.string().datetime({ message: 'Invalid end date format' }), autoRenew: z.boolean().optional().default(false), notes: z.string().max(500).optional(), }); // GET /api/memberships - List all memberships with filters export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } const { searchParams } = new URL(request.url); const status = searchParams.get('status'); const planId = searchParams.get('planId'); const search = searchParams.get('search'); const expiringOnly = searchParams.get('expiring') === 'true'; const limit = searchParams.get('limit'); const offset = searchParams.get('offset'); // Build where clause - we need to get memberships from clients in the organization interface MembershipWhereClause { plan: { organizationId: string; }; status?: 'ACTIVE' | 'EXPIRED' | 'CANCELLED' | 'SUSPENDED'; planId?: string; endDate?: { gte?: Date; lte?: Date; }; client?: { OR?: Array<{ firstName?: { contains: string; mode: 'insensitive' }; lastName?: { contains: string; mode: 'insensitive' }; email?: { contains: string; mode: 'insensitive' }; }>; }; } const whereClause: MembershipWhereClause = { plan: { organizationId: session.user.organizationId, }, }; // Filter by status if (status) { const validStatuses = ['ACTIVE', 'EXPIRED', 'CANCELLED', 'SUSPENDED'] as const; const upperStatus = status.toUpperCase(); if (validStatuses.includes(upperStatus as typeof validStatuses[number])) { whereClause.status = upperStatus as typeof validStatuses[number]; } } // Filter by plan if (planId) { whereClause.planId = planId; } // Filter by expiring (within 7 days) if (expiringOnly) { const now = new Date(); const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); whereClause.status = 'ACTIVE'; whereClause.endDate = { gte: now, lte: sevenDaysFromNow, }; } // Search by client name or email if (search && search.trim().length > 0) { const searchTerm = search.trim(); whereClause.client = { OR: [ { firstName: { contains: searchTerm, mode: 'insensitive' } }, { lastName: { contains: searchTerm, mode: 'insensitive' } }, { email: { contains: searchTerm, mode: 'insensitive' } }, ], }; } // Pagination const take = limit ? Math.min(parseInt(limit, 10), 100) : 50; const skip = offset ? parseInt(offset, 10) : 0; // Get total count const totalCount = await db.membership.count({ where: whereClause, }); const memberships = await db.membership.findMany({ where: whereClause, include: { plan: { select: { id: true, name: true, price: true, durationMonths: true, courtHours: true, discountPercent: true, benefits: true, }, }, client: { select: { id: true, firstName: true, lastName: true, email: true, phone: true, }, }, }, orderBy: [ { status: 'asc' }, { endDate: 'asc' }, ], take, skip, }); // Add computed fields const now = new Date(); const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); const membershipsWithDetails = memberships.map((membership) => ({ ...membership, isExpiring: membership.status === 'ACTIVE' && membership.endDate <= sevenDaysFromNow, daysUntilExpiry: membership.status === 'ACTIVE' ? Math.ceil((membership.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) : null, benefitsSummary: { freeHours: membership.plan.courtHours || 0, hoursRemaining: membership.remainingHours || 0, bookingDiscount: membership.plan.discountPercent ? Number(membership.plan.discountPercent) : 0, extraBenefits: membership.plan.benefits || [], }, })); return NextResponse.json({ data: membershipsWithDetails, pagination: { total: totalCount, limit: take, offset: skip, hasMore: skip + take < totalCount, }, }); } catch (error) { console.error('Error fetching memberships:', error); return NextResponse.json( { error: 'Failed to fetch memberships' }, { status: 500 } ); } } // POST /api/memberships - Create membership for client export async function POST(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } // Check if user has appropriate role const allowedRoles = ['SUPER_ADMIN', 'ORG_ADMIN', 'SITE_ADMIN', 'RECEPTIONIST']; if (!allowedRoles.includes(session.user.role)) { return NextResponse.json( { error: 'Forbidden: Insufficient permissions' }, { status: 403 } ); } const body = await request.json(); // Validate input const validationResult = createMembershipSchema.safeParse(body); if (!validationResult.success) { return NextResponse.json( { error: 'Invalid membership data', details: validationResult.error.flatten().fieldErrors, }, { status: 400 } ); } const { clientId, planId, startDate, endDate, autoRenew, notes } = validationResult.data; // Verify client belongs to organization const client = await db.client.findFirst({ where: { id: clientId, organizationId: session.user.organizationId, }, }); if (!client) { return NextResponse.json( { error: 'Client not found' }, { status: 404 } ); } // Verify plan belongs to organization and is active const plan = await db.membershipPlan.findFirst({ where: { id: planId, organizationId: session.user.organizationId, isActive: true, }, }); if (!plan) { return NextResponse.json( { error: 'Membership plan not found or inactive' }, { status: 404 } ); } // Check if client already has an active membership const existingMembership = await db.membership.findFirst({ where: { clientId, status: 'ACTIVE', endDate: { gte: new Date(), }, }, }); if (existingMembership) { return NextResponse.json( { error: 'Client already has an active membership', existingMembershipId: existingMembership.id, expiresAt: existingMembership.endDate, }, { status: 409 } ); } // Create the membership const membership = await db.membership.create({ data: { planId, clientId, startDate: new Date(startDate), endDate: new Date(endDate), status: 'ACTIVE', remainingHours: plan.courtHours || null, autoRenew: autoRenew || false, notes: notes || null, }, include: { plan: { select: { id: true, name: true, price: true, durationMonths: true, courtHours: true, discountPercent: true, benefits: true, }, }, client: { select: { id: true, firstName: true, lastName: true, email: true, phone: true, }, }, }, }); // Add computed fields const now = new Date(); const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); const membershipWithDetails = { ...membership, isExpiring: membership.endDate <= sevenDaysFromNow, daysUntilExpiry: Math.ceil((membership.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)), benefitsSummary: { freeHours: membership.plan.courtHours || 0, hoursRemaining: membership.remainingHours || 0, bookingDiscount: membership.plan.discountPercent ? Number(membership.plan.discountPercent) : 0, extraBenefits: membership.plan.benefits || [], }, }; return NextResponse.json(membershipWithDetails, { status: 201 }); } catch (error) { console.error('Error creating membership:', error); return NextResponse.json( { error: 'Failed to create membership' }, { status: 500 } ); } }