diff --git a/apps/bank-webhook/src/index.ts b/apps/bank-webhook/src/index.ts index bb92555..67a8bd9 100644 --- a/apps/bank-webhook/src/index.ts +++ b/apps/bank-webhook/src/index.ts @@ -2,54 +2,52 @@ import express from "express"; import db from "@repo/db/client"; const app = express(); -app.use(express.json()) +app.use(express.json()); app.post("/hdfcWebhook", async (req, res) => { - //TODO: Add zod validation here? - //TODO: HDFC bank should ideally send us a secret so we know this is sent by them - const paymentInformation: { - token: string; - userId: string; - amount: string - } = { - token: req.body.token, - userId: req.body.user_identifier, - amount: req.body.amount - }; + //TODO: Add zod validation here? + //TODO: HDFC bank should ideally send us a secret so we know this is sent by them + const paymentInformation: { + token: string; + userId: string; + amount: string; + } = { + token: req.body.token, + userId: req.body.user_identifier, + amount: req.body.amount, + }; - try { - await db.$transaction([ - db.balance.updateMany({ - where: { - userId: Number(paymentInformation.userId) - }, - data: { - amount: { - // You can also get this from your DB - increment: Number(paymentInformation.amount) - } - } - }), - db.onRampTransaction.updateMany({ - where: { - token: paymentInformation.token - }, - data: { - status: "Success", - } - }) - ]); + try { + await db.$transaction([ + db.balance.updateMany({ + where: { + userId: Number(paymentInformation.userId), + }, + data: { + amount: { + increment: Number(paymentInformation.amount), + }, + }, + }), + db.onRampTransaction.updateMany({ + where: { + token: paymentInformation.token, + }, + data: { + status: "Success", + }, + }), + ]); - res.json({ - message: "Captured" - }) - } catch(e) { - console.error(e); - res.status(411).json({ - message: "Error while processing webhook" - }) - } + res.json({ + message: "Captured", + }); + } catch (e) { + console.error(e); + res.status(411).json({ + message: "Error while processing webhook", + }); + } +}); -}) - -app.listen(3003); \ No newline at end of file +app.listen(3003); diff --git a/apps/user-app/app/(dashboard)/layout.tsx b/apps/user-app/app/(dashboard)/layout.tsx index 90dc203..8a10e9c 100644 --- a/apps/user-app/app/(dashboard)/layout.tsx +++ b/apps/user-app/app/(dashboard)/layout.tsx @@ -7,33 +7,98 @@ export default function Layout({ }): JSX.Element { return (
-
-
- } title="Home" /> - } title="Transfer" /> - } title="Transactions" /> -
+
+
+ } title="Home" /> + } + title="Transfer" + /> + } + title="Transactions" + /> + } title="P2P " />
- {children} +
+ {children}
); } // Icons Fetched from https://heroicons.com/ function HomeIcon() { - return - - + return ( + + + + ); } function TransferIcon() { - return - - + return ( + + + + ); } function TransactionsIcon() { - return - - - -} \ No newline at end of file + return ( + + + + ); +} +function P2PTr() { + return ( + + + + ); +} diff --git a/apps/user-app/app/(dashboard)/p2p/page.tsx b/apps/user-app/app/(dashboard)/p2p/page.tsx new file mode 100644 index 0000000..c190a64 --- /dev/null +++ b/apps/user-app/app/(dashboard)/p2p/page.tsx @@ -0,0 +1,38 @@ +"use server"; +import { getServerSession } from "next-auth"; +import { SendCard } from "../../../components/card-stack"; +import { authOptions } from "../../lib/auth"; +import prisma from "@repo/db/client"; +import P2PTransaction from "../../../components/p2pTransactions"; +async function getp2pTransaction() { + const session = await getServerSession(authOptions); + const txns = await prisma.p2pTransfer.findMany({ + where: { + fromUserId: Number(session?.user?.id), + }, + }); + return txns.map((t) => ({ + time: t.timestamp, + amount: t.amount, + toUserId: t.toUserId, + })); +} +export default async function () { + const transactions = await getp2pTransaction(); + + return ( +
+
+ Transfer +
+
+
+ +
+
+ +
+
+
+ ); +} diff --git a/apps/user-app/app/(dashboard)/transfer/page.tsx b/apps/user-app/app/(dashboard)/transfer/page.tsx index 1e0b889..8612f0a 100644 --- a/apps/user-app/app/(dashboard)/transfer/page.tsx +++ b/apps/user-app/app/(dashboard)/transfer/page.tsx @@ -6,51 +6,53 @@ import { getServerSession } from "next-auth"; import { authOptions } from "../../lib/auth"; async function getBalance() { - const session = await getServerSession(authOptions); - const balance = await prisma.balance.findFirst({ - where: { - userId: Number(session?.user?.id) - } - }); - return { - amount: balance?.amount || 0, - locked: balance?.locked || 0 - } + const session = await getServerSession(authOptions); + const balance = await prisma.balance.findFirst({ + where: { + userId: Number(session?.user?.id), + }, + }); + return { + amount: balance?.amount || 0, + locked: balance?.locked || 0, + }; } async function getOnRampTransactions() { - const session = await getServerSession(authOptions); - const txns = await prisma.onRampTransaction.findMany({ - where: { - userId: Number(session?.user?.id) - } - }); - return txns.map(t => ({ - time: t.startTime, - amount: t.amount, - status: t.status, - provider: t.provider - })) + const session = await getServerSession(authOptions); + const txns = await prisma.onRampTransaction.findMany({ + where: { + userId: Number(session?.user?.id), + }, + }); + return txns.map((t) => ({ + time: t.startTime, + amount: t.amount, + status: t.status, + provider: t.provider, + })); } -export default async function() { - const balance = await getBalance(); - const transactions = await getOnRampTransactions(); +export default async function () { + const balance = await getBalance(); + const transactions = await getOnRampTransactions(); - return
-
- Transfer + return ( +
+
+ Transfer +
+
+
+
-
-
- -
-
- -
- -
-
+
+ +
+ +
+
-} \ No newline at end of file + ); +} diff --git a/apps/user-app/app/lib/newTransactions/createOnRampTransactions.ts b/apps/user-app/app/lib/newTransactions/createOnRampTransactions.ts new file mode 100644 index 0000000..246429a --- /dev/null +++ b/apps/user-app/app/lib/newTransactions/createOnRampTransactions.ts @@ -0,0 +1,30 @@ +"use server"; + +import { getServerSession } from "next-auth"; +import prisma from "@repo/db/client"; +import { authOptions } from "../auth"; + +export default async function createOnRampTransactions( + amount: number, + provider: string +) { + const session = await getServerSession(authOptions); + const userId = session?.user?.id; + if (!userId) { + message: "User not logged in"; + } + const token = (Math.random() * 1000).toString(); + await prisma.onRampTransaction.create({ + data: { + provider, + status: "Processing", + userId: Number(session?.user?.id), + token: token, + amount: amount * 10, + startTime: new Date(), + }, + }); + return { + msg: "Done", + }; +} diff --git a/apps/user-app/app/lib/p2ptransfer/p2p.ts b/apps/user-app/app/lib/p2ptransfer/p2p.ts new file mode 100644 index 0000000..6870ee6 --- /dev/null +++ b/apps/user-app/app/lib/p2ptransfer/p2p.ts @@ -0,0 +1,55 @@ +"use server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../auth"; +import prisma from "@repo/db/client"; + +export async function p2pTransfer(to: string, amount: number) { + const session = await getServerSession(authOptions); + const from = session?.user?.id; + if (!from) { + return { + message: "Error while sending", + }; + } + const toUser = await prisma.user.findFirst({ + where: { + number: to, + }, + }); + + if (!toUser) { + return { + message: "User not found", + }; + } + + await prisma.$transaction(async (tx) => { + // await tx.$queryRaw`SELECT * FROM "Balance" WHERE "userId" = ${Number(from)} FOR UPDATE`; + + const fromBalance = await tx.balance.findUnique({ + where: { userId: Number(from) }, + }); + + if (!fromBalance || fromBalance.amount < amount) { + throw new Error("Insufficient funds"); + } + + await tx.balance.update({ + where: { userId: Number(from) }, + data: { amount: { decrement: amount } }, + }); + + await tx.balance.update({ + where: { userId: toUser.id }, + data: { amount: { increment: amount } }, + }); + await tx.p2pTransfer.create({ + data: { + fromUserId: Number(from), + toUserId: toUser.id, + amount, + timestamp: new Date(), + }, + }); + }); +} diff --git a/apps/user-app/components/AddMoneyCard.tsx b/apps/user-app/components/AddMoneyCard.tsx index 562f021..cfc4a94 100644 --- a/apps/user-app/components/AddMoneyCard.tsx +++ b/apps/user-app/components/AddMoneyCard.tsx @@ -1,42 +1,65 @@ -"use client" +"use client"; import { Button } from "@repo/ui/button"; import { Card } from "@repo/ui/card"; import { Center } from "@repo/ui/center"; import { Select } from "@repo/ui/select"; import { useState } from "react"; import { TextInput } from "@repo/ui/textinput"; +import createOnRampTransactions from "../app/lib/newTransactions/createOnRampTransactions"; -const SUPPORTED_BANKS = [{ +const SUPPORTED_BANKS = [ + { name: "HDFC Bank", - redirectUrl: "https://netbanking.hdfcbank.com" -}, { + redirectUrl: "https://netbanking.hdfcbank.com", + }, + { name: "Axis Bank", - redirectUrl: "https://www.axisbank.com/" -}]; + redirectUrl: "https://www.axisbank.com/", + }, +]; export const AddMoney = () => { - const [redirectUrl, setRedirectUrl] = useState(SUPPORTED_BANKS[0]?.redirectUrl); - return -
- { - - }} /> -
- Bank -
- { + setRedirectUrl( + SUPPORTED_BANKS.find((x) => x.name === value)?.redirectUrl || "" + ); + setProvider( + SUPPORTED_BANKS.find((x) => x.name === value)?.name || "" + ); + }} + options={SUPPORTED_BANKS.map((x) => ({ key: x.name, - value: x.name - }))} /> + value: x.name, + }))} + />
- +
-
-
-} \ No newline at end of file +
+ + ); +}; diff --git a/apps/user-app/components/card-stack.tsx b/apps/user-app/components/card-stack.tsx new file mode 100644 index 0000000..6274111 --- /dev/null +++ b/apps/user-app/components/card-stack.tsx @@ -0,0 +1,46 @@ +"use client"; +import { Button } from "@repo/ui/button"; +import { Card } from "@repo/ui/card"; +import { Center } from "@repo/ui/center"; +import { TextInput } from "@repo/ui/textinput"; +import { useState } from "react"; +import { p2pTransfer } from "../app/lib/p2ptransfer/p2p"; + +export function SendCard() { + const [number, setNumber] = useState(""); + const [amount, setAmount] = useState(""); + + return ( +
+
+ +
+ { + setNumber(value); + }} + /> + { + setAmount(value); + }} + /> +
+ +
+
+
+
+
+ ); +} diff --git a/apps/user-app/components/p2pTransactions.tsx b/apps/user-app/components/p2pTransactions.tsx new file mode 100644 index 0000000..6ac37f7 --- /dev/null +++ b/apps/user-app/components/p2pTransactions.tsx @@ -0,0 +1,39 @@ +import { Card } from "@repo/ui/card"; +const P2PTransaction = ({ + transactions, +}: { + transactions: { + time: Date; + amount: number; + toUserId: number; + }[]; +}) => { + if (!transactions.length) { + return ( + +
No Recent transactions
+
+ ); + } + return ( + +
+ {transactions.map((t) => ( +
+
+
{`Send Money TO ${t.toUserId}`}
+
+ {t.time.toDateString()} +
+
+
+ - Rs {t.amount / 100} +
+
+ ))} +
+
+ ); +}; + +export default P2PTransaction; diff --git a/apps/user-app/package.json b/apps/user-app/package.json index 4f85ee6..4008439 100644 --- a/apps/user-app/package.json +++ b/apps/user-app/package.json @@ -15,6 +15,7 @@ "bcrypt": "^5.1.1", "next": "^14.1.1", "next-auth": "^4.24.7", + "prisma": "^5.14.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/apps/user-app/utils/cn.ts b/apps/user-app/utils/cn.ts new file mode 100644 index 0000000..cec6ac9 --- /dev/null +++ b/apps/user-app/utils/cn.ts @@ -0,0 +1,6 @@ +import { ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/package-lock.json b/package-lock.json index 1ba3d23..cf235b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,11 @@ "apps/*", "packages/*" ], + "dependencies": { + "clsx": "^2.1.1", + "framer-motion": "^11.2.5", + "tailwind-merge": "^2.3.0" + }, "devDependencies": { "@repo/eslint-config": "*", "@repo/typescript-config": "*", @@ -85,6 +90,7 @@ "bcrypt": "^5.1.1", "next": "^14.1.1", "next-auth": "^4.24.7", + "prisma": "^5.14.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -103,6 +109,46 @@ "typescript": "^5.3.3" } }, + "apps/user-app/node_modules/@prisma/debug": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.14.0.tgz", + "integrity": "sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==" + }, + "apps/user-app/node_modules/@prisma/engines": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.14.0.tgz", + "integrity": "sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==", + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.14.0", + "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/fetch-engine": "5.14.0", + "@prisma/get-platform": "5.14.0" + } + }, + "apps/user-app/node_modules/@prisma/engines-version": { + "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz", + "integrity": "sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==" + }, + "apps/user-app/node_modules/@prisma/fetch-engine": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz", + "integrity": "sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==", + "dependencies": { + "@prisma/debug": "5.14.0", + "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/get-platform": "5.14.0" + } + }, + "apps/user-app/node_modules/@prisma/get-platform": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.14.0.tgz", + "integrity": "sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==", + "dependencies": { + "@prisma/debug": "5.14.0" + } + }, "apps/user-app/node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -131,6 +177,21 @@ "node": "^10 || ^12 || >=14" } }, + "apps/user-app/node_modules/prisma": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.14.0.tgz", + "integrity": "sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.14.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "apps/web": { "version": "1.0.0", "extraneous": true, @@ -3253,6 +3314,14 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -5374,6 +5443,30 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.5.tgz", + "integrity": "sha512-X22i42hWY423wx2C1TlQlC4UnWonD+udND0qX1Fkt0dDlreSmuNY76obO6Y2d/UdJPhqVd5Zn6g1jAIwF6Xx9A==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -9786,6 +9879,18 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tailwind-merge": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz", + "integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==", + "dependencies": { + "@babel/runtime": "^7.24.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", diff --git a/package.json b/package.json index 10f09f0..9489cfd 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,10 @@ "workspaces": [ "apps/*", "packages/*" - ] + ], + "dependencies": { + "clsx": "^2.1.1", + "framer-motion": "^11.2.5", + "tailwind-merge": "^2.3.0" + } } diff --git a/packages/db/.env.example b/packages/db/.env.example index fe001e3..ed7284f 100644 --- a/packages/db/.env.example +++ b/packages/db/.env.example @@ -1,2 +1,2 @@ -DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres" +DATABASE_URL="postgresql://ranu47243:Fg9sfTaItk5m@ep-withered-violet-a5esdc0g.us-east-2.aws.neon.tech/Demo?sslmode=require" diff --git a/packages/db/prisma/migrations/20240521195456_add_p2p_transfer/migration.sql b/packages/db/prisma/migrations/20240521195456_add_p2p_transfer/migration.sql new file mode 100644 index 0000000..f84afa1 --- /dev/null +++ b/packages/db/prisma/migrations/20240521195456_add_p2p_transfer/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "p2pTransfer" ( + "id" SERIAL NOT NULL, + "amount" INTEGER NOT NULL, + "timestamp" TIMESTAMP(3) NOT NULL, + "fromUserId" INTEGER NOT NULL, + "toUserId" INTEGER NOT NULL, + + CONSTRAINT "p2pTransfer_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "p2pTransfer" ADD CONSTRAINT "p2pTransfer_fromUserId_fkey" FOREIGN KEY ("fromUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "p2pTransfer" ADD CONSTRAINT "p2pTransfer_toUserId_fkey" FOREIGN KEY ("toUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 05ad1f7..14ee162 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -15,6 +15,8 @@ model User { password String OnRampTransaction OnRampTransaction[] Balance Balance[] + sentTransfers p2pTransfer[] @relation(name: "FromUserRelation") + receivedTransfers p2pTransfer[] @relation(name: "ToUserRelation") } model Merchant { @@ -43,6 +45,16 @@ model Balance { user User @relation(fields: [userId], references: [id]) } +model p2pTransfer { + id Int @id @default(autoincrement()) + amount Int + timestamp DateTime + fromUserId Int + fromUser User @relation(name: "FromUserRelation", fields: [fromUserId], references: [id]) + toUserId Int + toUser User @relation(name: "ToUserRelation", fields: [toUserId], references: [id]) +} + enum AuthType { Google Github