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'; import { Prisma } from '@prisma/client'; interface RouteContext { params: Promise<{ id: string }>; } // Validation schema for updating a membership const updateMembershipSchema = z.object({ status: z.enum(['ACTIVE', 'EXPIRED', 'CANCELLED', 'SUSPENDED']).optional(), planId: z.string().cuid().optional(), endDate: z.string().datetime().optional(), hoursUsed: z.number().int().min(0).optional(), autoRenew: z.boolean().optional(), notes: z.string().max(500).optional().nullable(), }); // GET /api/memberships/[id] - Get membership with plan and client details export async function GET( request: NextRequest, context: RouteContext ) { try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } const { id } = await context.params; const membership = await db.membership.findFirst({ where: { id, plan: { organizationId: session.user.organizationId, }, }, include: { plan: { select: { id: true, name: true, description: true, price: true, durationMonths: true, courtHours: true, discountPercent: true, benefits: true, isActive: true, }, }, client: { select: { id: true, firstName: true, lastName: true, email: true, phone: true, level: true, tags: true, }, }, payments: { select: { id: true, amount: true, paymentType: true, createdAt: true, }, orderBy: { createdAt: 'desc', }, }, }, }); if (!membership) { return NextResponse.json( { error: 'Membership not found' }, { status: 404 } ); } // Calculate computed fields const now = new Date(); const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); const hoursUsed = membership.plan.courtHours && membership.remainingHours !== null ? membership.plan.courtHours - membership.remainingHours : 0; const membershipWithDetails = { ...membership, isExpiring: membership.status === 'ACTIVE' && membership.endDate <= sevenDaysFromNow, isExpired: membership.status === 'ACTIVE' && membership.endDate < now, daysUntilExpiry: membership.status === 'ACTIVE' ? Math.ceil((membership.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) : null, hoursUsed, benefitsSummary: { freeHours: membership.plan.courtHours || 0, hoursRemaining: membership.remainingHours || 0, hoursUsed, bookingDiscount: membership.plan.discountPercent ? Number(membership.plan.discountPercent) : 0, extraBenefits: membership.plan.benefits || [], }, totalPaid: membership.payments.reduce( (sum, p) => sum + Number(p.amount), 0 ), }; return NextResponse.json(membershipWithDetails); } catch (error) { console.error('Error fetching membership:', error); return NextResponse.json( { error: 'Failed to fetch membership' }, { status: 500 } ); } } // PUT /api/memberships/[id] - Update membership (renew, cancel, change plan) export async function PUT( request: NextRequest, context: RouteContext ) { 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 { id } = await context.params; // Verify membership exists and belongs to organization const existingMembership = await db.membership.findFirst({ where: { id, plan: { organizationId: session.user.organizationId, }, }, include: { plan: true, }, }); if (!existingMembership) { return NextResponse.json( { error: 'Membership not found' }, { status: 404 } ); } const body = await request.json(); // Validate input const validationResult = updateMembershipSchema.safeParse(body); if (!validationResult.success) { return NextResponse.json( { error: 'Invalid membership data', details: validationResult.error.flatten().fieldErrors, }, { status: 400 } ); } const { status, planId, endDate, hoursUsed, autoRenew, notes } = validationResult.data; // Build update data const updateData: Prisma.MembershipUpdateInput = {}; if (status !== undefined) updateData.status = status; if (endDate !== undefined) updateData.endDate = new Date(endDate); if (autoRenew !== undefined) updateData.autoRenew = autoRenew; if (notes !== undefined) updateData.notes = notes; // Handle hours used - update remaining hours if (hoursUsed !== undefined && existingMembership.plan.courtHours) { const newRemainingHours = Math.max(0, existingMembership.plan.courtHours - hoursUsed); updateData.remainingHours = newRemainingHours; } // Handle plan change if (planId !== undefined && planId !== existingMembership.planId) { // Verify new plan exists and belongs to organization const newPlan = await db.membershipPlan.findFirst({ where: { id: planId, organizationId: session.user.organizationId, isActive: true, }, }); if (!newPlan) { return NextResponse.json( { error: 'New membership plan not found or inactive' }, { status: 404 } ); } updateData.plan = { connect: { id: planId }, }; // Reset remaining hours to new plan's court hours if (newPlan.courtHours !== null) { updateData.remainingHours = newPlan.courtHours; } } const membership = await db.membership.update({ where: { id }, data: updateData, 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.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(membershipWithDetails); } catch (error) { console.error('Error updating membership:', error); return NextResponse.json( { error: 'Failed to update membership' }, { status: 500 } ); } } // DELETE /api/memberships/[id] - Cancel membership (set status = CANCELLED) export async function DELETE( request: NextRequest, context: RouteContext ) { 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']; if (!allowedRoles.includes(session.user.role)) { return NextResponse.json( { error: 'Forbidden: Insufficient permissions' }, { status: 403 } ); } const { id } = await context.params; // Verify membership exists and belongs to organization const existingMembership = await db.membership.findFirst({ where: { id, plan: { organizationId: session.user.organizationId, }, }, include: { client: { select: { id: true, firstName: true, lastName: true, }, }, plan: { select: { name: true, }, }, }, }); if (!existingMembership) { return NextResponse.json( { error: 'Membership not found' }, { status: 404 } ); } // Already cancelled if (existingMembership.status === 'CANCELLED') { return NextResponse.json( { error: 'Membership is already cancelled' }, { status: 400 } ); } // Cancel the membership await db.membership.update({ where: { id }, data: { status: 'CANCELLED', autoRenew: false, }, }); return NextResponse.json({ message: 'Membership cancelled successfully', membershipId: id, client: `${existingMembership.client.firstName} ${existingMembership.client.lastName}`, plan: existingMembership.plan.name, previousStatus: existingMembership.status, }); } catch (error) { console.error('Error cancelling membership:', error); return NextResponse.json( { error: 'Failed to cancel membership' }, { status: 500 } ); } }