Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/bank-webhook/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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
//TODO : only fullfill processing transition

const paymentInformation: {
token: string;
userId: string;
Expand Down Expand Up @@ -52,4 +54,7 @@ app.post("/hdfcWebhook", async (req, res) => {

})

app.listen(3003);
app.listen(3003 , () => {
console.log('Bank-Webhook server is running on 3003');

});
2 changes: 0 additions & 2 deletions apps/user-app/.env.example

This file was deleted.

7 changes: 7 additions & 0 deletions apps/user-app/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function Layout({
<SidebarItem href={"/dashboard"} icon={<HomeIcon />} title="Home" />
<SidebarItem href={"/transfer"} icon={<TransferIcon />} title="Transfer" />
<SidebarItem href={"/transactions"} icon={<TransactionsIcon />} title="Transactions" />
<SidebarItem href={"/p2p"} icon={<P2PTransferIcon />} title="P2P Transfer" />
</div>
</div>
{children}
Expand All @@ -36,4 +37,10 @@ function TransactionsIcon() {
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>

}

function P2PTransferIcon() {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" className="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg>
}
7 changes: 7 additions & 0 deletions apps/user-app/app/(dashboard)/p2p/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SendCard } from "../../../components/SendCard";

export default function() {
return <div className="w-full">
<SendCard />
</div>
}
37 changes: 33 additions & 4 deletions apps/user-app/app/(dashboard)/transactions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
import { getServerSession } from "next-auth";
import { authOptions } from "../../lib/auth";
import prisma from "@repo/db/client";
import {
Case,
OnRampTransactions,
} from "../../../components/OnRampTransactions";

export default function() {
return <div>
Transactions
async function p2pTransferDetails() {
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,
case: "Debit" as Case,
}));
}

export default async function () {
const transactions = await p2pTransferDetails();
return (
<div>
<h3 className="p-4">Transactions</h3>
<div className="flex items-center">
<OnRampTransactions transactions={transactions}></OnRampTransactions>
</div>
</div>
}
);
}
8 changes: 5 additions & 3 deletions apps/user-app/app/(dashboard)/transfer/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import prisma from "@repo/db/client";
import { AddMoney } from "../../../components/AddMoneyCard";
import { BalanceCard } from "../../../components/BalanceCard";
import { OnRampTransactions } from "../../../components/OnRampTransactions";
import { OnRampTransactions, TransactionStatus } from "../../../components/OnRampTransactions";
import { getServerSession } from "next-auth";
import { authOptions } from "../../lib/auth";

async function getBalance() {
const session = await getServerSession(authOptions);
// console.log(session);

const balance = await prisma.balance.findFirst({
where: {
userId: Number(session?.user?.id)
}
});
return {
amount: balance?.amount || 0,
locked: balance?.locked || 0
locked: balance?.locked || 0
}
}

Expand All @@ -28,7 +30,7 @@ async function getOnRampTransactions() {
return txns.map(t => ({
time: t.startTime,
amount: t.amount,
status: t.status,
status: t.status as TransactionStatus,
provider: t.provider
}))
}
Expand Down
34 changes: 34 additions & 0 deletions apps/user-app/app/lib/actions/createOnrampTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use server"

import prisma from "@repo/db/client"
import { getServerSession } from "next-auth"
import { authOptions } from "../auth"

export async function createOnRampTransaction(provider:string , amount:number){
// Ideally the token should come from the banking provider (hdfc/axis)

const session = await getServerSession(authOptions)

if(!session?.user || !session?.user?.id){
return{
message : "Unauthenticated request"
}
}

const token = (Math.random() * 1000).toString()

await prisma.onRampTransaction.create({
data : {
provider,
status : "Processing",
startTime : new Date(),
token : token,
userId: Number(session?.user?.id),
amount: amount * 100
}
})

return {
message: "Done"
}
}
60 changes: 60 additions & 0 deletions apps/user-app/app/lib/actions/p2pTransfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"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`; //! LOCKING

const fromBalance = await tx.balance.findUnique({
where: { userId: Number(from) },
});

