diff --git a/app/api/events/subscriptions/[subscriptionId]/route.ts b/app/api/events/subscriptions/[subscriptionId]/route.ts index b37c136..6bd9137 100644 --- a/app/api/events/subscriptions/[subscriptionId]/route.ts +++ b/app/api/events/subscriptions/[subscriptionId]/route.ts @@ -11,13 +11,50 @@ export async function GET( const subscriptionData = await prisma.subscription.findUniqueOrThrow({ where: { id: subscriptionId }, include: { - plan: true, - requestedBy: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, appointments: { include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, }, @@ -64,20 +101,57 @@ export async function PUT( feedbackFromConsultee: body.feedbackFromConsultee, feedbackFromConsultant: body.feedbackFromConsultant, rating: body.rating, - plan: body.planId + subscriptionPlan: body.planId ? { connect: { id: body.planId }, } : undefined, }, include: { - plan: true, - requestedBy: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, appointments: { include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, }, @@ -105,13 +179,50 @@ export async function DELETE( const subscriptionData = await prisma.subscription.delete({ where: { id: subscriptionId }, include: { - plan: true, - requestedBy: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, appointments: { include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, }, diff --git a/app/api/events/subscriptions/book/route.ts b/app/api/events/subscriptions/book/route.ts index 78fa8c8..3225add 100644 --- a/app/api/events/subscriptions/book/route.ts +++ b/app/api/events/subscriptions/book/route.ts @@ -3,6 +3,13 @@ import prisma from "@/lib/prisma"; import { getServerSession } from "next-auth/next"; import authOptions from "@/app/api/auth/[...nextauth]/options"; +interface BookSubscriptionRequest { + subscriptionPlanId: string; + tentativeStartDate: string; + tentativeSchedule?: string; + requestNotes?: string; +} + export async function POST(request: Request) { try { const session = await getServerSession(authOptions); @@ -15,10 +22,20 @@ export async function POST(request: Request) { tentativeStartDate, tentativeSchedule, requestNotes, - } = await request.json(); + }: BookSubscriptionRequest = await request.json(); const consultee = await prisma.consulteeProfile.findUnique({ where: { userId: session.user.id }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, }); if (!consultee) { @@ -30,7 +47,20 @@ export async function POST(request: Request) { const subscriptionPlan = await prisma.subscriptionPlan.findUnique({ where: { id: subscriptionPlanId }, - include: { consultantProfile: true }, + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, }); if (!subscriptionPlan) { @@ -45,7 +75,7 @@ export async function POST(request: Request) { const subscription = await prisma.subscription.create({ data: { - plan: { connect: { id: subscriptionPlanId } }, + subscriptionPlan: { connect: { id: subscriptionPlanId } }, requestedBy: { connect: { id: consultee.id } }, startDate: new Date(tentativeStartDate), endDate: endDate, @@ -55,8 +85,34 @@ export async function POST(request: Request) { requestStatus: "PENDING", }, include: { - plan: true, - requestedBy: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }); diff --git a/app/api/events/subscriptions/route.ts b/app/api/events/subscriptions/route.ts index ed74250..82b19cd 100644 --- a/app/api/events/subscriptions/route.ts +++ b/app/api/events/subscriptions/route.ts @@ -1,7 +1,12 @@ import prisma from "@/lib/prisma"; -import { RequestStatus } from "@prisma/client"; +import { Prisma, RequestStatus } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; +interface UpdateSubscriptionRequest { + id: string; + status: RequestStatus; +} + export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const consultantProfileId = searchParams.get("consultantProfileId"); @@ -17,8 +22,8 @@ export async function GET(request: NextRequest) { } try { - const whereClause: any = { - plan: { + const whereClause: Prisma.SubscriptionWhereInput = { + subscriptionPlan: { consultantProfileId, }, }; @@ -31,18 +36,32 @@ export async function GET(request: NextRequest) { prisma.subscription.findMany({ where: whereClause, include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { - user: true, + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, }, }, requestedBy: { include: { - user: true, + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, }, @@ -75,7 +94,7 @@ export async function GET(request: NextRequest) { export async function PATCH(request: NextRequest) { try { - const body = await request.json(); + const body: UpdateSubscriptionRequest = await request.json(); const { id, status } = body; if (!id || !status) { @@ -85,7 +104,7 @@ export async function PATCH(request: NextRequest) { ); } - if (!Object.values(RequestStatus).includes(status as RequestStatus)) { + if (!Object.values(RequestStatus).includes(status)) { return NextResponse.json({ error: "Invalid status" }, { status: 400 }); } @@ -93,18 +112,32 @@ export async function PATCH(request: NextRequest) { where: { id }, data: { requestStatus: status }, include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { - user: true, + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, }, }, requestedBy: { include: { - user: true, + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, }, diff --git a/app/api/payments/razorpay/route.ts b/app/api/payments/razorpay/route.ts index c205705..ce0df28 100644 --- a/app/api/payments/razorpay/route.ts +++ b/app/api/payments/razorpay/route.ts @@ -3,6 +3,39 @@ import prisma from "@/lib/prisma"; import Razorpay from "razorpay"; import crypto from "crypto"; +interface RazorpayMock { + orders: { + create: () => Promise<{ + id: string; + amount: number; + currency: string; + receipt: string; + status: string; + }>; + }; + payments: { + fetch: () => Promise<{ + id: string; + status: string; + acquirer_data: { + rrn: string; + }; + }>; + }; +} + +interface CreatePaymentRequest { + appointmentId: string; + userId: string; +} + +interface UpdatePaymentRequest { + razorpay_order_id: string; + razorpay_payment_id: string; + razorpay_signature: string; + paymentId: string; +} + const razorpay = process.env.RAZORPAY_KEY_ID && process.env.RAZORPAY_KEY_SECRET ? new Razorpay({ @@ -28,21 +61,97 @@ const razorpay = }, }), }, - } as unknown as Razorpay); + } as RazorpayMock); export async function POST(req: NextRequest) { try { - const body = await req.json(); + const body: CreatePaymentRequest = await req.json(); const { appointmentId, userId } = body; // Fetch the appointment and related data const appointment = await prisma.appointment.findUnique({ where: { id: appointmentId }, include: { - consultation: { include: { consultationPlan: true } }, - subscription: { include: { plan: true } }, - webinar: { include: { webinarPlan: true } }, - class: { include: { classPlan: true } }, + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, }, }); @@ -60,8 +169,8 @@ export async function POST(req: NextRequest) { amount = appointment.consultation.consultationPlan.price; description = `Payment for Consultation: ${appointment.consultation.consultationPlan.title}`; } else if (appointment.subscription) { - amount = appointment.subscription.plan.price; - description = `Payment for Subscription: ${appointment.subscription.plan.title}`; + amount = appointment.subscription.subscriptionPlan.price; + description = `Payment for Subscription: ${appointment.subscription.subscriptionPlan.title}`; } else if (appointment.webinar) { amount = appointment.webinar.webinarPlan.price; description = `Payment for Webinar: ${appointment.webinar.webinarPlan.title}`; @@ -95,6 +204,100 @@ export async function POST(req: NextRequest) { user: { connect: { id: userId } }, appointment: { connect: { id: appointmentId } }, }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + appointment: { + include: { + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }); return NextResponse.json({ @@ -112,7 +315,7 @@ export async function POST(req: NextRequest) { export async function PUT(req: NextRequest) { try { - const body = await req.json(); + const body: UpdatePaymentRequest = await req.json(); const { razorpay_order_id, razorpay_payment_id, @@ -144,7 +347,101 @@ export async function PUT(req: NextRequest) { data: { paymentStatus: paymentDetails.status === "captured" ? "SUCCEEDED" : "FAILED", - receiptUrl: paymentDetails.acquirer_data?.rrn || null, // Using rrn as receipt URL, adjust if needed + receiptUrl: paymentDetails.acquirer_data?.rrn || null, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + appointment: { + include: { + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, }); diff --git a/app/api/payments/stripe/route.ts b/app/api/payments/stripe/route.ts index efabc9f..e5fdce2 100644 --- a/app/api/payments/stripe/route.ts +++ b/app/api/payments/stripe/route.ts @@ -2,6 +2,31 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; import Stripe from "stripe"; +interface StripeMock { + paymentIntents: { + create: () => Promise<{ + id: string; + client_secret: string; + status: string; + }>; + retrieve: () => Promise<{ + id: string; + status: string; + receipt_email: string; + }>; + }; +} + +interface CreatePaymentRequest { + appointmentId: string; + userId: string; +} + +interface UpdatePaymentRequest { + paymentIntentId: string; + paymentId: string; +} + const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2024-10-28.acacia", @@ -19,21 +44,97 @@ const stripe = process.env.STRIPE_SECRET_KEY receipt_email: "mock_receipt@example.com", }), }, - } as unknown as Stripe); + } as StripeMock); export async function POST(req: NextRequest) { try { - const body = await req.json(); + const body: CreatePaymentRequest = await req.json(); const { appointmentId, userId } = body; // Fetch the appointment and related data const appointment = await prisma.appointment.findUnique({ where: { id: appointmentId }, include: { - consultation: { include: { consultationPlan: true } }, - subscription: { include: { plan: true } }, - webinar: { include: { webinarPlan: true } }, - class: { include: { classPlan: true } }, + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, }, }); @@ -51,8 +152,8 @@ export async function POST(req: NextRequest) { amount = appointment.consultation.consultationPlan.price; description = `Payment for Consultation: ${appointment.consultation.consultationPlan.title}`; } else if (appointment.subscription) { - amount = appointment.subscription.plan.price; - description = `Payment for Subscription: ${appointment.subscription.plan.title}`; + amount = appointment.subscription.subscriptionPlan.price; + description = `Payment for Subscription: ${appointment.subscription.subscriptionPlan.title}`; } else if (appointment.webinar) { amount = appointment.webinar.webinarPlan.price; description = `Payment for Webinar: ${appointment.webinar.webinarPlan.title}`; @@ -82,6 +183,100 @@ export async function POST(req: NextRequest) { user: { connect: { id: userId } }, appointment: { connect: { id: appointmentId } }, }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + appointment: { + include: { + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }); return NextResponse.json({ @@ -99,7 +294,7 @@ export async function POST(req: NextRequest) { export async function PUT(req: NextRequest) { try { - const body = await req.json(); + const body: UpdatePaymentRequest = await req.json(); const { paymentIntentId, paymentId } = body; // Retrieve the PaymentIntent from Stripe @@ -111,7 +306,101 @@ export async function PUT(req: NextRequest) { data: { paymentStatus: paymentIntent.status === "succeeded" ? "SUCCEEDED" : "FAILED", - receiptUrl: paymentIntent.receipt_email, // TODO: fix this + receiptUrl: paymentIntent.receipt_email, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + appointment: { + include: { + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, }); diff --git a/app/api/plans/subscriptions/[subscriptionId]/route.ts b/app/api/plans/subscriptions/[subscriptionId]/route.ts index 05f3ae6..3ba0fed 100644 --- a/app/api/plans/subscriptions/[subscriptionId]/route.ts +++ b/app/api/plans/subscriptions/[subscriptionId]/route.ts @@ -2,6 +2,22 @@ import prisma from "@/lib/prisma"; import { Prisma, PlanEmailSupport } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; +interface UpdateSubscriptionPlanRequest { + title?: string; + description?: string; + durationInMonths?: number; + price?: number; + callsPerWeek?: number; + videoMeetings?: number; + emailSupport?: PlanEmailSupport; + language?: string; + level?: string; + prerequisites?: string; + materialProvided?: string; + learningOutcomes?: string[]; + consultantProfileId?: string; +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ subscriptionId: string }> }, @@ -11,8 +27,34 @@ export async function GET( const subscriptionPlan = await prisma.subscriptionPlan.findUniqueOrThrow({ where: { id: subscriptionId }, include: { - consultantProfile: true, - subscriptions: true, + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + subscriptions: { + include: { + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }); @@ -41,7 +83,7 @@ export async function PUT( ) { try { const { subscriptionId } = await params; - const body = await request.json(); + const body: UpdateSubscriptionPlanRequest = await request.json(); // Input validation if (body.durationInMonths && body.durationInMonths <= 0) { @@ -91,7 +133,7 @@ export async function PUT( price: body.price ? Math.round(body.price) : undefined, // Ensure price is an integer callsPerWeek: body.callsPerWeek, videoMeetings: body.videoMeetings, - emailSupport: body.emailSupport as PlanEmailSupport, + emailSupport: body.emailSupport, language: body.language, level: body.level, prerequisites: body.prerequisites, @@ -104,8 +146,34 @@ export async function PUT( : undefined, }, include: { - consultantProfile: true, - subscriptions: true, + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + subscriptions: { + include: { + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }); @@ -137,7 +205,7 @@ export async function DELETE( // Check if there are any associated subscriptions const associatedSubscriptions = await prisma.subscription.findMany({ - where: { planId: subscriptionId }, + where: { subscriptionPlanId: subscriptionId }, }); if (associatedSubscriptions.length > 0) { @@ -153,7 +221,18 @@ export async function DELETE( const subscriptionPlan = await prisma.subscriptionPlan.delete({ where: { id: subscriptionId }, include: { - consultantProfile: true, + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }); diff --git a/app/api/slots/appointments/[appointmentId]/route.ts b/app/api/slots/appointments/[appointmentId]/route.ts index 6944ee3..0bcd8a2 100644 --- a/app/api/slots/appointments/[appointmentId]/route.ts +++ b/app/api/slots/appointments/[appointmentId]/route.ts @@ -2,6 +2,15 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; import { Prisma, AppointmentsType } from "@prisma/client"; +interface UpdateAppointmentRequest { + appointmentType?: AppointmentsType; + slotOfAppointmentId?: string; + consultationId?: string; + subscriptionId?: string; + webinarId?: string; + classId?: string; +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ appointmentId: string }> }, @@ -13,30 +22,136 @@ export async function GET( include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, consultation: { include: { - consultationPlan: true, + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, subscription: { include: { - plan: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, webinar: { include: { - webinarPlan: true, + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }, class: { include: { - classPlan: true, + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + payment: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, - payment: true, }, }); @@ -63,7 +178,7 @@ export async function PUT( ) { try { const { appointmentId } = await params; - const body = await request.json(); + const body: UpdateAppointmentRequest = await request.json(); if ( body.appointmentType && @@ -96,30 +211,136 @@ export async function PUT( include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, consultation: { include: { - consultationPlan: true, + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, subscription: { include: { - plan: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, webinar: { include: { - webinarPlan: true, + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }, class: { include: { - classPlan: true, + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + payment: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, - payment: true, }, }); @@ -150,7 +371,20 @@ export async function DELETE( // Check if there's an associated payment const appointment = await prisma.appointment.findUnique({ where: { id: appointmentId }, - include: { payment: true }, + include: { + payment: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, }); if (appointment?.payment) { @@ -165,27 +399,122 @@ export async function DELETE( include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, consultation: { include: { - consultationPlan: true, + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, subscription: { include: { - plan: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, webinar: { include: { - webinarPlan: true, + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }, class: { include: { - classPlan: true, + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }, }, diff --git a/app/api/slots/appointments/route.ts b/app/api/slots/appointments/route.ts index 94481ef..3a402fd 100644 --- a/app/api/slots/appointments/route.ts +++ b/app/api/slots/appointments/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -import { AppointmentsType } from "@prisma/client"; +import { AppointmentsType, Prisma } from "@prisma/client"; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); @@ -51,7 +51,7 @@ export async function GET(request: NextRequest) { a.subscription?.requestedBy?.user?.name, consultant: a.consultation?.consultationPlan?.consultantProfile?.user?.name || - a.subscription?.plan?.consultantProfile?.user?.name || + a.subscription?.subscriptionPlan?.consultantProfile?.user?.name || a.webinar?.webinarPlan?.consultantProfile?.user?.name || a.class?.classPlan?.consultantProfile?.user?.name, }, @@ -93,11 +93,11 @@ async function getAppointments( // Get current time in UTC const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000); - const whereClause: any = { + const whereClause: Prisma.AppointmentWhereInput = { slotOfAppointment: { some: { slotStartTimeInUTC: { - gte: thirtyMinutesAgo, // Include appointments from 30 minutes ago + gte: thirtyMinutesAgo, }, }, }, @@ -114,7 +114,7 @@ async function getAppointments( }, { subscription: { - plan: { + subscriptionPlan: { consultantProfileId, }, }, @@ -142,9 +142,8 @@ async function getAppointments( if (consulteeProfileId) { whereClause.slotOfAppointment = { - ...whereClause.slotOfAppointment, some: { - ...whereClause.slotOfAppointment.some, + ...whereClause.slotOfAppointment?.some, consulteeProfileId, }, }; @@ -204,7 +203,7 @@ async function getAppointments( }, subscription: { include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { @@ -276,11 +275,6 @@ async function getAppointments( }, }, orderBy: [ - { - slotOfAppointment: { - _count: "desc", - }, - }, { createdAt: "desc", }, @@ -317,7 +311,7 @@ async function getAppointments( a.subscription?.requestedBy?.user?.name, consultant: a.consultation?.consultationPlan?.consultantProfile?.user?.name || - a.subscription?.plan?.consultantProfile?.user?.name || + a.subscription?.subscriptionPlan?.consultantProfile?.user?.name || a.webinar?.webinarPlan?.consultantProfile?.user?.name || a.class?.classPlan?.consultantProfile?.user?.name, })), @@ -401,7 +395,7 @@ export async function POST(request: NextRequest) { }, subscription: { include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { diff --git a/app/dashboard/consultant/[consultantId]/utils.ts b/app/dashboard/consultant/[consultantId]/utils.ts index b0b24b0..0d1f99b 100644 --- a/app/dashboard/consultant/[consultantId]/utils.ts +++ b/app/dashboard/consultant/[consultantId]/utils.ts @@ -18,7 +18,9 @@ export async function fetchConsultantData( try { const response = await fetch(`/api/user/consultants/${consultantId}`); if (!response.ok) { - throw new Error("Failed to fetch consultant data"); + throw new Error( + `Failed to fetch consultant data: ${response.statusText}`, + ); } const data: ApiResponse = await response.json(); return data.data; @@ -36,7 +38,7 @@ export async function fetchAppointments( `/api/slots/appointments?consultantProfileId=${consultantId}`, ); if (!response.ok) { - throw new Error("Failed to fetch appointments"); + throw new Error(`Failed to fetch appointments: ${response.statusText}`); } const data: ApiResponse = await response.json(); @@ -44,14 +46,14 @@ export async function fetchAppointments( return data.data.map((appointment) => ({ id: appointment.id, name: - appointment.slotOfAppointment?.[0]?.consulteeProfile?.user?.name || + appointment.slotOfAppointment?.[0]?.consulteeProfile?.user?.name ?? "Unknown", description: getAppointmentDescription(appointment), time: formatAppointmentTime( - appointment.slotOfAppointment?.[0]?.slotStartTimeInUTC?.toString(), + appointment.slotOfAppointment?.[0]?.slotStartTimeInUTC, ), badge: getAppointmentBadge( - appointment.slotOfAppointment?.[0]?.slotStartTimeInUTC?.toString(), + appointment.slotOfAppointment?.[0]?.slotStartTimeInUTC, ), })); } catch (error) { @@ -74,21 +76,30 @@ export async function fetchApprovals( ), ]); - if (!consultationsRes.ok || !subscriptionsRes.ok) { - throw new Error("Failed to fetch approvals"); + if (!consultationsRes.ok) { + throw new Error( + `Failed to fetch consultations: ${consultationsRes.statusText}`, + ); + } + if (!subscriptionsRes.ok) { + throw new Error( + `Failed to fetch subscriptions: ${subscriptionsRes.statusText}`, + ); } - const consultationsData = await consultationsRes.json(); - const subscriptionsData = await subscriptionsRes.json(); + const consultationsData: ApiResponse = + await consultationsRes.json(); + const subscriptionsData: ApiResponse = + await subscriptionsRes.json(); // Transform consultations into approvals const consultationApprovals = consultationsData.data.map( (consultation: TConsultation) => ({ id: consultation.id, type: "Consultation", - name: consultation.requestedBy?.user?.name || "Unknown", - date: formatDate(new Date(consultation.requestedAt)), - time: formatTime(new Date(consultation.requestedAt)), + name: consultation.requestedBy?.user?.name ?? "Unknown", + date: formatDate(consultation.requestedAt), + time: formatTime(consultation.requestedAt), }), ); @@ -97,17 +108,17 @@ export async function fetchApprovals( (subscription: TSubscription) => ({ id: subscription.id, type: "Subscription", - name: subscription.requestedBy?.user?.name || "Unknown", - date: formatDate(new Date(subscription.requestedAt)), - time: formatTime(new Date(subscription.requestedAt)), + name: subscription.requestedBy?.user?.name ?? "Unknown", + date: formatDate(subscription.requestedAt), + time: formatTime(subscription.requestedAt), }), ); // Combine and sort by requestedAt return [...consultationApprovals, ...subscriptionApprovals].sort( (a, b) => - new Date(b.date + " " + b.time).getTime() - - new Date(a.date + " " + a.time).getTime(), + new Date(`${b.date} ${b.time}`).getTime() - + new Date(`${a.date} ${a.time}`).getTime(), ); } catch (error) { console.error("Error fetching approvals:", error); @@ -134,21 +145,23 @@ function getAppointmentDescription(appointment: TAppointment): string { const type = appointment.appointmentType?.toLowerCase(); switch (type) { case "consultation": - return `Consultation - ${appointment.consultation?.consultationPlan?.title || "No title"}`; + return `Consultation - ${appointment.consultation?.consultationPlan?.title ?? "No title"}`; case "subscription": - return `Subscription - ${appointment.subscription?.plan?.title || "No title"}`; + return `Subscription - ${appointment.subscription?.subscriptionPlan?.title ?? "No title"}`; case "webinar": - return `Webinar - ${appointment.webinar?.webinarPlan?.title || "No title"}`; + return `Webinar - ${appointment.webinar?.webinarPlan?.title ?? "No title"}`; case "class": - return `Class - ${appointment.class?.classPlan?.title || "No title"}`; + return `Class - ${appointment.class?.classPlan?.title ?? "No title"}`; default: return "Appointment"; } } -function formatAppointmentTime(dateString: string): string { +function formatAppointmentTime(dateString?: string | Date | null): string { if (!dateString) return "Time not set"; const date = new Date(dateString); + if (isNaN(date.getTime())) return "Invalid date"; + return date.toLocaleString("en-US", { weekday: "short", month: "short", @@ -159,10 +172,12 @@ function formatAppointmentTime(dateString: string): string { }); } -function getAppointmentBadge(dateString: string): string { +function getAppointmentBadge(dateString?: string | Date | null): string { if (!dateString) return "Schedule unavailable"; const appointmentTime = new Date(dateString); + if (isNaN(appointmentTime.getTime())) return "Invalid date"; + const now = new Date(); const diffInMinutes = Math.floor( (appointmentTime.getTime() - now.getTime()) / (1000 * 60), @@ -189,8 +204,11 @@ function getAppointmentBadge(dateString: string): string { return `In ${Math.floor(diffInDays / 365)} years`; } -function formatDate(date: Date): string { - if (!date) return "Date not set"; +function formatDate(dateString?: string | Date | null): string { + if (!dateString) return "Date not set"; + const date = new Date(dateString); + if (isNaN(date.getTime())) return "Invalid date"; + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", @@ -198,8 +216,11 @@ function formatDate(date: Date): string { }); } -function formatTime(date: Date): string { - if (!date) return "Time not set"; +function formatTime(dateString?: string | Date | null): string { + if (!dateString) return "Time not set"; + const date = new Date(dateString); + if (isNaN(date.getTime())) return "Invalid time"; + return date.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e443b82..d087988 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -379,8 +379,8 @@ model Subscription { feedbackFromConsultant String? rating Float? - plan SubscriptionPlan @relation(fields: [planId], references: [id], onUpdate: Cascade, onDelete: Cascade) - planId String + subscriptionPlan SubscriptionPlan @relation(fields: [subscriptionPlanId], references: [id], onUpdate: Cascade, onDelete: Cascade) + subscriptionPlanId String appointments Appointment[] diff --git a/prisma/seedFiles/createAppointments.ts b/prisma/seedFiles/createAppointments.ts index 3191be0..fb633ab 100644 --- a/prisma/seedFiles/createAppointments.ts +++ b/prisma/seedFiles/createAppointments.ts @@ -5,6 +5,8 @@ import { Platform, WebinarStatus, ClassStatus, + SlotOfAvailabilityWeekly, + SlotOfAvailabilityCustom, } from "@prisma/client"; import prisma from "../../lib/prisma"; import { UserWithProfiles } from "./createUsers"; @@ -22,6 +24,16 @@ type SubscriptionCreate = NonNullable< type WebinarCreate = NonNullable["create"]; type ClassCreate = NonNullable["create"]; +type SlotData = + | { + type: "weekly"; + slot: SlotOfAvailabilityWeekly; + } + | { + type: "custom"; + slot: SlotOfAvailabilityCustom; + }; + // Helper function to generate tentative schedule function generateTentativeSchedule( startDate: Date, @@ -60,7 +72,7 @@ export async function createAppointments(consultees: UserWithProfiles[]) { const webinarPlans = await prisma.webinarPlan.findMany(); const classPlans = await prisma.classPlan.findMany(); - const allSlots = [ + const allSlots: SlotData[] = [ ...weeklySlots.map((slot) => ({ type: "weekly" as const, slot })), ...customSlots.map((slot) => ({ type: "custom" as const, slot })), ]; @@ -154,7 +166,7 @@ export async function createAppointments(consultees: UserWithProfiles[]) { case AppointmentsType.SUBSCRIPTION: const subscriptionData: SubscriptionCreate = { - plan: { + subscriptionPlan: { connect: { id: faker.helpers.arrayElement(subscriptionPlans).id, }, diff --git a/prisma/seedFiles/createConsultantReviews.ts b/prisma/seedFiles/createConsultantReviews.ts index 071fd3c..b46652f 100644 --- a/prisma/seedFiles/createConsultantReviews.ts +++ b/prisma/seedFiles/createConsultantReviews.ts @@ -1,7 +1,18 @@ import { faker } from "@faker-js/faker"; +import { Prisma } from "@prisma/client"; import prisma from "../../lib/prisma"; import { UserWithProfiles } from "./createUsers"; +type CompletedAppointment = Prisma.AppointmentGetPayload<{ + include: { + slotOfAppointment: { + include: { + consulteeProfile: true; + }; + }; + }; +}>; + export async function createConsultantReviews( consultants: UserWithProfiles[], consultees: UserWithProfiles[], @@ -30,7 +41,7 @@ export async function createConsultantReviews( }, { subscription: { - plan: { + subscriptionPlan: { consultantProfile: { id: consultant.consultantProfile.id }, }, requestStatus: "APPROVED", @@ -68,15 +79,21 @@ export async function createConsultantReviews( min: 1, max: Math.min(5, completedAppointments.length), }); - const appointmentsToReview = faker.helpers.arrayElements( - completedAppointments, - numReviews, - ); + const appointmentsToReview = + faker.helpers.arrayElements( + completedAppointments, + numReviews, + ); for (const appointment of appointmentsToReview) { const consulteeProfile = appointment.slotOfAppointment[0]?.consulteeProfile; - if (!consulteeProfile) continue; + if (!consulteeProfile) { + console.warn( + `Skipping review - no consultee profile found for appointment ${appointment.id}`, + ); + continue; + } const rating = faker.number.int({ min: 1, max: 5 }); @@ -97,21 +114,30 @@ export async function createConsultantReviews( select: { rating: true }, }); - const averageRating = - allReviews.reduce((acc, review) => acc + review.rating, 0) / - allReviews.length; + if (allReviews.length > 0) { + const totalRating = allReviews.reduce( + (acc, review) => acc + review.rating, + 0, + ); + const averageRating = Number( + (totalRating / allReviews.length).toFixed(2), + ); - await prisma.consultantProfile.update({ - where: { id: consultant.consultantProfile.id }, - data: { rating: averageRating }, - }); + await prisma.consultantProfile.update({ + where: { id: consultant.consultantProfile.id }, + data: { rating: averageRating }, + }); + } totalReviews++; + if (totalReviews % 10 === 0) { + console.log(`Created ${totalReviews} reviews so far...`); + } } } catch (error) { console.error( `Failed to create reviews for consultant ${consultant.id}:`, - error, + error instanceof Error ? error.message : String(error), ); } } diff --git a/prisma/seedFiles/createPayments.ts b/prisma/seedFiles/createPayments.ts index 98d4714..9305d91 100644 --- a/prisma/seedFiles/createPayments.ts +++ b/prisma/seedFiles/createPayments.ts @@ -1,10 +1,40 @@ import { faker } from "@faker-js/faker"; -import { PaymentGateway, PaymentStatus, Prisma } from "@prisma/client"; +import { + PaymentGateway, + PaymentStatus, + Prisma, + DiscountType, +} from "@prisma/client"; import prisma from "../../lib/prisma"; import { UserWithProfiles } from "./createUsers"; const NUM_PAYMENTS = 100; +type AppointmentWithPlans = Prisma.AppointmentGetPayload<{ + include: { + consultation: { + include: { + consultationPlan: true; + }; + }; + subscription: { + include: { + subscriptionPlan: true; + }; + }; + webinar: { + include: { + webinarPlan: true; + }; + }; + class: { + include: { + classPlan: true; + }; + }; + }; +}>; + export async function createPayments(users: UserWithProfiles[]) { console.log(`Creating payments...`); const discountCodes = await prisma.discountCode.findMany(); @@ -26,7 +56,7 @@ export async function createPayments(users: UserWithProfiles[]) { }, subscription: { include: { - plan: true, + subscriptionPlan: true, }, }, webinar: { @@ -45,19 +75,25 @@ export async function createPayments(users: UserWithProfiles[]) { for (let i = 0; i < appointments.length; i++) { const user = faker.helpers.arrayElement(users); - const appointment = appointments[i]; + const appointment = appointments[i] as AppointmentWithPlans; try { // Determine the amount based on the appointment type and plan let amount = 0; + let description = ""; + if (appointment.consultation?.consultationPlan) { amount = appointment.consultation.consultationPlan.price; - } else if (appointment.subscription?.plan) { - amount = appointment.subscription.plan.price; + description = `Payment for Consultation: ${appointment.consultation.consultationPlan.title}`; + } else if (appointment.subscription?.subscriptionPlan) { + amount = appointment.subscription.subscriptionPlan.price; + description = `Payment for Subscription: ${appointment.subscription.subscriptionPlan.title}`; } else if (appointment.webinar?.webinarPlan) { amount = appointment.webinar.webinarPlan.price; + description = `Payment for Webinar: ${appointment.webinar.webinarPlan.title}`; } else if (appointment.class?.classPlan) { amount = appointment.class.classPlan.price; + description = `Payment for Class: ${appointment.class.classPlan.title}`; } // Apply discount if available @@ -65,15 +101,17 @@ export async function createPayments(users: UserWithProfiles[]) { const discountCode = useDiscount ? faker.helpers.arrayElement(discountCodes) : null; + + let finalAmount = amount; if (discountCode) { switch (discountCode.discountType) { - case "PERCENTAGE": - amount = Math.round( + case DiscountType.PERCENTAGE: + finalAmount = Math.round( amount * (1 - discountCode.discountValue / 100), ); break; - case "FIXED_AMOUNT": - amount = Math.max(0, amount - discountCode.discountValue); + case DiscountType.FIXED_AMOUNT: + finalAmount = Math.max(0, amount - discountCode.discountValue); break; // FREE_SHIPPING doesn't affect the amount in this context } @@ -81,9 +119,9 @@ export async function createPayments(users: UserWithProfiles[]) { const paymentData: Prisma.PaymentCreateInput = { user: { connect: { id: user.id } }, - amount: amount, + amount: finalAmount, currency: faker.helpers.arrayElement(["USD", "EUR", "GBP"]), - description: faker.lorem.sentence(), + description, receiptUrl: faker.internet.url(), paymentMethod: faker.helpers.arrayElement([ "credit_card", @@ -92,10 +130,12 @@ export async function createPayments(users: UserWithProfiles[]) { "wallet", ]), paymentIntent: faker.string.uuid(), - paymentGateway: faker.helpers.arrayElement( + paymentGateway: faker.helpers.arrayElement( Object.values(PaymentGateway), ), - paymentStatus: faker.helpers.arrayElement(Object.values(PaymentStatus)), + paymentStatus: faker.helpers.arrayElement( + Object.values(PaymentStatus), + ), appointment: { connect: { id: appointment.id } }, ...(discountCode ? { discountCode: { connect: { id: discountCode.id } } } @@ -106,11 +146,16 @@ export async function createPayments(users: UserWithProfiles[]) { data: paymentData, }); } catch (error) { - console.error(`Failed to create payment for user ${user.id}:`, error); + console.error( + `Failed to create payment for user ${user.id}:`, + error instanceof Error ? error.message : String(error), + ); } if ((i + 1) % 20 === 0 || i === appointments.length - 1) { console.log(`Created ${i + 1} payments`); } } + + console.log(`Finished creating payments`); } diff --git a/types/appointment.ts b/types/appointment.ts index b0f2ee6..7c5149e 100644 --- a/types/appointment.ts +++ b/types/appointment.ts @@ -23,7 +23,7 @@ export type TConsultation = Prisma.ConsultationGetPayload<{ // Custom type for Subscription with specific nesting depth export type TSubscription = Prisma.SubscriptionGetPayload<{ include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { @@ -58,7 +58,7 @@ export type TAppointment = Prisma.AppointmentGetPayload<{ }; subscription: { include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: {