diff --git a/bun.lock b/bun.lock index 43f1fe9..6f4638f 100644 --- a/bun.lock +++ b/bun.lock @@ -7,12 +7,14 @@ "dependencies": { "@elysiajs/cors": "^1.4.1", "@prisma/adapter-pg": "^7.3.0", - "@prisma/client": "^7.3.0", + "@prisma/client": "7.3.0", "better-auth": "^1.4.18", + "dotenv": "^17.3.1", "elysia": "^1.4.22", "nodemailer": "^8.0.0", "pg": "^8.18.0", "pino": "^10.3.0", + "razorpay": "^2.9.6", }, "devDependencies": { "@biomejs/biome": "^2.3.14", @@ -136,10 +138,14 @@ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], + "better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="], "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], @@ -150,6 +156,8 @@ "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -162,6 +170,8 @@ "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], @@ -182,11 +192,15 @@ "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], @@ -200,6 +214,14 @@ "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], @@ -218,22 +240,40 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="], "graphmatch": ["graphmatch@1.1.0", "", {}, "sha512-0E62MaTW5rPZVRLyIJZG/YejmdA/Xr1QydHEw3Vt+qOKkMIOE8WDLc9ZX2bmAjtJFZcId4lEdrdmASsEy7D1QA=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], @@ -276,10 +316,16 @@ "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -362,12 +408,16 @@ "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "razorpay": ["razorpay@2.9.6", "", { "dependencies": { "axios": "^1.6.8" } }, "sha512-zsHAQzd6e1Cc6BNoCNZQaf65ElL6O6yw0wulxmoG5VQDr363fZC90Mp1V5EktVzG45yPyNomNXWlf4cQ3622gQ=="], + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], @@ -466,6 +516,8 @@ "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + "c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], diff --git a/package.json b/package.json index c81e3cb..9884fb8 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,14 @@ "dependencies": { "@elysiajs/cors": "^1.4.1", "@prisma/adapter-pg": "^7.3.0", - "@prisma/client": "^7.3.0", + "@prisma/client": "7.3.0", "better-auth": "^1.4.18", + "dotenv": "^17.3.1", "elysia": "^1.4.22", "nodemailer": "^8.0.0", "pg": "^8.18.0", - "pino": "^10.3.0" + "pino": "^10.3.0", + "razorpay": "^2.9.6" }, "devDependencies": { "@biomejs/biome": "^2.3.14", diff --git a/prisma/migrations/20260310174111_testing/migration.sql b/prisma/migrations/20260310174111_testing/migration.sql new file mode 100644 index 0000000..551db54 --- /dev/null +++ b/prisma/migrations/20260310174111_testing/migration.sql @@ -0,0 +1,42 @@ +-- CreateTable +CREATE TABLE "template" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "ownerId" TEXT, + "isBuiltIn" BOOLEAN NOT NULL DEFAULT false, + "sourceTemplateId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "template_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "template_fields" ( + "id" TEXT NOT NULL, + "fieldName" TEXT NOT NULL, + "label" TEXT, + "fieldValueType" TEXT NOT NULL, + "fieldType" TEXT NOT NULL, + "validation" JSONB, + "options" JSONB, + "prevFieldId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "templateId" TEXT NOT NULL, + + CONSTRAINT "template_fields_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "template_fields_templateId_idx" ON "template_fields"("templateId"); + +-- CreateIndex +CREATE INDEX "template_fields_templateId_prevFieldId_idx" ON "template_fields"("templateId", "prevFieldId"); + +-- AddForeignKey +ALTER TABLE "template" ADD CONSTRAINT "template_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "template_fields" ADD CONSTRAINT "template_fields_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "template"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260310184414_payment/migration.sql b/prisma/migrations/20260310184414_payment/migration.sql new file mode 100644 index 0000000..b274bb0 --- /dev/null +++ b/prisma/migrations/20260310184414_payment/migration.sql @@ -0,0 +1,48 @@ +/* + Warnings: + + - A unique constraint covering the columns `[paymentId]` on the table `form_response` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "form_response" ADD COLUMN "paymentId" TEXT; + +-- CreateTable +CREATE TABLE "payment" ( + "id" TEXT NOT NULL, + "amount" DECIMAL(10,2) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'usd', + "status" TEXT NOT NULL DEFAULT 'pending', + "stripeSessionId" TEXT, + "stripePaymentId" TEXT, + "formId" TEXT NOT NULL, + "formFieldId" TEXT NOT NULL, + "formResponseId" TEXT, + "respondentId" TEXT, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "payment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "payment_stripeSessionId_key" ON "payment"("stripeSessionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "payment_stripePaymentId_key" ON "payment"("stripePaymentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "payment_formResponseId_key" ON "payment"("formResponseId"); + +-- CreateIndex +CREATE INDEX "payment_formId_idx" ON "payment"("formId"); + +-- CreateIndex +CREATE INDEX "payment_formFieldId_idx" ON "payment"("formFieldId"); + +-- CreateIndex +CREATE UNIQUE INDEX "form_response_paymentId_key" ON "form_response"("paymentId"); + +-- AddForeignKey +ALTER TABLE "form_response" ADD CONSTRAINT "form_response_paymentId_fkey" FOREIGN KEY ("paymentId") REFERENCES "payment"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e8307e4..1fc71f5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -116,7 +116,7 @@ model Template { } model TemplateField { - id String @id @default(uuid()) + id String @id @default(uuid()) fieldName String label String? fieldValueType String @@ -124,10 +124,10 @@ model TemplateField { validation Json? options Json? prevFieldId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt templateId String - template Template @relation(fields: [templateId], references: [id], onDelete: Cascade) + template Template @relation(fields: [templateId], references: [id], onDelete: Cascade) @@index([templateId]) @@index([templateId, prevFieldId]) @@ -142,6 +142,8 @@ model FormResponse { submittedAt DateTime? updatedAt DateTime @updatedAt isSubmitted Boolean @default(false) + paymentId String? @unique + payment Payment? @relation(fields: [paymentId], references: [id]) form Form @relation(fields: [formId], references: [id], onDelete: Cascade) respondent User? @relation(fields: [respondentId], references: [id]) @@ -149,3 +151,25 @@ model FormResponse { @@index([formId]) @@map("form_response") } + +model Payment { + id String @id @default(uuid()) + amount Decimal @db.Decimal(10, 2) + currency String @default("INR") + status String @default("pending") + razorpayOrderId String? @unique + razorpayPaymentId String? @unique + razorpaySignature String? + formId String + formFieldId String + formResponseId String? @unique + respondentId String? + metadata Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + formResponse FormResponse? + + @@index([formId]) + @@index([formFieldId]) + @@map("payment") +} diff --git a/src/api/form-fields/controller.ts b/src/api/form-fields/controller.ts index bbcfda3..04727a4 100644 --- a/src/api/form-fields/controller.ts +++ b/src/api/form-fields/controller.ts @@ -37,19 +37,37 @@ export async function getAllFields({ params, set }: GetAllFieldsContext) { } const ordered: typeof fields = []; + const visited = new Set(); + // Try to reconstruct the ordered list by following prevFieldId pointers let current = fields.find( (f): f is (typeof fields)[number] => f.prevFieldId === null, ); - while (current) { + while (current && !visited.has(current.id)) { ordered.push(current); + visited.add(current.id); current = fields.find( (f): f is (typeof fields)[number] => f.prevFieldId === current!.id, ); } + // Fallback: If some fields were missed (e.g. corrupted pointers or multiple heads + // due to race conditions during creation), append them at the end. + // This ensures all fields show up in the UI even if the order is temporarily wrong. + if (ordered.length < fields.length) { + logger.warn( + `Field order for form ${params.formId} is corrupted. Appending ${fields.length - ordered.length} missing fields.`, + ); + fields.forEach((f) => { + if (!visited.has(f.id)) { + ordered.push(f); + visited.add(f.id); + } + }); + } + return { success: true, data: ordered }; } @@ -73,16 +91,20 @@ export async function createField({ const createdField = await prisma.$transaction(async (tx) => { /** - * INSERT AT HEAD + * INSERT AT TAIL (when no prevFieldId is given) */ if (!body.prevFieldId) { - const currentHead = await tx.formFields.findFirst({ - where: { - formId: params.formId, - prevFieldId: null, - }, + // Find all fields for this form + const allFields = await tx.formFields.findMany({ + where: { formId: params.formId }, }); + // Find the tail: the field that no other field points to as its prev + const idsPointedTo = new Set( + allFields.map((f) => f.prevFieldId).filter(Boolean), + ); + const tail = allFields.find((f) => !idsPointedTo.has(f.id)); + const created = await tx.formFields.create({ data: { fieldName: body.fieldName, @@ -92,17 +114,10 @@ export async function createField({ validation: body.validation ?? undefined, options: body.options ?? undefined, formId: params.formId, - prevFieldId: null, + prevFieldId: tail ? tail.id : null, }, }); - if (currentHead) { - await tx.formFields.update({ - where: { id: currentHead.id }, - data: { prevFieldId: created.id }, - }); - } - return created; } diff --git a/src/api/payment/controller.ts b/src/api/payment/controller.ts new file mode 100644 index 0000000..3dc5fd2 --- /dev/null +++ b/src/api/payment/controller.ts @@ -0,0 +1,251 @@ +import { prisma } from "../../db/prisma"; +import { logger } from "../../logger"; +import { + createOrder, + verifyPaymentSignature, + verifyWebhookSignature, +} from "../../services/razorpay"; +import type { + CreateOrderContext, + GetPaymentStatusContext, + VerifyPaymentContext, + WebhookContext, +} from "../../types/payment"; + +/** + * Create a Razorpay order for a payment field + */ +export async function createPaymentOrder({ + params, + body, + user, + set, +}: CreateOrderContext) { + const { formId, fieldId } = params; + + const form = await prisma.form.findUnique({ + where: { id: formId }, + }); + + if (!form) { + set.status = 404; + return { success: false, message: "Form not found" }; + } + + const field = await prisma.formFields.findFirst({ + where: { id: fieldId, formId }, + }); + + if (!field || field.fieldType !== "payment") { + set.status = 404; + return { success: false, message: "Payment field not found" }; + } + + const fieldOptions = field.options as { + amount: number; + currency?: string; + description?: string; + } | null; + + const amount = body.amount || fieldOptions?.amount || 0; + const currency = body.currency || fieldOptions?.currency || "INR"; + + if (amount <= 0) { + set.status = 400; + return { success: false, message: "Invalid amount" }; + } + + try { + const order = await createOrder({ + amount, + currency, + receipt: `form_${formId}_${Date.now()}`, + notes: { + formId, + fieldId, + userId: user.id, + responseId: body.responseId || "", + }, + }); + + await prisma.payment.create({ + data: { + amount, + currency, + status: "pending", + razorpayOrderId: order.id, + formId, + formFieldId: fieldId, + respondentId: user.id, + metadata: { + description: + fieldOptions?.description || `Payment for form: ${form.title}`, + }, + }, + }); + + logger.info( + `Created Razorpay order ${order.id} for user ${user.id}, form ${formId}`, + ); + + return { + success: true, + data: { + orderId: order.id, + amount: order.amount, + currency: order.currency, + keyId: process.env.RAZORPAY_KEY_ID, + }, + }; + } catch (error) { + logger.error(`Failed to create Razorpay order: ${error}`); + set.status = 500; + return { success: false, message: "Failed to create payment order" }; + } +} + +/** + * Verify payment after Razorpay checkout success callback + */ +export async function verifyPayment({ body, user, set }: VerifyPaymentContext) { + const { + razorpay_order_id, + razorpay_payment_id, + razorpay_signature, + responseId, + } = body; + + const isValid = verifyPaymentSignature({ + orderId: razorpay_order_id, + paymentId: razorpay_payment_id, + signature: razorpay_signature, + }); + + if (!isValid) { + set.status = 400; + return { success: false, message: "Invalid payment signature" }; + } + + try { + await prisma.payment.updateMany({ + where: { razorpayOrderId: razorpay_order_id }, + data: { + status: "completed", + razorpayPaymentId: razorpay_payment_id, + razorpaySignature: razorpay_signature, + }, + }); + + if (responseId) { + const payment = await prisma.payment.findFirst({ + where: { razorpayOrderId: razorpay_order_id }, + }); + if (payment) { + await prisma.formResponse.update({ + where: { id: responseId }, + data: { paymentId: payment.id }, + }); + } + } + + logger.info( + `Payment verified for order ${razorpay_order_id}, user ${user.id}`, + ); + + return { + success: true, + data: { + orderId: razorpay_order_id, + paymentId: razorpay_payment_id, + status: "completed", + }, + }; + } catch (error) { + logger.error(`Payment verification failed: ${error}`); + set.status = 500; + return { success: false, message: "Payment verification failed" }; + } +} + +/** + * Get payment status by Razorpay order ID + */ +export async function getPaymentStatus({ params }: GetPaymentStatusContext) { + const payment = await prisma.payment.findFirst({ + where: { razorpayOrderId: params.orderId }, + }); + + if (!payment) { + return { success: false, message: "Payment not found" }; + } + + return { + success: true, + data: { + status: payment.status, + amount: Number(payment.amount), + currency: payment.currency, + }, + }; +} + +/** + * Handle Razorpay webhook events + */ +export async function handleWebhook({ body, set, request }: WebhookContext) { + const signature = request.headers.get("x-razorpay-signature"); + + if (!signature) { + set.status = 400; + return { success: false, message: "Missing x-razorpay-signature header" }; + } + + try { + const payload = typeof body === "string" ? body : JSON.stringify(body); + const isValid = verifyWebhookSignature(payload, signature); + + if (!isValid) { + set.status = 400; + return { success: false, message: "Invalid webhook signature" }; + } + + const event = JSON.parse(payload); + + if (event.event === "payment.captured") { + const paymentEntity = event.payload?.payment?.entity; + if (paymentEntity) { + const orderId = paymentEntity.order_id; + + await prisma.payment.updateMany({ + where: { razorpayOrderId: orderId }, + data: { + status: "completed", + razorpayPaymentId: paymentEntity.id, + }, + }); + + logger.info(`Webhook: Payment captured for order ${orderId}`); + } + } + + return { success: true }; + } catch (error) { + logger.error(`Webhook error: ${error}`); + set.status = 400; + return { success: false, message: "Webhook processing failed" }; + } +} + +/** + * Get Razorpay key ID for frontend config + */ +export function getConfig() { + const key = process.env.RAZORPAY_KEY_ID; + if (!key) { + return { success: false, message: "Razorpay is not configured" }; + } + return { + success: true, + data: { keyId: key }, + }; +} diff --git a/src/api/payment/routes.ts b/src/api/payment/routes.ts new file mode 100644 index 0000000..ca62499 --- /dev/null +++ b/src/api/payment/routes.ts @@ -0,0 +1,33 @@ +import { Elysia } from "elysia"; +import { + createOrderDTO, + getPaymentStatusDTO, + verifyPaymentDTO, +} from "../../types/payment"; +import { requireAuth } from "../auth/requireAuth"; +import { + createPaymentOrder, + getConfig, + getPaymentStatus, + handleWebhook, + verifyPayment, +} from "./controller"; + +// Webhook route — no auth, raw body for signature verification +const webhookRoutes = new Elysia({ prefix: "/payments" }).post( + "/webhook", + handleWebhook, + { parse: "text" }, +); + +// Authenticated payment routes +const authedPaymentRoutes = new Elysia({ prefix: "/payments" }) + .use(requireAuth) + .post("/order/:formId/:fieldId", createPaymentOrder, createOrderDTO) + .post("/verify", verifyPayment, verifyPaymentDTO) + .get("/status/:orderId", getPaymentStatus, getPaymentStatusDTO) + .get("/config", getConfig); + +export const paymentRoutes = new Elysia() + .use(webhookRoutes) + .use(authedPaymentRoutes); diff --git a/src/index.ts b/src/index.ts index 06697e5..16433ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { authRoutes } from "./api/auth/routes"; import { formFieldRoutes } from "./api/form-fields/routes"; import { formResponseRoutes } from "./api/form-response/routes"; import { formRoutes, publicFormRoutes } from "./api/forms/routes"; +import { paymentRoutes } from "./api/payment/routes"; import { logger } from "./logger/index"; const app = new Elysia() @@ -48,7 +49,8 @@ const app = new Elysia() .use(publicFormRoutes) // Public routes first (no auth) .use(formRoutes) .use(formFieldRoutes) - .use(formResponseRoutes); + .use(formResponseRoutes) + .use(paymentRoutes); app.listen(8000); diff --git a/src/services/razorpay.ts b/src/services/razorpay.ts new file mode 100644 index 0000000..382fed2 --- /dev/null +++ b/src/services/razorpay.ts @@ -0,0 +1,85 @@ +import crypto from "node:crypto"; +import Razorpay from "razorpay"; +import { logger } from "../logger"; + +const keyId = process.env.RAZORPAY_KEY_ID; +const keySecret = process.env.RAZORPAY_KEY_SECRET; + +if (!keyId || !keySecret) { + logger.warn( + "RAZORPAY_KEY_ID or RAZORPAY_KEY_SECRET is not set. Payment features will be disabled.", + ); +} + +export const razorpay = + keyId && keySecret + ? new Razorpay({ + key_id: keyId, + key_secret: keySecret, + }) + : null; + +/** + * Create a Razorpay Order + */ +export async function createOrder(params: { + amount: number; + currency?: string; + receipt?: string; + notes?: Record; +}) { + if (!razorpay) { + throw new Error("Razorpay is not configured"); + } + + const order = await razorpay.orders.create({ + amount: Math.round(params.amount * 100), // Razorpay expects amount in paise + currency: params.currency || "INR", + receipt: params.receipt || `receipt_${Date.now()}`, + notes: params.notes || {}, + }); + + logger.info(`Created Razorpay order: ${order.id}`); + return order; +} + +/** + * Verify Razorpay payment signature + */ +export function verifyPaymentSignature(params: { + orderId: string; + paymentId: string; + signature: string; +}): boolean { + if (!keySecret) { + throw new Error("Razorpay is not configured"); + } + + const body = `${params.orderId}|${params.paymentId}`; + const expectedSignature = crypto + .createHmac("sha256", keySecret) + .update(body) + .digest("hex"); + + return expectedSignature === params.signature; +} + +/** + * Verify Razorpay webhook signature + */ +export function verifyWebhookSignature( + body: string, + signature: string, +): boolean { + const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET; + if (!webhookSecret) { + throw new Error("RAZORPAY_WEBHOOK_SECRET is not set"); + } + + const expectedSignature = crypto + .createHmac("sha256", webhookSecret) + .update(body) + .digest("hex"); + + return expectedSignature === signature; +} diff --git a/src/test/form-fields.test.ts b/src/test/form-fields.test.ts index bfc1112..e43049f 100644 --- a/src/test/form-fields.test.ts +++ b/src/test/form-fields.test.ts @@ -109,6 +109,7 @@ describe("Form Fields Controller", () => { transactionMock.mockImplementation(async (cb: any) => { return cb({ formFields: { + findMany: async () => [], findFirst: async () => null, create: async () => ({ id: "new" }), update: async () => {}, diff --git a/src/types/payment.ts b/src/types/payment.ts new file mode 100644 index 0000000..462bf9a --- /dev/null +++ b/src/types/payment.ts @@ -0,0 +1,57 @@ +import { type Static, t } from "elysia"; + +export const createOrderDTO = { + params: t.Object({ + formId: t.String({ + format: "uuid", + }), + fieldId: t.String({ + format: "uuid", + }), + }), + body: t.Object({ + amount: t.Number(), + currency: t.Optional(t.String()), + responseId: t.Optional(t.String()), + }), +}; + +export interface CreateOrderContext { + params: Static; + body: Static; + user: { id: string }; + set: { status?: number | string }; +} + +export const verifyPaymentDTO = { + body: t.Object({ + razorpay_order_id: t.String(), + razorpay_payment_id: t.String(), + razorpay_signature: t.String(), + formId: t.String(), + fieldId: t.String(), + responseId: t.Optional(t.String()), + }), +}; + +export interface VerifyPaymentContext { + body: Static; + user: { id: string }; + set: { status?: number | string }; +} + +export const getPaymentStatusDTO = { + params: t.Object({ + orderId: t.String(), + }), +}; + +export interface GetPaymentStatusContext { + params: Static; +} + +export interface WebhookContext { + body: unknown; + set: { status?: number | string }; + request: Request; +}