From e55d2c8b1ffb112745815db4223632e1b3e76be5 Mon Sep 17 00:00:00 2001 From: Chhatrapalsinh Zala Date: Sun, 31 Mar 2024 15:42:19 +0530 Subject: [PATCH 1/2] completed p2p transaction with history. --- apps/user-app/app/(dashboard)/layout.tsx | 114 ++++++++++++++---- apps/user-app/app/(dashboard)/p2p/page.tsx | 63 ++++++++++ .../app/lib/actions/createOnRampTxn.ts | 33 +++++ apps/user-app/app/lib/actions/p2pTransfer.ts | 60 +++++++++ apps/user-app/components/AddMoneyCard.tsx | 95 +++++++++------ apps/user-app/components/P2PTransactions.tsx | 43 +++++++ apps/user-app/components/SendCard.tsx | 46 +++++++ .../migration.sql | 16 +++ packages/db/prisma/schema.prisma | 12 ++ 9 files changed, 424 insertions(+), 58 deletions(-) create mode 100644 apps/user-app/app/(dashboard)/p2p/page.tsx create mode 100644 apps/user-app/app/lib/actions/createOnRampTxn.ts create mode 100644 apps/user-app/app/lib/actions/p2pTransfer.ts create mode 100644 apps/user-app/components/P2PTransactions.tsx create mode 100644 apps/user-app/components/SendCard.tsx create mode 100644 packages/db/prisma/migrations/20240331072220_added_p2p_txn/migration.sql diff --git a/apps/user-app/app/(dashboard)/layout.tsx b/apps/user-app/app/(dashboard)/layout.tsx index 90dc203..3149a2f 100644 --- a/apps/user-app/app/(dashboard)/layout.tsx +++ b/apps/user-app/app/(dashboard)/layout.tsx @@ -1,39 +1,109 @@ -import { SidebarItem } from "../../components/SidebarItem"; +import { SidebarItem } from '../../components/SidebarItem' export default function Layout({ children, }: { - children: React.ReactNode; + children: React.ReactNode }): JSX.Element { return ( -
-
-
- } title="Home" /> - } title="Transfer" /> - } title="Transactions" /> -
+
+
+
+ } title='Home' /> + } + title='Transfer' + /> + } + title='Transactions' + /> + } + title='Transactions' + />
- {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 P2PTransferIcon() { + 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..b050b19 --- /dev/null +++ b/apps/user-app/app/(dashboard)/p2p/page.tsx @@ -0,0 +1,63 @@ +import { getServerSession } from 'next-auth' +import { SendCard } from '../../../components/SendCard' +import { authOptions } from '../../lib/auth' +import prisma from '@repo/db/client' +import { BalanceCard } from '../../../components/BalanceCard' +import { P2PTransactions } from '../../../components/P2PTransactions' + +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, + } +} + +async function getP2PTransactions() { + const session = await getServerSession(authOptions) + const txns = await prisma.p2pTransfer.findMany({ + where: { + OR: [ + { + fromUserId: Number(session?.user?.id), + }, + { toUserId: Number(session?.user?.id) }, + ], + }, + }) + + return txns.map((t) => ({ + time: t.timestamp, + amount: t.amount, + transactionType: t.fromUserId == session?.user?.id ? 'Sent' : 'Received', + })) +} + +export default async function () { + const balance = await getBalance() + const transactions = await getP2PTransactions() + + return ( +
+
+ P2P Transfer +
+
+
+ +
+
+ +
+ +
+
+
+
+ ) +} diff --git a/apps/user-app/app/lib/actions/createOnRampTxn.ts b/apps/user-app/app/lib/actions/createOnRampTxn.ts new file mode 100644 index 0000000..adf70a9 --- /dev/null +++ b/apps/user-app/app/lib/actions/createOnRampTxn.ts @@ -0,0 +1,33 @@ +'use server' + +import { getServerSession } from 'next-auth' +import { authOptions } from '../auth' +import prisma from '@repo/db/client' + +export async function createOnRampTransaction( + amount: number, + provider: string +) { + const session = await getServerSession(authOptions) + const token = Math.random().toString() + const userId = await session.user.id + + if (!userId) { + return { + message: 'User not logged in.', + } + } + await prisma.onRampTransaction.create({ + data: { + status: 'Processing', + provider: provider, + amount: amount, + startTime: new Date(), + userId: Number(userId), + token: token, + }, + }) + return { + message: 'On ramp transaction added.', + } +} diff --git a/apps/user-app/app/lib/actions/p2pTransfer.ts b/apps/user-app/app/lib/actions/p2pTransfer.ts new file mode 100644 index 0000000..7f40fdb --- /dev/null +++ b/apps/user-app/app/lib/actions/p2pTransfer.ts @@ -0,0 +1,60 @@ +'use server' +import { getServerSession } from 'next-auth' +import { authOptions } from '../auth' +import prisma from '@repo/db/client' +import { resolve } from 'path' + +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) => { + // Lockin for the prostgress so that while updating can be done only by one user. + await tx.$queryRaw`SELECT * FROM "Balance" WHERE "userId" = ${Number(from)} FOR UPDATE` + + const fromBalance = await tx.balance.findUnique({ + where: { userId: Number(from) }, + }) + // await new Promise((resolve) => setTimeout(resolve, 4000)) + + 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(), + }, + }) + }) + return { + message: 'Transaction successful...', + } +} diff --git a/apps/user-app/components/AddMoneyCard.tsx b/apps/user-app/components/AddMoneyCard.tsx index 562f021..711c2cb 100644 --- a/apps/user-app/components/AddMoneyCard.tsx +++ b/apps/user-app/components/AddMoneyCard.tsx @@ -1,42 +1,65 @@ -"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"; +'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 { createOnRampTransaction } from '../app/lib/actions/createOnRampTxn' -const SUPPORTED_BANKS = [{ - name: "HDFC Bank", - redirectUrl: "https://netbanking.hdfcbank.com" -}, { - name: "Axis Bank", - redirectUrl: "https://www.axisbank.com/" -}]; +const SUPPORTED_BANKS = [ + { + name: 'HDFC Bank', + redirectUrl: 'https://netbanking.hdfcbank.com', + }, + { + name: 'Axis Bank', + 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)?.redirectUrl || '' + ) + }} + options={SUPPORTED_BANKS.map((x) => ({ key: x.name, - value: x.name - }))} /> -
- +
-
-
-} \ No newline at end of file +
+ + ) +} diff --git a/apps/user-app/components/P2PTransactions.tsx b/apps/user-app/components/P2PTransactions.tsx new file mode 100644 index 0000000..09a212d --- /dev/null +++ b/apps/user-app/components/P2PTransactions.tsx @@ -0,0 +1,43 @@ +import { Card } from '@repo/ui/card' + +export const P2PTransactions = ({ + transactions, +}: { + transactions: { + time: Date + amount: number + transactionType: string + }[] +}) => { + if (!transactions.length) { + return ( + +
No transactions
+
+ ) + } + return ( + +
+ {transactions.map((t) => ( +
+
+ {t.transactionType === 'Sent' ? ( +
Sent INR
+ ) : ( +
Received INR
+ )} +
+ {t.time.toDateString()} +
+
+
+ {t.transactionType === 'Send' ? <>- : <>+} Rs{' '} + {t.amount / 100} +
+
+ ))} +
+
+ ) +} diff --git a/apps/user-app/components/SendCard.tsx b/apps/user-app/components/SendCard.tsx new file mode 100644 index 0000000..68e728d --- /dev/null +++ b/apps/user-app/components/SendCard.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/actions/p2pTransfer' + +export function SendCard() { + const [number, setNumber] = useState('') + const [amount, setAmount] = useState(0) + + return ( +
+
+ +
+ { + setNumber(value) + }} + /> + { + setAmount(Number(value)) + }} + /> +
+ +
+
+
+
+
+ ) +} diff --git a/packages/db/prisma/migrations/20240331072220_added_p2p_txn/migration.sql b/packages/db/prisma/migrations/20240331072220_added_p2p_txn/migration.sql new file mode 100644 index 0000000..f84afa1 --- /dev/null +++ b/packages/db/prisma/migrations/20240331072220_added_p2p_txn/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..8c0c5bc 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") + receiveTransfers p2pTransfer[] @relation(name : "ToUserRelation") } model Merchant { @@ -35,6 +37,16 @@ model OnRampTransaction { 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]) +} + model Balance { id Int @id @default(autoincrement()) userId Int @unique From e36628e8dbdd9ce2a6288576ce0a085f515c2096 Mon Sep 17 00:00:00 2001 From: Chhatrapalsinh Zala Date: Sun, 31 Mar 2024 16:04:36 +0530 Subject: [PATCH 2/2] fixed small bug --- apps/user-app/components/P2PTransactions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/user-app/components/P2PTransactions.tsx b/apps/user-app/components/P2PTransactions.tsx index 09a212d..8af1c30 100644 --- a/apps/user-app/components/P2PTransactions.tsx +++ b/apps/user-app/components/P2PTransactions.tsx @@ -32,7 +32,7 @@ export const P2PTransactions = ({
- {t.transactionType === 'Send' ? <>- : <>+} Rs{' '} + {t.transactionType === 'Sent' ? <>- : <>+} Rs{' '} {t.amount / 100}