Skip to content

Commit

Permalink
added course payment and auth system
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvilmehta committed Apr 11, 2024
1 parent 612fa7f commit 2cab054
Show file tree
Hide file tree
Showing 22 changed files with 981 additions and 195 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"next-themes": "^0.2.1",
"node-fetch": "^3.3.2",
"notion-client": "^6.16.0",
"razorpay": "^2.9.2",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.50.1",
Expand Down
54 changes: 54 additions & 0 deletions prisma/migrations/20240402232925_purchase/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
-- AlterTable
ALTER TABLE "Course" ADD COLUMN "price" INTEGER NOT NULL DEFAULT 1000;

-- CreateTable
CREATE TABLE "Receipt" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"courseId" INTEGER NOT NULL DEFAULT 0,
"razorpayOrderId" TEXT,

CONSTRAINT "Receipt_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Purchase" (
"id" TEXT NOT NULL,
"receiptId" TEXT NOT NULL,
"razorpay_payment_id" TEXT NOT NULL,
"razorpay_order_id" TEXT NOT NULL,
"razorpay_signature" TEXT NOT NULL,
"purchasedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"paymentVerified" BOOLEAN NOT NULL DEFAULT false,
"purchasedCourseId" INTEGER NOT NULL,
"purchasedById" TEXT NOT NULL,

CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Receipt_razorpayOrderId_key" ON "Receipt"("razorpayOrderId");

-- CreateIndex
CREATE UNIQUE INDEX "Purchase_receiptId_key" ON "Purchase"("receiptId");

-- CreateIndex
CREATE UNIQUE INDEX "Purchase_razorpay_payment_id_key" ON "Purchase"("razorpay_payment_id");

-- CreateIndex
CREATE UNIQUE INDEX "Purchase_razorpay_order_id_key" ON "Purchase"("razorpay_order_id");

-- AddForeignKey
ALTER TABLE "Receipt" ADD CONSTRAINT "Receipt_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Receipt" ADD CONSTRAINT "Receipt_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_receiptId_fkey" FOREIGN KEY ("receiptId") REFERENCES "Receipt"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_purchasedCourseId_fkey" FOREIGN KEY ("purchasedCourseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_purchasedById_fkey" FOREIGN KEY ("purchasedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
30 changes: 30 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ datasource db {

model Course {
id Int @id @default(autoincrement())
price Int @default(1000)
appxCourseId Int
discordRoleId String
title String
Expand All @@ -20,6 +21,8 @@ model Course {
content CourseContent[]
purchasedBy UserPurchases[]
bookmarks Bookmark[]
purchases Purchase[]
receipts Receipt[]
}

model UserPurchases {
Expand Down Expand Up @@ -132,6 +135,33 @@ model User {
password String?
appxUserId String?
appxUsername String?
transactions Purchase[]
receipts Receipt[]
}

model Receipt{
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
course Course @relation(fields: [courseId], references: [id])
courseId Int @default(0)
razorpayOrderId String? @unique
purchase Purchase?
}

model Purchase {
id String @id @default(cuid())
receipt Receipt @relation(fields: [receiptId], references: [id])
receiptId String @unique
razorpay_payment_id String @unique
razorpay_order_id String @unique
razorpay_signature String
purchasedAt DateTime @default(now())
paymentVerified Boolean @default(false)
purchasedCourse Course @relation(fields: [purchasedCourseId], references: [id])
purchasedCourseId Int
purchasedBy User @relation(fields: [purchasedById], references: [id])
purchasedById String
}

model DiscordConnect {
Expand Down
36 changes: 36 additions & 0 deletions src/app/api/auth/register/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from 'next/server';
import db from '@/db';
import bcrypt from 'bcrypt';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';

export async function POST(req: NextRequest) {
try {
const { name, email, password } = await req.json();

const hashedPassword = await bcrypt.hash(password, 10);
await db.user.create({
data: {
email,
name,
password: hashedPassword,
},
});

return NextResponse.json(
{ message: 'Account created sucessfully' },
{ status: 201 },
);
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
return NextResponse.json(
{ error: 'Email already taken.' },
{ status: 400 },
);
}
console.log(e);
return NextResponse.json(
{ error: 'Failed to parse JSON input' },
{ status: 400 },
);
}
}
83 changes: 83 additions & 0 deletions src/app/api/razorpay/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { NextRequest, NextResponse } from 'next/server';
import Razorpay from 'razorpay';
import db from '@/db';
import { z } from 'zod';

const schema = z.object({
courseId: z.number(),
userId: z.string(),
});

const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_SECRET!,
});

export async function POST(req: NextRequest) {
const reqBody = await req.json();
const body = schema.safeParse(reqBody);

if (!body.success) {
return NextResponse.json(
{
error: 'Error parsing the body of the request',
},
{
status: 422,
},
);
}

const course = await db.course.findFirst({
where: {
id: body.data.courseId,
},
});

if (!course)
return NextResponse.json({ error: 'Course Not Found' }, { status: 404 });

const receipt = await db.receipt.create({
data: {
userId: body.data.userId,
courseId: body.data.courseId,
},
});

const payment_capture = 1;
const amount = course.price;
const options = {
amount: (amount * 100).toString(),
currency: 'INR',
receipt: receipt.id,
payment_capture,
notes: {
userId: body.data.userId,
courseId: body.data.courseId,
receipt: receipt.id,
},
};

try {
const response = await razorpay.orders.create(options);

await db.receipt.update({
where: {
id: receipt.id,
},
data: {
razorpayOrderId: response.id,
},
});

return NextResponse.json({
id: response.id,
currency: response.currency,
amount: response.amount,
razorPayKey: process.env.RAZORPAY_KEY_ID,
});
} catch (err) {
console.log(err);
return NextResponse.json(err);
}
}
80 changes: 80 additions & 0 deletions src/app/api/razorpay/verify/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from 'next/server';
import { validatePaymentVerification } from 'razorpay/dist/utils/razorpay-utils';
import { z } from 'zod';
import db from '@/db';

const razorPayZodSchema = z.object({
userId: z.string(),
courseId: z.number(),
razorpay_order_id: z.string(),
razorpay_payment_id: z.string(),
razorpay_signature: z.string(),
});

export async function POST(req: NextRequest) {
const jsonBody = await req.json();

const body = razorPayZodSchema.safeParse(jsonBody);

if (!body.success) {
return NextResponse.json(
{
error: 'Invalid Body',
},
{ status: 422 },
);
}

const {
razorpay_order_id,
razorpay_payment_id,
razorpay_signature,
courseId,
userId,
} = body.data;

const isPaymentValid = validatePaymentVerification(
{
order_id: razorpay_order_id,
payment_id: razorpay_payment_id,
},
razorpay_signature,
process.env.RAZORPAY_SECRET!,
);

if (!isPaymentValid)
return NextResponse.json(
{ error: 'Payment not verified' },
{ status: 404 },
);

const receipt = await db.receipt.findFirst({
where: {
razorpayOrderId: razorpay_order_id,
},
});

if (!receipt)
return NextResponse.json({ error: 'Receipt not found' }, { status: 404 });

await db.purchase.create({
data: {
receiptId: receipt.id,
razorpay_order_id,
razorpay_payment_id,
razorpay_signature,
paymentVerified: isPaymentValid,
purchasedById: userId,
purchasedCourseId: courseId,
},
});

await db.userPurchases.create({
data: {
userId,
courseId,
},
});

return NextResponse.json({ message: 'Purchase Successful' }, { status: 200 });
}
66 changes: 66 additions & 0 deletions src/app/new-courses/[courseId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { RazorPayComponent } from '@/components/razorpay/Razorpay';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import Image from 'next/image';
import db from '@/db';
import { authOptions } from '@/lib/auth';
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';

export default async function CourseDetails({
params,
}: {
params: { courseId: string[] };
}) {
const session = await getServerSession(authOptions);
if (!session?.user) {
redirect('/signin');
}
const course = await db.course.findFirst({
where: {
id: Number(params.courseId),
},
});
if (!course) return <div>Course does not exist</div>;

const ifPurchasedByUser =
(await db.userPurchases.count({
where: {
userId: session.user.id,
courseId: course.id,
},
})) > 0
? true
: false;

return (
<Card className="w-500" key={course.id}>
<CardContent className="flex justify-center">
<Image src={'/harkirat.png'} alt="" width={200} height={100} />
</CardContent>
<CardHeader>
<CardTitle>{course.title}</CardTitle>
<CardDescription>{course.description}</CardDescription>
</CardHeader>
<CardFooter className="flex justify-between">
<Button variant="outline">INR {course.price}</Button>
{ifPurchasedByUser ? (
<button>View Receipt</button>
) : (
<RazorPayComponent
userId={session.user.id}
courseId={course.id}
key={course.id}
/>
)}
</CardFooter>
</Card>
);
}
Loading

0 comments on commit 2cab054

Please sign in to comment.