diff --git a/apps/client/app/(lobby)/events/page.tsx b/apps/client/app/(lobby)/events/page.tsx index 6ed6dea6..c4314953 100644 --- a/apps/client/app/(lobby)/events/page.tsx +++ b/apps/client/app/(lobby)/events/page.tsx @@ -1,83 +1,85 @@ +"use server"; import { EventCard } from "@/components/landing/Home/EventCard"; +import { getEvents } from "@/actions/Event/getEvents"; +import { TEvent } from "@opinix/types"; -const Page = () => { - const events = [ - { - icon: "/assets/event1.png", - traders: 31823, - question: "Centre to constitute the 8th Pay Commission?", - yesValue: 4, - noValue: 6, - }, - { - icon: "/assets/event2.png", - traders: 6410, - question: - "Kane Williamson to announce his retirement from international T20 cricket?", - yesValue: 4.5, - noValue: 5.5, - }, - { - icon: "/assets/event3.png", - traders: 7467, - question: - "Tesla to open their first showroom in India by the end of 2024?", - yesValue: 2.5, - noValue: 7.5, - }, - { - icon: "/assets/event4.png", - traders: 1367, - question: "Red Bull Racing to win the F1 Constructors Championship 2024?", - yesValue: 5, - noValue: 5, - }, - { - icon: "/assets/event1.png", - traders: 31823, - question: "Centre to constitute the 8th Pay Commission?", - yesValue: 4, - noValue: 6, - }, - { - icon: "/assets/event2.png", - traders: 6410, - question: - "Kane Williamson to announce his retirement from international T20 cricket?", - yesValue: 4.5, - noValue: 5.5, - }, - { - icon: "/assets/event3.png", - traders: 7467, - question: - "Tesla to open their first showroom in India by the end of 2024?", - yesValue: 2.5, - noValue: 7.5, - }, - { - icon: "/assets/event4.png", - traders: 1367, - question: "Red Bull Racing to win the F1 Constructors Championship 2024?", - yesValue: 5, - noValue: 5, - }, - ]; +const Page = async () => { + const events: TEvent[] = await getEvents(); + // const events = [ + // { + // icon: "/assets/event1.png", + // traders: 31823, + // question: "Centre to constitute the 8th Pay Commission?", + // yesValue: 4, + // noValue: 6, + // }, + // { + // icon: "/assets/event2.png", + // traders: 6410, + // question: + // "Kane Williamson to announce his retirement from international T20 cricket?", + // yesValue: 4.5, + // noValue: 5.5, + // }, + // { + // icon: "/assets/event3.png", + // traders: 7467, + // question: + // "Tesla to open their first showroom in India by the end of 2024?", + // yesValue: 2.5, + // noValue: 7.5, + // }, + // { + // icon: "/assets/event4.png", + // traders: 1367, + // question: "Red Bull Racing to win the F1 Constructors Championship 2024?", + // yesValue: 5, + // noValue: 5, + // }, + // { + // icon: "/assets/event1.png", + // traders: 31823, + // question: "Centre to constitute the 8th Pay Commission?", + // yesValue: 4, + // noValue: 6, + // }, + // { + // icon: "/assets/event2.png", + // traders: 6410, + // question: + // "Kane Williamson to announce his retirement from international T20 cricket?", + // yesValue: 4.5, + // noValue: 5.5, + // }, + // { + // icon: "/assets/event3.png", + // traders: 7467, + // question: + // "Tesla to open their first showroom in India by the end of 2024?", + // yesValue: 2.5, + // noValue: 7.5, + // }, + // { + // icon: "/assets/event4.png", + // traders: 1367, + // question: "Red Bull Racing to win the F1 Constructors Championship 2024?", + // yesValue: 5, + // noValue: 5, + // }, + // ]; return ( <> -
+
{/* Events Grid Section */}
{events.map((event, index) => ( - + ))}
- ); }; - -export default Page; \ No newline at end of file +export default Page; diff --git a/apps/client/app/actions/event.action.ts b/apps/client/app/actions/event.action.ts new file mode 100644 index 00000000..35b22921 --- /dev/null +++ b/apps/client/app/actions/event.action.ts @@ -0,0 +1,32 @@ +"use server"; + +import prisma from "@repo/db/client"; + +import { withServerActionAsyncCatcher } from "@/lib/async-catch"; +import { ServerActionReturnType } from "../types/api"; +import { ErrorHandler } from "@/lib/error"; + +import { SuccessResponse } from "@/lib/success"; + +export const getEvents = withServerActionAsyncCatcher< + null, + ServerActionReturnType +>(async () => { + let events = await prisma.event.findMany({ + select: { + eventId: true, + title: true, + slug: true, + quantity: true, + }, + }); + if (!events) { + throw new ErrorHandler("No events found", "NOT_FOUND"); + } + + return new SuccessResponse( + "Events fetched successfully", + 200, + events + ).serialize(); +}); diff --git a/apps/client/app/config/error.config.ts b/apps/client/app/config/error.config.ts new file mode 100644 index 00000000..27f204ab --- /dev/null +++ b/apps/client/app/config/error.config.ts @@ -0,0 +1,44 @@ +export const ERROR_NAME = { + UNAUTHORIZED: "Unauthorized access", + INTERNAL_SERVER_ERROR: "Internal server error", + BAD_REQUEST: "Bad request", + NOT_FOUND: "Resource not found", + FORBIDDEN: "Access forbidden", + CONFLICT: "Resource conflict", + UNPROCESSABLE_ENTITY: "Unprocessable entity", + TOO_MANY_REQUESTS: "Too many requests", + SERVICE_UNAVAILABLE: "Service unavailable", + GATEWAY_TIMEOUT: "Gateway timeout", + VALIDATION_ERROR: "Validation error", + AUTHENTICATION_FAILED: "Authentication failed", + INSUFFICIENT_PERMISSIONS: "Insufficient permissions", + REQUEST_TIMEOUT: "Request timeout", + UNSUPPORTED_MEDIA_TYPE: "Unsupported media type", + METHOD_NOT_ALLOWED: "Method not allowed", + DATABASE_ERROR: "Database error", + NETWORK_ERROR: "Network error", + RESOURCE_GONE: "Resource gone", + PRECONDITION_FAILED: "Precondition failed", +}; +export const ERROR_CODE = { + UNAUTHORIZED: 401, + INTERNAL_SERVER_ERROR: 500, + BAD_REQUEST: 400, + NOT_FOUND: 404, + FORBIDDEN: 403, + CONFLICT: 409, + UNPROCESSABLE_ENTITY: 422, + TOO_MANY_REQUESTS: 429, + SERVICE_UNAVAILABLE: 503, + GATEWAY_TIMEOUT: 504, + VALIDATION_ERROR: 422, + AUTHENTICATION_FAILED: 401, + INSUFFICIENT_PERMISSIONS: 403, + REQUEST_TIMEOUT: 408, + UNSUPPORTED_MEDIA_TYPE: 415, + METHOD_NOT_ALLOWED: 405, + DATABASE_ERROR: 500, + NETWORK_ERROR: 502, + RESOURCE_GONE: 410, + PRECONDITION_FAILED: 412, +}; diff --git a/apps/client/app/types/api.ts b/apps/client/app/types/api.ts new file mode 100644 index 00000000..b36e5de6 --- /dev/null +++ b/apps/client/app/types/api.ts @@ -0,0 +1,6 @@ +import { ErrorResponseType } from "@/lib/error"; +import { SuccessResponseType } from "@/lib/success"; + +export type ServerActionReturnType = + | SuccessResponseType + | ErrorResponseType; diff --git a/apps/client/components/landing/Home/EventCard.tsx b/apps/client/components/landing/Home/EventCard.tsx index d2898479..1d6e20d9 100644 --- a/apps/client/components/landing/Home/EventCard.tsx +++ b/apps/client/components/landing/Home/EventCard.tsx @@ -1,35 +1,37 @@ import Image from "next/image"; +import { TEvent } from "@opinix/types"; -interface EventCardProps { - icon: string; - traders: number; - question: string; - yesValue: number; - noValue: number; -} - -export const EventCard = ({ - icon, - traders, - question, - yesValue, - noValue, -}: EventCardProps) => ( +export const EventCard = ({ event }: { event: TEvent }) => (
- Event icon + Event icon
- -
{traders} traders
+ +
+ {event.traders} traders +
-

{question}

+

{event.title}

+ {/* TODO: change the min_bet and max_bet with yes or no values ( 19th oct ) */}
diff --git a/apps/client/lib/async-catch.ts b/apps/client/lib/async-catch.ts new file mode 100644 index 00000000..9e2ca2e4 --- /dev/null +++ b/apps/client/lib/async-catch.ts @@ -0,0 +1,14 @@ +import { standardizeApiError } from "./error"; +type withServerActionAsyncCatcherType = (args: T) => Promise; + +export function withServerActionAsyncCatcher( + serverAction: withServerActionAsyncCatcherType +): withServerActionAsyncCatcherType { + return async (args: T): Promise => { + try { + return await serverAction(args); + } catch (error) { + return standardizeApiError(error) as R; + } + }; +} diff --git a/apps/client/lib/error.ts b/apps/client/lib/error.ts new file mode 100644 index 00000000..38f7f2c9 --- /dev/null +++ b/apps/client/lib/error.ts @@ -0,0 +1,65 @@ +import { ERROR_NAME, ERROR_CODE } from "@/app/config/error.config"; +import { ZodError } from "zod"; +import { generateErrorMessage } from "zod-error"; +export type ErrorResponseType = { + name: string; + message: string; + code: number; + status: false; + error?: any; +}; +class ErrorHandler extends Error { + status: false; + error?: any; + code: number; + constructor(message: string, code: keyof typeof ERROR_CODE, error?: any) { + super(message); + this.status = false; + this.error = error; + this.code = ERROR_CODE[code]; + this.name = ERROR_NAME[code]; + } +} + +function standardizeApiError(error: unknown): ErrorResponseType { + if (error instanceof ErrorHandler) { + return { + name: error.name, + message: error.message, + code: error.code, + status: false, + error: error.error, + }; + } + if (error instanceof ZodError) { + return { + name: error.name, + message: generateErrorMessage(error.issues, { + maxErrors: 2, + delimiter: { + component: ": ", + }, + message: { + enabled: true, + label: "", + }, + path: { + enabled: false, + }, + code: { + enabled: false, + }, + }), + code: ERROR_CODE.UNPROCESSABLE_ENTITY, + status: false, + }; + } + return { + name: ERROR_NAME.INTERNAL_SERVER_ERROR, + message: + "We're sorry for the inconvenience. Please report this issue to our support team ", + code: ERROR_CODE.INTERNAL_SERVER_ERROR, + status: false, + }; +} +export { ErrorHandler, standardizeApiError }; diff --git a/apps/client/lib/success.ts b/apps/client/lib/success.ts new file mode 100644 index 00000000..ccb73649 --- /dev/null +++ b/apps/client/lib/success.ts @@ -0,0 +1,27 @@ +class SuccessResponse { + status: true; + code: number; + additional?: T; + message: string; + constructor(message: string, code: number, additional?: T) { + this.message = message; + this.status = true; + this.code = code; + this.additional = additional; + } + serialize() { + return { + status: this.status, + code: this.code, + message: this.message, + additional: this.additional as T, + }; + } +} +export type SuccessResponseType = { + status: true; + code: number; + message: string; + additional?: T; +}; +export { SuccessResponse }; diff --git a/apps/client/package.json b/apps/client/package.json index bc113011..1f643fe3 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-toast": "^1.2.1", "@repo/db": "*", "@repo/order-queue": "*", + "@opinix/types": "*", "chart.js": "^4.4.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/apps/server/package.json b/apps/server/package.json index 7547daef..46584055 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -5,16 +5,16 @@ "scripts": { "start": "node dist/index.js", "build": "npx esbuild ./src/index.ts --bundle --platform=node --outfile=dist/index.js", - "dev": "npm run build && npm run start" + "dev": "nodemon src/index.ts" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { + "@opinix/types": "*", "@prisma/client": "5.20.0", "@repo/db": "*", - "@opinix/types":"*", - "@repo/engine":"*", + "@repo/engine": "*", "@repo/order-queue": "*", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", @@ -25,6 +25,7 @@ "dotenv": "^16.4.5", "express": "^4.21.0", "jsonwebtoken": "^9.0.2", + "nodemon": "^3.1.7", "prisma": "^5.20.0", "ws": "^7.5.5", "zod-error": "^1.5.0" diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma deleted file mode 100644 index cbda196b..00000000 --- a/apps/server/prisma/schema.prisma +++ /dev/null @@ -1,142 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id String @id @default(cuid()) - phoneNumber String @unique - balance Float @default(0.0) - role UserRole - createdAt DateTime @default(now()) - updatedAt DateTime - Event Event[] - OTP OTP[] - Payout Payout[] - portfolio Portfolio? - events Event[] @relation("EventParticipants") -} - -model Portfolio { - id String @id @default(cuid()) - userId String @unique - currentBalances Float @default(0.0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id]) - trades Trade[] -} - -model Event { - id String @id @default(cuid()) - title String - description String - adminId String - status EventStatus @default(ONGOING) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - admin User @relation(fields: [adminId], references: [id]) - orderBook OrderBook? - Trade Trade[] - participants User[] @relation("EventParticipants") -} - -model OrderBook { - id String @id @default(cuid()) - eventId String @unique - topPriceYes Float - topPriceNo Float - no NoOrder[] - event Event @relation(fields: [eventId], references: [id]) - yes YesOrder[] -} - -model YesOrder { - id String @id @default(cuid()) - orderBookId String - price Float - quantity Int - status OrderStatus - createdAt DateTime @default(now()) - orderBook OrderBook @relation(fields: [orderBookId], references: [id]) -} - -model NoOrder { - id String @id @default(cuid()) - orderBookId String - price Float - quantity Int - status OrderStatus - createdAt DateTime @default(now()) - orderBook OrderBook @relation(fields: [orderBookId], references: [id]) -} - -model Trade { - id String @id @default(cuid()) - portfolioId String - eventId String - price Float - quantity Int - side TradeSide - createdAt DateTime @default(now()) - event Event @relation(fields: [eventId], references: [id]) - portfolio Portfolio @relation(fields: [portfolioId], references: [id]) - status TradeStatus @default(ACTIVE) - gainloss Float? @default(0) -} - -model Payout { - id String @id @default(cuid()) - userId String - amount Float - status PayoutStatus - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id]) -} - -model OTP { - id String @id @default(uuid()) - otpID String @unique - otp String @unique - createdAt DateTime @default(now()) - expiresAt DateTime - isVerified Boolean @default(false) - user User? @relation(fields: [userId], references: [id]) - userId String? -} - -enum UserRole { - ADMIN - USER -} - -enum EventStatus { - ONGOING - ENDED -} - -enum TradeSide { - YES - NO -} - -enum PayoutStatus { - PENDING - COMPLETED - FAILED -} -enum TradeStatus{ - ACTIVE - PAST -} - -enum OrderStatus { - PENDING - PLACED -} - diff --git a/apps/server/src/controllers/event/index.ts b/apps/server/src/controllers/event/index.ts new file mode 100644 index 00000000..2b9a47cc --- /dev/null +++ b/apps/server/src/controllers/event/index.ts @@ -0,0 +1,112 @@ +/** + * + */ + +import { AsyncWrapper } from "../../utils/asynCatch"; +import { TEvent } from "@opinix/types"; +import { Request, Response } from "express"; +import { slugify, eventCodeGenerator } from "../../utils/utils"; +import prisma from "@repo/db/client"; +import { + SuccessResponse, + SuccessResponseType, +} from "../../utils/wrappers/success.res"; +import { ErrorHandler } from "../../utils/wrappers/error.res"; +export const createEventHandler = AsyncWrapper( + async (req: Request<{}, {}, Omit>, res) => { + const { + title, + description, + start_date, + end_date, + min_bet, + max_bet, + sot, + quantity, + } = req.body; + console.log(req.body); + let slug = slugify(title); + + let eventCode = eventCodeGenerator(); + // check if event already exists using the slug name + const isEventExists = await prisma.event.findFirst({ + where: { + slug: slug, + }, + }); + if (isEventExists) { + throw new ErrorHandler("Event already exists", "BAD_REQUEST"); + } + // creating the event and sending back the response to the client with the event code + await prisma.event.create({ + data: { + eventId: eventCode, + title, + slug, + description, + start_date, + end_date, + min_bet, + max_bet, + sot, + expiresAt: end_date, + quantity, + }, + }); + + let response = new SuccessResponse("Event created successfully", eventCode); + return res.status(201).json(response); + } +); + +/** + * @description + */ +/** + * url - https://prod.api.probo.in/api/v3/product/events/tradeSummary?eventId=3169798&page=1&pageSize=5 + * @description - Get trade summary for an event + */ + +type TTradeSummary = { + order_book_details: { + orderbook_config: { + socket_events: { + subscribe_msg_name: string; + unsubscribe_msg_name: string; + listener_msg_name: string; + subscription_data: string; + }; + }; + }; +}; +export const getTradeSummaryHandler = AsyncWrapper( + async (req, res: Response>) => { + const { eventId } = req.query; + + const event = await prisma.event.findUnique({ + where: { + eventId: eventId as unknown as number, + }, + }); + if (!event) { + throw new ErrorHandler("Event not found", "NOT_FOUND"); + } + let response = new SuccessResponse( + "Trade summary fetched successfully", + 200, + { + order_book_details: { + orderbook_config: { + socket_events: { + subscribe_msg_name: "subscribe_orderbook", + unsubscribe_msg_name: "unsubscribe_orderbook", + listener_msg_name: `event_orderbook_${eventId}`, + subscription_data: `${eventId}`, + }, + }, + }, + } + ); + return res.status(200).json(response.serialize()); + } +); diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 7d056c51..67cec610 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,8 +1,11 @@ import express from "express"; -import {ORDERBOOK} from "@repo/engine" +import { eventRouter } from "./router/eventRouter"; const app = express(); -console.log(ORDERBOOK); -app.listen(3001, () =>{ - console.log(`server is runnning on http://localhost:3001`) -}) \ No newline at end of file +app.use(express.json()); + +app.use("/events", eventRouter); + +app.listen(3001, () => { + console.log(`server is runnning on http://localhost:3001`); +}); diff --git a/apps/server/src/router/eventRouter.ts b/apps/server/src/router/eventRouter.ts new file mode 100644 index 00000000..da327df6 --- /dev/null +++ b/apps/server/src/router/eventRouter.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import { + createEventHandler, + getTradeSummaryHandler, +} from "../controllers/event"; +const eventRouter = Router(); + +// TODO; implement validation middleware inbetween +eventRouter.post("/create", createEventHandler); + +eventRouter.get("/tradeSummary", getTradeSummaryHandler); +export { eventRouter }; diff --git a/apps/server/src/utils/asynCatch.ts b/apps/server/src/utils/asynCatch.ts index 1926e58b..d9298933 100644 --- a/apps/server/src/utils/asynCatch.ts +++ b/apps/server/src/utils/asynCatch.ts @@ -9,6 +9,7 @@ type AsyncHandler = ( export const AsyncWrapper = (handler: AsyncHandler) => { return (req: Request, res: Response, next: NextFunction) => { handler(req, res, next).catch((error: unknown) => { + console.log(error); const standardizedError = standardizeApiError(error); res.status(standardizedError.code).json(standardizedError); }); diff --git a/apps/server/src/utils/utils.ts b/apps/server/src/utils/utils.ts new file mode 100644 index 00000000..46050461 --- /dev/null +++ b/apps/server/src/utils/utils.ts @@ -0,0 +1,18 @@ +/** + * @description: misc utils functions are defined here + */ + +function randomFourDigitNumber() { + return Math.random().toString(36).substring(2, 6); +} + +function eventCodeGenerator(): number { + return parseInt(Math.random().toString(36).substring(2, 8)); +} +function slugify(name: string) { + return ( + name.toLowerCase().split(" ").join("-") + "-" + randomFourDigitNumber() + ); +} + +export { slugify, eventCodeGenerator }; diff --git a/package-lock.json b/package-lock.json index c0b04a0c..3d054357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@cashfreepayments/cashfree-js": "^1.0.5", "@hookform/resolvers": "^3.9.0", "@next-auth/prisma-adapter": "^1.0.7", + "@opinix/types": "*", "@prisma/client": "^5.20.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-avatar": "^1.1.0", @@ -366,6 +367,7 @@ "dotenv": "^16.4.5", "express": "^4.21.0", "jsonwebtoken": "^9.0.2", + "nodemon": "^3.1.7", "prisma": "^5.20.0", "ws": "^7.5.5", "zod-error": "^1.5.0" @@ -4413,7 +4415,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/constant-case": { @@ -7750,7 +7751,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -7935,6 +7935,11 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9751,6 +9756,53 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -10746,6 +10798,11 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11776,6 +11833,17 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12270,7 +12338,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -12514,6 +12581,14 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -12936,6 +13011,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/packages/db/prisma/migrations/20241019105418_/migration.sql b/packages/db/prisma/migrations/20241019105418_/migration.sql new file mode 100644 index 00000000..d3302ec3 --- /dev/null +++ b/packages/db/prisma/migrations/20241019105418_/migration.sql @@ -0,0 +1,69 @@ +/* + Warnings: + + - You are about to alter the column `balance` on the `User` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Integer`. + - You are about to drop the `NoOrder` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `OrderBook` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Trade` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `YesOrder` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[slug]` on the table `Event` will be added. If there are existing duplicate values, this will fail. + - Added the required column `end_date` to the `Event` table without a default value. This is not possible if the table is not empty. + - Added the required column `expiresAt` to the `Event` table without a default value. This is not possible if the table is not empty. + - Added the required column `max_bet` to the `Event` table without a default value. This is not possible if the table is not empty. + - Added the required column `min_bet` to the `Event` table without a default value. This is not possible if the table is not empty. + - Added the required column `quantity` to the `Event` table without a default value. This is not possible if the table is not empty. + - Added the required column `slug` to the `Event` table without a default value. This is not possible if the table is not empty. + - Added the required column `sot` to the `Event` table without a default value. This is not possible if the table is not empty. + - Added the required column `start_date` to the `Event` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "TradeStatus" AS ENUM ('ACTIVE', 'PAST'); + +-- DropForeignKey +ALTER TABLE "Event" DROP CONSTRAINT "Event_adminId_fkey"; + +-- DropForeignKey +ALTER TABLE "NoOrder" DROP CONSTRAINT "NoOrder_orderBookId_fkey"; + +-- DropForeignKey +ALTER TABLE "OrderBook" DROP CONSTRAINT "OrderBook_eventId_fkey"; + +-- DropForeignKey +ALTER TABLE "Trade" DROP CONSTRAINT "Trade_eventId_fkey"; + +-- DropForeignKey +ALTER TABLE "Trade" DROP CONSTRAINT "Trade_portfolioId_fkey"; + +-- DropForeignKey +ALTER TABLE "YesOrder" DROP CONSTRAINT "YesOrder_orderBookId_fkey"; + +-- AlterTable +ALTER TABLE "Event" ADD COLUMN "end_date" TIMESTAMP(3) NOT NULL, +ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL, +ADD COLUMN "max_bet" DOUBLE PRECISION NOT NULL, +ADD COLUMN "min_bet" DOUBLE PRECISION NOT NULL, +ADD COLUMN "quantity" INTEGER NOT NULL, +ADD COLUMN "slug" TEXT NOT NULL, +ADD COLUMN "sot" TEXT NOT NULL, +ADD COLUMN "start_date" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "balance" SET DEFAULT 0, +ALTER COLUMN "balance" SET DATA TYPE INTEGER, +ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; + +-- DropTable +DROP TABLE "NoOrder"; + +-- DropTable +DROP TABLE "OrderBook"; + +-- DropTable +DROP TABLE "Trade"; + +-- DropTable +DROP TABLE "YesOrder"; + +-- CreateIndex +CREATE UNIQUE INDEX "Event_slug_key" ON "Event"("slug"); diff --git a/packages/db/prisma/migrations/20241019110708_y/migration.sql b/packages/db/prisma/migrations/20241019110708_y/migration.sql new file mode 100644 index 00000000..d32de3c6 --- /dev/null +++ b/packages/db/prisma/migrations/20241019110708_y/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `adminId` on the `Event` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Event" DROP COLUMN "adminId"; diff --git a/packages/db/prisma/migrations/20241019113842_/migration.sql b/packages/db/prisma/migrations/20241019113842_/migration.sql new file mode 100644 index 00000000..fc560737 --- /dev/null +++ b/packages/db/prisma/migrations/20241019113842_/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[eventId]` on the table `Event` will be added. If there are existing duplicate values, this will fail. + - Added the required column `eventId` to the `Event` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Event" ADD COLUMN "eventId" INTEGER NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Event_eventId_key" ON "Event"("eventId"); diff --git a/packages/db/prisma/migrations/20241019122505_traders/migration.sql b/packages/db/prisma/migrations/20241019122505_traders/migration.sql new file mode 100644 index 00000000..9ac614ba --- /dev/null +++ b/packages/db/prisma/migrations/20241019122505_traders/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Event" ADD COLUMN "traders" INTEGER NOT NULL DEFAULT 0; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index ed1f3f0c..4301b83c 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -10,11 +10,10 @@ datasource db { model User { id String @id @default(cuid()) phoneNumber String @unique - balance Int @default(0) + balance Int @default(0) role UserRole createdAt DateTime @default(now()) updatedAt DateTime @default(now()) - Event Event[] OTP OTP[] Payout Payout[] portfolio Portfolio? @@ -28,67 +27,29 @@ model Portfolio { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) - trades Trade[] } +// SOT - source of truth model Event { id String @id @default(cuid()) - title String + eventId Int @unique + slug String @unique description String - adminId String + title String + start_date DateTime + end_date DateTime + expiresAt DateTime + min_bet Float + max_bet Float + quantity Int + sot String + traders Int @default(0) status EventStatus @default(ONGOING) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - admin User @relation(fields: [adminId], references: [id]) - orderBook OrderBook? - Trade Trade[] participants User[] @relation("EventParticipants") } -model OrderBook { - id String @id @default(cuid()) - eventId String @unique - topPriceYes Float - topPriceNo Float - no NoOrder[] - event Event @relation(fields: [eventId], references: [id]) - yes YesOrder[] -} - -model YesOrder { - id String @id @default(cuid()) - orderBookId String - price Float - quantity Int - createdAt DateTime @default(now()) - status OrderStatus - orderBook OrderBook @relation(fields: [orderBookId], references: [id]) -} - -model NoOrder { - id String @id @default(cuid()) - orderBookId String - price Float - quantity Int - createdAt DateTime @default(now()) - status OrderStatus - orderBook OrderBook @relation(fields: [orderBookId], references: [id]) -} - -model Trade { - id String @id @default(cuid()) - portfolioId String - eventId String - price Float - quantity Int - side TradeSide - createdAt DateTime @default(now()) - gainloss Float? @default(0) - status TradeStatus @default(ACTIVE) - event Event @relation(fields: [eventId], references: [id]) - portfolio Portfolio @relation(fields: [portfolioId], references: [id]) -} - model Payout { id String @id @default(cuid()) userId String diff --git a/packages/order-queue/.env.example b/packages/order-queue/.env.example index 0d4defd3..5ef19dff 100644 --- a/packages/order-queue/.env.example +++ b/packages/order-queue/.env.example @@ -1,3 +1 @@ -REDIS_HOST="localhost" -REDIS_PORT="6379" -REDIS_PASSWORD="" \ No newline at end of file +REDIS_URI=redis://localhost:6379 \ No newline at end of file diff --git a/packages/order-queue/logs/server.log b/packages/order-queue/logs/server.log new file mode 100644 index 00000000..a9ea3e47 --- /dev/null +++ b/packages/order-queue/logs/server.log @@ -0,0 +1,10 @@ +[ 2024-10-19 19:59:55 ] - info - WORKER | Starting order worker +[ 2024-10-19 20:00:56 ] - info - WORKER | Starting order worker +[ 2024-10-19 20:00:56 ] - info - SERVER | REDIS: Connected to Redis +[ 2024-10-19 20:00:56 ] - info - SERVER | REDIS: Redis connection is ready +[ 2024-10-19 20:01:12 ] - info - WORKER | Starting order worker +[ 2024-10-19 20:01:12 ] - info - SERVER | REDIS: Connected to Redis +[ 2024-10-19 20:01:12 ] - info - SERVER | REDIS: Redis connection is ready +[ 2024-10-19 20:02:18 ] - info - WORKER | Starting order worker +[ 2024-10-19 20:02:18 ] - info - SERVER | REDIS: Connected to Redis +[ 2024-10-19 20:02:18 ] - info - SERVER | REDIS: Redis connection is ready to start execution diff --git a/packages/order-queue/package.json b/packages/order-queue/package.json index 869c13b1..af93c22f 100644 --- a/packages/order-queue/package.json +++ b/packages/order-queue/package.json @@ -3,9 +3,15 @@ "version": "1.0.0", "main": "index.js", "dependencies": { + "@opinix/logger": "*", "dotenv": "^16.4.5", + "nodemon": "^3.1.7", "redis": "^4.7.0" }, + "scripts": { + "start": "ts-node src/index.ts", + "dev": "nodemon src/index.ts" + }, "exports": { ".": "./src/index.ts" } diff --git a/packages/order-queue/src/config/redisClient.ts b/packages/order-queue/src/config/redisClient.ts new file mode 100644 index 00000000..8106799d --- /dev/null +++ b/packages/order-queue/src/config/redisClient.ts @@ -0,0 +1,38 @@ +import IORedis, { Redis } from "ioredis"; +import "dotenv/config"; +import { logger } from "@opinix/logger"; +const redisUri = process.env.REDIS_URI || "redis://localhost:6379"; +let redisParams = { + maxRetriesPerRequest: null, +}; +let redisClient: Redis | null; +const getRedisClient = () => { + if (!redisClient) { + redisClient = new IORedis(redisUri, { + ...redisParams, + }); + redisClient.on("connect", () => { + logger.info("SERVER | REDIS: Connected to Redis"); + }); + + redisClient.on("ready", () => { + logger.info( + "SERVER | REDIS: Redis connection is ready to start execution" + ); + }); + + redisClient.on("error", (err) => { + logger.error("SERVER: ERROR Connecting to Redis", err); + }); + + redisClient.on("close", () => { + logger.warn("SERVER | REDIS: Connection closed"); + }); + + redisClient.on("reconnecting", () => { + logger.info("SERVER | REDIS: Reconnecting..."); + }); + } + return redisClient; +}; +export default getRedisClient; diff --git a/packages/order-queue/src/index.ts b/packages/order-queue/src/index.ts index 50de55e8..b62f1f02 100644 --- a/packages/order-queue/src/index.ts +++ b/packages/order-queue/src/index.ts @@ -1,11 +1,10 @@ -import { createClient } from "redis"; -import dotenv from "dotenv"; -dotenv.config(); +import { addToOrderQueue } from "./queues/orderQueue"; +import orderWorker from "./queues/orderProcessor"; +import { logger } from "@opinix/logger"; +const startWorker = async () => { + logger.info("WORKER | Starting order worker"); + orderWorker; +}; -export const redisClient = createClient({ -// password: process.env.REDIS_PASSWORD, - socket: { - host: process.env.REDIS_HOST, - port: process.env.REDIS_PORT as unknown as number, - }, -}); \ No newline at end of file +startWorker(); +export { addToOrderQueue }; diff --git a/packages/order-queue/src/queues/orderProcessor.ts b/packages/order-queue/src/queues/orderProcessor.ts new file mode 100644 index 00000000..a188127c --- /dev/null +++ b/packages/order-queue/src/queues/orderProcessor.ts @@ -0,0 +1,21 @@ +import { Worker } from "bullmq"; +import getRedisClient from "../config/redisClient"; +let redisClient = getRedisClient(); + +const orderWorker = new Worker( + "orderQueue", + async (job) => { + try { + console.log(`Processing order: ${JSON.stringify(job.data)}`); + } catch (error) { + if (error instanceof Error) + console.error(`Error processing order: ${error.message}`); + else console.error(`Error processing order: ${error}`); + } + }, + { + connection: redisClient, + } +); + +export default orderWorker; diff --git a/packages/order-queue/src/queues/orderQueue.ts b/packages/order-queue/src/queues/orderQueue.ts new file mode 100644 index 00000000..5456a2e1 --- /dev/null +++ b/packages/order-queue/src/queues/orderQueue.ts @@ -0,0 +1,16 @@ +import { Queue } from "bullmq"; +import getRedisClient from "../config/redisClient"; +let redisClient = getRedisClient(); +export const orderQueue = new Queue("orderQueue", { + connection: redisClient, +}); + +export const addToOrderQueue = async (order: object) => { + await orderQueue.add("order", order, { + attempts: 3, + backoff: { + type: "exponential", + delay: 5000, + }, + }); +}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index abfe4d54..245222d8 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,3 +1,4 @@ +import { OrderStatus } from "@prisma/client"; export enum EOrderStatus { PENDING = "PENDING", PLACED = "PLACED", @@ -8,7 +9,6 @@ export enum sides { NO = "no", } -import { OrderStatus } from "@prisma/client"; export type TOrder = { id: string; orderBookId: string; @@ -26,3 +26,18 @@ export type TOrderbookForOrders = { yes: TOrder[]; no: TOrder[]; }; + +export type TEvent = { + id: string; + title: string; + slug: string; + description: string; + start_date: Date; + end_date: Date; + createdAt: Date; + min_bet: number; + max_bet: number; + sot: string; + traders: number; + quantity: number; +};