// console.log("above sleep");
// await new Promise(resolve => setTimeout(resolve , 4000))
// console.log("after sleep");

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(),
},
});
});
}
2 changes: 1 addition & 1 deletion apps/user-app/app/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const authOptions = {
phone: { label: "Phone number", type: "text", placeholder: "1231231231", required: true },
password: { label: "Password", type: "password", required: true }
},
// TODO: User credentials type from next-aut
// TODO: User credentials type from next-auth
async authorize(credentials: any) {
// Do zod validation, OTP validation here
const hashedPassword = await bcrypt.hash(credentials.password, 10);
Expand Down
17 changes: 13 additions & 4 deletions apps/user-app/components/AddMoneyCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"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/createOnrampTransaction";

const SUPPORTED_BANKS = [{
name: "HDFC Bank",
Expand All @@ -16,22 +17,30 @@ const SUPPORTED_BANKS = [{

export const AddMoney = () => {
const [redirectUrl, setRedirectUrl] = useState(SUPPORTED_BANKS[0]?.redirectUrl);

const [provider, setProvider] = useState(SUPPORTED_BANKS[0]?.name || "");
const [value, setValue] = useState(0)


return <Card title="Add Money">
<div className="w-full">
<TextInput label={"Amount"} placeholder={"Amount"} onChange={() => {

<TextInput label={"Amount"} placeholder={"Amount"} onChange={(val) => {
setValue(Number(val))
}} />
<div className="py-4 text-left">
Bank
</div>
<Select onSelect={(value) => {

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
}))} />
<div className="flex justify-center pt-4">
<Button onClick={() => {
<Button onClick={ async () => {
await createOnRampTransaction(provider,value)
window.location.href = redirectUrl || "";
}}>
Add Money
Expand Down
2 changes: 1 addition & 1 deletion apps/user-app/components/AppbarClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function AppbarClient() {
<Appbar onSignin={signIn} onSignout={async () => {
await signOut()
router.push("/api/auth/signin")
}} user={session.data?.user} />
}} user={session?.data?.user} />
</div>
);
}
85 changes: 53 additions & 32 deletions apps/user-app/components/OnRampTransactions.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,60 @@
import { Card } from "@repo/ui/card"
import { Card } from "@repo/ui/card";

export enum TransactionStatus {
Success = "Success",
Failure = "Failure",
Processing = "Processing",
}

export enum Case {
Debit = "Debit",
Credit = "Credit",
}

export const OnRampTransactions = ({
transactions
transactions,
}: {
transactions: {
time: Date,
amount: number,
// TODO: Can the type of `status` be more specific?
status: string,
provider: string
}[]
transactions: {
time: Date;
amount: number;
// TODO: Can the type of `status` be more specific?
status?: TransactionStatus;
provider?: string;
case?: Case;
}[];
}) => {
if (!transactions.length) {
return <Card title="Recent Transactions">
<div className="text-center pb-8 pt-8">
No Recent transactions
if (!transactions.length) {
return (
<Card title="Recent Transactions">
<div className="text-center pb-8 pt-8">No Recent transactions</div>
</Card>
);
}
return (

<Card title="Recent Transactions">
<div className="pt-2">
{transactions.map((t) => (
<div className="flex justify-between gap-6 border-b">
<div>
{t.case === "Debit" ? (
<div className="text-sm">Transfered INR</div>
) : (
<div className="text-sm">Received INR</div>
)}

<div className="text-slate-600 text-xs pb-2">
{t.time.toDateString()}
</div>
</div>
</Card>
}
return <Card title="Recent Transactions">
<div className="pt-2">
{transactions.map(t => <div className="flex justify-between">
<div>
<div className="text-sm">
Received INR
</div>
<div className="text-slate-600 text-xs">
{t.time.toDateString()}
</div>
</div>
<div className="flex flex-col justify-center">
+ Rs {t.amount / 100}
</div>

</div>)}
</div>
<div className="flex flex-col justify-center">
{t.case === "Debit" ? <span className="inline-block"> - Rs {t.amount / 100}</span> : <span> + Rs {t.amount / 100}</span>}

</div>
</div>
))}
</div>
</Card>
}
);
};
Loading