1 Commits

88 changed files with 1416 additions and 13443 deletions

View File

@@ -0,0 +1,40 @@
# Split Serverless services (deploy order)
This repo is split into multiple Serverless configs so you can deploy smaller CloudFormation stacks instead of one huge stack.
## Config files
- `serverless.layers.yml`: Prisma layer stack (deploy once per stage)
- `serverless.host.yml`: Host + PQQ functions (owns the shared HTTP API)
- `serverless.admin.yml`: Minglar Admin functions (attaches routes to Host HTTP API)
- `serverless.user.yml`: User functions (attaches routes to Host HTTP API)
- `serverless.prepopulate.yml`: Prepopulate functions (attaches routes to Host HTTP API)
## Deploy order (per stage)
1) Deploy the layer:
```bash
npx serverless deploy --config serverless.layers.yml --stage dev
```
2) Deploy Host (creates the HTTP API + routes for host functions):
```bash
npx serverless deploy --config serverless.host.yml --stage dev
```
3) Deploy remaining services (they reuse Host's HTTP API id):
```bash
npx serverless deploy --config serverless.admin.yml --stage dev
npx serverless deploy --config serverless.user.yml --stage dev
npx serverless deploy --config serverless.prepopulate.yml --stage dev
```
## Deploy a single function
```bash
npx serverless deploy function --config serverless.host.yml --stage dev -f getHosts
```

533
package-lock.json generated
View File

@@ -13,7 +13,7 @@
"@aws-crypto/sha256-browser": "^5.2.0", "@aws-crypto/sha256-browser": "^5.2.0",
"@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/sha256-js": "^5.2.0",
"@aws-sdk/client-s3": "^3.928.0", "@aws-sdk/client-s3": "^3.928.0",
"@aws-sdk/s3-request-presigner": "^3.928.0", "@aws-sdk/s3-request-presigner": "^3.310.0",
"@aws/lambda-invoke-store": "^0.2.1", "@aws/lambda-invoke-store": "^0.2.1",
"@nestjs/common": "^10.3.0", "@nestjs/common": "^10.3.0",
"@nestjs/config": "^3.1.1", "@nestjs/config": "^3.1.1",
@@ -31,27 +31,18 @@
"@types/http-status": "^1.1.2", "@types/http-status": "^1.1.2",
"ajv": "8.12.0", "ajv": "8.12.0",
"aws-lambda": "^1.0.7", "aws-lambda": "^1.0.7",
"aws-sdk": "^2.1692.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dayjs": "^1.11.19",
"docx": "^9.6.0",
"docxtemplater": "^3.68.3",
"fast-xml-parser": "^5.3.1", "fast-xml-parser": "^5.3.1",
"fs": "^0.0.1-security",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"http-status": "^2.1.0", "http-status": "^2.1.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"number-to-words": "^1.2.4",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"path": "^0.12.7",
"pdf-lib": "^1.17.1",
"pizzip": "^3.2.0",
"prisma": "^7.0.1", "prisma": "^7.0.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@@ -82,7 +73,6 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"serverless-esbuild": "^1.55.1", "serverless-esbuild": "^1.55.1",
"serverless-offline": "^14.4.0", "serverless-offline": "^14.4.0",
"serverless-plugin-split-stacks": "^1.14.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^6.3.4", "supertest": "^6.3.4",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
@@ -4660,24 +4650,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@pdf-lib/standard-fonts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.6"
}
},
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -5779,13 +5751,6 @@
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -6581,15 +6546,6 @@
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
}, },
"node_modules/@xmldom/xmldom": {
"version": "0.9.8",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz",
"integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==",
"license": "MIT",
"engines": {
"node": ">=14.6"
}
},
"node_modules/@xtuc/ieee754": { "node_modules/@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -6668,16 +6624,6 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/aggregate-error": { "node_modules/aggregate-error": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -7018,19 +6964,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/async": { "node_modules/async": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -7059,13 +6992,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/aws-info": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/aws-info/-/aws-info-1.3.0.tgz",
"integrity": "sha512-dYE3J2GQOMXjirx54IonDisZ6Ok4vBSYjNklNAGGDj2FzGHkWpGOlGAn5/BC8TRh8ttmYRy+Fsmxc8EJMnzSCg==",
"dev": true,
"license": "MIT"
},
"node_modules/aws-lambda": { "node_modules/aws-lambda": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/aws-lambda/-/aws-lambda-1.0.7.tgz", "resolved": "https://registry.npmjs.org/aws-lambda/-/aws-lambda-1.0.7.tgz",
@@ -7331,16 +7257,6 @@
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
} }
}, },
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/bcrypt": { "node_modules/bcrypt": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@@ -8470,6 +8386,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cors": { "node_modules/cors": {
@@ -8622,12 +8539,6 @@
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
} }
}, },
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -8722,21 +8633,6 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -8867,50 +8763,6 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/docx": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.6.0.tgz",
"integrity": "sha512-y6EaJJMDvt4P7wgGQB9KsZf4wsRkQMJfkc9LlNufRshggI5BT35hGNkXBCAeEoI3MLMwApKguxzjdqqVcBCqNA==",
"license": "MIT",
"dependencies": {
"@types/node": "^25.2.3",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/docx/node_modules/@types/node": {
"version": "25.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/docx/node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/docxtemplater": {
"version": "3.68.3",
"resolved": "https://registry.npmjs.org/docxtemplater/-/docxtemplater-3.68.3.tgz",
"integrity": "sha512-hTZfGcHgN60A09w68Qj0EQRCnF5kf2/ohFlZlUVqAOozCFwx9QMm8naCTvmTsXafuO3nG9qpS4pQWSjFdaCWfQ==",
"license": "MIT",
"dependencies": {
"@xmldom/xmldom": "^0.9.8"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.5", "version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
@@ -9179,39 +9031,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/escodegen/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.57.1", "version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
@@ -10203,12 +10022,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==",
"license": "ISC"
},
"node_modules/fs-constants": { "node_modules/fs-constants": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -10386,31 +10199,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/get-uri/node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/giget": { "node_modules/giget": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
@@ -10649,16 +10437,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -10721,20 +10499,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/http-status": { "node_modules/http-status": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/http-status/-/http-status-2.1.0.tgz", "resolved": "https://registry.npmjs.org/http-status/-/http-status-2.1.0.tgz",
@@ -10750,20 +10514,6 @@
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -10806,6 +10556,7 @@
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/import-fresh": { "node_modules/import-fresh": {
@@ -10910,16 +10661,6 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -12245,6 +11986,7 @@
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dev": true,
"license": "(MIT OR GPL-3.0-or-later)", "license": "(MIT OR GPL-3.0-or-later)",
"dependencies": { "dependencies": {
"lie": "~3.3.0", "lie": "~3.3.0",
@@ -12257,6 +11999,7 @@
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"core-util-is": "~1.0.0", "core-util-is": "~1.0.0",
@@ -12272,12 +12015,14 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/jszip/node_modules/string_decoder": { "node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
@@ -12404,6 +12149,7 @@
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"immediate": "~3.0.5" "immediate": "~3.0.5"
@@ -12802,12 +12548,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.3", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
@@ -12958,24 +12698,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/nanoid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -13000,16 +12722,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/nock": { "node_modules/nock": {
"version": "13.5.6", "version": "13.5.6",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz",
@@ -13161,12 +12873,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/number-to-words": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/number-to-words/-/number-to-words-1.2.4.tgz",
"integrity": "sha512-/fYevVkXRcyBiZDg6yzZbm0RuaD6i0qRfn8yr+6D0KgBMOndFPxuW10qCHpzs50nN8qKuv78k8MuotZhcVX6Pw==",
"license": "MIT"
},
"node_modules/nypm": { "node_modules/nypm": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
@@ -13412,40 +13118,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.6",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"dev": true,
"license": "MIT",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/package-json-from-dist": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -13456,6 +13128,7 @@
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true,
"license": "(MIT AND Zlib)" "license": "(MIT AND Zlib)"
}, },
"node_modules/parent-module": { "node_modules/parent-module": {
@@ -13546,16 +13219,6 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
"license": "MIT",
"dependencies": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -13630,21 +13293,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
"license": "ISC"
},
"node_modules/path/node_modules/util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"license": "MIT",
"dependencies": {
"inherits": "2.0.3"
}
},
"node_modules/pathe": { "node_modules/pathe": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -13656,24 +13304,6 @@
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
}, },
"node_modules/pdf-lib": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
"license": "MIT",
"dependencies": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"pako": "^1.0.11",
"tslib": "^1.11.1"
}
},
"node_modules/pdf-lib/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/perfect-debounce": { "node_modules/perfect-debounce": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
@@ -13808,21 +13438,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/pizzip": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/pizzip/-/pizzip-3.2.0.tgz",
"integrity": "sha512-X4NPNICxCfIK8VYhF6wbksn81vTiziyLbvKuORVAmolvnUzl1A1xmz9DAWKxPRq9lZg84pJOOAMq3OE61bD8IQ==",
"license": "(MIT OR GPL-3.0)",
"dependencies": {
"pako": "^2.1.0"
}
},
"node_modules/pizzip/node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/pkg-dir": { "node_modules/pkg-dir": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -14074,19 +13689,11 @@
} }
} }
}, },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/prompts": { "node_modules/prompts": {
@@ -14149,36 +13756,6 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.6",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.1.0",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-agent/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -15179,23 +14756,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/serverless-plugin-split-stacks": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/serverless-plugin-split-stacks/-/serverless-plugin-split-stacks-1.14.0.tgz",
"integrity": "sha512-VksNqvJUPnGHqef0jHNiN0BzTVr0Hy0cWaLxCG75HiQ3vnIog8qeyiu7uWH6LKNhJnGP1jiTNh0YcheCN8kaKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"aws-info": "^1.2.0",
"lodash": "^4.17.21",
"proxy-agent": "^6.3.1",
"semver": "^7.3.5",
"throat": "^6.0.1"
},
"peerDependencies": {
"serverless": "1 || 2 || 3 || 4"
}
},
"node_modules/serverless/node_modules/rimraf": { "node_modules/serverless/node_modules/rimraf": {
"version": "5.0.10", "version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
@@ -15232,6 +14792,7 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
@@ -15362,47 +14923,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/sorted-array-functions": { "node_modules/sorted-array-functions": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
@@ -15984,13 +15504,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/throat": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz",
"integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==",
"dev": true,
"license": "MIT"
},
"node_modules/through": { "node_modules/through": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -17563,30 +17076,6 @@
} }
} }
}, },
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"license": "MIT"
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/xml-js/node_modules/sax": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
"license": "BlueOak-1.0.0"
},
"node_modules/xml2js": { "node_modules/xml2js": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",

View File

@@ -70,7 +70,6 @@
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pizzip": "^3.2.0", "pizzip": "^3.2.0",
"prisma": "^7.0.1", "prisma": "^7.0.1",
"razorpay": "^2.9.6",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"serverless": "4.24.0", "serverless": "4.24.0",
@@ -98,9 +97,7 @@
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"serverless-esbuild": "^1.55.1",
"serverless-offline": "^14.4.0", "serverless-offline": "^14.4.0",
"serverless-plugin-split-stacks": "^1.14.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^6.3.4", "supertest": "^6.3.4",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",

View File

@@ -1,8 +1,7 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Lambda Node 18/20 (Amazon Linux) target binaryTargets = ["native", "rhel-openssl-3.0.x"] // Add Linux target
previewFeatures = ["multiSchema"] previewFeatures = ["multiSchema"]
engineType = "library"
} }
datasource db { datasource db {
@@ -12,32 +11,30 @@ datasource db {
} }
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
firstName String? @map("first_name") @db.VarChar(50) firstName String? @map("first_name") @db.VarChar(50)
lastName String? @map("last_name") @db.VarChar(50) lastName String? @map("last_name") @db.VarChar(50)
roleXid Int? @map("role_xid") roleXid Int? @map("role_xid")
dateOfBirth DateTime? @map("date_of_birth") dateOfBirth DateTime? @map("date_of_birth")
genderName String? @map("gender_name") @db.VarChar(20) genderName String? @map("gender_name") @db.VarChar(20)
role Roles? @relation(fields: [roleXid], references: [id], onDelete: Restrict) role Roles? @relation(fields: [roleXid], references: [id], onDelete: Restrict)
emailAddress String? @unique @map("email_address") @db.VarChar(150) emailAddress String? @unique @map("email_address") @db.VarChar(150)
isdCode String? @map("isd_code") @db.VarChar(6) // +91, +1, +971 etc. isdCode String? @map("isd_code") @db.VarChar(6) // +91, +1, +971 etc.
mobileNumber String? @unique @map("mobile_number") @db.VarChar(15) // international safe limit mobileNumber String? @unique @map("mobile_number") @db.VarChar(15) // international safe limit
userPassword String? @map("user_password") @db.VarChar(255) // hashed passwords userPassword String? @map("user_password") @db.VarChar(255) // hashed passwords
userPasscode String? @map("user_passcode") @db.VarChar(255) // 46 digit passcode userPasscode String? @map("user_passcode") @db.VarChar(255) // 46 digit passcode
profileImage String? @map("profile_image") @db.VarChar(500) // S3 key or URL profileImage String? @map("profile_image") @db.VarChar(500) // S3 key or URL
userLat String? @map("user_lat") @db.VarChar(20) // "-23.44444" userLat String? @map("user_lat") @db.VarChar(20) // "-23.44444"
userLong String? @map("user_long") @db.VarChar(20) userLong String? @map("user_long") @db.VarChar(20)
userStatus String? @default("pending") @map("user_status") @db.VarChar(20) userStatus String? @default("pending") @map("user_status") @db.VarChar(20)
isEmailVerfied Boolean? @default(false) @map("is_email_verified") isEmailVerfied Boolean? @default(false) @map("is_email_verified")
isMobileVerfied Boolean? @default(false) @map("is_mobile_verified") isMobileVerfied Boolean? @default(false) @map("is_mobile_verified")
isProfileUpdated Boolean? @default(false) @map("is_profile_updated") isProfileUpdated Boolean? @default(false) @map("is_profile_updated")
userRefNumber String? @unique @map("user_ref_number") @db.VarChar(20) userRefNumber String? @unique @map("user_ref_number") @db.VarChar(20)
dataConsentAccepted Boolean? @default(false) @map("data_consent_accepted") isActive Boolean? @default(true) @map("is_active")
dataConsentAcceptedOn DateTime? @map("data_consent_accepted_on") createdAt DateTime? @default(now()) @map("created_at")
isActive Boolean? @default(true) @map("is_active") updatedAt DateTime? @updatedAt @map("updated_at")
createdAt DateTime? @default(now()) @map("created_at") deletedAt DateTime? @map("deleted_at")
updatedAt DateTime? @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
// Relations // Relations
UserOtp UserOtp[] UserOtp UserOtp[]
@@ -61,11 +58,8 @@ model User {
ActivitySOSDetails ActivitySOSDetails[] ActivitySOSDetails ActivitySOSDetails[]
ActivityFeedbacks ActivityFeedbacks[] ActivityFeedbacks ActivityFeedbacks[]
ItineraryDetails ItineraryDetails[] ItineraryDetails ItineraryDetails[]
paymentOrders PaymentOrders[]
inviteDetails InviteDetails[] @relation("InvitedUser") inviteDetails InviteDetails[] @relation("InvitedUser")
invitedInviteDetails InviteDetails[] @relation("InviterUser") invitedInviteDetails InviteDetails[] @relation("InviterUser")
hostMembers HostMembers[] @relation("HostMemberUser")
invitedHostMembers HostMembers[] @relation("HostMemberInviter")
userRevenues UserRevenue[] userRevenues UserRevenue[]
userInterests UserInterests[] userInterests UserInterests[]
connectDetails ConnectDetails[] connectDetails ConnectDetails[]
@@ -81,9 +75,6 @@ model User {
// 🔹 Activities where this user is Account Manager // 🔹 Activities where this user is Account Manager
managedActivities Activities[] @relation("ActivityAccountManager") managedActivities Activities[] @relation("ActivityAccountManager")
activitySortings ActivitySorting[] activitySortings ActivitySorting[]
sentActivityMessages ActivityMessages[] @relation("ActivityMessageSender")
receivedActivityMessages ActivityMessages[] @relation("ActivityMessageReceiver")
chatConnections ChatConnections[]
@@map("users") @@map("users")
@@schema("usr") @@schema("usr")
@@ -563,6 +554,20 @@ model Frequencies {
@@schema("mst") @@schema("mst")
} }
model NavigationModes {
id Int @id @default(autoincrement())
navigationModeName String @unique @map("navigation_mode_name") @db.VarChar(30)
navigationModeIcon String @map("navigation_mode_icon") @db.VarChar(500)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityNavigationModes ActivityNavigationModes[]
@@map("navigation_modes")
@@schema("mst")
}
model TransportModes { model TransportModes {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
transportModeName String @unique @map("transport_mode_name") @db.VarChar(60) transportModeName String @unique @map("transport_mode_name") @db.VarChar(60)
@@ -679,8 +684,6 @@ model Roles {
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
User User[] User User[]
hostMembers HostMembers[] @relation("HostMemberRole")
hostRolePermissionMasters HostRolePermissionMasters[] @relation("HostRolePermissionMasterRole")
@@map("roles") @@map("roles")
@@schema("mst") @@schema("mst")
@@ -739,22 +742,6 @@ model Token {
@@schema("usr") @@schema("usr")
} }
model ChatConnections {
id Int @id @default(autoincrement())
userXid Int @map("user_xid")
user User @relation(fields: [userXid], references: [id], onDelete: Cascade)
activityXid Int? @map("activity_xid")
connectionId String @unique @map("connection_id") @db.VarChar(200)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([userXid, activityXid])
@@map("chat_connections")
@@schema("usr")
}
//HOST MODELS //HOST MODELS
model HostHeader { model HostHeader {
@@ -813,8 +800,6 @@ model HostHeader {
HostBankDetails HostBankDetails[] HostBankDetails HostBankDetails[]
HostDocuments HostDocuments[] HostDocuments HostDocuments[]
HostSuggestion HostSuggestion[] HostSuggestion HostSuggestion[]
hostMembers HostMembers[]
hostRolePermissionMasters HostRolePermissionMasters[]
hostParent HostParent[] hostParent HostParent[]
HostTrack HostTrack[] HostTrack HostTrack[]
Activities Activities[] Activities Activities[]
@@ -846,90 +831,13 @@ model HostBankDetails {
@@schema("hst") @@schema("hst")
} }
model HostMembers {
id Int @id @default(autoincrement())
hostXid Int @map("host_xid")
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
userXid Int @map("user_xid")
user User @relation("HostMemberUser", fields: [userXid], references: [id], onDelete: Cascade)
roleXid Int @map("role_xid")
role Roles @relation("HostMemberRole", fields: [roleXid], references: [id], onDelete: Restrict)
hostRolePermissionMasterXid Int? @map("host_role_permission_master_xid")
hostRolePermissionMaster HostRolePermissionMasters? @relation(fields: [hostRolePermissionMasterXid], references: [id], onDelete: Restrict)
memberStatus String @default("invited") @map("member_status") @db.VarChar(20)
invitedByXid Int? @map("invited_by_xid")
invitedBy User? @relation("HostMemberInviter", fields: [invitedByXid], references: [id], onDelete: Restrict)
invitedOn DateTime @default(now()) @map("invited_on")
acceptedOn DateTime? @map("accepted_on")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
managedActivities HostMemberActivities[]
@@unique([hostXid, userXid])
@@map("host_members")
@@schema("hst")
}
model HostRolePermissionMasters {
id Int @id @default(autoincrement())
hostXid Int @map("host_xid")
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
roleXid Int @map("role_xid")
role Roles @relation("HostRolePermissionMasterRole", fields: [roleXid], references: [id], onDelete: Restrict)
permissionMasterXids Json @map("permission_master_xids")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
hostMembers HostMembers[]
@@unique([hostXid, roleXid])
@@map("host_role_permission_masters")
@@schema("hst")
}
model HostPermissionMasters {
id Int @id @default(autoincrement())
permissionKey String @unique @map("permission_key") @db.VarChar(120)
permissionGroup String @map("permission_group") @db.VarChar(80)
permissionSection String @map("permission_section") @db.VarChar(80)
permissionAction String @map("permission_action") @db.VarChar(20)
displayLabel String @map("display_label") @db.VarChar(120)
displayOrder Int @map("display_order")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("host_permission_masters")
@@schema("mst")
}
model HostMemberActivities {
id Int @id @default(autoincrement())
hostMemberXid Int @map("host_member_xid")
hostMember HostMembers @relation(fields: [hostMemberXid], references: [id], onDelete: Cascade)
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@unique([hostMemberXid, activityXid])
@@map("host_member_activities")
@@schema("hst")
}
model HostDocuments { model HostDocuments {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
hostXid Int @map("host_xid") hostXid Int @map("host_xid")
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade) host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
documentTypeXid Int @map("document_type_xid") documentTypeXid Int @map("document_type_xid")
documentType DocumentType @relation(fields: [documentTypeXid], references: [id], onDelete: Restrict) documentType DocumentType @relation(fields: [documentTypeXid], references: [id], onDelete: Restrict)
documentName String @map("document_name") @db.VarChar(50) documentName String @map("document_name") @db.VarChar(20)
filePath String @map("file_path") @db.VarChar(400) filePath String @map("file_path") @db.VarChar(400)
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
@@ -979,10 +887,7 @@ model HostParent {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
hostXid Int @map("host_xid") hostXid Int @map("host_xid")
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade) host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
companyName String? @map("company_name") @db.VarChar(100) companyName String @map("company_name") @db.VarChar(100)
firstName String? @map("first_name") @db.VarChar(50)
lastName String? @map("last_name") @db.VarChar(50)
mobileNumber String? @map("mobile_number") @db.VarChar(15)
address1 String? @map("address_1") @db.VarChar(150) address1 String? @map("address_1") @db.VarChar(150)
address2 String? @map("address_2") @db.VarChar(150) address2 String? @map("address_2") @db.VarChar(150)
cityXid Int? @map("city_xid") cityXid Int? @map("city_xid")
@@ -1138,42 +1043,22 @@ model Activities {
activityCuisines ActivityCuisine[] activityCuisines ActivityCuisine[]
activityPickUpTransports ActivityPickUpTransport[] activityPickUpTransports ActivityPickUpTransport[]
userBucketInterests UserBucketInterested[] userBucketInterests UserBucketInterested[]
activityMessages ActivityMessages[]
assignedHostMembers HostMemberActivities[]
@@map("activities") @@map("activities")
@@schema("act") @@schema("act")
} }
model ActivityMessages {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
senderXid Int @map("sender_xid")
sender User @relation("ActivityMessageSender", fields: [senderXid], references: [id], onDelete: Restrict)
receivedXid Int @map("received_xid")
received User @relation("ActivityMessageReceiver", fields: [receivedXid], references: [id], onDelete: Restrict)
message String @map("message") @db.VarChar(2000)
status String @default("unread") @map("status") @db.VarChar(30)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([activityXid, senderXid, receivedXid])
@@map("activity_messages")
@@schema("act")
}
model ActivityOtherDetails { model ActivityOtherDetails {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
activityXid Int @map("activity_xid") activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
exclusiveNotes String? @map("exclusive_notes") @db.Text exclusiveNotes String? @map("exclusive_notes") @db.VarChar(500)
SafetyInstruction String? @map("safety_instruction") @db.Text SafetyInstruction String? @map("safety_instruction") @db.VarChar(400)
Cancellations String? @map("cancellations") @db.Text Cancellations String? @map("cancellations") @db.VarChar(400)
dosNotes String? @map("dos_notes") @db.Text dosNotes String? @map("dos_notes") @db.VarChar(400)
dontsNotes String? @map("donts_notes") @db.Text dontsNotes String? @map("donts_notes") @db.VarChar(400)
tipsNotes String? @map("tips_notes") @db.Text tipsNotes String? @map("tips_notes") @db.VarChar(400)
termsAndCondition String? @map("terms_and_condition") @db.Text termsAndCondition String? @map("terms_and_condition") @db.VarChar(500)
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@ -1486,16 +1371,15 @@ model ActivityFoodCost {
} }
model ActivityFoodTypes { model ActivityFoodTypes {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
activityXid Int @map("activity_xid") activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
foodTypeXid Int @map("food_type_xid") foodTypeXid Int @map("food_type_xid")
foodType FoodTypes @relation(fields: [foodTypeXid], references: [id], onDelete: Restrict) foodType FoodTypes @relation(fields: [foodTypeXid], references: [id], onDelete: Restrict)
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
itineraryActivitySelectionFoodTypes ItineraryActivitySelectionFoodType[]
@@map("activity_food_types") @@map("activity_food_types")
@@schema("act") @@schema("act")
@@ -1534,19 +1418,18 @@ model ActivityFoodTaxes {
} }
model ActivityEquipments { model ActivityEquipments {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
activityXid Int @map("activity_xid") activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
equipmentName String @map("equipment_name") @db.VarChar(30) equipmentName String @map("equipment_name") @db.VarChar(30)
isEquipmentChargeable Boolean @default(false) @map("is_equipment_chargeable") isEquipmentChargeable Boolean @default(false) @map("is_equipment_chargeable")
equipmentBasePrice Int @map("equipment_base_price") equipmentBasePrice Int @map("equipment_base_price")
equipmentTotalPrice Int @map("equipment_total_price") equipmentTotalPrice Int @map("equipment_total_price")
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
ActivityEquipmentTaxes ActivityEquipmentTaxes[] ActivityEquipmentTaxes ActivityEquipmentTaxes[]
itineraryActivitySelectionEquipments ItineraryActivitySelectionEquipment[]
@@map("activity_equipments") @@map("activity_equipments")
@@schema("act") @@schema("act")
@@ -1573,7 +1456,8 @@ model ActivityNavigationModes {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
activityXid Int @map("activity_xid") activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
navigationModeName String @map("navigation_mode_name") @db.VarChar(30) navigationModeXid Int @map("navigation_mode_xid")
navigationMode NavigationModes @relation(fields: [navigationModeXid], references: [id], onDelete: Restrict)
isInActivityChargeable Boolean @default(false) @map("is_in_activity_chargeable") isInActivityChargeable Boolean @default(false) @map("is_in_activity_chargeable")
navigationModesBasePrice Int @map("navigation_modes_base_price") navigationModesBasePrice Int @map("navigation_modes_base_price")
navigationModesTotalPrice Int @map("navigation_modes_total_price") navigationModesTotalPrice Int @map("navigation_modes_total_price")
@@ -1582,7 +1466,6 @@ model ActivityNavigationModes {
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
ActivityNavigationModesTaxes ActivityNavigationModesTaxes[] ActivityNavigationModesTaxes ActivityNavigationModesTaxes[]
ItineraryActivitySelections ItineraryActivitySelection[]
@@map("activity_navigation_modes") @@map("activity_navigation_modes")
@@schema("act") @@schema("act")
@@ -1754,8 +1637,8 @@ model Cancellations {
scheduleHeaderXid Int @map("schedule_header_xid") scheduleHeaderXid Int @map("schedule_header_xid")
scheduleHeader ScheduleHeader @relation(fields: [scheduleHeaderXid], references: [id], onDelete: Cascade) scheduleHeader ScheduleHeader @relation(fields: [scheduleHeaderXid], references: [id], onDelete: Cascade)
occurenceDate DateTime? @map("occurence_date") occurenceDate DateTime? @map("occurence_date")
startTime String? @map("start_time") @db.VarChar(30) startTime String? @map("start_time") @db.VarChar(30)
endTime String? @map("end_time") @db.VarChar(30) endTime String? @map("end_time") @db.VarChar(30)
cancellationReason String? @map("cancellation_reason") cancellationReason String? @map("cancellation_reason")
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
@@ -1779,7 +1662,6 @@ model ItineraryHeader {
toDate DateTime @map("to_date") toDate DateTime @map("to_date")
toTime String @map("to_time") @db.VarChar(30) toTime String @map("to_time") @db.VarChar(30)
itineraryStatus String @default("draft") @map("itinerary_status") @db.VarChar(30) itineraryStatus String @default("draft") @map("itinerary_status") @db.VarChar(30)
cancellationReason String? @map("cancellation_reason")
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@ -1787,31 +1669,29 @@ model ItineraryHeader {
ItineraryMembers ItineraryMembers[] ItineraryMembers ItineraryMembers[]
ItineraryStartStopDetails ItineraryStartStopDetails[] ItineraryStartStopDetails ItineraryStartStopDetails[]
ItineraryActivities ItineraryActivities[] ItineraryActivities ItineraryActivities[]
paymentOrders PaymentOrders[]
@@map("itinerary_header") @@map("itinerary_header")
@@schema("itn") @@schema("itn")
} }
model ItineraryMembers { model ItineraryMembers {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
itineraryHeaderXid Int @map("itinerary_header_xid") itineraryHeaderXid Int @map("itinerary_header_xid")
itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade) itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
memberXid Int @map("member_xid") memberXid Int @map("member_xid")
member User @relation("MemberUser", fields: [memberXid], references: [id], onDelete: Restrict) member User @relation("MemberUser", fields: [memberXid], references: [id], onDelete: Restrict)
memberRole String @map("member_role") @db.VarChar(30) memberRole String @map("member_role") @db.VarChar(30)
memberStatus String @default("pending") @map("member_status") @db.VarChar(30) memberStatus String @default("pending") @map("member_status") @db.VarChar(30)
invitedByXid Int @map("invited_by_xid") invitedByXid Int @map("invited_by_xid")
invitedBy User @relation("InvitedByUser", fields: [invitedByXid], references: [id], onDelete: Restrict) invitedBy User @relation("InvitedByUser", fields: [invitedByXid], references: [id], onDelete: Restrict)
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
ItineraryStartStopDetails ItineraryStartStopDetails[] ItineraryStartStopDetails ItineraryStartStopDetails[]
User User? @relation(fields: [userId], references: [id]) User User? @relation(fields: [userId], references: [id])
userId Int? userId Int?
ItineraryDetails ItineraryDetails[] ItineraryDetails ItineraryDetails[]
itineraryActivitySelections ItineraryActivitySelection[]
@@map("itinerary_members") @@map("itinerary_members")
@@schema("itn") @@schema("itn")
@@ -1842,124 +1722,41 @@ model ItineraryStartStopDetails {
} }
model ItineraryActivities { model ItineraryActivities {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
itineraryHeaderXid Int @map("itinerary_header_xid") itineraryHeaderXid Int @map("itinerary_header_xid")
itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade) itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
displayOrder Int @default(0) @map("display_order") itineraryType String @map("itinerary_type") @db.VarChar(30)
itineraryType String @map("itinerary_type") @db.VarChar(30) activityXid Int @map("activity_xid")
activityXid Int? @map("activity_xid") activity Activities @relation(fields: [activityXid], references: [id], onDelete: Restrict)
activity Activities? @relation(fields: [activityXid], references: [id], onDelete: Restrict) scheduledHeaderXid Int @map("scheduled_header_xid")
scheduledHeaderXid Int? @map("scheduled_header_xid") scheduledHeader ScheduleHeader @relation(fields: [scheduledHeaderXid], references: [id], onDelete: Restrict)
scheduledHeader ScheduleHeader? @relation(fields: [scheduledHeaderXid], references: [id], onDelete: Restrict) occurenceDate DateTime @map("occurence_date")
occurenceDate DateTime @map("occurence_date") startTime String @map("start_time") @db.VarChar(30)
startTime String @map("start_time") @db.VarChar(30) endTime String @map("end_time") @db.VarChar(30)
endTime String @map("end_time") @db.VarChar(30) endDate DateTime @map("end_date")
endDate DateTime @map("end_date") venueXid Int @map("venue_xid")
venueXid Int? @map("venue_xid") venue ActivityVenues @relation(fields: [venueXid], references: [id], onDelete: Restrict)
venue ActivityVenues? @relation(fields: [venueXid], references: [id], onDelete: Restrict) locationLat Float? @map("location_lat")
locationLat Float? @map("location_lat") locationLong Float? @map("location_long")
locationLong Float? @map("location_long") locationAddress Json? @map("location_address")
locationAddress Json? @map("location_address") travelMode String? @map("travel_mode") @db.VarChar(30)
travelMode String? @map("travel_mode") @db.VarChar(30) kmForNextPoint Float? @map("km_for_next_point")
kmForNextPoint Float? @map("km_for_next_point") timeForNextPointMins Int? @map("time_for_next_point_mins")
timeForNextPointMins Int? @map("time_for_next_point_mins") paxCount Int @map("pax_count")
paxCount Int? @map("pax_count") totalAmount Int @map("total_amount")
totalAmount Int? @map("total_amount") bookingStatus String @default("pending") @map("booking_status") @db.VarChar(30)
bookingStatus String @default("pending") @map("booking_status") @db.VarChar(30) isActive Boolean @default(true) @map("is_active")
isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at")
createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at")
updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at")
deletedAt DateTime? @map("deleted_at") ActivitySOSDetails ActivitySOSDetails[]
ActivitySOSDetails ActivitySOSDetails[] ActivityFeedbacks ActivityFeedbacks[]
ActivityFeedbacks ActivityFeedbacks[] ItineraryDetails ItineraryDetails[]
ItineraryDetails ItineraryDetails[]
itineraryActivitySelections ItineraryActivitySelection[]
@@map("itinerary_activities") @@map("itinerary_activities")
@@schema("itn") @@schema("itn")
} }
model PaymentOrders {
id Int @id @default(autoincrement())
userXid Int @map("user_xid")
user User @relation(fields: [userXid], references: [id], onDelete: Cascade)
itineraryHeaderXid Int? @map("itinerary_header_xid")
itineraryHeader ItineraryHeader? @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
razorpayOrderId String? @unique @map("razorpay_order_id") @db.VarChar(100)
razorpayPaymentId String? @unique @map("razorpay_payment_id") @db.VarChar(100)
razorpaySignature String? @map("razorpay_signature") @db.VarChar(255)
receipt String @unique @map("receipt") @db.VarChar(100)
amount Int @map("amount")
currency String @default("INR") @map("currency") @db.VarChar(10)
paymentStatus String @default("created") @map("payment_status") @db.VarChar(30)
notes Json? @map("notes")
verifiedAt DateTime? @map("verified_at")
paidAt DateTime? @map("paid_at")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([userXid, itineraryHeaderXid])
@@map("payment_orders")
@@schema("itn")
}
model ItineraryActivitySelection {
id Int @id @default(autoincrement())
itineraryActivityXid Int @map("itinerary_activity_xid")
itineraryActivity ItineraryActivities @relation(fields: [itineraryActivityXid], references: [id], onDelete: Cascade)
itineraryMemberXid Int @map("itinerary_member_xid")
itineraryMember ItineraryMembers @relation(fields: [itineraryMemberXid], references: [id], onDelete: Cascade)
isFoodOpted Boolean @default(false) @map("is_food_opted")
isTrainerOpted Boolean @default(false) @map("is_trainer_opted")
isInActivityNavigationOpted Boolean @default(false) @map("is_in_activity_navigation_opted")
activityNavigationModeXid Int? @map("activity_navigation_mode_xid")
activityNavigationMode ActivityNavigationModes? @relation(fields: [activityNavigationModeXid], references: [id], onDelete: Restrict)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
selectedFoodTypes ItineraryActivitySelectionFoodType[]
selectedEquipments ItineraryActivitySelectionEquipment[]
@@unique([itineraryActivityXid, itineraryMemberXid])
@@map("itinerary_activity_selection")
@@schema("itn")
}
model ItineraryActivitySelectionFoodType {
id Int @id @default(autoincrement())
itineraryActivitySelectionXid Int @map("itinerary_activity_selection_xid")
itineraryActivitySelection ItineraryActivitySelection @relation(fields: [itineraryActivitySelectionXid], references: [id], onDelete: Cascade)
activityFoodTypeXid Int @map("activity_food_type_xid")
activityFoodType ActivityFoodTypes @relation(fields: [activityFoodTypeXid], references: [id], onDelete: Cascade)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@unique([itineraryActivitySelectionXid, activityFoodTypeXid])
@@map("itinerary_activity_selection_food_type")
@@schema("itn")
}
model ItineraryActivitySelectionEquipment {
id Int @id @default(autoincrement())
itineraryActivitySelectionXid Int @map("itinerary_activity_selection_xid")
itineraryActivitySelection ItineraryActivitySelection @relation(fields: [itineraryActivitySelectionXid], references: [id], onDelete: Cascade)
activityEquipmentXid Int @map("activity_equipment_xid")
activityEquipment ActivityEquipments @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@unique([itineraryActivitySelectionXid, activityEquipmentXid])
@@map("itinerary_activity_selection_equipment")
@@schema("itn")
}
model ActivitySOSDetails { model ActivitySOSDetails {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
itineraryActivityXid Int @map("itinerary_activity_xid") itineraryActivityXid Int @map("itinerary_activity_xid")
@@ -2012,8 +1809,8 @@ model ItineraryDetails {
activityStatus String @map("activity_status") @db.VarChar(30) activityStatus String @map("activity_status") @db.VarChar(30)
isChargeable Boolean @default(false) @map("is_chargeable") isChargeable Boolean @default(false) @map("is_chargeable")
baseAmount Int @map("base_amount") baseAmount Int @map("base_amount")
totalAmount Int? @map("total_amount") totalAmount Int @map("total_amount")
itineraryStatus String? @map("itinerary_status") @db.VarChar(30) itineraryStatus String @map("itinerary_status") @db.VarChar(30)
isPaid Boolean @default(false) @map("is_paid") isPaid Boolean @default(false) @map("is_paid")
paidByXid Int? @map("paid_by_xid") paidByXid Int? @map("paid_by_xid")
paidBy User? @relation(fields: [paidByXid], references: [id], onDelete: Restrict) paidBy User? @relation(fields: [paidByXid], references: [id], onDelete: Restrict)

View File

@@ -7,73 +7,7 @@ const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }), adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
}); });
const HOST_PERMISSION_MASTER_SEED = [
{ permissionKey: 'profile.company_profile.edit', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'Edit', displayLabel: 'Company Profile - Edit', displayOrder: 1 },
{ permissionKey: 'profile.company_profile.view', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'View', displayLabel: 'Company Profile - View', displayOrder: 2 },
{ permissionKey: 'profile.company_profile.hide', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'Hide', displayLabel: 'Company Profile - Hide', displayOrder: 3 },
{ permissionKey: 'activity_management.onboarding.edit', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'Edit', displayLabel: 'Onboarding - Edit', displayOrder: 4 },
{ permissionKey: 'activity_management.onboarding.view', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'View', displayLabel: 'Onboarding - View', displayOrder: 5 },
{ permissionKey: 'activity_management.onboarding.hide', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'Hide', displayLabel: 'Onboarding - Hide', displayOrder: 6 },
{ permissionKey: 'activity_management.scheduling.edit', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'Edit', displayLabel: 'Scheduling - Edit', displayOrder: 7 },
{ permissionKey: 'activity_management.scheduling.view', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'View', displayLabel: 'Scheduling - View', displayOrder: 8 },
{ permissionKey: 'activity_management.scheduling.hide', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'Hide', displayLabel: 'Scheduling - Hide', displayOrder: 9 },
{ permissionKey: 'activity_management.reservation.edit', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'Edit', displayLabel: 'Reservation - Edit', displayOrder: 10 },
{ permissionKey: 'activity_management.reservation.view', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'View', displayLabel: 'Reservation - View', displayOrder: 11 },
{ permissionKey: 'activity_management.reservation.hide', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'Hide', displayLabel: 'Reservation - Hide', displayOrder: 12 },
{ permissionKey: 'analytics_statements.revenue_statistics.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'Edit', displayLabel: 'Revenue Statistics - Edit', displayOrder: 13 },
{ permissionKey: 'analytics_statements.revenue_statistics.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'View', displayLabel: 'Revenue Statistics - View', displayOrder: 14 },
{ permissionKey: 'analytics_statements.revenue_statistics.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'Hide', displayLabel: 'Revenue Statistics - Hide', displayOrder: 15 },
{ permissionKey: 'analytics_statements.technical_statistics.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'Edit', displayLabel: 'Technical Statistics - Edit', displayOrder: 16 },
{ permissionKey: 'analytics_statements.technical_statistics.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'View', displayLabel: 'Technical Statistics - View', displayOrder: 17 },
{ permissionKey: 'analytics_statements.technical_statistics.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'Hide', displayLabel: 'Technical Statistics - Hide', displayOrder: 18 },
{ permissionKey: 'analytics_statements.reservation.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'Edit', displayLabel: 'Reservation - Edit', displayOrder: 19 },
{ permissionKey: 'analytics_statements.reservation.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'View', displayLabel: 'Reservation - View', displayOrder: 20 },
{ permissionKey: 'analytics_statements.reservation.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'Hide', displayLabel: 'Reservation - Hide', displayOrder: 21 },
{ permissionKey: 'communication.messages.edit', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'Edit', displayLabel: 'Messages - Edit', displayOrder: 22 },
{ permissionKey: 'communication.messages.view', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'View', displayLabel: 'Messages - View', displayOrder: 23 },
{ permissionKey: 'communication.messages.hide', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'Hide', displayLabel: 'Messages - Hide', displayOrder: 24 },
{ permissionKey: 'communication.broadcast.edit', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'Edit', displayLabel: 'Broadcast - Edit', displayOrder: 25 },
{ permissionKey: 'communication.broadcast.view', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'View', displayLabel: 'Broadcast - View', displayOrder: 26 },
{ permissionKey: 'communication.broadcast.hide', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'Hide', displayLabel: 'Broadcast - Hide', displayOrder: 27 },
{ permissionKey: 'promotions.creating_new_promotions.edit', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'Edit', displayLabel: 'Creating New Promotions - Edit', displayOrder: 28 },
{ permissionKey: 'promotions.creating_new_promotions.view', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'View', displayLabel: 'Creating New Promotions - View', displayOrder: 29 },
{ permissionKey: 'promotions.creating_new_promotions.hide', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'Hide', displayLabel: 'Creating New Promotions - Hide', displayOrder: 30 },
{ permissionKey: 'promotions.view_promotions.edit', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'Edit', displayLabel: 'View Promotions - Edit', displayOrder: 31 },
{ permissionKey: 'promotions.view_promotions.view', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'View', displayLabel: 'View Promotions - View', displayOrder: 32 },
{ permissionKey: 'promotions.view_promotions.hide', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'Hide', displayLabel: 'View Promotions - Hide', displayOrder: 33 },
{ permissionKey: 'user_management.inviting_new_users.edit', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'Edit', displayLabel: 'Inviting New Users - Edit', displayOrder: 34 },
{ permissionKey: 'user_management.inviting_new_users.view', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'View', displayLabel: 'Inviting New Users - View', displayOrder: 35 },
{ permissionKey: 'user_management.inviting_new_users.hide', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'Hide', displayLabel: 'Inviting New Users - Hide', displayOrder: 36 },
];
async function seedHostPermissionMasters() {
for (const permission of HOST_PERMISSION_MASTER_SEED) {
await prisma.hostPermissionMasters.upsert({
where: { permissionKey: permission.permissionKey },
update: {
permissionGroup: permission.permissionGroup,
permissionSection: permission.permissionSection,
permissionAction: permission.permissionAction,
displayLabel: permission.displayLabel,
displayOrder: permission.displayOrder,
isActive: true,
deletedAt: null,
},
create: {
permissionKey: permission.permissionKey,
permissionGroup: permission.permissionGroup,
permissionSection: permission.permissionSection,
permissionAction: permission.permissionAction,
displayLabel: permission.displayLabel,
displayOrder: permission.displayOrder,
isActive: true,
},
});
}
}
async function main() { async function main() {
await seedHostPermissionMasters();
// ✅ Countries // ✅ Countries
// const india = await prisma.countries.upsert({ // const india = await prisma.countries.upsert({
// where: { countryName: 'India' }, // where: { countryName: 'India' },
@@ -334,9 +268,9 @@ async function main() {
create: { interestName: 'Nightlife & Events', displayOrder: 10, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/NightlifeandEvents.png', interestCode: 'NE' }, create: { interestName: 'Nightlife & Events', displayOrder: 10, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/NightlifeandEvents.png', interestCode: 'NE' },
}); });
const furfam = await prisma.interests.upsert({ const furfam = await prisma.interests.upsert({
where: { interestName: 'Pet space' }, where: { interestName: 'Fur Fam' },
update: {}, update: {},
create: { interestName: 'Pet space', displayOrder: 11, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/petspace.jpg', interestCode: 'PS' }, create: { interestName: 'Fur Fam', displayOrder: 11, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/petspace.jpg', interestCode: 'PS' },
}); });
const dogoodfeelgood = await prisma.interests.upsert({ const dogoodfeelgood = await prisma.interests.upsert({
where: { interestName: 'Do Good, Feel Good' }, where: { interestName: 'Do Good, Feel Good' },
@@ -759,6 +693,16 @@ async function main() {
skipDuplicates: true, skipDuplicates: true,
}); });
// ✅ Navigation Modes
await prisma.navigationModes.createMany({
data: [
{ navigationModeName: 'Elephant Ride', navigationModeIcon: '🚗' },
{ navigationModeName: 'Horse Ride', navigationModeIcon: '🏍️' },
{ navigationModeName: 'Camel Ride', navigationModeIcon: '🚶' },
],
skipDuplicates: true,
});
// ✅ Transport Modes // ✅ Transport Modes
await prisma.transportModes.createMany({ await prisma.transportModes.createMany({
data: [ data: [

View File

@@ -1,3 +1,5 @@
service: minglar-admin
useDotenv: true useDotenv: true
params: params:
@@ -13,17 +15,12 @@ provider:
runtime: nodejs22.x runtime: nodejs22.x
region: ap-south-1 region: ap-south-1
stage: ${opt:stage, 'dev'} stage: ${opt:stage, 'dev'}
deploymentBucket:
# use a fixed bucket name to prevent Serverless from creating/quashing a resource
name: serverless-framework-deployments-ap-south-1-50264b8e-d2b9
# optionally uncomment below to enable serverless to create if missing
# serverSideEncryption: AES256
versionFunctions: false versionFunctions: false
memorySize: 512 memorySize: 512
# Apply Prisma layer to all functions
# Reference the layer defined in the dedicated layer stack
layers: layers:
- ${cf:minglar-prisma-layer-${sls:stage}.PrismaLambdaLayerQualifiedArn} - ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
httpApi:
id: ${cf:minglar-host-${sls:stage}.HttpApiId}
apiGateway: apiGateway:
binaryMediaTypes: binaryMediaTypes:
- '*/*' - '*/*'
@@ -57,12 +54,8 @@ provider:
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME} MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL} MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK} AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
AM_INTERFACE_LINK: ${env:AM_INTERFACE_LINK}
HOST_LINK: ${env:HOST_LINK} HOST_LINK: ${env:HOST_LINK}
HOST_LINK_PQ: ${env:HOST_LINK_PQ} HOST_LINK_PQ: ${env:HOST_LINK_PQ}
RAZORPAY_KEY_ID: ${env:RAZORPAY_KEY_ID}
RAZORPAY_KEY_SECRET: ${env:RAZORPAY_KEY_SECRET}
RAZORPAY_WEBHOOK_SECRET: ${env:RAZORPAY_WEBHOOK_SECRET}
iam: iam:
role: role:
@@ -77,14 +70,17 @@ provider:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}' - 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*' - 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
custom:
serverless-offline:
reloadHandler: true
build: build:
esbuild: esbuild:
bundle: true bundle: true
minify: true minify: true
sourcemap: false sourcemap: false
target: node20 target: node22
platform: node platform: node
# Mark as external so they're not bundled into the JS
external: external:
- '@prisma/client' - '@prisma/client'
- '.prisma/client' - '.prisma/client'
@@ -95,7 +91,6 @@ build:
- '@aws-sdk/*' - '@aws-sdk/*'
- '@smithy/*' - '@smithy/*'
- '@aws-crypto/*' - '@aws-crypto/*'
# Exclude prevents npm install of these packages in the zip
exclude: exclude:
- 'aws-sdk' - 'aws-sdk'
- '@aws-sdk/*' - '@aws-sdk/*'
@@ -115,6 +110,7 @@ build:
package: package:
individually: true individually: true
excludeDevDependencies: true
patterns: patterns:
- '!node_modules/**' - '!node_modules/**'
- '!node_modules/@prisma/**' - '!node_modules/@prisma/**'
@@ -130,9 +126,8 @@ package:
- '!.git/**' - '!.git/**'
- '!.github/**' - '!.github/**'
functions:
- ${file(./serverless/functions/minglaradmin.yml)}
plugins: plugins:
- serverless-offline - serverless-offline
custom:
serverless-offline:
reloadHandler: true

View File

@@ -1,13 +1,132 @@
service: minglar-host service: minglar-host
useDotenv: ${file(./serverless/common.yml):useDotenv} useDotenv: true
params: ${file(./serverless/common.yml):params}
provider: ${file(./serverless/common.yml):provider} params:
build: ${file(./serverless/common.yml):build} dev:
package: ${file(./serverless/common.yml):package} stage: dev
plugins: ${file(./serverless/common.yml):plugins} test:
custom: ${file(./serverless/common.yml):custom} stage: test
uat:
stage: uat
provider:
name: aws
runtime: nodejs22.x
region: ap-south-1
stage: ${opt:stage, 'dev'}
versionFunctions: false
memorySize: 512
layers:
- ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
environment:
DATABASE_URL: ${env:DATABASE_URL}
DB_USERNAME: ${env:DB_USERNAME}
DB_PASSWORD: ${env:DB_PASSWORD}
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
DB_HOSTNAME: ${env:DB_HOSTNAME}
DB_PORT: ${env:DB_PORT}
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
BYPASS_OTP: ${env:BYPASS_OTP}
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
JWT_SECRET: ${env:JWT_SECRET}
JWT_ACCESS_EXPIRATION_MINUTES: ${env:JWT_ACCESS_EXPIRATION_MINUTES}
JWT_REFRESH_EXPIRATION_DAYS: ${env:JWT_REFRESH_EXPIRATION_DAYS}
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: ${env:JWT_RESET_PASSWORD_EXPIRATION_MINUTES}
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: ${env:JWT_VERIFY_EMAIL_EXPIRATION_MINUTES}
SALT_ROUNDS: ${env:SALT_ROUNDS}
NODE_ENV: ${env:NODE_ENV}
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
HOST_LINK: ${env:HOST_LINK}
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
iam:
role:
statements:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
custom:
serverless-offline:
reloadHandler: true
build:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node22
platform: node
external:
- '@prisma/client'
- '.prisma/client'
- '.prisma'
- '@prisma/adapter-pg'
- 'pg'
- 'zod'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/adapter-pg'
- '@prisma/client'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'zod'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
package:
individually: true
excludeDevDependencies: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
- '!node_modules/.prisma/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!*.config.js'
- '!.git/**'
- '!.github/**'
functions: functions:
- ${file(./serverless/functions/host.yml)} - ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/pqq.yml)}
plugins:
- serverless-offline

View File

@@ -1,4 +1,4 @@
service: minglar-prisma-layer service: minglar-layers
useDotenv: true useDotenv: true
@@ -15,13 +15,9 @@ provider:
runtime: nodejs22.x runtime: nodejs22.x
region: ap-south-1 region: ap-south-1
stage: ${opt:stage, 'dev'} stage: ${opt:stage, 'dev'}
deploymentBucket:
# use a fixed bucket name to prevent Serverless from creating/quashing a resource
name: serverless-framework-deployments-ap-south-1-50264b8e-d2b9
# optionally uncomment below to enable serverless to create if missing
# serverSideEncryption: AES256
versionFunctions: false versionFunctions: false
# Define layers (deployed once; other stacks reference via cf output)
layers: layers:
prisma: prisma:
path: layers/prisma path: layers/prisma
@@ -30,3 +26,5 @@ layers:
compatibleRuntimes: compatibleRuntimes:
- nodejs22.x - nodejs22.x
retain: false retain: false
plugins: []

View File

@@ -1,13 +0,0 @@
service: minglar-admin
useDotenv: ${file(./serverless/common.yml):useDotenv}
params: ${file(./serverless/common.yml):params}
provider: ${file(./serverless/common.yml):provider}
build: ${file(./serverless/common.yml):build}
package: ${file(./serverless/common.yml):package}
plugins: ${file(./serverless/common.yml):plugins}
custom: ${file(./serverless/common.yml):custom}
functions:
- ${file(./serverless/functions/minglaradmin.yml)}

View File

@@ -1,13 +1,133 @@
service: minglar-prepopulate service: minglar-prepopulate
useDotenv: ${file(./serverless/common.yml):useDotenv} useDotenv: true
params: ${file(./serverless/common.yml):params}
provider: ${file(./serverless/common.yml):provider} params:
build: ${file(./serverless/common.yml):build} dev:
package: ${file(./serverless/common.yml):package} stage: dev
plugins: ${file(./serverless/common.yml):plugins} test:
custom: ${file(./serverless/common.yml):custom} stage: test
uat:
stage: uat
provider:
name: aws
runtime: nodejs22.x
region: ap-south-1
stage: ${opt:stage, 'dev'}
versionFunctions: false
memorySize: 512
layers:
- ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
httpApi:
id: ${cf:minglar-host-${sls:stage}.HttpApiId}
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
environment:
DATABASE_URL: ${env:DATABASE_URL}
DB_USERNAME: ${env:DB_USERNAME}
DB_PASSWORD: ${env:DB_PASSWORD}
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
DB_HOSTNAME: ${env:DB_HOSTNAME}
DB_PORT: ${env:DB_PORT}
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
BYPASS_OTP: ${env:BYPASS_OTP}
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
JWT_SECRET: ${env:JWT_SECRET}
JWT_ACCESS_EXPIRATION_MINUTES: ${env:JWT_ACCESS_EXPIRATION_MINUTES}
JWT_REFRESH_EXPIRATION_DAYS: ${env:JWT_REFRESH_EXPIRATION_DAYS}
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: ${env:JWT_RESET_PASSWORD_EXPIRATION_MINUTES}
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: ${env:JWT_VERIFY_EMAIL_EXPIRATION_MINUTES}
SALT_ROUNDS: ${env:SALT_ROUNDS}
NODE_ENV: ${env:NODE_ENV}
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
HOST_LINK: ${env:HOST_LINK}
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
iam:
role:
statements:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
custom:
serverless-offline:
reloadHandler: true
build:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node22
platform: node
external:
- '@prisma/client'
- '.prisma/client'
- '.prisma'
- '@prisma/adapter-pg'
- 'pg'
- 'zod'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/adapter-pg'
- '@prisma/client'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'zod'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
package:
individually: true
excludeDevDependencies: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
- '!node_modules/.prisma/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!*.config.js'
- '!.git/**'
- '!.github/**'
functions: functions:
- ${file(./serverless/functions/prepopulate.yml)} - ${file(./serverless/functions/prepopulate.yml)}
plugins:
- serverless-offline

View File

@@ -1,13 +1,133 @@
service: minglar-user service: minglar-user
useDotenv: ${file(./serverless/common.yml):useDotenv} useDotenv: true
params: ${file(./serverless/common.yml):params}
provider: ${file(./serverless/common.yml):provider} params:
build: ${file(./serverless/common.yml):build} dev:
package: ${file(./serverless/common.yml):package} stage: dev
plugins: ${file(./serverless/common.yml):plugins} test:
custom: ${file(./serverless/common.yml):custom} stage: test
uat:
stage: uat
provider:
name: aws
runtime: nodejs22.x
region: ap-south-1
stage: ${opt:stage, 'dev'}
versionFunctions: false
memorySize: 512
layers:
- ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
httpApi:
id: ${cf:minglar-host-${sls:stage}.HttpApiId}
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
environment:
DATABASE_URL: ${env:DATABASE_URL}
DB_USERNAME: ${env:DB_USERNAME}
DB_PASSWORD: ${env:DB_PASSWORD}
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
DB_HOSTNAME: ${env:DB_HOSTNAME}
DB_PORT: ${env:DB_PORT}
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
BYPASS_OTP: ${env:BYPASS_OTP}
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
JWT_SECRET: ${env:JWT_SECRET}
JWT_ACCESS_EXPIRATION_MINUTES: ${env:JWT_ACCESS_EXPIRATION_MINUTES}
JWT_REFRESH_EXPIRATION_DAYS: ${env:JWT_REFRESH_EXPIRATION_DAYS}
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: ${env:JWT_RESET_PASSWORD_EXPIRATION_MINUTES}
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: ${env:JWT_VERIFY_EMAIL_EXPIRATION_MINUTES}
SALT_ROUNDS: ${env:SALT_ROUNDS}
NODE_ENV: ${env:NODE_ENV}
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
HOST_LINK: ${env:HOST_LINK}
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
iam:
role:
statements:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
custom:
serverless-offline:
reloadHandler: true
build:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node22
platform: node
external:
- '@prisma/client'
- '.prisma/client'
- '.prisma'
- '@prisma/adapter-pg'
- 'pg'
- 'zod'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/adapter-pg'
- '@prisma/client'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'zod'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
package:
individually: true
excludeDevDependencies: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
- '!node_modules/.prisma/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!*.config.js'
- '!.git/**'
- '!.github/**'
functions: functions:
- ${file(./serverless/functions/user.yml)} - ${file(./serverless/functions/user.yml)}
plugins:
- serverless-offline

View File

@@ -1,4 +1,3 @@
# Legacy monolith config. For new deployments use serverless.*.yml files.
service: minglar service: minglar
@@ -17,11 +16,6 @@ provider:
runtime: nodejs22.x runtime: nodejs22.x
region: ap-south-1 region: ap-south-1
stage: ${opt:stage, 'dev'} stage: ${opt:stage, 'dev'}
deploymentBucket:
# use a fixed bucket name to prevent Serverless from creating/quashing a resource
name: serverless-framework-deployments-ap-south-1-50264b8e-d2b9
# optionally uncomment below to enable serverless to create if missing
# serverSideEncryption: AES256
versionFunctions: false versionFunctions: false
memorySize: 512 memorySize: 512
# Apply Prisma layer to all functions # Apply Prisma layer to all functions
@@ -29,13 +23,11 @@ provider:
layers: layers:
# Use the exported stack output so deploy function works (expects a string ARN) # Use the exported stack output so deploy function works (expects a string ARN)
# For offline/local, fall back to an empty string so the CF lookup is optional. # For offline/local, fall back to an empty string so the CF lookup is optional.
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn, ''} - ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn}
apiGateway: apiGateway:
binaryMediaTypes: binaryMediaTypes:
- '*/*' - '*/*'
minimumCompressionSize: 1024 minimumCompressionSize: 1024
websocketsApiName: minglar-ws-${sls:stage}
websocketsApiRouteSelectionExpression: $request.body.action
environment: environment:
DATABASE_URL: ${env:DATABASE_URL} DATABASE_URL: ${env:DATABASE_URL}
@@ -67,9 +59,6 @@ provider:
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK} AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
HOST_LINK: ${env:HOST_LINK} HOST_LINK: ${env:HOST_LINK}
HOST_LINK_PQ: ${env:HOST_LINK_PQ} HOST_LINK_PQ: ${env:HOST_LINK_PQ}
RAZORPAY_KEY_ID: ${env:RAZORPAY_KEY_ID}
RAZORPAY_KEY_SECRET: ${env:RAZORPAY_KEY_SECRET}
RAZORPAY_WEBHOOK_SECRET: ${env:RAZORPAY_WEBHOOK_SECRET}
iam: iam:
role: role:
@@ -83,18 +72,17 @@ provider:
Resource: Resource:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}' - 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*' - 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
- Effect: Allow
Action:
- execute-api:ManageConnections
Resource:
- 'arn:aws:execute-api:${self:provider.region}:*:*/*/@connections/*'
custom:
serverless-offline:
reloadHandler: true
build: build:
esbuild: esbuild:
bundle: true bundle: true
minify: true minify: true
sourcemap: false sourcemap: false
target: node20 target: node22
platform: node platform: node
# Mark as external so they're not bundled into the JS # Mark as external so they're not bundled into the JS
external: external:
@@ -137,6 +125,7 @@ layers:
package: package:
individually: true individually: true
excludeDevDependencies: true
patterns: patterns:
- '!node_modules/**' - '!node_modules/**'
- '!node_modules/@prisma/**' - '!node_modules/@prisma/**'
@@ -157,19 +146,8 @@ functions:
- ${file(./serverless/functions/host.yml)} - ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/minglaradmin.yml)} - ${file(./serverless/functions/minglaradmin.yml)}
- ${file(./serverless/functions/prepopulate.yml)} - ${file(./serverless/functions/prepopulate.yml)}
- ${file(./serverless/functions/pqq.yml)}
- ${file(./serverless/functions/user.yml)} - ${file(./serverless/functions/user.yml)}
- ${file(./serverless/functions/websocket.yml)}
plugins: plugins:
- serverless-offline - serverless-offline
- serverless-plugin-split-stacks
custom:
serverless-offline:
reloadHandler: true
# split-stacks configuration to avoid CloudFormation resource limit
splitStacks:
perFunction: true
perType: true
perGroupFunction: false

View File

@@ -14,7 +14,7 @@ getHosts:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: / path: /host
method: get method: get
verifyOTP: verifyOTP:
@@ -30,7 +30,7 @@ verifyOTP:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Host_Admin/onboarding/verify-otp path: /host/Host_Admin/onboarding/verify-otp
method: post method: post
login: login:
@@ -46,7 +46,7 @@ login:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Host_Admin/onboarding/login path: /host/Host_Admin/onboarding/login
method: post method: post
signUp: signUp:
@@ -62,7 +62,7 @@ signUp:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Host_Admin/onboarding/registration path: /host/Host_Admin/onboarding/registration
method: post method: post
createPassword: createPassword:
@@ -78,7 +78,7 @@ createPassword:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Host_Admin/onboarding/create-password path: /host/Host_Admin/onboarding/create-password
method: post method: post
updateBankDetails: updateBankDetails:
@@ -94,7 +94,7 @@ updateBankDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Host_Admin/onboarding/add-payment-details path: /host/Host_Admin/onboarding/add-payment-details
method: post method: post
saveActivity_ForPQQ: saveActivity_ForPQQ:
@@ -110,7 +110,7 @@ saveActivity_ForPQQ:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/add-activity path: /host/Activity_Hub/OnBoarding/add-activity
method: post method: post
getHostById: getHostById:
@@ -126,7 +126,7 @@ getHostById:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /getById path: /host/getById
method: get method: get
getPQQ_ByQuestionId: getPQQ_ByQuestionId:
@@ -142,7 +142,7 @@ getPQQ_ByQuestionId:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/get-pqq-question-details path: /host/Activity_Hub/OnBoarding/get-pqq-question-details
method: get method: get
getPQQ_LastUpdatedQuestion: getPQQ_LastUpdatedQuestion:
@@ -158,7 +158,7 @@ getPQQ_LastUpdatedQuestion:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/get-latest-pqq-question-details path: /host/Activity_Hub/OnBoarding/get-latest-pqq-question-details
method: get method: get
prePopulateNewActivity: prePopulateNewActivity:
@@ -174,7 +174,7 @@ prePopulateNewActivity:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/prepopulate-new-activity path: /host/Activity_Hub/OnBoarding/prepopulate-new-activity
method: get method: get
createNewActivity: createNewActivity:
@@ -191,7 +191,7 @@ createNewActivity:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/create-new-activity path: /host/Activity_Hub/OnBoarding/create-new-activity
method: patch method: patch
showSuggestion: showSuggestion:
@@ -207,7 +207,7 @@ showSuggestion:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-suggestion path: /host/get-suggestion
method: get method: get
getAllActivitySuggestion: getAllActivitySuggestion:
@@ -223,7 +223,7 @@ getAllActivitySuggestion:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-Activity-suggestion path: /host/get-Activity-suggestion
method: get method: get
getAllHostActivity: getAllHostActivity:
@@ -239,7 +239,7 @@ getAllHostActivity:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/get-all-host-activity path: /host/Activity_Hub/OnBoarding/get-all-host-activity
method: get method: get
acceptAggrement: acceptAggrement:
@@ -255,25 +255,9 @@ acceptAggrement:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Host_Admin/onboarding/accept-agreement path: /host/Host_Admin/onboarding/accept-agreement
method: patch method: patch
getLatestAgreement:
handler: src/modules/host/handlers/Host_Admin/onboarding/getLatestAgreement.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Host_Admin/onboarding/getLatestAgreement.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /Host_Admin/onboarding/get-latest-agreement
method: get
getStepperInfo: getStepperInfo:
handler: src/modules/host/handlers/getStepper.handler handler: src/modules/host/handlers/getStepper.handler
memorySize: 384 memorySize: 384
@@ -292,102 +276,6 @@ getStepperInfo:
path: /stepper path: /stepper
method: get method: get
updateHostProfile:
handler: src/modules/host/handlers/updateHostProfile.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/updateHostProfile.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /profile
method: patch
inviteHostMember:
handler: src/modules/host/handlers/settings/inviteMember.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/settings/**'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /settings/invite-member
method: post
getAllInvitedCoadminAndOperator:
handler: src/modules/host/handlers/settings/getAllInvitedCoadminAndOperator.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/settings/**'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /settings/invited-coadmin-operators
method: get
saveRolePermissions:
handler: src/modules/host/handlers/settings/saveRolePermissions.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/settings/**'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /settings/save-role-permissions
method: post
getPermissionMasters:
handler: src/modules/host/handlers/settings/getPermissionMasters.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/settings/**'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /settings/permission-masters
method: get
getHostMemberRoles:
handler: src/modules/host/handlers/settings/getMemberRoles.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/settings/**'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /settings/member-roles
method: get
# Functions with S3/AWS SDK dependencies # Functions with S3/AWS SDK dependencies
submitCompanyDetails: submitCompanyDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
@@ -400,7 +288,7 @@ submitCompanyDetails:
- 'src/common/**' - 'src/common/**'
events: events:
- httpApi: - httpApi:
path: /Host_Admin/onboarding/add-company-details path: /host/Host_Admin/onboarding/add-company-details
method: patch method: patch
submitPQQ_Answer: submitPQQ_Answer:
@@ -416,7 +304,7 @@ submitPQQ_Answer:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/submit-pqq-answer path: /host/Activity_Hub/OnBoarding/submit-pqq-answer
method: patch method: patch
updatePQQ_LastAnswer: updatePQQ_LastAnswer:
@@ -432,7 +320,7 @@ updatePQQ_LastAnswer:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/submit-final-pqq-answer path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer
method: post method: post
submitPQQForReview: submitPQQForReview:
@@ -448,7 +336,7 @@ submitPQQForReview:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/submit-pqq-for-review path: /host/Activity_Hub/OnBoarding/submit-pqq-for-review
method: patch method: patch
getAllPQQwithSubmittedAns: getAllPQQwithSubmittedAns:
@@ -463,7 +351,7 @@ getAllPQQwithSubmittedAns:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/get-all-pqq-ques-submited-ans path: /host/Activity_Hub/OnBoarding/get-all-pqq-ques-submited-ans
method: get method: get
getAllDetailsOfActivityAndVenue: getAllDetailsOfActivityAndVenue:
@@ -478,7 +366,7 @@ getAllDetailsOfActivityAndVenue:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/get-all-details-activity-venue/{activityXid} path: /host/Activity_Hub/OnBoarding/get-all-details-activity-venue/{activityXid}
method: get method: get
updateSuggestionAsReviewed: updateSuggestionAsReviewed:
@@ -493,7 +381,7 @@ updateSuggestionAsReviewed:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /Activity_Hub/OnBoarding/update-suggestion-reviewed path: /host/Activity_Hub/OnBoarding/update-suggestion-reviewed
method: patch method: patch
resendOTPmail: resendOTPmail:
@@ -634,34 +522,4 @@ openCanceledSlotForActivity:
events: events:
- httpApi: - httpApi:
path: /scheduling/open-canceled-slot path: /scheduling/open-canceled-slot
method: patch method: patch
createActivityAndAllQuestionsEntry:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/createActivityAndAllQuestionsEntry.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/createActivityAndAllQuestionsEntry**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /Activity_Hub/OnBoarding/create-activity
method: post
submitPQAnswer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQAnswer.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQAnswer**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /Activity_Hub/OnBoarding/submit-pq-answer
method: patch

View File

@@ -13,7 +13,7 @@ minglarRegistration:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /registration path: /minglaradmin/registration
method: post method: post
minglarLoginForAdmin: minglarLoginForAdmin:
@@ -28,7 +28,7 @@ minglarLoginForAdmin:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /login path: /minglaradmin/login
method: post method: post
minglarCreatePassword: minglarCreatePassword:
@@ -43,7 +43,7 @@ minglarCreatePassword:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /create-password path: /minglaradmin/create-password
method: post method: post
updateMinglarProfile: updateMinglarProfile:
@@ -60,7 +60,7 @@ updateMinglarProfile:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /update-profile path: /minglaradmin/update-profile
method: patch method: patch
prepopulateRole: prepopulateRole:
@@ -75,7 +75,7 @@ prepopulateRole:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /prepopulate-Roles path: /minglaradmin/prepopulate-Roles
method: get method: get
getHostDetailsById: getHostDetailsById:
@@ -90,7 +90,7 @@ getHostDetailsById:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/get-host-details/{host_xid} path: /minglaradmin/hosthub/hosts/get-host-details/{host_xid}
method: get method: get
inviteTeammate: inviteTeammate:
@@ -105,7 +105,7 @@ inviteTeammate:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /settings/teammates/invite-teammate path: /minglaradmin/settings/teammates/invite-teammate
method: post method: post
getAllHostApplication: getAllHostApplication:
@@ -121,7 +121,7 @@ getAllHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/get-all-host-applications-am path: /minglaradmin/hosthub/hosts/get-all-host-applications-am
method: get method: get
getAllHostActivityForAdmin: getAllHostActivityForAdmin:
@@ -137,7 +137,7 @@ getAllHostActivityForAdmin:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-all-activity-of-host/{id} path: /minglaradmin/get-all-activity-of-host/{id}
method: get method: get
getAllOnboardingHostApplications: getAllOnboardingHostApplications:
@@ -153,7 +153,7 @@ getAllOnboardingHostApplications:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/onboarding/get-all-host-applications-admin path: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin
method: get method: get
getAllOnboardingHostApplications_New: getAllOnboardingHostApplications_New:
@@ -169,7 +169,7 @@ getAllOnboardingHostApplications_New:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/onboarding/get-all-host-applications-admin-new path: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin-new
method: get method: get
getAllInvitationDetails: getAllInvitationDetails:
@@ -184,7 +184,7 @@ getAllInvitationDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /settings/teammates/get-all-invitation-details path: /minglaradmin/settings/teammates/get-all-invitation-details
method: get method: get
addSuggestion: addSuggestion:
@@ -200,7 +200,7 @@ addSuggestion:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/add-suggestion path: /minglaradmin/hosthub/hosts/add-suggestion
method: post method: post
getAllCoadminAndAMDetails: getAllCoadminAndAMDetails:
@@ -215,7 +215,7 @@ getAllCoadminAndAMDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /settings/teammates/get-all-coadmin-am path: /minglaradmin/settings/teammates/get-all-coadmin-am
method: get method: get
getAllInvitedCoadminAndAMDetails: getAllInvitedCoadminAndAMDetails:
@@ -230,7 +230,7 @@ getAllInvitedCoadminAndAMDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /settings/teammates/get-all-invited-coadmin-am path: /minglaradmin/settings/teammates/get-all-invited-coadmin-am
method: get method: get
getAmDetailsbyId: getAmDetailsbyId:
@@ -245,7 +245,7 @@ getAmDetailsbyId:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /settings/teammates/get-am-details-by-id/{amXid} path: /minglaradmin/settings/teammates/get-am-details-by-id/{amXid}
method: get method: get
assignAMToHost: assignAMToHost:
@@ -261,7 +261,7 @@ assignAMToHost:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/onboarding/assign-am path: /minglaradmin/hosthub/onboarding/assign-am
method: patch method: patch
editAgreementDetailsAndAccept: editAgreementDetailsAndAccept:
@@ -277,7 +277,7 @@ editAgreementDetailsAndAccept:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/onboarding/edit-agreement-accept-host path: /minglaradmin/hosthub/onboarding/edit-agreement-accept-host
method: patch method: patch
getAllPqqQuesAnsForAM: getAllPqqQuesAnsForAM:
@@ -292,7 +292,7 @@ getAllPqqQuesAnsForAM:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/onboarding/get-all-pqq-ques-ans-for-am path: /minglaradmin/hosthub/onboarding/get-all-pqq-ques-ans-for-am
method: get method: get
acceptHostApplication: acceptHostApplication:
@@ -308,7 +308,7 @@ acceptHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/accept-host-application path: /minglaradmin/hosthub/hosts/accept-host-application
method: patch method: patch
RejectPQQByAM: RejectPQQByAM:
@@ -324,7 +324,7 @@ RejectPQQByAM:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/reject-pq-by-am path: /minglaradmin/hosthub/hosts/reject-pq-by-am
method: patch method: patch
rejectActivityDetailsApplicationByAM: rejectActivityDetailsApplicationByAM:
@@ -340,7 +340,7 @@ rejectActivityDetailsApplicationByAM:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/reject-activity-application-by-am path: /minglaradmin/hosthub/hosts/reject-activity-application-by-am
method: patch method: patch
acceptPQByAM: acceptPQByAM:
@@ -356,7 +356,7 @@ acceptPQByAM:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/accept-pq-by-am path: /minglaradmin/hosthub/hosts/accept-pq-by-am
method: patch method: patch
acceptActivityDetailsApplicationByAM: acceptActivityDetailsApplicationByAM:
@@ -372,7 +372,7 @@ acceptActivityDetailsApplicationByAM:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/accept-activity-application-by-am path: /minglaradmin/hosthub/hosts/accept-activity-application-by-am
method: patch method: patch
rejectHostApplication: rejectHostApplication:
@@ -388,7 +388,7 @@ rejectHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/onboarding/reject-host-application path: /minglaradmin/hosthub/onboarding/reject-host-application
method: patch method: patch
rejectHostApplicationAM: rejectHostApplicationAM:
@@ -404,7 +404,7 @@ rejectHostApplicationAM:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/reject-host-application-am path: /minglaradmin/hosthub/hosts/reject-host-application-am
method: patch method: patch
addPQQSuggestion: addPQQSuggestion:
@@ -420,7 +420,7 @@ addPQQSuggestion:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/add-Pqq-suggestion path: /minglaradmin/hosthub/hosts/add-Pqq-suggestion
method: post method: post
addActivitySuggestion: addActivitySuggestion:
@@ -436,7 +436,7 @@ addActivitySuggestion:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/hosts/add-Activity-suggestion path: /minglaradmin/hosthub/hosts/add-Activity-suggestion
method: post method: post
getAllPQPDetailsForAM: getAllPQPDetailsForAM:
@@ -452,7 +452,7 @@ getAllPQPDetailsForAM:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/pqp/pqp-details-for-am/{activityXid} path: /minglaradmin/hosthub/pqp/pqp-details-for-am/{activityXid}
method: get method: get
getSuggestionsForAM: getSuggestionsForAM:
@@ -468,5 +468,5 @@ getSuggestionsForAM:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /hosthub/onboarding/show-suggestion-to-am/{hostXid} path: /minglaradmin/hosthub/onboarding/show-suggestion-to-am/{hostXid}
method: get method: get

View File

@@ -0,0 +1,29 @@
createActivityAndAllQuestionsEntry:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/createActivityAndAllQuestionsEntry.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/createActivityAndAllQuestionsEntry**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/create-activity
method: post
submitPQAnswer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQAnswer.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQAnswer**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-pq-answer
method: patch

View File

@@ -13,7 +13,7 @@ getAllBankAndCurrencyDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-all-bank-currency-details path: /prepopulate/get-all-bank-currency-details
method: get method: get
getCityByState: getCityByState:
@@ -29,7 +29,7 @@ getCityByState:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-city-by-state path: /prepopulate/get-city-by-state
method: get method: get
getBranchByBankXid: getBranchByBankXid:
@@ -45,7 +45,7 @@ getBranchByBankXid:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-branch-by-bank path: /prepopulate/get-branch-by-bank
method: get method: get
getAllDocumentCountryStateCityDetails: getAllDocumentCountryStateCityDetails:
@@ -60,7 +60,7 @@ getAllDocumentCountryStateCityDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-all-doc-country path: /prepopulate/get-all-doc-country
method: get method: get
getAllPqqQuesAns: getAllPqqQuesAns:
@@ -75,7 +75,7 @@ getAllPqqQuesAns:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-all-pqq-ques-ans path: /prepopulate/get-all-pqq-ques-ans
method: get method: get
getFrequenciesOfActivity: getFrequenciesOfActivity:
@@ -90,7 +90,7 @@ getFrequenciesOfActivity:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-all-Frequencies path: /prepopulate/get-all-Frequencies
method: get method: get
getAddActivityPrePopulate: getAddActivityPrePopulate:
@@ -105,5 +105,5 @@ getAddActivityPrePopulate:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /get-add-activity-prepopulate path: /prepopulate/get-add-activity-prepopulate
method: get method: get

View File

@@ -13,7 +13,7 @@ registerUser:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /register path: /user/register
method: post method: post
submitPersonalInfo: submitPersonalInfo:
@@ -28,7 +28,7 @@ submitPersonalInfo:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /submit-personal-info path: /user/submit-personal-info
method: post method: post
verifyOtpForUser: verifyOtpForUser:
@@ -43,7 +43,7 @@ verifyOtpForUser:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /verify-otp path: /user/verify-otp
method: post method: post
generateAccessFromRefreshToken: generateAccessFromRefreshToken:
@@ -58,7 +58,7 @@ generateAccessFromRefreshToken:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /generate-access-from-refresh path: /user/generate-access-from-refresh
method: post method: post
@@ -74,7 +74,7 @@ setPasscodeForMobile:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /set-passcode path: /user/set-passcode
method: post method: post
@@ -90,7 +90,7 @@ verifyPasscode:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /verify-passcode path: /user/verify-passcode
method: post method: post
setUserInterest: setUserInterest:
@@ -105,7 +105,7 @@ setUserInterest:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /set-interests path: /user/set-interests
method: post method: post
setUserLocationss: setUserLocationss:
@@ -120,7 +120,7 @@ setUserLocationss:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /set-location-user path: /user/set-location-user
method: post method: post
getLandingPageDetails: getLandingPageDetails:
@@ -135,7 +135,7 @@ getLandingPageDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/get-landing-page-details path: /user/activities/get-landing-page-details
method: get method: get
getSurpriseMePageDetails: getSurpriseMePageDetails:
@@ -150,7 +150,7 @@ getSurpriseMePageDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/get-surprise-me-page-details path: /user/activities/get-surprise-me-page-details
method: get method: get
getActivityDetailsById: getActivityDetailsById:
@@ -165,7 +165,7 @@ getActivityDetailsById:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/get-activity-details-by-id/{activity_xid} path: /user/activities/get-activity-details-by-id/{activity_xid}
method: get method: get
checkAvailabilityDetails: checkAvailabilityDetails:
@@ -180,7 +180,7 @@ checkAvailabilityDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/check-availability/{activity_xid} path: /user/activities/check-availability/{activity_xid}
method: get method: get
searchActivities: searchActivities:
@@ -195,7 +195,7 @@ searchActivities:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/specific-search path: /user/activities/specific-search
method: get method: get
searchSchoolsAndCompanies: searchSchoolsAndCompanies:
@@ -210,7 +210,7 @@ searchSchoolsAndCompanies:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /connections/search-schools-companies path: /user/connections/search-schools-companies
method: get method: get
searchCities: searchCities:
@@ -225,7 +225,7 @@ searchCities:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /connections/search-cities path: /user/connections/search-cities
method: get method: get
addSchoolCompanyDetail: addSchoolCompanyDetail:
@@ -240,7 +240,7 @@ addSchoolCompanyDetail:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /connections/add-school-company path: /user/connections/add-school-company
method: post method: post
removeConnectionDetails: removeConnectionDetails:
@@ -255,7 +255,7 @@ removeConnectionDetails:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /connections/remove-connection-details path: /user/connections/remove-connection-details
method: delete method: delete
getAllConnectionOfUser: getAllConnectionOfUser:
@@ -270,7 +270,7 @@ getAllConnectionOfUser:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /connections/get-all-connections-details path: /user/connections/get-all-connections-details
method: get method: get
getActivityFromConnectionsInterest: getActivityFromConnectionsInterest:
@@ -285,22 +285,7 @@ getActivityFromConnectionsInterest:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /connections/get-activity-from-connections-interest path: /user/connections/get-activity-from-connections-interest
method: get
searchConnectionPeople:
handler: src/modules/user/handlers/connections/searchConnectionPeople.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/handlers/connections/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /connections/search-connection-people
method: get method: get
viewMoreActivitiesByInterest: viewMoreActivitiesByInterest:
@@ -315,7 +300,7 @@ viewMoreActivitiesByInterest:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/view-more-activities path: /user/activities/view-more-activities
method: get method: get
viewMoreActivitiesUpperSection: viewMoreActivitiesUpperSection:
@@ -330,7 +315,7 @@ viewMoreActivitiesUpperSection:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/view-more-activities-upper-section path: /user/activities/view-more-activities-upper-section
method: get method: get
getRandomActiveActivity: getRandomActiveActivity:
@@ -345,7 +330,7 @@ getRandomActiveActivity:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/get-random-active-activity path: /user/activities/get-random-active-activity
method: get method: get
getNearbyActivities: getNearbyActivities:
@@ -360,215 +345,5 @@ getNearbyActivities:
- ${file(./serverless/patterns/base.yml):pattern4} - ${file(./serverless/patterns/base.yml):pattern4}
events: events:
- httpApi: - httpApi:
path: /activities/get-nearby-activities path: /user/activities/get-nearby-activities
method: get method: get
addActivityToBucketInterested:
handler: src/modules/user/handlers/activities/addToBucketInterested.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/handlers/activities/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /activities/add-to-bucket-interested
method: post
removeActivityFromBucketInterested:
handler: src/modules/user/handlers/activities/removeFromBucketInterested.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/handlers/activities/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /activities/remove-from-bucket-interested
method: post
getFilteredLandingPageAllDetails:
handler: src/modules/user/handlers/activities/filteredLandingPageAllDetails.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /activities/get-filtered-landing-page-details
method: get
getAllBucketActivities:
handler: src/modules/user/handlers/activities/getAllBucketActivities.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /activities/get-all-bucket-activities
method: get
getUserItineraryDetails:
handler: src/modules/user/handlers/itinerary/getUserItineraryDetails.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/get-user-itinerary-details
method: get
getItineraryCheckoutDetails:
handler: src/modules/user/handlers/itinerary/getItineraryCheckoutDetails.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/get-itinerary-checkout-details
method: get
saveUserItinerary:
handler: src/modules/user/handlers/itinerary/saveUserItinerary.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/save-user-itinerary
method: post
saveItineraryActivitySelections:
handler: src/modules/user/handlers/itinerary/saveItineraryActivitySelections.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/save-itinerary-activity-selections
method: post
getAllUserSavedItineraries:
handler: src/modules/user/handlers/itinerary/getAllUserSavedItineraries.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/get-all-user-saved-itineraries
method: get
cancelUserItinerary:
handler: src/modules/user/handlers/itinerary/cancelUserItinerary.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/cancel-itinerary
method: post
createRazorpayOrder:
handler: src/modules/user/handlers/payment/createOrder.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /payment/create-order
method: post
verifyRazorpayPayment:
handler: src/modules/user/handlers/payment/verifyPayment.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /payment/verify-payment
method: post
razorpayWebhook:
handler: src/modules/user/handlers/payment/razorpayWebhook.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /payment/webhook/razorpay
method: post
getMatchingBucketInterestedActivities:
handler: src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/get-matching-bucket-interested-activities
method: post

View File

@@ -1,64 +0,0 @@
websocketConnect:
handler: src/modules/websocket/handlers/connect.handler
memorySize: 256
package:
patterns:
- 'src/modules/websocket/**'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- websocket:
route: $connect
websocketDisconnect:
handler: src/modules/websocket/handlers/disconnect.handler
memorySize: 256
package:
patterns:
- 'src/modules/websocket/**'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- websocket:
route: $disconnect
websocketDefault:
handler: src/modules/websocket/handlers/default.handler
memorySize: 256
package:
patterns:
- 'src/modules/websocket/**'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- websocket:
route: $default
websocketSendMessage:
handler: src/modules/websocket/handlers/sendMessage.handler
memorySize: 384
package:
patterns:
- 'src/modules/websocket/**'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- websocket:
route: sendMessage
websocketGetMessages:
handler: src/modules/websocket/handlers/getMessages.handler
memorySize: 384
package:
patterns:
- 'src/modules/websocket/**'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- websocket:
route: getMessages

View File

@@ -1,78 +0,0 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '@/common/utils/constants/common.constant';
import { prisma } from '../../database/prisma.client';
interface DecodedToken {
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
}
export async function verifyAnyToken(
token: string
): Promise<{ id: number; roleXid: number; role?: string }> {
if (!token) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
if (!userId) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
}
const user = await prisma.user.findUnique({
where: { id: userId },
include: { role: true },
});
const latestToken = await prisma.token.findFirst({
where: { userXid: userId },
orderBy: { id: 'desc' },
});
if (latestToken?.isBlackListed === true) {
throw new ApiError(401, 'This session is expired. Please login.');
}
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
if (user.isActive === false) {
throw new ApiError(
httpStatus.FORBIDDEN,
'Your account is deactivated by admin.'
);
}
if (user.roleXid !== ROLE.USER && user.roleXid !== ROLE.HOST) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
return { id: user.id, roleXid: user.roleXid || 0, role: user.role?.roleName };
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new ApiError(
httpStatus.UNAUTHORIZED,
'Your session has expired. Please log in again.'
);
}
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
httpStatus.FORBIDDEN,
'Invalid or expired authentication token.'
);
}
}

View File

@@ -1,150 +0,0 @@
import { PrismaClient } from '@prisma/client';
import ApiError from '../utils/helper/ApiError';
interface SendMessageInput {
activityXid: number;
senderXid: number;
receiverXid: number;
message: string;
status?: string;
}
interface GetMessagesInput {
activityXid: number;
userXid: number;
otherUserXid: number;
limit?: number;
}
export class ChatService {
constructor(private prisma: PrismaClient) {}
private async getHostUserIdForActivity(activityXid: number): Promise<number> {
const activity = await this.prisma.activities.findUnique({
where: { id: activityXid },
select: { host: { select: { userXid: true } } },
});
if (!activity) {
throw new ApiError(404, 'Activity not found');
}
const hostUserXid = activity.host?.userXid;
if (!hostUserXid) {
throw new ApiError(400, 'Host user not found for activity');
}
return hostUserXid;
}
async sendMessage(input: SendMessageInput) {
if (!input.activityXid || isNaN(input.activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!input.senderXid || isNaN(input.senderXid)) {
throw new ApiError(400, 'Valid senderXid is required');
}
if (!input.receiverXid || isNaN(input.receiverXid)) {
throw new ApiError(400, 'Valid receiverXid is required');
}
if (input.senderXid === input.receiverXid) {
throw new ApiError(400, 'Sender and receiver cannot be the same');
}
const message = input.message?.trim();
if (!message) {
throw new ApiError(400, 'Message is required');
}
const hostUserXid = await this.getHostUserIdForActivity(input.activityXid);
if (input.senderXid !== hostUserXid && input.receiverXid !== hostUserXid) {
throw new ApiError(
400,
'Sender or receiver must be the host for this activity'
);
}
const receiverExists = await this.prisma.user.findUnique({
where: { id: input.receiverXid },
select: { id: true },
});
if (!receiverExists) {
throw new ApiError(404, 'Receiver not found');
}
return this.prisma.activityMessages.create({
data: {
activityXid: input.activityXid,
senderXid: input.senderXid,
receivedXid: input.receiverXid,
message,
status: input.status?.trim() || 'unread',
},
});
}
async getMessages(input: GetMessagesInput) {
if (!input.activityXid || isNaN(input.activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!input.userXid || isNaN(input.userXid)) {
throw new ApiError(400, 'Valid userXid is required');
}
if (!input.otherUserXid || isNaN(input.otherUserXid)) {
throw new ApiError(400, 'Valid otherUserXid is required');
}
if (input.userXid === input.otherUserXid) {
throw new ApiError(400, 'Invalid otherUserXid');
}
const hostUserXid = await this.getHostUserIdForActivity(input.activityXid);
if (input.userXid !== hostUserXid && input.otherUserXid !== hostUserXid) {
throw new ApiError(
400,
'Conversation must include the host for this activity'
);
}
const limit = Math.min(Math.max(input.limit || 50, 1), 200);
const messages = await this.prisma.activityMessages.findMany({
where: {
activityXid: input.activityXid,
OR: [
{
senderXid: input.userXid,
receivedXid: input.otherUserXid,
},
{
senderXid: input.otherUserXid,
receivedXid: input.userXid,
},
],
},
orderBy: { createdAt: 'asc' },
take: limit,
});
await this.prisma.activityMessages.updateMany({
where: {
activityXid: input.activityXid,
senderXid: input.otherUserXid,
receivedXid: input.userXid,
status: 'unread',
},
data: { status: 'read' },
});
return messages;
}
}

View File

@@ -1,58 +0,0 @@
import { PrismaClient } from '@prisma/client';
export class WebSocketService {
constructor(private prisma: PrismaClient) {}
async connect(params: {
connectionId: string;
userXid: number;
activityXid?: number | null;
}) {
const { connectionId, userXid, activityXid } = params;
return this.prisma.chatConnections.upsert({
where: { connectionId },
create: {
connectionId,
userXid,
activityXid: activityXid ?? null,
isActive: true,
},
update: {
userXid,
activityXid: activityXid ?? null,
isActive: true,
deletedAt: null,
},
});
}
async disconnect(connectionId: string) {
return this.prisma.chatConnections.updateMany({
where: { connectionId },
data: {
isActive: false,
deletedAt: new Date(),
},
});
}
async getConnectionById(connectionId: string) {
return this.prisma.chatConnections.findFirst({
where: { connectionId, isActive: true },
});
}
async getConnectionsForUser(params: {
userXid: number;
activityXid?: number | null;
}) {
const { userXid, activityXid } = params;
return this.prisma.chatConnections.findMany({
where: {
userXid,
isActive: true,
...(activityXid ? { activityXid } : {}),
},
});
}
}

View File

@@ -55,7 +55,7 @@ export const ACTIVITY_DISPLAY_STATUS = {
PQ_IN_REVIEW: 'PQ In Review', PQ_IN_REVIEW: 'PQ In Review',
PQ_APPROVED: 'PQ Approved', PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft', ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_IN_REVIEW: 'In Review', ACTIVITY_IN_REVIEW: 'In Review',
ACTIVITY_TO_REVIEW: 'Re-submitted', ACTIVITY_TO_REVIEW: 'Re-submitted',
NOT_LISTED: 'Not Listed', NOT_LISTED: 'Not Listed',
@@ -94,7 +94,7 @@ export const ACTIVITY_AM_DISPLAY_STATUS = {
PQ_APPROVED: 'PQ Approved', PQ_APPROVED: 'PQ Approved',
REVISED: 'Revised', REVISED: 'Revised',
ACTIVITY_DRAFT: 'Draft', ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_NEW: 'New', ACTIVITY_NEW: 'New',
ACTIVITY_TO_REVIEW: 'Activity To Review', ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_ENHANCING: 'Enhancing', ACTIVITY_ENHANCING: 'Enhancing',

View File

@@ -46,18 +46,6 @@ export const parentCompanySchema = z.object({
companyTypeXid: z.number() companyTypeXid: z.number()
.optional(), .optional(),
firstName: z.string()
.max(50, "First name cannot exceed 50 characters")
.optional(),
lastName: z.string()
.max(50, "Last name cannot exceed 50 characters")
.optional(),
mobileNumber: z.string()
.max(15, "Mobile number cannot exceed 15 characters")
.optional(),
websiteUrl: z.string().nullable().optional(), websiteUrl: z.string().nullable().optional(),
instagramUrl: z.string().nullable().optional(), instagramUrl: z.string().nullable().optional(),
facebookUrl: z.string().nullable().optional(), facebookUrl: z.string().nullable().optional(),

View File

@@ -83,12 +83,8 @@ const envVarsSchema = yup
BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'), BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'),
// Email links // Email links
AM_INVITATION_LINK: yup.string().required('Link to send in AM invitation mail is required'), AM_INVITATION_LINK: yup.string().required('Link to send in AM invitation mail is required'),
AM_INTERFACE_LINK:yup.string().required('Link to am interface is required'),
HOST_LINK: yup.string().required('Link to host panel is required'), HOST_LINK: yup.string().required('Link to host panel is required'),
HOST_LINK_PQ: yup.string().required('Link to host panel pqp is required'), HOST_LINK_PQ: yup.string().required('Link to host panel pqp is required')
RAZORPAY_KEY_SECRET: yup.string().required('Razorpay key secret is required'),
RAZORPAY_KEY_ID: yup.string().required('Razorpay key id is required'),
RAZORPAY_WEBHOOK_SECRET: yup.string().required('Razorpay webhook secret is required'),
}) })
.noUnknown(true); .noUnknown(true);
@@ -169,11 +165,6 @@ function getConfig() {
AM_INVITATION_LINK: envVars.AM_INVITATION_LINK, AM_INVITATION_LINK: envVars.AM_INVITATION_LINK,
HOST_LINK: envVars.HOST_LINK, HOST_LINK: envVars.HOST_LINK,
HOST_LINK_PQ: envVars.HOST_LINK_PQ, HOST_LINK_PQ: envVars.HOST_LINK_PQ,
RAZORPAY_KEY_ID: envVars.RAZORPAY_KEY_ID,
RAZORPAY_KEY_SECRET: envVars.RAZORPAY_KEY_SECRET,
RAZORPAY_WEBHOOK_SECRET: envVars.RAZORPAY_WEBHOOK_SECRET,
AM_INTERFACE_LINK: envVars.AM_INTERFACE_LINK,
// oneSignal: { // oneSignal: {
// appID: envVars.ONESIGNAL_APPID, // appID: envVars.ONESIGNAL_APPID,
// restApiKey: envVars.ONESIGNAL_REST_APIKEY, // restApiKey: envVars.ONESIGNAL_REST_APIKEY,

View File

@@ -54,7 +54,7 @@ export const EquipmentDto = z.object({
/* ================= NAVIGATION MODE ================= */ /* ================= NAVIGATION MODE ================= */
export const NavigationModeDto = z.object({ export const NavigationModeDto = z.object({
navigationModeName: z.string().optional(), navigationModeXid: z.number().int(),
isChargeable: z.boolean().optional(), isChargeable: z.boolean().optional(),
totalPrice: z.number().int().optional().default(0), totalPrice: z.number().int().optional().default(0),
}); });

View File

@@ -8,7 +8,6 @@ import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHo
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError'; import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service'; import { HostService } from '../../../services/host.service';
import { sendPQPEmailToAM } from '../../../services/sendHostResubmitEmailToAM.service';
const hostService = new HostService(prismaClient); const hostService = new HostService(prismaClient);
@@ -178,15 +177,6 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const getAllUpdatedQuestionResponse = await hostService.getAllPQUpdatedResponse(activityXid) const getAllUpdatedQuestionResponse = await hostService.getAllPQUpdatedResponse(activityXid)
const details = await hostService.getSuggestionDetails(user.id);
await sendPQPEmailToAM(
details.hostDetails.accountManager.emailAddress,
details.hostDetails.accountManager.firstName,
details.hostDetails.companyName,
details.hostDetails.user.userRefNumber,
)
// CASE 2 — NO deletion & NO new files => DO NOTHING to existing files // CASE 2 — NO deletion & NO new files => DO NOTHING to existing files
return { return {

View File

@@ -13,40 +13,6 @@ const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region }); const s3 = new AWS.S3({ region: config.aws.region });
function parseMultipartFieldValue(val: string) {
if (val === '' || val === 'null' || val === 'undefined') return null;
const cleaned = val.trim();
const looksLikeJson =
(cleaned.startsWith('{') && cleaned.endsWith('}')) ||
(cleaned.startsWith('[') && cleaned.endsWith(']')) ||
(cleaned.startsWith('"') && cleaned.endsWith('"'));
if (!looksLikeJson) return val;
try {
return JSON.parse(cleaned);
} catch {
return val;
}
}
function normalizeComments(comments: unknown): string | null {
if (comments === null || comments === undefined || comments === '') return null;
const value = String(comments).trim();
if (!value) return null;
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
return value.slice(1, -1);
}
return value;
}
// Function to extract S3 key from URL // Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string { function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`; const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
@@ -156,7 +122,22 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
bb.on("field", (fieldname, val) => { bb.on("field", (fieldname, val) => {
console.log(`FIELD RAW: ${fieldname} =`, val); console.log(`FIELD RAW: ${fieldname} =`, val);
fields[fieldname] = parseMultipartFieldValue(val); if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null;
else {
try {
const cleaned = val.trim();
// If it starts and ends with quotes, remove them
const withoutQuotes =
(cleaned.startsWith('"') && cleaned.endsWith('"'))
? cleaned.slice(1, -1)
: cleaned;
fields[fieldname] = JSON.parse(withoutQuotes);
} catch {
fields[fieldname] = val;
}
}
}); });
bb.on("close", () => resolve()); bb.on("close", () => resolve());
@@ -173,7 +154,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const activityXid = Number(fields.activityXid); const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid); const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid); const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = normalizeComments(fields.comments); const comments = fields.comments || null;
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Please provide a valid activity"); if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Please provide a valid activity");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Please select a valid question"); if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Please select a valid question");

View File

@@ -1,6 +1,6 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError'; import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service'; import { HostService } from '../../../services/host.service';
@@ -25,8 +25,9 @@ export const handler = safeHandler(async (
// Verify token and get user info // Verify token and get user info
const userInfo = await verifyHostToken(token); const userInfo = await verifyHostToken(token);
// Accept agreement and get dynamic fields and PDF URL
const result = await hostService.acceptMinglarAgreement(userInfo.id); // Add suggestion using service
await hostService.acceptMinglarAgreement(userInfo.id);
return { return {
statusCode: 200, statusCode: 200,
@@ -37,10 +38,7 @@ export const handler = safeHandler(async (
body: JSON.stringify({ body: JSON.stringify({
success: true, success: true,
message: 'Application accepted successfully', message: 'Application accepted successfully',
data: { data: null,
filePath: result.filePath,
dynamicFields: result.dynamicFields,
},
}), }),
}; };
}); });

View File

@@ -4,7 +4,6 @@ import { prismaClient } from '../../../../../common/database/prisma.lambda.servi
import { HostService } from '../../../services/host.service'; import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError'; import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { sendWelcomeEmailToHost } from '../../../services/sendOTPEmail.service';
const hostService = new HostService(prismaClient); const hostService = new HostService(prismaClient);
@@ -47,8 +46,7 @@ export const handler = safeHandler(async (
throw new ApiError(400, 'Password must be at least 8 characters long'); throw new ApiError(400, 'Password must be at least 8 characters long');
} }
const result = await hostService.createPassword(user_xid, password); await hostService.createPassword(user_xid, password);
await sendWelcomeEmailToHost(result.emailAddress);
return { return {
statusCode: 200, statusCode: 200,

View File

@@ -1,54 +0,0 @@
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const hostService = new HostService(prismaClient);
/**
* Get latest active agreement for a specific host by hostXid.
* Accessible for Minglar Admin / Host Admin using admin-host token.
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Validate admin/host admin token
await verifyMinglarAdminHostToken(token);
const hostXidParam =
event.queryStringParameters?.hostXid ?? event.queryStringParameters?.host_xid;
const hostXid = Number(hostXidParam);
if (!hostXidParam) {
throw new ApiError(400, 'hostXid is required');
}
if (Number.isNaN(hostXid)) {
throw new ApiError(400, 'Invalid hostXid format');
}
const agreement = await hostService.getLatestHostAgreement(hostXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Latest host agreement retrieved successfully',
data: agreement,
}),
};
});

View File

@@ -4,9 +4,13 @@ import { prismaClient } from '../../../../../common/database/prisma.lambda.servi
import { ROLE } from '../../../../../common/utils/constants/common.constant'; import { ROLE } from '../../../../../common/utils/constants/common.constant';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError'; import ApiError from '../../../../../common/utils/helper/ApiError';
import { encryptUserId } from '../../../../../common/utils/helper/CodeGenerator';
import { OtpGeneratorSixDigit } from '../../../../../common/utils/helper/OtpGenerator'; import { OtpGeneratorSixDigit } from '../../../../../common/utils/helper/OtpGenerator';
import { HostService } from '../../../services/host.service';
import { sendOtpEmailForHost } from '@/modules/host/services/sendOTPEmail.service'; import { sendOtpEmailForHost } from '@/modules/host/services/sendOTPEmail.service';
const hostService = new HostService(prismaClient);
export async function generateHostRefNumber(tx: any) { export async function generateHostRefNumber(tx: any) {
const lastrecord = await tx.user.findFirst({ const lastrecord = await tx.user.findFirst({
orderBy: { orderBy: {
@@ -41,23 +45,13 @@ export const handler = safeHandler(async (
throw new ApiError(400, 'Email is required'); throw new ApiError(400, 'Email is required');
} }
const emailToLowerCase = email.trim().toLowerCase(); const emailToLowerCase = email.toLowerCase()
if (!emailToLowerCase) {
throw new ApiError(400, 'Email is required');
}
// Use a single transaction for user creation/lookup and OTP storage // Use a single transaction for user creation/lookup and OTP storage
const transactionResult = await prismaClient.$transaction(async (tx) => { const transactionResult = await prismaClient.$transaction(async (tx) => {
const user = await tx.user.findUnique({ const user = await tx.user.findUnique({
where: { emailAddress: emailToLowerCase }, where: { emailAddress: emailToLowerCase },
select: { select: { emailAddress: true, id: true, userPassword: true },
emailAddress: true,
id: true,
userPassword: true,
dataConsentAccepted: true,
dataConsentAcceptedOn: true,
},
}); });
if (user && user.userPassword) { if (user && user.userPassword) {
@@ -99,18 +93,9 @@ export const handler = safeHandler(async (
}, },
}); });
await tx.user.update({ const encryptedId = encryptUserId(String(newUserLocal.id));
where: { id: Number(newUserLocal.id) },
data: {
dataConsentAccepted: true,
dataConsentAcceptedOn:
user?.dataConsentAccepted && user?.dataConsentAcceptedOn
? user.dataConsentAcceptedOn
: new Date(),
},
});
return { newUser: newUserLocal, otp }; return { newUser: newUserLocal, otp, encryptedId };
}); });
if (!transactionResult || !transactionResult.otp) { if (!transactionResult || !transactionResult.otp) {

View File

@@ -142,10 +142,6 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const deletedFiles = normalizeJsonField(fields, "deletedFiles") || []; const deletedFiles = normalizeJsonField(fields, "deletedFiles") || [];
const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || []; const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || [];
const deleteCompanyLogo =
fields.deleteCompanyLogo === 'true' || fields.deleteCompanyLogo === true;
const deleteParentCompanyLogo =
fields.deleteParentCompanyLogo === 'true' || fields.deleteParentCompanyLogo === true;
/** 4) Extract and clean isDraft flag */ /** 4) Extract and clean isDraft flag */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true; const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
@@ -176,46 +172,14 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
if (fields.userProfile) { if (fields.userProfile) {
const userProfileRaw = normalizeJsonField(fields, 'userProfile'); const userProfileRaw = normalizeJsonField(fields, 'userProfile');
if (userProfileRaw) { if (userProfileRaw) {
const firstName = const { firstName, lastName, mobileNumber } = userProfileRaw;
typeof userProfileRaw.firstName === 'string'
? userProfileRaw.firstName.trim()
: undefined;
const lastName =
typeof userProfileRaw.lastName === 'string'
? userProfileRaw.lastName.trim()
: undefined;
const mobileNumber =
typeof userProfileRaw.mobileNumber === 'string'
? userProfileRaw.mobileNumber.trim()
: undefined;
if (mobileNumber) {
const existingUser = await prismaClient.user.findFirst({
where: {
mobileNumber,
id: {
not: Number(userInfo.id),
},
},
select: {
id: true,
},
});
if (existingUser) {
throw new ApiError(
409,
'Mobile number already exists for another user. Please use a different mobile number.',
);
}
}
await prismaClient.user.update({ await prismaClient.user.update({
where: { id: userInfo.id }, where: { id: userInfo.id },
data: { data: {
...(firstName !== undefined && { firstName }), ...(firstName && { firstName }),
...(lastName !== undefined && { lastName }), ...(lastName && { lastName }),
...(mobileNumber !== undefined && { mobileNumber }), ...(mobileNumber && { mobileNumber }),
}, },
}); });
} }
@@ -415,63 +379,6 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}); });
} }
/** DELETE EXISTING LOGO IF REQUESTED */
if (deleteCompanyLogo) {
const existingHost = await prismaClient.hostHeader.findFirst({
where: { userXid: userInfo.id },
select: { logoPath: true },
});
if (existingHost?.logoPath) {
try {
const s3Key = getS3KeyFromUrl(existingHost.logoPath);
await deleteFromS3(s3Key);
} catch (e) {
console.error('S3 delete failed for company logo:', existingHost.logoPath, e);
}
}
parsedCompany.logoPath = null;
}
/** DELETE EXISTING PARENT COMPANY LOGO IF REQUESTED */
if (deleteParentCompanyLogo && parsedCompany.isSubsidairy) {
const existingHost = await prismaClient.hostHeader.findFirst({
where: { userXid: userInfo.id },
select: {
id: true,
hostParent: {
select: {
id: true,
logoPath: true,
},
take: 1,
},
},
});
const existingParent = Array.isArray(existingHost?.hostParent)
? existingHost.hostParent[0]
: existingHost?.hostParent;
if (existingParent?.logoPath) {
try {
const s3Key = getS3KeyFromUrl(existingParent.logoPath);
await deleteFromS3(s3Key);
} catch (e) {
console.error('S3 delete failed for parent company logo:', existingParent.logoPath, e);
}
}
if (parsedParentCompany) {
parsedParentCompany.logoPath = null;
} else {
parsedParentCompany = {
logoPath: null,
};
}
}
/** UPLOAD LOGO (if provided) */ /** UPLOAD LOGO (if provided) */
const logoFile = files.find( const logoFile = files.find(
(f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile' (f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
@@ -542,7 +449,6 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
parsedParentCompany, parsedParentCompany,
uploadedParentDocs, uploadedParentDocs,
isDraft, isDraft,
{ deleteCompanyLogo, deleteParentCompanyLogo },
); );
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.'); if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');

View File

@@ -39,7 +39,6 @@ export const handler = safeHandler(async (
data: { data: {
stepper: host?.host?.stepper || null, stepper: host?.host?.stepper || null,
emailAddress: host.user?.emailAddress || null, emailAddress: host.user?.emailAddress || null,
hostId: host.user?.userRefNumber || null,
}, },
}), }),
}; };

View File

@@ -1,61 +0,0 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
import { paginationService } from '../../../../common/utils/pagination/pagination.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { HostMemberService } from '../../services/hostMember.service';
const hostMemberService = new HostMemberService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyHostToken(token);
const search = event.queryStringParameters?.search || '';
const paginationParams = paginationService.getPaginationFromEvent(event);
const paginationOptions =
paginationService.parsePaginationParams(paginationParams);
const { data, totalCount } =
await hostMemberService.getAllInvitedCoadminAndOperator({
hostUserXid: userInfo.id,
search,
paginationOptions,
});
const paginatedResponse = paginationService.createPaginatedResponse(
data,
totalCount,
paginationOptions,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Invited co-admin and operator members fetched successfully',
...paginatedResponse,
}),
};
});

View File

@@ -1,59 +0,0 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
import { ROLE } from '../../../../common/utils/constants/common.constant';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
await verifyHostToken(token);
const roles = await prismaClient.roles.findMany({
where: {
id: {
in: [ROLE.CO_ADMIN, ROLE.OPERATOR],
},
isActive: true,
deletedAt: null,
},
select: {
id: true,
roleName: true,
},
orderBy: {
id: 'asc',
},
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host member roles fetched successfully',
data: {
roles,
},
}),
};
});

View File

@@ -1,60 +0,0 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
await verifyHostToken(token);
const permissionMasters = await prismaClient.hostPermissionMasters.findMany({
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
permissionKey: true,
permissionGroup: true,
permissionSection: true,
permissionAction: true,
displayLabel: true,
displayOrder: true,
},
orderBy: {
displayOrder: 'asc',
},
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Permission masters fetched successfully',
data: {
permissionMasters,
},
}),
};
});

View File

@@ -1,111 +0,0 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { HostMemberService } from '../../services/hostMember.service';
import { sendHostMemberInvitationEmail } from '../../services/sendHostMemberInvitationEmail.service';
const hostMemberService = new HostMemberService(prismaClient);
interface InviteMemberBody {
emailAddress: string;
roleXid: number;
permissionMasterXid: number;
activityXids: number[];
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyHostToken(token);
let body: Partial<InviteMemberBody> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const emailAddress =
typeof body.emailAddress === 'string' ? body.emailAddress.trim() : '';
const roleXid = Number(body.roleXid);
const permissionMasterXid = Number(body.permissionMasterXid);
const activityXids = Array.isArray(body.activityXids)
? body.activityXids
: [];
if (!emailAddress) {
throw new ApiError(400, 'emailAddress is required.');
}
if (!Number.isInteger(roleXid) || roleXid <= 0) {
throw new ApiError(400, 'roleXid is required.');
}
if (!Number.isInteger(permissionMasterXid) || permissionMasterXid <= 0) {
throw new ApiError(400, 'permissionMasterXid is required.');
}
if (!activityXids.length) {
throw new ApiError(400, 'activityXids is required.');
}
const inviteResult = await hostMemberService.inviteMember({
inviterUserXid: userInfo.id,
emailAddress,
roleXid,
permissionMasterXid,
activityXids,
});
await sendHostMemberInvitationEmail(
inviteResult.user.emailAddress ?? emailAddress,
inviteResult.host.companyName,
inviteResult.permissionMaster.role.roleName,
inviteResult.permissionDetails.map((permission) => permission.displayLabel),
inviteResult.activities.map((activity) => activity.activityTitle ?? `Activity #${activity.id}`),
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host member invited successfully',
data: {
hostMemberId: inviteResult.hostMember.id,
hostXid: inviteResult.hostMember.hostXid,
userXid: inviteResult.hostMember.userXid,
emailAddress: inviteResult.user.emailAddress,
roleXid: inviteResult.hostMember.roleXid,
permissionMasterXid: inviteResult.hostMember.hostRolePermissionMasterXid,
permissionMasterXids: inviteResult.permissionMaster.permissionMasterXids,
permissionLabels: inviteResult.permissionDetails.map((permission) => permission.displayLabel),
activityXids: inviteResult.activities.map((activity) => activity.id),
activityNames: inviteResult.activities.map((activity) => activity.activityTitle ?? null),
memberStatus: inviteResult.hostMember.memberStatus,
},
}),
};
});

View File

@@ -1,81 +0,0 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { HostRolePermissionService } from '../../services/hostRolePermission.service';
const hostRolePermissionService = new HostRolePermissionService(prismaClient);
interface SaveRolePermissionsBody {
roleXid: number;
permissionMasterXids: number[];
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyHostToken(token);
let body: Partial<SaveRolePermissionsBody> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const roleXid = Number(body.roleXid);
const permissionMasterXids = Array.isArray(body.permissionMasterXids)
? body.permissionMasterXids
: [];
if (!Number.isInteger(roleXid) || roleXid <= 0) {
throw new ApiError(400, 'roleXid is required.');
}
if (!permissionMasterXids.length) {
throw new ApiError(400, 'permissionMasterXids is required.');
}
const result = await hostRolePermissionService.saveRolePermissions({
hostUserXid: userInfo.id,
roleXid,
permissionMasterXids,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Role permissions saved successfully',
data: {
permissionMasterXid: result.saved.id,
hostXid: result.saved.hostXid,
roleXid: result.saved.roleXid,
permissionMasterXids: result.saved.permissionMasterXids,
selectedPermissions: result.selectedPermissions,
},
}),
};
});

View File

@@ -1,344 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import AWS from 'aws-sdk';
import dayjs from 'dayjs';
import { z } from 'zod';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
import { ROLE } from '../../../common/utils/constants/common.constant';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from "../../../common/utils/helper/ApiError";
import { parseMultipartFormData } from '../../../common/utils/helper/parseMultipartFormData';
import config from '../../../config/config';
const s3 = new AWS.S3({
region: config.aws.region,
});
const updateHostProfileSchema = z
.strictObject({
// Personal
fullName: z.string().min(1).optional(),
firstName: z.string().min(1).optional(),
lastName: z.string().min(1).optional(),
isdCode: z.string().min(1).max(6).optional(),
mobileNumber: z.string().min(5).max(15).optional(),
dateOfBirth: z.string().min(1).optional(),
profileImage: z.string().url().optional(),
// Address
address1: z.string().min(1).optional(),
address2: z.string().min(1).optional(),
countryXid: z.number().int().positive().optional(),
stateXid: z.number().int().positive().optional(),
cityXid: z.number().int().positive().optional(),
pinCode: z.string().min(1).optional(),
// explicitly forbidden
emailAddress: z.any().optional(),
})
.strip();
async function uploadProfileImageToS3(buffer: Buffer, mimeType: string, originalName: string, userId: number) {
const sanitizeFileName = (name: string) => {
return name
.toLowerCase()
.replace(/[^a-z0-9.]/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
};
const fileExtension = originalName.split('.').pop() || 'jpg';
const fileName = `profile_image.${fileExtension}`;
const sanitizedFileName = sanitizeFileName(fileName);
const s3Key = `Host/ProfileImages/${userId}/${sanitizedFileName}`;
await s3
.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private',
})
.promise();
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
function parseDob(dateOfBirth: string): Date {
const parsed = dayjs(dateOfBirth, ['YYYY-MM-DD', 'MM/DD/YYYY', 'DD/MM/YYYY'], true);
if (!parsed.isValid()) {
throw new ApiError(400, 'Invalid dateOfBirth. Use YYYY-MM-DD (recommended) or MM/DD/YYYY.');
}
return parsed.toDate();
}
function splitFullName(fullName: string): { firstName: string; lastName: string | null } {
const parts = fullName.trim().split(/\s+/).filter(Boolean);
const firstName = parts[0] || '';
const lastName = parts.length > 1 ? parts.slice(1).join(' ') : null;
return { firstName, lastName };
}
function getAuthToken(event: APIGatewayProxyEvent): string {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
return token;
}
function parseJsonBody(event: APIGatewayProxyEvent): any {
try {
return event.body ? JSON.parse(event.body) : {};
} catch {
throw new ApiError(400, 'Invalid JSON in request body');
}
}
function validateBody(body: any) {
const parsed = updateHostProfileSchema.safeParse(body);
if (!parsed.success) {
throw new ApiError(400, parsed.error.issues.map((i) => i.message).join(', '));
}
if (parsed.data.emailAddress !== undefined) {
throw new ApiError(400, 'Email address cannot be updated.');
}
return parsed.data;
}
function normalizeNameFields(data: any): { firstName?: string; lastName?: string | null } {
if (data.fullName && !data.firstName && !data.lastName) {
const split = splitFullName(data.fullName);
return { firstName: split.firstName, lastName: split.lastName };
}
return { firstName: data.firstName, lastName: data.lastName };
}
function buildAddressInput(data: any) {
return {
address1: data.address1,
address2: data.address2,
countryXid: data.countryXid,
stateXid: data.stateXid,
cityXid: data.cityXid,
pinCode: data.pinCode,
};
}
function hasAnyDefined(obj: Record<string, unknown>) {
return Object.values(obj).some((v) => v !== undefined);
}
async function ensureHostUser(tx: any, userId: number) {
const user = await tx.user.findUnique({
where: { id: userId, isActive: true },
select: { id: true, roleXid: true },
});
if (!user) throw new ApiError(404, 'User not found');
if (user.roleXid !== ROLE.HOST) throw new ApiError(403, 'Access denied.');
}
async function updateUserIfNeeded(
tx: any,
userId: number,
input: {
firstName?: string;
lastName?: string | null;
isdCode?: string;
mobileNumber?: string;
dateOfBirth?: string;
profileImage?: string;
},
) {
const userUpdateData: any = {};
if (input.firstName !== undefined) userUpdateData.firstName = input.firstName || null;
if (input.lastName !== undefined) userUpdateData.lastName = input.lastName;
if (input.isdCode !== undefined) userUpdateData.isdCode = input.isdCode || null;
if (input.mobileNumber !== undefined) userUpdateData.mobileNumber = input.mobileNumber || null;
if (input.dateOfBirth !== undefined) {
userUpdateData.dateOfBirth = input.dateOfBirth ? parseDob(input.dateOfBirth) : null;
}
if (input.profileImage !== undefined) {
userUpdateData.profileImage = input.profileImage || null;
}
if (!hasAnyDefined(userUpdateData)) return;
await tx.user.update({
where: { id: userId },
data: {
...userUpdateData,
isProfileUpdated: true,
},
});
}
async function upsertAddressIfNeeded(tx: any, userId: number, addressData: Record<string, any>) {
if (!hasAnyDefined(addressData)) return;
const existingAddress = await tx.userAddressDetails.findFirst({
where: { userXid: userId, isActive: true },
select: { id: true },
});
const addressUpdateData: any = {};
if (addressData.address1 !== undefined) addressUpdateData.address1 = addressData.address1;
if (addressData.address2 !== undefined) addressUpdateData.address2 = addressData.address2;
if (addressData.countryXid !== undefined) addressUpdateData.countryXid = addressData.countryXid;
if (addressData.stateXid !== undefined) addressUpdateData.stateXid = addressData.stateXid;
if (addressData.cityXid !== undefined) addressUpdateData.cityXid = addressData.cityXid;
if (addressData.pinCode !== undefined) addressUpdateData.pinCode = addressData.pinCode;
if (existingAddress) {
await tx.userAddressDetails.update({
where: { id: existingAddress.id },
data: addressUpdateData,
});
return;
}
const required = ['address1', 'countryXid', 'stateXid', 'cityXid', 'pinCode'] as const;
const missing = required.filter((k) => addressData[k] === undefined);
if (missing.length) {
throw new ApiError(400, `Missing required address fields: ${missing.join(', ')}`);
}
await tx.userAddressDetails.create({
data: {
userXid: userId,
...addressUpdateData,
},
});
}
async function getProfileSnapshot(tx: any, userId: number) {
const updated = await tx.user.findUnique({
where: { id: userId },
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
isdCode: true,
mobileNumber: true,
dateOfBirth: true,
profileImage: true,
isProfileUpdated: true,
userAddressDetails: {
where: { isActive: true },
take: 1,
select: {
id: true,
address1: true,
address2: true,
countryXid: true,
stateXid: true,
cityXid: true,
pinCode: true,
},
},
},
});
return {
user: updated,
address: updated?.userAddressDetails?.[0] ?? null,
};
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = getAuthToken(event);
const userInfo = await verifyHostToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user id');
}
const contentType = event.headers['Content-Type'] || event.headers['content-type'] || '';
const isMultipart = contentType.includes('multipart/form-data');
let body: any;
if (isMultipart) {
const isBase64Encoded = event.isBase64Encoded || false;
const { fields, files } = parseMultipartFormData(event.body || null, contentType, isBase64Encoded);
const multipartBody: any = {};
const copyIfPresent = (key: string) => {
if (fields[key] !== undefined) {
multipartBody[key] = fields[key];
}
};
['fullName', 'firstName', 'lastName', 'isdCode', 'mobileNumber', 'dateOfBirth', 'address1', 'address2', 'pinCode'].forEach(
copyIfPresent,
);
const parseNumberField = (key: string) => {
if (fields[key] !== undefined) {
const value = Number(fields[key]);
if (!Number.isNaN(value)) {
multipartBody[key] = value;
}
}
};
['countryXid', 'stateXid', 'cityXid'].forEach(parseNumberField);
const profileImageFile = files.find((f) => f.fieldName === 'profileImage');
if (profileImageFile) {
const uploadedUrl = await uploadProfileImageToS3(
profileImageFile.data,
profileImageFile.contentType,
profileImageFile.fileName,
userId,
);
multipartBody.profileImage = uploadedUrl;
} else if (fields.profileImage) {
multipartBody.profileImage = fields.profileImage;
}
body = multipartBody;
} else {
body = parseJsonBody(event);
}
const data = validateBody(body);
const name = normalizeNameFields(data);
const address = buildAddressInput(data);
const result = await prismaClient.$transaction(async (tx) => {
await ensureHostUser(tx, userId);
await updateUserIfNeeded(tx, userId, {
firstName: name.firstName,
lastName: name.lastName,
isdCode: data.isdCode,
mobileNumber: data.mobileNumber,
dateOfBirth: data.dateOfBirth,
profileImage: data.profileImage,
});
await upsertAddressIfNeeded(tx, userId, address);
return getProfileSnapshot(tx, userId);
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Profile updated successfully',
data : null// no data payload per request
}),
};
});

View File

@@ -362,9 +362,6 @@ export class SchedulingService {
isActive: true, isActive: true,
startDate: { lte: date }, startDate: { lte: date },
OR: [{ endDate: null }, { endDate: { gte: date } }], OR: [{ endDate: null }, { endDate: { gte: date } }],
ScheduleDetails: {
some: {}
}
}, },
include: { include: {
activityVenue: { activityVenue: {

View File

@@ -177,8 +177,8 @@ function computeBasePriceAndTaxes(
return { basePrice, taxDetails }; return { basePrice, taxDetails };
} }
const normalize = (v?: string | null, maxLength: number = 50) => const normalize = (v?: string | null) =>
v ? v.trim().toLowerCase().substring(0, maxLength) : null; v ? v.trim().toLowerCase() : null;
async function renderAgreementPdf(vars: { async function renderAgreementPdf(vars: {
effectiveDate: string; effectiveDate: string;
@@ -338,11 +338,9 @@ const findOrCreateState = async (
) => { ) => {
if (!stateName || !countryXid) return null; if (!stateName || !countryXid) return null;
const trimmedStateName = stateName.trim().substring(0, 50);
const state = await tx.states.findFirst({ const state = await tx.states.findFirst({
where: { where: {
stateName: { equals: trimmedStateName, mode: 'insensitive' }, stateName: { equals: stateName.trim(), mode: 'insensitive' },
countryXid, countryXid,
isActive: true, isActive: true,
}, },
@@ -352,7 +350,7 @@ const findOrCreateState = async (
const created = await tx.states.create({ const created = await tx.states.create({
data: { data: {
stateName: trimmedStateName, stateName: stateName.trim(),
countryXid, countryXid,
}, },
}); });
@@ -367,11 +365,9 @@ const findOrCreateCity = async (
) => { ) => {
if (!cityName || !stateXid) return null; if (!cityName || !stateXid) return null;
const trimmedCityName = cityName.trim().substring(0, 50);
const city = await tx.cities.findFirst({ const city = await tx.cities.findFirst({
where: { where: {
cityName: { equals: trimmedCityName, mode: 'insensitive' }, cityName: { equals: cityName.trim(), mode: 'insensitive' },
stateXid, stateXid,
isActive: true, isActive: true,
}, },
@@ -381,7 +377,7 @@ const findOrCreateCity = async (
const created = await tx.cities.create({ const created = await tx.cities.create({
data: { data: {
cityName: trimmedCityName, cityName: cityName.trim(),
stateXid, stateXid,
}, },
}); });
@@ -395,74 +391,10 @@ const s3 = new AWS.S3({
region: config.aws.region, region: config.aws.region,
}); });
function getS3KeyFromStoredPath(path?: string | null) {
if (!path) return null;
return path.startsWith('http') ? path.split('.com/')[1] || null : path;
}
function resolveIncomingLogoPath(path?: string | null) {
if (typeof path !== 'string') return null;
const trimmed = path.trim();
return trimmed.length ? trimmed : null;
}
type UpdateHostProfileInput = {
firstName?: string;
lastName?: string | null;
isdCode?: string;
mobileNumber?: string;
dateOfBirth?: Date;
address?: {
address1?: string;
address2?: string;
countryXid?: number;
stateXid?: number;
cityXid?: number;
pinCode?: string;
};
};
@Injectable() @Injectable()
export class HostService { export class HostService {
constructor(private prisma: PrismaClient) { } constructor(private prisma: PrismaClient) { }
private async getValidLogoUrl(
model: 'hostHeader' | 'hostParent',
recordId: number,
logoPath?: string | null,
) {
const key = getS3KeyFromStoredPath(logoPath);
if (!key) return null;
try {
await s3
.headObject({
Bucket: bucket,
Key: key,
})
.promise();
return await getPresignedUrl(bucket, key);
} catch (error: any) {
const statusCode = error?.statusCode;
const errorCode = error?.code;
const isMissingObject =
statusCode === 404 ||
errorCode === 'NotFound' ||
errorCode === 'NoSuchKey';
if (isMissingObject) {
await (this.prisma as any)[model].update({
where: { id: recordId },
data: { logoPath: null },
});
return null;
}
throw error;
}
}
async createHost(data: CreateHostDto) { async createHost(data: CreateHostDto) {
return this.prisma.user.create({ data }); return this.prisma.user.create({ data });
} }
@@ -483,8 +415,8 @@ export class HostService {
}); });
const user = await this.prisma.user.findUnique({ const user = await this.prisma.user.findUnique({
where: { id: user_xid, isActive: true }, where: { id: user_xid },
select: { id: true, emailAddress: true, userRefNumber: true }, select: { id: true, emailAddress: true },
}); });
return { host, user }; return { host, user };
} }
@@ -492,86 +424,9 @@ export class HostService {
async getHostById(id: number) { async getHostById(id: number) {
const host = await this.prisma.hostHeader.findFirst({ const host = await this.prisma.hostHeader.findFirst({
where: { userXid: id }, where: { userXid: id },
select: { include: {
id: true,
logoPath: true,
companyName: true,
address1: true,
address2: true,
pinCode: true,
isSubsidairy: true,
registrationNumber: true,
panNumber: true,
gstNumber: true,
formationDate: true,
companyTypeXid: true,
websiteUrl: true,
instagramUrl: true,
facebookUrl: true,
linkedinUrl: true,
twitterUrl: true,
stepper: true,
hostStatusInternal: true,
hostStatusDisplay: true,
adminStatusInternal: true,
adminStatusDisplay: true,
amStatus: true,
agreementAccepted: true,
assignedOn: true,
agreementStartDate: true,
isApproved: true,
durationNumber: true,
durationFrequency: true,
isCommisionBase: true,
commisionPer: true,
amountPerBooking: true,
payoutDurationNum: true,
payoutDurationFrequency: true,
referencedBy: true,
hostParent: { hostParent: {
select: { include: {
id: true,
logoPath: true,
companyName: true,
firstName: true,
lastName: true,
mobileNumber: true,
address1: true,
address2: true,
cities: {
select: {
id: true,
cityName: true
}
},
states: {
select: {
id: true,
stateName: true
}
},
countries: {
select: {
id: true,
countryName: true
}
},
pinCode: true,
registrationNumber: true,
panNumber: true,
gstNumber: true,
formationDate: true,
companyTypes: {
select: {
id: true,
companyTypeName: true
}
},
websiteUrl: true,
instagramUrl: true,
facebookUrl: true,
linkedinUrl: true,
twitterUrl: true,
HostParenetDocuments: { HostParenetDocuments: {
select: { select: {
id: true, id: true,
@@ -604,46 +459,12 @@ export class HostService {
select: { select: {
id: true, id: true,
emailAddress: true, emailAddress: true,
dateOfBirth: true,
firstName: true, firstName: true,
lastName: true, lastName: true,
mobileNumber: true, mobileNumber: true,
profileImage: true, profileImage: true,
userStatus: true, userStatus: true,
userRefNumber: true, userRefNumber: true,
userAddressDetails: {
where: { isActive: true },
select: {
id: true,
address1: true,
address2: true,
locationAddress: true,
locationLat: true,
locationLong: true,
pinCode: true,
cityXid: true,
cities: {
select: {
id: true,
cityName: true,
}
},
stateXid: true,
states: {
select: {
id: true,
stateName: true,
}
},
countryXid: true,
country: {
select: {
id: true,
countryName: true
}
}
}
}
}, },
}, },
companyTypes: { companyTypes: {
@@ -701,15 +522,11 @@ export class HostService {
} }
if (host?.logoPath) { if (host?.logoPath) {
const resolvedLogoUrl = await this.getValidLogoUrl( const key = host.logoPath.startsWith('http')
'hostHeader', ? host.logoPath.split('.com/')[1]
host.id, : host.logoPath;
host.logoPath,
); host.logoPath = await getPresignedUrl(bucket, key);
if (!resolvedLogoUrl) {
host.logoPath = null;
}
(host as any).logoPresignedUrl = resolvedLogoUrl;
} }
if (host.accountManager?.profileImage) { if (host.accountManager?.profileImage) {
@@ -725,15 +542,11 @@ export class HostService {
// Parent company logo // Parent company logo
if (parent.logoPath) { if (parent.logoPath) {
const resolvedParentLogoUrl = await this.getValidLogoUrl( const key = parent.logoPath.startsWith('http')
'hostParent', ? parent.logoPath.split('.com/')[1]
parent.id, : parent.logoPath;
parent.logoPath,
); parent.logoPath = await getPresignedUrl(bucket, key);
if (!resolvedParentLogoUrl) {
parent.logoPath = null;
}
(parent as any).logoPresignedUrl = resolvedParentLogoUrl;
} }
// Parent documents // Parent documents
@@ -764,114 +577,6 @@ export class HostService {
return this.prisma.user.delete({ where: { id } }); return this.prisma.user.delete({ where: { id } });
} }
/**
* Update the logged-in Host's personal profile details.
* Email is intentionally NOT editable here.
*/
async updateHostProfileDetails(userId: number, input: UpdateHostProfileInput) {
return this.prisma.$transaction(async (tx) => {
const user = await tx.user.findUnique({
where: { id: userId, isActive: true },
select: { id: true, roleXid: true },
});
if (!user) throw new ApiError(404, 'User not found');
if (user.roleXid !== ROLE.HOST) throw new ApiError(403, 'Access denied.');
// 1) Update `User` (whitelist only)
const userUpdateData: any = {};
if (input.firstName !== undefined) userUpdateData.firstName = input.firstName || null;
if (input.lastName !== undefined) userUpdateData.lastName = input.lastName;
if (input.isdCode !== undefined) userUpdateData.isdCode = input.isdCode || null;
if (input.mobileNumber !== undefined) userUpdateData.mobileNumber = input.mobileNumber || null;
if (input.dateOfBirth !== undefined) userUpdateData.dateOfBirth = input.dateOfBirth;
if (Object.keys(userUpdateData).length > 0) {
await tx.user.update({
where: { id: userId },
data: {
...userUpdateData,
isProfileUpdated: true,
},
});
}
// 2) Update/Create `UserAddressDetails` (if any address field sent)
const addressData = input.address || {};
const hasAnyAddressField = Object.values(addressData).some((v) => v !== undefined);
if (hasAnyAddressField) {
const existingAddress = await tx.userAddressDetails.findFirst({
where: { userXid: userId, isActive: true },
select: { id: true },
});
const addressUpdateData: any = {};
if (addressData.address1 !== undefined) addressUpdateData.address1 = addressData.address1;
if (addressData.address2 !== undefined) addressUpdateData.address2 = addressData.address2;
if (addressData.countryXid !== undefined) addressUpdateData.countryXid = addressData.countryXid;
if (addressData.stateXid !== undefined) addressUpdateData.stateXid = addressData.stateXid;
if (addressData.cityXid !== undefined) addressUpdateData.cityXid = addressData.cityXid;
if (addressData.pinCode !== undefined) addressUpdateData.pinCode = addressData.pinCode;
if (existingAddress) {
await tx.userAddressDetails.update({
where: { id: existingAddress.id },
data: addressUpdateData,
});
} else {
const required = ['address1', 'countryXid', 'stateXid', 'cityXid', 'pinCode'] as const;
const missing = required.filter((k) => addressData[k] === undefined);
if (missing.length) {
throw new ApiError(400, `Missing required address fields: ${missing.join(', ')}`);
}
await tx.userAddressDetails.create({
data: {
userXid: userId,
...addressUpdateData,
},
});
}
}
// 3) Return updated profile snapshot (including read-only email)
const updated = await tx.user.findUnique({
where: { id: userId },
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
isdCode: true,
mobileNumber: true,
dateOfBirth: true,
profileImage: true,
isProfileUpdated: true,
userAddressDetails: {
where: { isActive: true },
take: 1,
select: {
id: true,
address1: true,
address2: true,
countryXid: true,
stateXid: true,
cityXid: true,
pinCode: true,
},
},
},
});
return {
user: updated,
address: updated?.userAddressDetails?.[0] ?? null,
};
});
}
async getHostByEmail(email: string): Promise<User> { async getHostByEmail(email: string): Promise<User> {
return this.prisma.user.findUnique({ where: { emailAddress: email } }); return this.prisma.user.findUnique({ where: { emailAddress: email } });
} }
@@ -971,10 +676,10 @@ export class HostService {
return newUser; return newUser;
} }
async createPassword(user_xid: number, password: string): Promise<Partial<User>> { async createPassword(user_xid: number, password: string): Promise<boolean> {
// Find user by id // Find user by id
const user = await this.prisma.user.findUnique({ const user = await this.prisma.user.findUnique({
where: { id: user_xid, isActive: true }, where: { id: user_xid },
select: { id: true, emailAddress: true, userPassword: true }, select: { id: true, emailAddress: true, userPassword: true },
}); });
@@ -1004,7 +709,7 @@ export class HostService {
}, },
}); });
return user; return true;
} }
async getBankBranchById(bankBranchXid: number) { async getBankBranchById(bankBranchXid: number) {
@@ -1142,23 +847,6 @@ export class HostService {
}, },
}, },
}, },
ActivitiesMedia: {
where: {
isActive: true,
isCoverImage: true,
deletedAt: null,
},
select: {
id: true,
mediaFileName: true,
mediaType: true,
isCoverImage: true,
},
orderBy: {
displayOrder: 'asc',
},
take: 1,
},
}, },
skip: paginationOptions?.skip || 0, skip: paginationOptions?.skip || 0,
take: paginationOptions?.limit || 10, take: paginationOptions?.limit || 10,
@@ -1185,31 +873,11 @@ export class HostService {
} }
} }
const hostActivitiesWithAssets = await Promise.all(
hostAllActivities.map(async (activity) => {
const coverImage = activity.ActivitiesMedia?.[0] ?? null;
const coverImagePresignedUrl = coverImage?.mediaFileName
? await getPresignedUrl(
bucket,
coverImage.mediaFileName.startsWith('http')
? coverImage.mediaFileName.split('.com/')[1]
: coverImage.mediaFileName,
)
: null;
return {
...activity,
coverImage: coverImage?.mediaFileName ?? null,
coverImagePresignedUrl,
};
}),
);
const { const {
paginationService, paginationService,
} = require('@/common/utils/pagination/pagination.service'); } = require('@/common/utils/pagination/pagination.service');
return paginationService.createPaginatedResponse( return paginationService.createPaginatedResponse(
hostActivitiesWithAssets, hostAllActivities,
totalCount, totalCount,
paginationOptions || { page: 1, limit: 10, skip: 0 }, paginationOptions || { page: 1, limit: 10, skip: 0 },
); );
@@ -1251,150 +919,55 @@ export class HostService {
acceptDate, acceptDate,
}; };
let pdfUrl: string | null = null; const pdfBuffer = await renderAgreementPdf(agreementVars);
try { const existingCount = await this.prisma.hostAgreement.count({
const pdfBuffer = await renderAgreementPdf(agreementVars); where: { hostXid: host.id, isActive: true },
const existingCount = await this.prisma.hostAgreement.count({
where: { hostXid: host.id, isActive: true },
});
const nextVersionNumber = `AG${existingCount + 1}`;
const baseKey = `Documents/Host/${host.id}/agreements/${nextVersionNumber}`;
const pdfKey = `${baseKey}.pdf`;
await s3
.upload({
Bucket: config.aws.bucketName,
Key: pdfKey,
Body: pdfBuffer,
ContentType: 'application/pdf',
ACL: 'private',
})
.promise();
pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`;
} catch (error) {
console.error('Error generating or uploading PDF:', error);
// Continue without PDF - will return dynamic fields instead
}
try {
const existingCount = await this.prisma.hostAgreement.count({
where: { hostXid: host.id, isActive: true },
});
const nextVersionNumber = `AG${existingCount + 1}`;
await this.prisma.$transaction(async (tx) => {
// Optional: mark previous agreements inactive
await tx.hostAgreement.updateMany({
where: { hostXid: host.id, isActive: true },
data: { isActive: false },
});
await tx.hostAgreement.create({
data: {
hostXid: host.id,
filePath: pdfUrl,
versionNumber: nextVersionNumber,
isActive: true,
},
});
await tx.hostHeader.update({
where: { id: host.id },
data: {
stepper: STEPPER.AGREEMENT_ACCEPTED,
isApproved: true,
agreementAccepted: true,
agreementStartDate: host.agreementStartDate || new Date(),
},
});
});
} catch (error) {
console.error('Error creating host agreement record:', error);
// Continue without creating agreement record - will return dynamic fields instead
}
// Return dynamic fields and PDF URL
return {
filePath: pdfUrl,
dynamicFields: agreementVars,
};
}
/**
* Get the latest (active) agreement for a specific host by hostXid.
*/
async getLatestHostAgreement(hostXid: number) {
if (!hostXid || Number.isNaN(hostXid)) {
throw new ApiError(400, 'Valid hostXid is required');
}
const hostHeader = await this.prisma.hostHeader.findFirst({
where: { id: hostXid, isActive: true },
select: {
id: true,
isCommisionBase: true,
commisionPer: true,
durationNumber: true,
durationFrequency: true,
amountPerBooking: true,
agreementStartDate: true,
payoutDurationNum: true,
payoutDurationFrequency: true,
registrationNumber: true,
companyName: true,
companyTypes: {
select: {
id: true,
companyTypeName: true
}
}
}
}); });
const agreement = await this.prisma.hostAgreement.findFirst({ const nextVersionNumber = `AG${existingCount + 1}`;
where: { hostXid, isActive: true }, const baseKey = `Documents/Host/${host.id}/agreements/${nextVersionNumber}`;
orderBy: { createdAt: 'desc' },
select: { const pdfKey = `${baseKey}.pdf`;
id: true,
hostXid: true, await s3
filePath: true, .upload({
versionNumber: true, Bucket: config.aws.bucketName,
createdAt: true, Key: pdfKey,
updatedAt: true, Body: pdfBuffer,
}, ContentType: 'application/pdf',
ACL: 'private',
})
.promise();
const pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`;
await this.prisma.$transaction(async (tx) => {
// Optional: mark previous agreements inactive
await tx.hostAgreement.updateMany({
where: { hostXid: host.id, isActive: true },
data: { isActive: false },
});
await tx.hostAgreement.create({
data: {
hostXid: host.id,
filePath: pdfUrl,
versionNumber: nextVersionNumber,
isActive: true,
},
});
await tx.hostHeader.update({
where: { id: host.id },
data: {
stepper: STEPPER.AGREEMENT_ACCEPTED,
isApproved: true,
agreementAccepted: true,
agreementStartDate: host.agreementStartDate || new Date(),
},
});
}); });
// ❌ If both missing
if (!agreement && !hostHeader) {
throw new ApiError(404, 'No active agreement found for this host');
}
let presignedUrl = "";
if (agreement?.filePath) {
const key = agreement.filePath.startsWith('http')
? agreement.filePath.split('.com/')[1]
: agreement.filePath;
const bucket = config.aws.bucketName;
presignedUrl = await getPresignedUrl(bucket, key);
}
return {
hostHeader: hostHeader || null,
agreement: agreement
? {
...agreement,
presignedUrl
}
: null
};
} }
async getPQQQuestionDetail(question_xid: number, activity_xid: number) { async getPQQQuestionDetail(question_xid: number, activity_xid: number) {
@@ -1522,10 +1095,6 @@ export class HostService {
parentCompanyData?: any | null, parentCompanyData?: any | null,
parentDocuments?: HostDocumentInput[], parentDocuments?: HostDocumentInput[],
isDraft: boolean = false, isDraft: boolean = false,
options?: {
deleteCompanyLogo?: boolean;
deleteParentCompanyLogo?: boolean;
},
) { ) {
return await this.prisma.$transaction(async (tx) => { return await this.prisma.$transaction(async (tx) => {
// Check if host already has a company // Check if host already has a company
@@ -1570,7 +1139,7 @@ export class HostService {
hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW; hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW;
minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW; minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW;
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.RE_SUBMITTED; minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW;
} }
// CASE 2: Admin has rejected but host can resubmit // CASE 2: Admin has rejected but host can resubmit
else if ( else if (
@@ -1695,9 +1264,6 @@ export class HostService {
data: { data: {
host: { connect: { id: createdHost.id } }, host: { connect: { id: createdHost.id } },
companyName: parentCompanyData.companyName || null, companyName: parentCompanyData.companyName || null,
firstName: parentCompanyData.firstName || null,
lastName: parentCompanyData.lastName || null,
mobileNumber: parentCompanyData.mobileNumber || null,
address1: parentCompanyData.address1 || null, address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null, address2: parentCompanyData.address2 || null,
// Safely handle city connection - only connect if valid ID exists // Safely handle city connection - only connect if valid ID exists
@@ -1734,7 +1300,7 @@ export class HostService {
facebookUrl: parentCompanyData.facebookUrl || null, facebookUrl: parentCompanyData.facebookUrl || null,
linkedinUrl: parentCompanyData.linkedinUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null,
twitterUrl: parentCompanyData.twitterUrl || null, twitterUrl: parentCompanyData.twitterUrl || null,
} as any, },
}); });
// parent docs // parent docs
@@ -1789,11 +1355,7 @@ export class HostService {
? { connect: { id: Number(companyData.countryXid) } } ? { connect: { id: Number(companyData.countryXid) } }
: undefined, : undefined,
pinCode: companyData.pinCode, pinCode: companyData.pinCode,
logoPath: options?.deleteCompanyLogo logoPath: companyData.logoPath || existingHostCompany.logoPath,
? null
: resolveIncomingLogoPath(companyData.logoPath) ??
existingHostCompany.logoPath ??
null,
isSubsidairy: companyData.isSubsidairy, isSubsidairy: companyData.isSubsidairy,
registrationNumber: companyData.registrationNumber, registrationNumber: companyData.registrationNumber,
panNumber: companyData.panNumber, panNumber: companyData.panNumber,
@@ -1914,9 +1476,6 @@ export class HostService {
data: { data: {
host: { connect: { id: updatedHost.id } }, host: { connect: { id: updatedHost.id } },
companyName: parentCompanyData.companyName || null, companyName: parentCompanyData.companyName || null,
firstName: parentCompanyData.firstName || null,
lastName: parentCompanyData.lastName || null,
mobileNumber: parentCompanyData.mobileNumber || null,
address1: parentCompanyData.address1 || null, address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null, address2: parentCompanyData.address2 || null,
cities: cities:
@@ -1937,10 +1496,9 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } } ? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined, : undefined,
pinCode: parentCompanyData.pinCode || null, pinCode: parentCompanyData.pinCode || null,
logoPath: options?.deleteParentCompanyLogo logoPath:
? null parentCompanyData?.logoPath ||
: resolveIncomingLogoPath(parentCompanyData?.logoPath) ?? existingParentCompany?.logoPath ||
existingParentCompany?.logoPath ??
null, null,
registrationNumber: parentCompanyData.registrationNumber || null, registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null, panNumber: parentCompanyData.panNumber || null,
@@ -1956,7 +1514,7 @@ export class HostService {
facebookUrl: parentCompanyData.facebookUrl || null, facebookUrl: parentCompanyData.facebookUrl || null,
linkedinUrl: parentCompanyData.linkedinUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null,
twitterUrl: parentCompanyData.twitterUrl || null, twitterUrl: parentCompanyData.twitterUrl || null,
} as any, },
}); });
if (parentDocuments?.length) { if (parentDocuments?.length) {
@@ -1976,9 +1534,6 @@ export class HostService {
where: { id: parentRecord.id }, where: { id: parentRecord.id },
data: { data: {
companyName: parentCompanyData.companyName || null, companyName: parentCompanyData.companyName || null,
firstName: parentCompanyData.firstName || null,
lastName: parentCompanyData.lastName || null,
mobileNumber: parentCompanyData.mobileNumber || null,
address1: parentCompanyData.address1 || null, address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null, address2: parentCompanyData.address2 || null,
cities: cities:
@@ -1999,10 +1554,9 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } } ? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined, : undefined,
pinCode: parentCompanyData.pinCode || null, pinCode: parentCompanyData.pinCode || null,
logoPath: options?.deleteParentCompanyLogo logoPath:
? null parentCompanyData?.logoPath ||
: resolveIncomingLogoPath(parentCompanyData?.logoPath) ?? existingParentCompany?.logoPath ||
existingParentCompany?.logoPath ??
null, null,
registrationNumber: parentCompanyData.registrationNumber || null, registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null, panNumber: parentCompanyData.panNumber || null,
@@ -2018,7 +1572,7 @@ export class HostService {
facebookUrl: parentCompanyData.facebookUrl || null, facebookUrl: parentCompanyData.facebookUrl || null,
linkedinUrl: parentCompanyData.linkedinUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null,
twitterUrl: parentCompanyData.twitterUrl || null, twitterUrl: parentCompanyData.twitterUrl || null,
} as any, },
}); });
// if (parentDocuments?.length) { // if (parentDocuments?.length) {
@@ -2820,9 +2374,15 @@ export class HostService {
}, },
select: { select: {
id: true, id: true,
navigationModeName: true,
isInActivityChargeable: true, isInActivityChargeable: true,
navigationModesTotalPrice: true, navigationModesTotalPrice: true,
navigationMode: {
select: {
id: true,
navigationModeName: true,
navigationModeIcon: true,
},
},
}, },
}, },
equipmentAvailable: true, equipmentAvailable: true,
@@ -3527,34 +3087,6 @@ export class HostService {
throw new ApiError(404, 'Activity not found'); throw new ApiError(404, 'Activity not found');
} }
const normalizedActivityTitle =
typeof payload.activityTitle === 'string'
? payload.activityTitle.trim()
: '';
if (normalizedActivityTitle) {
payload.activityTitle = normalizedActivityTitle;
const duplicateActivity = await tx.activities.findFirst({
where: {
id: { not: existingActivity.id },
isActive: true,
activityTitle: {
equals: normalizedActivityTitle,
mode: 'insensitive',
},
},
select: { id: true },
});
if (duplicateActivity) {
throw new ApiError(
400,
'Same activity name already exists. Please choose a different name.',
);
}
}
/* -------------------------------- /* --------------------------------
* 3⃣ STATUS DECISION * 3⃣ STATUS DECISION
* -------------------------------- */ * -------------------------------- */
@@ -4174,7 +3706,7 @@ export class HostService {
const navMode = await tx.activityNavigationModes.create({ const navMode = await tx.activityNavigationModes.create({
data: { data: {
activityXid, activityXid,
navigationModeName: mode.navigationModeName, navigationModeXid: mode.navigationModeXid,
isInActivityChargeable: isChargeable, isInActivityChargeable: isChargeable,
navigationModesBasePrice: basePrice, navigationModesBasePrice: basePrice,
navigationModesTotalPrice: totalPrice, navigationModesTotalPrice: totalPrice,

View File

@@ -1,497 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant';
import { PaginationOptions } from '../../../common/utils/pagination/pagination.types';
import ApiError from '../../../common/utils/helper/ApiError';
const ALLOWED_MEMBER_ROLES = new Set([ROLE.CO_ADMIN, ROLE.OPERATOR]);
function normalizeIdArray(values: unknown): number[] {
if (!Array.isArray(values)) {
return [];
}
return Array.from(
new Set(
values
.map((item) => Number(item))
.filter((item) => Number.isInteger(item) && item > 0),
),
);
}
@Injectable()
export class HostMemberService {
constructor(private prisma: PrismaClient) {}
async getAllInvitedCoadminAndOperator(input: {
hostUserXid: number;
search?: string;
paginationOptions?: PaginationOptions;
}) {
const host = await this.prisma.hostHeader.findFirst({
where: {
userXid: input.hostUserXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
companyName: true,
},
});
if (!host) {
throw new ApiError(404, 'Host company not found for the logged-in user.');
}
const filters: any = {
hostXid: host.id,
roleXid: {
in: [ROLE.CO_ADMIN, ROLE.OPERATOR],
},
memberStatus: 'invited',
isActive: true,
deletedAt: null,
user: {
isActive: true,
deletedAt: null,
},
};
if (input.search?.trim()) {
const term = input.search.trim();
filters.user = {
...filters.user,
OR: [
{ emailAddress: { contains: term, mode: 'insensitive' as const } },
{ firstName: { contains: term, mode: 'insensitive' as const } },
{ lastName: { contains: term, mode: 'insensitive' as const } },
{ mobileNumber: { contains: term, mode: 'insensitive' as const } },
{ userRefNumber: { contains: term, mode: 'insensitive' as const } },
],
};
}
const totalCount = await this.prisma.hostMembers.count({
where: filters,
});
const members = await this.prisma.hostMembers.findMany({
where: filters,
select: {
id: true,
hostXid: true,
userXid: true,
roleXid: true,
memberStatus: true,
invitedOn: true,
acceptedOn: true,
hostRolePermissionMasterXid: true,
user: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
userRefNumber: true,
userStatus: true,
role: {
select: {
id: true,
roleName: true,
},
},
},
},
role: {
select: {
id: true,
roleName: true,
},
},
invitedBy: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
},
},
hostRolePermissionMaster: {
select: {
id: true,
permissionMasterXids: true,
},
},
managedActivities: {
where: {
isActive: true,
deletedAt: null,
},
select: {
activityXid: true,
activity: {
select: {
id: true,
activityTitle: true,
},
},
},
orderBy: {
activityXid: 'asc',
},
},
},
orderBy: {
invitedOn: 'desc',
},
skip: input.paginationOptions?.skip ?? 0,
take: input.paginationOptions?.limit ?? 10,
});
const permissionIds = Array.from(
new Set(
members.flatMap((member) =>
normalizeIdArray(member.hostRolePermissionMaster?.permissionMasterXids),
),
),
);
const permissionMasters = permissionIds.length
? await this.prisma.hostPermissionMasters.findMany({
where: {
id: { in: permissionIds },
isActive: true,
deletedAt: null,
},
select: {
id: true,
displayLabel: true,
},
})
: [];
const permissionLabelMap = new Map(
permissionMasters.map((permission) => [permission.id, permission.displayLabel]),
);
const data = members.map((member) => {
const permissionMasterXids = normalizeIdArray(
member.hostRolePermissionMaster?.permissionMasterXids,
);
return {
hostMemberId: member.id,
hostXid: member.hostXid,
hostCompanyName: host.companyName,
userXid: member.userXid,
roleXid: member.roleXid,
roleName: member.role?.roleName ?? member.user.role?.roleName ?? null,
permissionMasterXid: member.hostRolePermissionMasterXid,
permissionMasterXids,
permissionLabels: permissionMasterXids
.map((permissionId) => permissionLabelMap.get(permissionId))
.filter(Boolean),
memberStatus: member.memberStatus,
invitedOn: member.invitedOn,
acceptedOn: member.acceptedOn,
invitedBy: member.invitedBy
? {
id: member.invitedBy.id,
firstName: member.invitedBy.firstName,
lastName: member.invitedBy.lastName,
emailAddress: member.invitedBy.emailAddress,
}
: null,
user: {
id: member.user.id,
firstName: member.user.firstName,
lastName: member.user.lastName,
emailAddress: member.user.emailAddress,
mobileNumber: member.user.mobileNumber,
userRefNumber: member.user.userRefNumber,
userStatus: member.user.userStatus,
},
activities: member.managedActivities.map((activityLink) => ({
id: activityLink.activity.id,
activityXid: activityLink.activityXid,
activityTitle: activityLink.activity.activityTitle,
})),
};
});
return {
data,
totalCount,
};
}
async inviteMember(input: {
inviterUserXid: number;
emailAddress: string;
roleXid: number;
permissionMasterXid: number;
activityXids: unknown;
}) {
const normalizedEmail = input.emailAddress.trim().toLowerCase();
const roleXid = Number(input.roleXid);
const permissionMasterXid = Number(input.permissionMasterXid);
const activityXids = normalizeIdArray(input.activityXids);
if (!normalizedEmail) {
throw new ApiError(400, 'emailAddress is required.');
}
if (!Number.isInteger(roleXid) || !ALLOWED_MEMBER_ROLES.has(roleXid)) {
throw new ApiError(
400,
'roleXid must be one of CO_ADMIN or OPERATOR.',
);
}
if (!Number.isInteger(permissionMasterXid) || permissionMasterXid <= 0) {
throw new ApiError(400, 'permissionMasterXid is required.');
}
if (!activityXids.length) {
throw new ApiError(400, 'At least one activity is required.');
}
return this.prisma.$transaction(async (tx) => {
const host = await tx.hostHeader.findFirst({
where: {
userXid: input.inviterUserXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
companyName: true,
userXid: true,
},
});
if (!host) {
throw new ApiError(404, 'Host company not found for the logged-in user.');
}
if (host.userXid !== input.inviterUserXid) {
throw new ApiError(403, 'Only the host owner can invite members.');
}
const permissionMaster = await tx.hostRolePermissionMasters.findFirst({
where: {
id: permissionMasterXid,
hostXid: host.id,
roleXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
hostXid: true,
roleXid: true,
permissionMasterXids: true,
role: {
select: {
id: true,
roleName: true,
},
},
},
});
if (!permissionMaster) {
throw new ApiError(
404,
'Permission master not found for the selected host and role.',
);
}
const selectedPermissionMasterXids = normalizeIdArray(
permissionMaster.permissionMasterXids,
);
const permissionDetails = await tx.hostPermissionMasters.findMany({
where: {
id: { in: selectedPermissionMasterXids },
isActive: true,
deletedAt: null,
},
select: {
id: true,
permissionKey: true,
permissionGroup: true,
permissionSection: true,
permissionAction: true,
displayLabel: true,
displayOrder: true,
},
orderBy: {
displayOrder: 'asc',
},
});
if (permissionDetails.length !== selectedPermissionMasterXids.length) {
throw new ApiError(
400,
'One or more saved permission XIDs no longer exist in the master table.',
);
}
const activities = await tx.activities.findMany({
where: {
id: { in: activityXids },
hostXid: host.id,
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityTitle: true,
},
});
if (activities.length !== activityXids.length) {
throw new ApiError(
400,
'One or more selected activities are invalid for this host.',
);
}
const existingUser = await tx.user.findFirst({
where: {
emailAddress: normalizedEmail,
isActive: true,
deletedAt: null,
},
select: {
id: true,
emailAddress: true,
roleXid: true,
userStatus: true,
},
});
let user =
existingUser ??
(await tx.user.create({
data: {
emailAddress: normalizedEmail,
roleXid: ROLE.HOST,
userStatus: USER_STATUS.INVITED,
isActive: true,
},
select: {
id: true,
emailAddress: true,
roleXid: true,
userStatus: true,
},
}));
if (existingUser && existingUser.roleXid !== ROLE.HOST) {
user = await tx.user.update({
where: { id: existingUser.id },
data: {
roleXid: ROLE.HOST,
userStatus: USER_STATUS.INVITED,
isActive: true,
},
select: {
id: true,
emailAddress: true,
roleXid: true,
userStatus: true,
},
});
}
const existingMembership = await tx.hostMembers.findFirst({
where: {
userXid: user.id,
isActive: true,
deletedAt: null,
},
select: {
id: true,
hostXid: true,
},
});
if (existingMembership && existingMembership.hostXid !== host.id) {
throw new ApiError(
409,
'This person is already invited or assigned to another host company.',
);
}
const membershipData = {
hostXid: host.id,
userXid: user.id,
roleXid,
hostRolePermissionMasterXid: permissionMaster.id,
memberStatus: 'invited',
invitedByXid: input.inviterUserXid,
invitedOn: new Date(),
acceptedOn: null,
isActive: true,
};
const hostMember = existingMembership
? await tx.hostMembers.update({
where: { id: existingMembership.id },
data: membershipData,
select: {
id: true,
hostXid: true,
userXid: true,
roleXid: true,
hostRolePermissionMasterXid: true,
memberStatus: true,
invitedByXid: true,
invitedOn: true,
},
})
: await tx.hostMembers.create({
data: membershipData,
select: {
id: true,
hostXid: true,
userXid: true,
roleXid: true,
hostRolePermissionMasterXid: true,
memberStatus: true,
invitedByXid: true,
invitedOn: true,
},
});
await tx.hostMemberActivities.deleteMany({
where: {
hostMemberXid: hostMember.id,
},
});
await tx.hostMemberActivities.createMany({
data: activities.map((activity) => ({
hostMemberXid: hostMember.id,
activityXid: activity.id,
isActive: true,
})),
});
return {
host,
user,
hostMember,
permissionMaster,
permissionDetails,
activities,
};
});
}
}

View File

@@ -1,129 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import ApiError from '../../../common/utils/helper/ApiError';
function normalizeIdArray(values: unknown): number[] {
if (!Array.isArray(values)) {
return [];
}
return Array.from(
new Set(
values
.map((item) => Number(item))
.filter((item) => Number.isInteger(item) && item > 0),
),
);
}
@Injectable()
export class HostRolePermissionService {
constructor(private prisma: PrismaClient) {}
async saveRolePermissions(input: {
hostUserXid: number;
roleXid: number;
permissionMasterXids: unknown;
}) {
const permissionMasterXids = normalizeIdArray(input.permissionMasterXids);
if (!permissionMasterXids.length) {
throw new ApiError(400, 'permissionMasterXids is required.');
}
return this.prisma.$transaction(async (tx) => {
const host = await tx.hostHeader.findFirst({
where: {
userXid: input.hostUserXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
companyName: true,
userXid: true,
},
});
if (!host) {
throw new ApiError(404, 'Host company not found for the logged-in user.');
}
const role = await tx.roles.findFirst({
where: {
id: input.roleXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
roleName: true,
},
});
if (!role) {
throw new ApiError(400, 'Invalid roleXid.');
}
const selectedPermissions = await tx.hostPermissionMasters.findMany({
where: {
id: { in: permissionMasterXids },
isActive: true,
deletedAt: null,
},
select: {
id: true,
permissionKey: true,
permissionGroup: true,
permissionSection: true,
permissionAction: true,
displayLabel: true,
displayOrder: true,
},
orderBy: {
displayOrder: 'asc',
},
});
if (selectedPermissions.length !== permissionMasterXids.length) {
throw new ApiError(400, 'One or more permissionMasterXids are invalid.');
}
const saved = await tx.hostRolePermissionMasters.upsert({
where: {
hostXid_roleXid: {
hostXid: host.id,
roleXid: role.id,
},
},
create: {
hostXid: host.id,
roleXid: role.id,
permissionMasterXids,
isActive: true,
},
update: {
permissionMasterXids,
isActive: true,
deletedAt: null,
},
select: {
id: true,
hostXid: true,
roleXid: true,
permissionMasterXids: true,
createdAt: true,
updatedAt: true,
},
});
return {
host,
role,
saved,
selectedPermissions,
};
});
}
}

View File

@@ -10,16 +10,13 @@ export async function resendOtpEmail(
// messageId: string // messageId: string
}> { }> {
const subject = "Your Minglar Verification Code"; const subject = "New OTP from Minglar Team";
const htmlContent = ` const htmlContent = `
<p>Hi ${role},</p> <p>Dear ${role},</p>
<p>Here's your verification code to get started:</p> <p>Your new OTP is: <strong>${otp}</strong></p>
<p><strong>${otp}</strong></p> <p>This code will be valid for the next 5 minutes.</p>
<p>This code is valid for the next 5 minutes.</p> <p>Warm regards,<br/>Minglar Team</p>
<p>Once verified, you can continue setting up your Minglar account. If you didn't request this, you can safely ignore this email.</p>
<p>Need help? Reach out to us at info@minglargroup.com.</p>
<p>Warm regards,<br/>Team Minglar</p>
`; `;
try { try {
@@ -40,5 +37,3 @@ export async function resendOtpEmail(
throw new ApiError(500, "Failed to send OTP to host via email."); throw new ApiError(500, "Failed to send OTP to host via email.");
} }
} }

View File

@@ -1,51 +0,0 @@
import { brevoService } from '../../../common/email/brevoApi';
import ApiError from '../../../common/utils/helper/ApiError';
import config from '../../../config/config';
export async function sendHostMemberInvitationEmail(
emailAddress: string,
hostName: string,
memberRole: string,
permissionLabels: string[],
activityNames: string[],
): Promise<{
sent: boolean;
}> {
const subject = `Invitation to join ${hostName} on Minglar Host`;
const permissionsHtml = permissionLabels.length
? `<ul>${permissionLabels.map((permission) => `<li>${permission}</li>`).join('')}</ul>`
: '<p>No permissions were assigned.</p>';
const activitiesHtml = activityNames.length
? `<ul>${activityNames.map((activity) => `<li>${activity}</li>`).join('')}</ul>`
: '<p>No activities were assigned.</p>';
const htmlContent = `
<p>Hi there,</p>
<p>You have been invited by <strong>${hostName}</strong> to join the Minglar Host portal as <strong>${memberRole}</strong>.</p>
<p>The following permissions have been assigned to your account:</p>
${permissionsHtml}
<p>The following activities have been assigned to you:</p>
${activitiesHtml}
<p>You can access the host portal using the link below:</p>
<p><a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a></p>
<p>If you were not expecting this invitation, you can ignore this email.</p>
<p>Warm regards,<br/>Team Minglar</p>
`;
try {
await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
return {
sent: true,
};
} catch (err) {
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send host member invitation email.');
}
}

View File

@@ -1,40 +1,24 @@
import { brevoService } from "@/common/email/brevoApi"; import { brevoService } from "@/common/email/brevoApi";
import ApiError from "@/common/utils/helper/ApiError"; import ApiError from "@/common/utils/helper/ApiError";
import config from '../../../config/config';
export async function sendEmailToAM( export async function sendEmailToAM(
emailAddress: string, emailAddress: string,
amName: string, amName: string,
hostCompanyName: string, hostCompanyName: string,
activityName: string hostRefNumber: string
): Promise<{ ): Promise<{
sent: boolean; sent: boolean;
// messageId: string // messageId: string
}> { }> {
const subject = `${hostCompanyName} Has Resubmitted Their Application`; const subject = `Host Application Re-Submited : ${hostCompanyName}`;
const htmlContent = ` const htmlContent = `
<p>Hello ${amName},</p> <p>Dear ${amName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has re-submited the application with implimented suggestions.</p>
<p> <p>Please review their appliaction and take the necessary action.</p>
${hostCompanyName} has updated and re-submitted their pre-qualification details of ${activityName} for your review. <p>Best regards,<br/>Minglar Team</p>
</p> `;
<p>
Please click on the link below to log in to your dashboard to review the revised submission and proceed with the necessary action.
</p>
<p>
<strong>Review Application</strong><br/>
<a href="${config.AM_INTERFACE_LINK}" target="_blank">${config.AM_INTERFACE_LINK}</a>
</p>
<p>
Thank you,<br/>
Minglar Team
</p>
`;
try { try {
const result = await brevoService.sendEmail({ const result = await brevoService.sendEmail({
@@ -65,14 +49,13 @@ export async function sendEmailToMinglarAdmin(
// messageId: string // messageId: string
}> { }> {
const subject = "New Host Application Submitted for Review"; const subject = `New Host Application Recieved : ${hostCompanyName}`;
const htmlContent = ` const htmlContent = `
<p>Hi ${minglarAdminName},</p> <p>Dear ${minglarAdminName},</p>
<p>${hostCompanyName} has submitted their application and is awaiting your review.</p> <p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has submited their application.</p>
<p>Reference number: <strong>${hostRefNumber}</strong></p> <p>Please review their appliaction and take the necessary action.</p>
<p>Please log in to your dashboard to review the submission and take the necessary action.</p> <p>Best regards,<br/>Minglar Team</p>
<p>Thank you,<br/>Minglar Team</p>
`; `;
try { try {
@@ -93,57 +76,3 @@ export async function sendEmailToMinglarAdmin(
throw new ApiError(500, "Failed to send OTP to host via email."); throw new ApiError(500, "Failed to send OTP to host via email.");
} }
} }
export async function sendPQPEmailToAM(
emailAddress: string,
minglarAdminName: string,
hostCompanyName: string,
activityName: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "New Host Application Submitted for Review";
const htmlContent = `
<p>Hi ${minglarAdminName},</p>
<p>
${hostCompanyName} has submitted the pre-qualification details for ${activityName} and is awaiting your review.
</p>
<p>
Please click the link below to log in to your dashboard, review the submission, and take the necessary action:
</p>
<p>
<strong>Review Application</strong><br/>
<a href="${config.HOST_LINK_PQ}" target="_blank">${config.HOST_LINK_PQ}</a>
</p>
<p>
Thank you,<br/>
Minglar Team
</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}

View File

@@ -1,24 +1,22 @@
import { brevoService } from '../../../common/email/brevoApi'; import { brevoService } from "@/common/email/brevoApi";
import ApiError from '../../../common/utils/helper/ApiError'; import ApiError from "@/common/utils/helper/ApiError";
import config from '../../../config/config';
export async function sendOtpEmailForHost( export async function sendOtpEmailForHost(
emailAddress: string, emailAddress: string,
otp: string | number, otp: string | number
): Promise<{ ): Promise<{
sent: boolean; sent: boolean;
// messageId: string // messageId: string
}> { }> {
const subject = 'Your Minglar Verification Code';
const subject = "OTP for Host Registration";
const htmlContent = ` const htmlContent = `
<p>Hi there 👋</p> <p>Dear Host,</p>
<p>Heres your verification code to get started:</p> <p>Youre almost all set! 🎉</p>
<p><strong>${otp}</strong></p> <p>Enter <strong>${otp}</strong> to wrap your registration.</p>
<p>This code is valid for the next 5 minutes.</p> <p>This code will be valid for the next 5 minutes.</p>
<p>Once verified, you can continue setting up your Minglar account. If you didnt request this, you can safely ignore this email.</p> <p>Warm regards,<br/>Minglar Team</p>
<p>Need help? Reach out to us at info@minglargroup.com.</p>
<p>Warm regards,<br />Team Minglar</p>
`; `;
try { try {
@@ -35,67 +33,7 @@ export async function sendOtpEmailForHost(
// messageId: result.messageId // messageId: result.messageId
}; };
} catch (err) { } catch (err) {
console.error('Brevo email send failed:', err); console.error("Brevo email send failed:", err);
throw new ApiError(500, 'Failed to send OTP to host via email.'); throw new ApiError(500, "Failed to send OTP to host via email.");
}
}
export async function sendWelcomeEmailToHost(emailAddress: string): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = 'Get Started as a Minglar Host';
const htmlContent = `
<p>Hi ${emailAddress},</p>
<p>Were excited to have you join Minglar as a host. Welcome aboard! 🌟</p>
<p>To get started and bring your activities live, heres what comes next:</p>
<p><strong>Your next steps:</strong></p>
<p>1. Complete your host profile</p>
<p>2. Complete the pre-qualification process for all your activities</p>
<p>3. Submit your activity details for review</p>
<p>4. Go live and start receiving bookings</p>
<p>
👉 <strong>Access your Host Portal:</strong><br/>
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
</p>
<p>
If you need any support along the way, our team is always here to help.
You can reach us anytime at
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
</p>
<p>
Were looking forward to seeing your experiences come to life on Minglar.
</p>
<p>
Warm regards,<br/>
Team Minglar
</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send OTP to host via email.');
} }
} }

View File

@@ -53,10 +53,10 @@ export class TokenService {
config.jwt.secret config.jwt.secret
); );
// Optionally keep existing refresh tokens alive instead of deleting await this.prisma.token.deleteMany({
// Removed deleteMany call so the same refresh token can be used multiple where: { userXid: user_xid }
// times. If you want to limit refresh tokens later you can implement })
// rotation or blacklist logic elsewhere.
await this.prisma.token.create({ await this.prisma.token.create({
data: { data: {
token: refreshToken.token, token: refreshToken.token,

View File

@@ -3,6 +3,7 @@ import { prismaClient } from '../../../../../common/database/prisma.lambda.servi
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin'; import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError'; import ApiError from '../../../../../common/utils/helper/ApiError';
import { sendActivityAcceptanceMailtoHost } from '../../../../minglaradmin/services/approvalMailtoHost.service';
import { MinglarService } from '../../../services/minglar.service'; import { MinglarService } from '../../../services/minglar.service';
const minglarService = new MinglarService(prismaClient); const minglarService = new MinglarService(prismaClient);
@@ -39,6 +40,9 @@ export const handler = safeHandler(async (
Number(activityId), Number(activityId),
Number(userInfo.id) Number(userInfo.id)
); );
const hostXid = await minglarService.getHostXidByActivityId(activityId)
const hostDetails = await minglarService.getUserDetails(hostXid)
await sendActivityAcceptanceMailtoHost(hostDetails.emailAddress, hostDetails.firstName)
return { return {
statusCode: 201, statusCode: 201,

View File

@@ -50,7 +50,7 @@ export const handler = safeHandler(async (
if (!hostDetails?.emailAddress) { if (!hostDetails?.emailAddress) {
throw new ApiError(404, 'Host details or email address not found'); throw new ApiError(404, 'Host details or email address not found');
} }
await sendEmailToHostForRejectedApplication(hostDetails.emailAddress, hostDetails.firstName || 'Host'); await sendEmailToHostForRejectedApplication(hostDetails.emailAddress)
return { return {
statusCode: 200, statusCode: 200,

View File

@@ -2,30 +2,19 @@
import { brevoService } from '@/common/email/brevoApi'; import { brevoService } from '@/common/email/brevoApi';
import ApiError from '@/common/utils/helper/ApiError'; import ApiError from '@/common/utils/helper/ApiError';
export async function sendAMEmailForHostAssign(emailAddress: string, accountManagerName?: string): Promise<{ export async function sendAMEmailForHostAssign(emailAddress: string): Promise<{
sent: boolean; sent: boolean;
// messageId: string // messageId: string
}> { }> {
const subject = "You've Been Assigned a New Host"; const subject = 'Minglar Admin: Host Assignment Notification';
const displayName = accountManagerName?.trim() || "there"; const htmlContent = `
<p>Hi,</p>
const htmlContent = ` <p>Youve been assigned the <strong>Host</strong> role by Minglar Admin.</p>
<p>Hi ${displayName},</p>
<p> <p>Best regards,<br/>Minglar Admin Team</p>
A new host has been assigned to you by the Minglar team. `;
</p>
<p>
You can now manage and support this host through your admin dashboard. Log in to review the hosts details, connect with them, and take the next steps as needed.
</p>
<p>
Warm regards,<br/>
Minglar Team
</p>
`;
try { try {
const result = await brevoService.sendEmail({ const result = await brevoService.sendEmail({
@@ -46,5 +35,3 @@ export async function sendAMEmailForHostAssign(emailAddress: string, accountMana
} }
} }
// ...existing code... // ...existing code...

View File

@@ -24,8 +24,7 @@ export class AMNotificationService {
} }
try { try {
const amName = [amUser.firstName, amUser.lastName].filter(Boolean).join(' ').trim(); await sendAMEmailForHostAssign(amUser.emailAddress);
await sendAMEmailForHostAssign(amUser.emailAddress, amName);
return true; return true;
} catch (err) { } catch (err) {
console.error('Error sending AM assignment email', err); console.error('Error sending AM assignment email', err);

View File

@@ -1,277 +1,152 @@
import { brevoService } from '../../../common/email/brevoApi'; import { brevoService } from "../../../common/email/brevoApi";
import ApiError from '../../../common/utils/helper/ApiError'; import ApiError from "../../../common/utils/helper/ApiError";
import config from '../../../config/config'; import config from "../../../config/config";
export async function sendEmailToHostForApprovedApplication( export async function sendEmailToHostForApprovedApplication(
emailAddress: string, emailAddress: string,
name: string, name: string
): Promise<{ ): Promise<{
sent: boolean; sent: boolean;
// messageId: string // messageId: string
}> { }> {
const subject = 'Host Onboarding Application Approved';
const htmlContent = ` const subject = "Approval for your application";
<p>Hi ${emailAddress},</p>
<p> const htmlContent = `
Were pleased to inform you that your host onboarding application has been approved by our team. <p>Dear ${name},</p>
</p> <p>Congratulations, Your application to minglar admin has been approved.</p>
<p>You can start onboarding your activities through the host panel.</p>
<p> You can login to your account using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK} </p>
<p>Best regards,<br/>Minglar Team</p>
`;
<p> try {
You can now proceed with completing your activity pre-qualification process. const result = await brevoService.sendEmail({
</p> recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
<p> console.log("📧 Email sent successfully:", result);
Please click the link below to log in to your account and continue:
</p>
<p> return {
<strong>Host Portal Login</strong><br/> sent: true,
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a> // messageId: result.messageId
</p> };
} catch (err) {
<p> console.error("Brevo email send failed:", err);
If you have any questions or need assistance, feel free to reach out to us at throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>. }
</p>
<p>
Warm regards,<br/>
Minglar Team
</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
console.log('📧 Email sent successfully:', result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
}
} }
export async function sendEmailToHostForMinglarApproval( export async function sendEmailToHostForMinglarApproval(
emailAddress: string, emailAddress: string,
): Promise<{ ): Promise<{
sent: boolean; sent: boolean;
// messageId: string // messageId: string
}> { }> {
const subject = 'Host Onboarding Application Approved';
const htmlContent = ` const subject = "Approval for your application";
<p>Hi there,</p>
<p>We're pleased to inform you that your host onboarding application has been approved by our team.</p> const htmlContent = `
<p>You can now proceed with completing your activity pre-qualification process.</p> <p>Dear Host,</p>
<p>Please click the link below to log in to your account and continue:</p> <p>Congratulations, Your application to minglar admin has been approved by minglar admin.</p>
<p><a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a></p> <p>Minglar admin will assign account manager to your application.</p>
<p>If you have any questions or need assistance, feel free to reach out to us at info@minglargroup.com.</p> <p>Best regards,<br/>Minglar Team</p>
<p>Warm regards,<br/>Minglar Team</p>
`; `;
try { try {
const result = await brevoService.sendEmail({ const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }], recipients: [{ email: emailAddress }],
subject, subject,
htmlContent, htmlContent,
}); });
console.log('📧 Email sent successfully:', result); console.log("📧 Email sent successfully:", result);
return { return {
sent: true, sent: true,
// messageId: result.messageId // messageId: result.messageId
}; };
} catch (err) { } catch (err) {
console.error('Brevo email send failed:', err); console.error("Brevo email send failed:", err);
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.'); throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
} }
} }
export async function sendAMPQQAcceptanceMailtoHost( export async function sendAMPQQAcceptanceMailtoHost(
emailAddress: string, emailAddress: string,
name: string, name: string
): Promise<{ ): Promise<{
sent: boolean; sent: boolean;
// messageId: string // messageId: string
}> { }> {
const subject = 'Your Activity Has Been Qualified for Onboarding';
const htmlContent = ` const subject = "Approval for your activity onboarding application";
<p>Hi ${name},</p>
<p> const htmlContent = `
Were pleased to inform you that your activity has been qualified on the Minglar platform. <p>Dear ${name},</p>
</p> <p>Congratulations, Your activity onboarding application to minglar admin has been approved.</p>
<p>You can start adding other details of your activity through the host panel.</p>
<p> You can login to your account using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK_PQ} </p>
<p>Best regards,<br/>Minglar Team</p>
`;
<p> try {
You can now proceed to complete the details of your activity through the Host portal. const result = await brevoService.sendEmail({
</p> recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
<p> console.log("📧 Email sent successfully:", result);
Please click the link below to log in to your account and continue:
</p>
<p> return {
<strong>Host Portal Login</strong><br/> sent: true,
<a href="${config.HOST_LINK_PQ}" target="_blank">${config.HOST_LINK_PQ}</a> // messageId: result.messageId
</p> };
} catch (err) {
<p> console.error("Brevo email send failed:", err);
If you have any questions or need assistance, feel free to reach out at throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>. }
</p>
<p>
Warm regards,<br/>
Minglar Team
</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
console.log('📧 Email sent successfully:', result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
}
} }
export async function sendActivityAcceptanceMailtoHost( export async function sendActivityAcceptanceMailtoHost(
emailAddress: string, emailAddress: string,
name: string, name: string
): Promise<{ ): Promise<{
sent: boolean; sent: boolean;
// messageId: string // messageId: string
}> { }> {
const subject =
'Onboarding Completed | You Can Now Set Up Your Activity Schedule and Listing';
const htmlContent = ` const subject = "Approval for your activity details application";
<p>Hi ${name},</p>
<p>Great news! 🎉</p> const htmlContent = `
<p>Dear ${name},</p>
<p>Congratulations, Your activity details application to minglar admin has been approved.</p>
<p>You can start getting orders for your activity.</p>
<p>You can login to your account using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK} </p>
<p>Best regards,<br/>Minglar Team</p>
`;
<p> try {
You have successfully completed the onboarding process for your activity on Minglar. const result = await brevoService.sendEmail({
</p> recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
<p> console.log("📧 Email sent successfully:", result);
You can now move on to the next step by setting up your activitys schedule. Once this is done, your activity will be ready to be listed on the Minglar app.
</p>
<p> return {
👉 <strong>Access your Host Portal:</strong><br/> sent: true,
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a> // messageId: result.messageId
</p> };
} catch (err) {
<p> console.error("Brevo email send failed:", err);
If you have any questions or need assistance while setting things up, our team is here to help at throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>. }
</p>
<p>
Were excited to see your activity take shape and look forward to having it live on Minglar soon.
</p>
<p>
Warm regards,<br/>
Team Minglar
</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
console.log('📧 Email sent successfully:', result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
}
} }
export async function sendActivityScheduleApprovedMailtoHost(
emailAddress: string,
name: string,
): Promise<{
sent: boolean;
}> {
const subject = 'Activity Schedule Approved | Lets Go Live!!';
const htmlContent = `
<p>Hi ${name},</p>
<p>Your activity schedule has been officially approved.</p>
<p>
Everything is now in place. Your experience is fully configured and queued for launch.
Our team is completing the final activation before it goes live on Minglar.
</p>
<p>
You can continue managing your availability or adding new time slots anytime through your Host Portal:
</p>
<p>
👉 <strong>Host Portal</strong><br/>
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
</p>
<p>
This is a big step. Were excited to bring your experience to life on Minglar.
</p>
<p>
Warm regards,<br/>
The Minglar Team
</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
console.log('📧 Email sent successfully:', result);
return {
sent: true,
};
} catch (err) {
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send schedule approval email to host.');
}
}

View File

@@ -9,36 +9,23 @@ export async function sendInvitationEmailForMinglarAdmin(
// messageId: string // messageId: string
}> { }> {
const subject = "Team Invitation: Account Manager at Minglar"; const subject = "Minglar Admin: Your Team Invitation";
const htmlContent = ` const htmlContent = `
<p>Hi ${emailAddress},</p> <p>Hi there,</p>
<p>We're excited to invite you to join the <strong>Minglar Admin Team</strong>!<br/>
Please use the link below to set up your account and get started.</p>
<p> <p><strong>Access Your Invitation:</strong><br/>
Were happy to invite you to join the Minglar team as an Account Manager. <a href="${link}">${link}</a></p>
</p>
<p> <p>If you have any questions or need assistance, feel free to reach out — were here to help.<br/>
To get started, please set up your account using the link below: We look forward to having you on board!</p>
</p>
<p> <p>Welcome aboard!<br/>
<a href="${link}" target="_blank">${link}</a> <strong>The Minglar Admin Team</strong></p>
</p>
<p> `;
If you have any questions or need help during the setup process, feel free to reach out.
</p>
<p>
We look forward to working with you.
</p>
<p>
Warm regards,<br/>
Minglar Team
</p>
`;
try { try {
const result = await brevoService.sendEmail({ const result = await brevoService.sendEmail({
@@ -58,4 +45,3 @@ export async function sendInvitationEmailForMinglarAdmin(
throw new ApiError(500, "Failed to send invitation via email."); throw new ApiError(500, "Failed to send invitation via email.");
} }
} }

View File

@@ -34,7 +34,7 @@ const bucket = config.aws.bucketName;
@Injectable() @Injectable()
export class MinglarService { export class MinglarService {
constructor(private prisma: PrismaService | PrismaClient) { } constructor(private prisma: PrismaService | PrismaClient) {}
async createPassword(user_xid: number, password: string): Promise<boolean> { async createPassword(user_xid: number, password: string): Promise<boolean> {
// Find user by id // Find user by id
@@ -144,10 +144,10 @@ export class MinglarService {
async getUserDetails(id: number) { async getUserDetails(id: number) {
const hostDetail = await this.prisma.hostHeader.findFirst({ const hostDetail = await this.prisma.hostHeader.findFirst({
where: { id: id, isActive: true }, where: { id: id },
}); });
const userDetails = await this.prisma.user.findUnique({ const userDetails = await this.prisma.user.findUnique({
where: { id: hostDetail.userXid, isActive: true }, where: { id: hostDetail.userXid },
}); });
return userDetails; return userDetails;
} }
@@ -314,8 +314,6 @@ export class MinglarService {
companyName: true, companyName: true,
user: { user: {
select: { select: {
firstName: true,
lastName: true,
userRefNumber: true, userRefNumber: true,
}, },
}, },
@@ -377,52 +375,11 @@ export class MinglarService {
const { const {
paginationService, paginationService,
} = require('@/common/utils/pagination/pagination.service'); } = require('@/common/utils/pagination/pagination.service');
return paginationService.createPaginatedResponse(
let hostDetails = null;
if (hostXid) {
hostDetails = await this.prisma.hostHeader.findUnique({
where: { id: hostXid },
select: {
companyName: true,
user: {
select: {
firstName: true,
lastName: true,
userRefNumber: true,
},
},
},
});
}
const paginatedResponse = paginationService.createPaginatedResponse(
hostActivities, hostActivities,
totalCount, totalCount,
paginationOptions || { page: 1, limit: 10, skip: 0 }, paginationOptions || { page: 1, limit: 10, skip: 0 },
); );
// 👇 ADD THIS BLOCK
if (hostActivities.length === 0 && hostDetails) {
paginatedResponse.data = [
{
id: null,
activityRefNumber: null,
activityTitle: null,
totalScore: null,
activityInternalStatus: null,
activityDisplayStatus: null,
amInternalStatus: null,
amDisplayStatus: null,
createdAt: null,
host: hostDetails,
ActivityAmDetails: [],
activityType: null,
},
];
}
return paginatedResponse;
} }
async createUserRevenue( async createUserRevenue(
@@ -861,7 +818,7 @@ export class MinglarService {
if ( if (
userStatus && userStatus &&
userStatus.trim().toLowerCase() === userStatus.trim().toLowerCase() ===
MINGLAR_STATUS_DISPLAY.NEW.toLowerCase() MINGLAR_STATUS_DISPLAY.NEW.toLowerCase()
) { ) {
filters.adminStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW; filters.adminStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW;
} }
@@ -875,9 +832,9 @@ export class MinglarService {
internal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW, internal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
display: MINGLAR_STATUS_DISPLAY.NEW, display: MINGLAR_STATUS_DISPLAY.NEW,
}, },
Re_Submitted: { To_Review: {
internal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW, internal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
display: MINGLAR_STATUS_DISPLAY.RE_SUBMITTED, display: MINGLAR_STATUS_DISPLAY.TO_REVIEW,
}, },
Enhancing: { Enhancing: {
internal: MINGLAR_STATUS_INTERNAL.AM_REJECTED, internal: MINGLAR_STATUS_INTERNAL.AM_REJECTED,
@@ -988,7 +945,6 @@ export class MinglarService {
const where: any = { const where: any = {
isActive: true, isActive: true,
hostStatusInternal: { notIn: [HOST_STATUS_INTERNAL.DRAFT] }, hostStatusInternal: { notIn: [HOST_STATUS_INTERNAL.DRAFT] },
adminStatusInternal: { notIn: [MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW] },
}; };
if (search?.trim()) { if (search?.trim()) {
@@ -1231,15 +1187,15 @@ export class MinglarService {
// Build search filter if search term is provided // Build search filter if search term is provided
const searchFilter = search const searchFilter = search
? { ? {
OR: [ OR: [
{ email: { contains: search, mode: 'insensitive' as const } }, { email: { contains: search, mode: 'insensitive' as const } },
{ firstName: { contains: search, mode: 'insensitive' as const } }, { firstName: { contains: search, mode: 'insensitive' as const } },
{ lastName: { contains: search, mode: 'insensitive' as const } }, { lastName: { contains: search, mode: 'insensitive' as const } },
{ {
userRefNumber: { contains: search, mode: 'insensitive' as const }, userRefNumber: { contains: search, mode: 'insensitive' as const },
}, },
], ],
} }
: {}; : {};
// 1. Fetch all required users (Admin, Co-Admin, AM) // 1. Fetch all required users (Admin, Co-Admin, AM)
@@ -1411,7 +1367,7 @@ export class MinglarService {
const amUser = await this.prisma.user.findUnique({ const amUser = await this.prisma.user.findUnique({
where: { id: accountManagerXid, isActive: true }, where: { id: accountManagerXid, isActive: true },
select: { emailAddress: true, firstName: true, lastName: true }, select: { emailAddress: true },
}); });
if (!amUser || !amUser.emailAddress) { if (!amUser || !amUser.emailAddress) {
@@ -1422,8 +1378,7 @@ export class MinglarService {
} }
try { try {
const amName = [amUser.firstName, amUser.lastName].filter(Boolean).join(' ').trim(); await sendAMEmailForHostAssign(amUser.emailAddress);
await sendAMEmailForHostAssign(amUser.emailAddress, amName);
return true; return true;
} catch (err) { } catch (err) {
console.error('Error sending AM assignment email', err); console.error('Error sending AM assignment email', err);
@@ -1756,7 +1711,6 @@ export class MinglarService {
isEmailVerfied: true, isEmailVerfied: true,
isMobileVerfied: true, isMobileVerfied: true,
isBiometric: true, isBiometric: true,
createdAt: true,
userAddressDetails: { userAddressDetails: {
select: { select: {
id: true, id: true,
@@ -1872,8 +1826,8 @@ export class MinglarService {
}); });
}); });
} }
async rejectActivityApplicationByAM(activityId: number, user_xid: number) { async rejectActivityApplicationByAM(activityId: number, user_xid: number) {
return await this.prisma.$transaction(async (tx) => { return await this.prisma.$transaction(async (tx) => {
await tx.activities.update({ await tx.activities.update({

View File

@@ -4,35 +4,20 @@ import config from "../../../config/config";
export async function sendEmailToHostForRejectedApplication( export async function sendEmailToHostForRejectedApplication(
emailAddress: string, emailAddress: string,
firstName: string
): Promise<{ ): Promise<{
sent: boolean; sent: boolean;
// messageId: string // messageId: string
}> { }> {
const subject = "Action Needed: Host Onboarding Application"; const subject = "Rejection for your application";
const htmlContent = ` const htmlContent = `
<p>Hi ${firstName},</p> <p>Dear Host,</p>
<p>Sorry to say that, But your application to minglar admin has been rejected.</p>
<p> <p>Please update your application and resubmit it.</p>
After reviewing your submission, were unable to proceed at this stage, as some details require further updates. We encourage you to log in to your Host portal to review the feedback provided and make the necessary changes. <p>If you have any questions please contact to minglar admin.</p>
</p> <p>Best regards,<br/>Minglar Team</p>
`;
<p>
<strong>Host Portal Login</strong><br/>
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
</p>
<p>
We appreciate your interest in Minglar and look forward to reviewing your updated application.
</p>
<p>
Warm regards,<br/>
Minglar Team
</p>
`;
try { try {
const result = await brevoService.sendEmail({ const result = await brevoService.sendEmail({
@@ -62,14 +47,21 @@ export async function sendAMRejectionMailtoHost(
// messageId: string // messageId: string
}> { }> {
const subject = "Action Needed: Host Onboarding Application"; const subject = "Improvement of your application";
const htmlContent = ` const htmlContent = `
<p>Hi ${name},</p> <p>Dear ${name},</p>
<p>After reviewing your submission, we're unable to proceed at this stage, as some details require further updates. We encourage you to log in to your Host portal to review the feedback provided and make the necessary changes.</p> <p> Your account manager has reviewed your application and provided some suggestions. <br/>
<p><a href="${link}" target="_blank">${link}</a></p> Please make the necessary improvements and re-submit your application to proceed with the onboarding process on Minglar.</p>
<p>We appreciate your interest in Minglar and look forward to reviewing your updated application.</p> <p> You may access your application using the link below:<br/>
<p>Warm regards,<br/>Team Minglar</p> <strong>Link:</strong>
<a href="${link}" target="_blank">
${link}
</a>
</p>
<p> If you have any questions, please feel free to contact the Minglar Support Team. </p>
<p> Best regards,<br/>
<strong>Minglar Team</strong> </p>
`; `;
try { try {
@@ -100,42 +92,23 @@ export async function sendAMPQQRejectionMailtoHost(
// messageId: string // messageId: string
}> { }> {
const subject = "Action Needed: Activity Pre-qualification"; const subject = "Improvement of your activity onboarding application";
const htmlContent = ` const htmlContent = `
<p>Hi ${name},</p> <p>Dear ${name},</p>
<p> <p>Your account manager has reviewed your activity application and provided some suggestions.<br/>
Thank you for taking the time to submit your activity pre-qualification details on the Minglar platform. Please make the necessary improvements and re-submit your activity application along with the pre-qualification answers to proceed with the onboarding process on Minglar.</p>
</p>
<p> <p>You may access your activity onboarding application using the link below:<br/>
After reviewing your submission, were unable to approve the application at this stage. However, we encourage you to make the suggested updates and refinements, as many applications are successfully approved after revision. <strong>Link:</strong> ${config.HOST_LINK}</p>
</p>
<p> <p>If you have any questions, please feel free to contact the Minglar Support Team.</p>
You can log in to the Host portal to review the feedback and continue updating your application:
</p>
<p> <p>Best regards,<br/>
<strong>Host Portal Login</strong><br/> <strong>Minglar Team</strong></p>
<a href="${config.HOST_LINK_PQ}" target="_blank">${config.HOST_LINK_PQ}</a>
</p>
<p> `;
If you need any guidance, feel free to reach out to us at
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
</p>
<p>
We appreciate your interest in partnering with Minglar and look forward to reviewing your updated submission.
</p>
<p>
Thank you,<br/>
Minglar Team
</p>
`;
try { try {
const result = await brevoService.sendEmail({ const result = await brevoService.sendEmail({
@@ -165,42 +138,23 @@ export async function sendActivityRejectionMailtoHost(
// messageId: string // messageId: string
}> { }> {
const subject = "Action Needed: Activity Onboarding"; const subject = "Improvement of your activity onboarding application";
const htmlContent = ` const htmlContent = `
<p>Hi ${name},</p> <p>Dear ${name},</p>
<p> <p>Your account manager has reviewed your activity application and provided some suggestions.<br/>
Thank you for submitting your activity for review. Please make the necessary improvements and re-submit your activity application along with the pre-qualification answers to proceed with the onboarding process on Minglar.</p>
</p>
<p> <p>You may access your activity onboarding application using the link below:<br/>
After evaluating the details provided, were unable to approve the listing at this stage. A few updates are required before we can proceed. <strong>Link:</strong> ${config.HOST_LINK}</p>
</p>
<p> <p>If you have any questions, please feel free to contact the Minglar Support Team.</p>
Please log in to your Host Portal to review the feedback and make the necessary revisions.
</p>
<p> <p>Best regards,<br/>
👉 <strong>Access your Host Portal:</strong><br/> <strong>Minglar Team</strong></p>
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
</p>
<p> `;
Once the updates have been submitted, our team will re-evaluate your activity promptly.
</p>
<p>
If you have any questions or need clarification on the feedback, feel free to reach out to us at
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>. Were happy to assist.
</p>
<p>
Warm regards,<br/>
The Minglar Team
</p>
`;
try { try {
const result = await brevoService.sendEmail({ const result = await brevoService.sendEmail({
@@ -220,58 +174,3 @@ export async function sendActivityRejectionMailtoHost(
throw new ApiError(500, "Failed to send OTP to minglar admin via email."); throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
} }
} }
export async function sendActivityScheduleRejectedMailtoHost(
emailAddress: string,
name: string,
): Promise<{
sent: boolean;
}> {
const subject = 'Changes Required to Approve Your Activity Schedule';
const htmlContent = `
<p>Hi ${name},</p>
<p>Thank you for submitting your activity schedule for review.</p>
<p>
At this stage, were unable to approve the schedule. Please log in to your Host Portal
to review the changes required and update the details accordingly.
</p>
<p>
👉 <a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
</p>
<p>
Once the revisions have been made, our team will promptly review the schedule again so
we can move you closer to going live on Minglar.
</p>
<p>
Were looking forward to activating your experience soon.
</p>
<p>
The Minglar Team
</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
console.log('📧 Email sent successfully:', result);
return {
sent: true,
};
} catch (err) {
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send schedule rejection email to host.');
}
}

View File

@@ -9,16 +9,13 @@ export async function sendOtpEmailForMinglarAdmin(
// messageId: string // messageId: string
}> { }> {
const subject = "Your Minglar Verification Code"; const subject = "OTP for Minglar Admin Registration";
const htmlContent = ` const htmlContent = `
<p>Hi there,</p> <p>Dear User,</p>
<p>To continue your Minglar Admin registration, please use the following One-Time Password (OTP):</p> <p>Your OTP for minglar admin registration is: <strong>${otp}</strong></p>
<p><strong>${otp}</strong></p> <p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
<p>This code is valid for 5 minutes.</p> <p>Best regards,<br/>Minglar Team</p>
<p>For your security, please do not share this code with anyone.</p>
<p>If you did not request this OTP, please ignore this email.</p>
<p>Warm regards,<br/>Minglar Team</p>
`; `;
try { try {
@@ -39,4 +36,3 @@ export async function sendOtpEmailForMinglarAdmin(
throw new ApiError(500, "Failed to send OTP to minglar admin via email."); throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
} }
} }

View File

@@ -27,21 +27,15 @@ export const handler = safeHandler(async (
// 2) Authenticate user // 2) Authenticate user
await verifyMinglarAdminHostToken(token); await verifyMinglarAdminHostToken(token);
// 3) Get stateXid and optional search term from query params // 3) Get bankXid from query params
const stateXid = Number(event.queryStringParameters?.stateXid); const stateXid = Number(event.queryStringParameters?.stateXid);
const search = event.queryStringParameters?.search?.trim();
if (!stateXid || isNaN(stateXid)) { if (!stateXid || isNaN(stateXid)) {
throw new ApiError(400, "Valid stateXid is required in query params."); throw new ApiError(400, "Valid stateXid is required in query params.");
} }
// If search is provided, enforce minimum 3 characters // 4) Fetch branches for the bank
if (search && search.length < 3) { const branches = await prePopulateService.getCityByStateId(stateXid);
throw new ApiError(400, "Search term must be at least 3 characters long.");
}
// 4) Fetch cities for the state (optionally filtered by search)
const branches = await prePopulateService.getCityByStateId(stateXid, search);
return { return {
statusCode: 200, statusCode: 200,

View File

@@ -39,20 +39,12 @@ export class PrePopulateService {
} }
async getCityByStateId(stateXid: number, search?: string) { async getCityByStateId(stateXid: number) {
return await this.prisma.cities.findMany({ return await this.prisma.cities.findMany({
where: { where: {
stateXid, stateXid,
isActive: true, isActive: true,
deletedAt: null, deletedAt: null
...(search && search.length >= 3
? {
cityName: {
contains: search,
mode: 'insensitive',
},
}
: {}),
}, },
select: { select: {
id: true, id: true,
@@ -140,9 +132,7 @@ export class PrePopulateService {
}), }),
]); ]);
const adminEmail = config.MinglarAdminEmail; return { documentDetails, countryDetails, stateDetails, companyTypeDetails };
return { documentDetails, countryDetails, stateDetails, companyTypeDetails, adminEmail };
} }
async getAllFrequencies() { async getAllFrequencies() {
@@ -163,6 +153,7 @@ export class PrePopulateService {
foodType, foodType,
cuisineDetails, cuisineDetails,
vehicleType, vehicleType,
navigationMode,
taxDetails, taxDetails,
energyLevel, energyLevel,
aminitiesDetails, aminitiesDetails,
@@ -180,6 +171,9 @@ export class PrePopulateService {
this.prisma.transportModes.findMany({ this.prisma.transportModes.findMany({
where: { isActive: true }, where: { isActive: true },
}), }),
this.prisma.navigationModes.findMany({
where: { isActive: true },
}),
this.prisma.taxes.findMany({ this.prisma.taxes.findMany({
where: { isActive: true }, where: { isActive: true },
}), }),
@@ -221,6 +215,7 @@ export class PrePopulateService {
foodType, foodType,
cuisineDetails, cuisineDetails,
vehicleType, vehicleType,
navigationMode,
taxDetails, taxDetails,
energyLevel, energyLevel,
aminitiesDetails, aminitiesDetails,

View File

@@ -1,74 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { UserService } from '../../services/user.service';
const userService = new UserService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Extract token from headers
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Authenticate user using verifyUserToken
const userInfo = await verifyUserToken(token);
const userId = userInfo.id;
if (Number.isNaN(userId)) {
throw new ApiError(400, 'User id must be a number');
}
const user = await userService.getUserById(userId);
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse request body
let body: { activityXid: number; isBucket: boolean; bucketTypeName: string; };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityXid, isBucket, bucketTypeName } = body;
// Validate required fields
if (
typeof activityXid !== 'number' ||
typeof isBucket !== 'boolean' ||
!bucketTypeName
) {
throw new ApiError(400, 'Required fields missing or invalid');
}
// Set the passcode
const counts = await userService.addToBucketInterested(userId, isBucket, bucketTypeName, activityXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: `Activity added to ${isBucket ? 'bucket' : 'interested'} successfully`,
data: {
bucketCount: counts.bucketCount,
interestedCount: counts.interestedCount,
coverImage: counts.coverImage,
coverImagePresignedUrl: counts.coverImagePresignedUrl,
}
}),
};
});

View File

@@ -50,73 +50,64 @@ export const handler = safeHandler(async (
const activity = activityDetails.activity; const activity = activityDetails.activity;
// Rooms: combine ActivityVenues with their respective slots for the selected date // Rooms: combine ActivityVenues with their respective slots for the selected date
const Venues = (activity.ActivityVenues || []) const Venues = (activity.ActivityVenues || []).map((v: any) => {
.map((v: any) => { const header = scheduleDetails.find((h: any) => h.activityVenue?.venueXid === v.id);
const header = scheduleDetails.find(
(h: any) => h.activityVenue?.venueXid === v.id
);
if (!header || !header.slots?.length) { const roomSlots = (header?.slots || []).map((s: any) => {
return null; // ❌ venue has no slots for selected date let status = 'Available';
} if (s.maxCapacity === 0) status = 'Housefull';
else if (s.maxCapacity <= 2) status = '2 Slots Left';
const roomSlots = header.slots.map((s: any) => { else if (s.maxCapacity <= 5) status = 'Fast Filling';
let status = "Available";
if (s.maxCapacity === 0) status = "Housefull";
else if (s.maxCapacity <= 2) status = "2 Slots Left";
else if (s.maxCapacity <= 5) status = "Fast Filling";
return {
slotId: s.slotId,
startTime: s.startTime,
endTime: s.endTime,
status,
maxCapacity: s.maxCapacity,
};
});
return { return {
venueXid: v.id, slotId: s.slotId,
venueName: v.venueName, startTime: s.startTime,
venueLabel: v.venueLabel, endTime: s.endTime,
venueCapacity: v.venueCapacity, status,
availableSeats: v.availableSeats ?? null, maxCapacity: s.maxCapacity,
price: v.ActivityPrices?.[0]?.sellPrice ?? null,
endDate: header?.endDate ?? null,
slots: roomSlots,
slotsCount: roomSlots.length,
venueMedia: (v.ActivityVenueArtifacts || []).map((media: any) => ({
id: media.id,
mediaType: media.mediaType,
mediaFileName: media.mediaFileName,
presignedUrl: media.presignedUrl,
})),
}; };
}) });
.filter(Boolean); // ✅ removes null venues
// derive check-in/out from all room slots (earliest start, latest end) return {
const allSlots = Venues.flatMap(r => r.slots || []); venueXid: v.id,
const startTimes = allSlots.map(s => s.startTime).filter(Boolean); venueName: v.venueName,
const endTimes = allSlots.map(s => s.endTime).filter(Boolean); venueLabel: v.venueLabel,
const checkInTime = startTimes.length ? startTimes.sort()[0] : null; venueCapacity: v.venueCapacity,
const checkOutTime = endTimes.length ? endTimes.sort().reverse()[0] : null; availableSeats: v.availableSeats ?? null,
price: v.ActivityPrices?.[0]?.sellPrice ?? null,
const responsePayload = { endDate: header?.endDate ?? null,
selectedDate, slots: roomSlots,
Venues, slotsCount: roomSlots.length,
checkInTime, venueMedia: (v.ActivityVenueArtifacts || []).map((media: any) => ({
checkOutTime, id: media.id,
}; mediaType: media.mediaType,
mediaFileName: media.mediaFileName, // original S3 key / URL
return { presignedUrl: media.presignedUrl, // presigned URL
statusCode: 200, })),
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({ success: true, data: responsePayload }),
}; };
}); });
// derive check-in/out from all room slots (earliest start, latest end)
const allSlots = Venues.flatMap(r => r.slots || []);
const startTimes = allSlots.map(s => s.startTime).filter(Boolean);
const endTimes = allSlots.map(s => s.endTime).filter(Boolean);
const checkInTime = startTimes.length ? startTimes.sort()[0] : null;
const checkOutTime = endTimes.length ? endTimes.sort().reverse()[0] : null;
const responsePayload = {
selectedDate,
Venues,
checkInTime,
checkOutTime,
};
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({ success: true, data: responsePayload }),
};
});

View File

@@ -1,74 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { FilteredLandingPageService } from '../../services/filteredLandingPage.service';
const filteredLandingPageService = new FilteredLandingPageService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Extract token from headers
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const page = Number(event.queryStringParameters?.page ?? 1);
const limit = Number(event.queryStringParameters?.limit ?? 20);
const countryName = event.queryStringParameters?.countryName ?? '';
const stateName = event.queryStringParameters?.stateName ?? '';
const cityName = event.queryStringParameters?.cityName ?? '';
const userLat = event.queryStringParameters?.userLat ?? '';
const userLong = event.queryStringParameters?.userLong ?? '';
let activityTypeXids: number[] | undefined;
if (event.queryStringParameters?.activityTypeXids) {
try {
activityTypeXids = JSON.parse(event.queryStringParameters.activityTypeXids);
} catch (error) {
// Handle invalid JSON if needed
}
}
if (page < 1 || limit < 1) {
throw new ApiError(400, 'Invalid pagination values');
}
// Fetch filtered landing page details
const result = await filteredLandingPageService.getFilteredLandingPageAllDetails(
userId,
page,
limit,
countryName,
stateName,
cityName,
userLat,
userLong,
activityTypeXids
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Filtered landing page data retrieved successfully',
data: result,
}),
};
});

View File

@@ -1,46 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import ApiError from '../../../../common/utils/helper/ApiError';
import { UserService } from '../../services/user.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
const userService = new UserService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Extract token from headers
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
// Fetch user with their HostHeader stepper info
const result = await userService.getAllBucketActivities(
userId
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data: result,
}),
};
});

View File

@@ -27,16 +27,16 @@ export const handler = safeHandler(async (
const longParam = event.queryStringParameters?.long ?? event.queryStringParameters?.lng ?? event.queryStringParameters?.longitude; const longParam = event.queryStringParameters?.long ?? event.queryStringParameters?.lng ?? event.queryStringParameters?.longitude;
const radiusParam = event.queryStringParameters?.radiusKm ?? event.queryStringParameters?.radius; const radiusParam = event.queryStringParameters?.radiusKm ?? event.queryStringParameters?.radius;
const userLat = latParam ? Number(latParam) : undefined; if (!latParam || !longParam || !radiusParam) {
const userLong = longParam ? Number(longParam) : undefined; throw new ApiError(400, 'lat, long and radiusKm (in km) are required as query parameters');
const radiusKm = radiusParam ? Number(radiusParam) : 15; // default 15km }
if ( const userLat = Number(latParam);
(userLat !== undefined && Number.isNaN(userLat)) || const userLong = Number(longParam);
(userLong !== undefined && Number.isNaN(userLong)) || const radiusKm = Number(radiusParam);
Number.isNaN(radiusKm)
) { if (Number.isNaN(userLat) || Number.isNaN(userLong) || Number.isNaN(radiusKm)) {
throw new ApiError(400, 'Invalid lat/long values'); throw new ApiError(400, 'lat, long and radiusKm must be valid numbers');
} }
const page = Number(event.queryStringParameters?.page ?? 1); const page = Number(event.queryStringParameters?.page ?? 1);

View File

@@ -26,11 +26,19 @@ export const handler = safeHandler(async (
} }
// Extract query parameters for search // Extract query parameters for search
const activityTitle = event.queryStringParameters?.activityTitle?.trim();
const activityType = event.queryStringParameters?.activityType?.trim(); const activityType = event.queryStringParameters?.activityType?.trim();
const checkInCity = event.queryStringParameters?.checkInCity?.trim();
// At least one search parameter should be provided
if (!activityTitle && !activityType && !checkInCity) {
throw new ApiError(400, 'At least one search parameter (activityTitle, activityType, or checkInCity) must be provided');
}
// Fetch activities based on search criteria // Fetch activities based on search criteria
const result = await userService.searchActivities( const result = await userService.searchActivities(
activityType userId,
{ activityTitle, activityType, checkInCity }
); );
return { return {

View File

@@ -1,71 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { UserService } from '../../services/user.service';
const userService = new UserService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Extract token from headers
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Authenticate user using verifyUserToken
const userInfo = await verifyUserToken(token);
const userId = userInfo.id;
if (Number.isNaN(userId)) {
throw new ApiError(400, 'User id must be a number');
}
const user = await userService.getUserById(userId);
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse request body
let body: { activityXid: number; isBucket: boolean; bucketTypeName: string; };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityXid, isBucket, bucketTypeName } = body;
// Validate required fields
if (
typeof activityXid !== 'number' ||
typeof isBucket !== 'boolean' ||
!bucketTypeName
) {
throw new ApiError(400, 'Required fields missing or invalid');
}
// Remove from bucket/interested
const counts = await userService.removeFromBucketInterested(userId, isBucket, bucketTypeName, activityXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: `Activity removed from ${isBucket ? 'bucket' : 'interested'} successfully`,
data: {
bucketCount: counts.bucketCount,
interestedCount: counts.interestedCount,
}
}),
};
});

View File

@@ -86,10 +86,9 @@ export const handler = safeHandler(
body: JSON.stringify({ body: JSON.stringify({
success: true, success: true,
message: 'Access token generated successfully', message: 'Access token generated successfully',
data: {
accessToken: newAccessToken.access.token, accessToken: newAccessToken.access.token,
accessTokenExpires: newAccessToken.access.expires, accessTokenExpires: newAccessToken.access.expires,
}, data: null,
}), }),
}; };
}, },

View File

@@ -30,17 +30,16 @@ export const handler = safeHandler(async (
const transactionResult = await prismaClient.$transaction(async (tx) => { const transactionResult = await prismaClient.$transaction(async (tx) => {
const user = await tx.user.findFirst({ const user = await tx.user.findFirst({
where: { mobileNumber: mobileNumber, isActive: true, userStatus: USER_STATUS.ACTIVE }, where: { mobileNumber: mobileNumber, isActive: true, userStatus: USER_STATUS.ACTIVE },
select: { id: true, userPasscode: true, mobileNumber: true, firstName: true }, select: { id: true, userPasscode: true, mobileNumber: true },
}); });
let newUserLocal; let newUserLocal;
let isNewUser = false; let isNewUser = false;
if (user && (!user.userPasscode || !user.firstName)) { if (user && !user.userPasscode) {
// reuse existing invited user record // reuse existing invited user record
newUserLocal = user; newUserLocal = user;
isNewUser = true;
} else if (user) { } else if (user) {
// Fully registered user already exists // Fully registered user already exists
newUserLocal = user; newUserLocal = user;

View File

@@ -1,44 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import ApiError from '../../../../common/utils/helper/ApiError';
import { UserService } from '../../services/user.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
const userService = new UserService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const searchQuery = event.queryStringParameters?.searchQuery ?? '';
const result = await userService.searchConnectionPeople(userId, searchQuery);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Connection people retrieved successfully',
data: result,
}),
};
});

View File

@@ -1,71 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const itineraryHeaderXid =
body.itineraryHeaderXid !== undefined && body.itineraryHeaderXid !== null
? Number(body.itineraryHeaderXid)
: NaN;
const reason =
typeof body.reason === 'string' ? body.reason.trim() : '';
if (!Number.isInteger(itineraryHeaderXid) || itineraryHeaderXid <= 0) {
throw new ApiError(400, 'Invalid itineraryHeaderXid.');
}
if (!reason) {
throw new ApiError(400, 'Cancellation reason is required.');
}
const result = await itineraryService.cancelUserItinerary(
userId,
itineraryHeaderXid,
reason,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary cancelled successfully',
data: result,
}),
};
});

View File

@@ -1,128 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
const parseQueryDate = (value: string, fieldName: string) => {
const trimmedValue = value.trim();
const isoMatch = trimmedValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoMatch) {
const [, year, month, day] = isoMatch;
const parsedDate = new Date(Number(year), Number(month) - 1, Number(day));
if (!Number.isNaN(parsedDate.getTime())) {
return parsedDate;
}
}
const slashMatch = trimmedValue.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
if (slashMatch) {
const [, year, month, day] = slashMatch;
const parsedDate = new Date(Number(year), Number(month) - 1, Number(day));
if (!Number.isNaN(parsedDate.getTime())) {
return parsedDate;
}
}
const parsedDate = new Date(trimmedValue);
if (Number.isNaN(parsedDate.getTime())) {
throw new ApiError(400, `Invalid ${fieldName}`);
}
return parsedDate;
};
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const itineraryHeaderXidRaw =
event.queryStringParameters?.itineraryHeaderXid ?? null;
const startDateRaw = event.queryStringParameters?.startDate ?? null;
const endDateRaw = event.queryStringParameters?.endDate ?? null;
let itineraryHeaderXid: number | undefined;
if (
itineraryHeaderXidRaw !== null &&
itineraryHeaderXidRaw !== undefined &&
itineraryHeaderXidRaw !== ''
) {
itineraryHeaderXid = Number(itineraryHeaderXidRaw);
if (!Number.isInteger(itineraryHeaderXid) || itineraryHeaderXid <= 0) {
throw new ApiError(400, 'Invalid itineraryHeaderXid');
}
}
const hasStartDate =
startDateRaw !== null &&
startDateRaw !== undefined &&
startDateRaw.trim() !== '';
const hasEndDate =
endDateRaw !== null &&
endDateRaw !== undefined &&
endDateRaw.trim() !== '';
if (hasStartDate !== hasEndDate) {
throw new ApiError(
400,
'startDate and endDate must be provided together',
);
}
let startDate: Date | undefined;
let endDate: Date | undefined;
if (hasStartDate && hasEndDate) {
startDate = parseQueryDate(startDateRaw, 'startDate');
endDate = parseQueryDate(endDateRaw, 'endDate');
if (startDate > endDate) {
throw new ApiError(
400,
'startDate must be earlier than or equal to endDate',
);
}
}
const result = await itineraryService.getAllUserSavedItineraries(
userId,
itineraryHeaderXid,
startDate,
endDate,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Saved itineraries retrieved successfully',
data: result,
}),
};
});

View File

@@ -1,61 +0,0 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const itineraryHeaderXidRaw = event.queryStringParameters?.itineraryHeaderXid;
if (!itineraryHeaderXidRaw) {
throw new ApiError(400, 'itineraryHeaderXid is required.');
}
const itineraryHeaderXid = Number(itineraryHeaderXidRaw);
if (!Number.isInteger(itineraryHeaderXid) || itineraryHeaderXid <= 0) {
throw new ApiError(400, 'Invalid itineraryHeaderXid.');
}
const result = await itineraryService.getItineraryCheckoutDetails(
userId,
itineraryHeaderXid,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary checkout details retrieved successfully',
data: result,
}),
};
});

View File

@@ -1,95 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const payload = {
userLat: Number(body.userLat),
userLong: Number(body.userLong),
startDate: body.startDate,
endDate: body.endDate,
startTime: body.startTime,
endTime: body.endTime,
energyLevelXid:
body.energyLevelXid !== undefined && body.energyLevelXid !== null
? Number(body.energyLevelXid)
: undefined,
entryTypeXid: Number(body.entryTypeXid),
groupCount:
body.groupCount !== undefined && body.groupCount !== null
? Number(body.groupCount)
: undefined,
page: body.page !== undefined ? Number(body.page) : 1,
limit: body.limit !== undefined ? Number(body.limit) : 20,
};
if (
Number.isNaN(payload.userLat) ||
Number.isNaN(payload.userLong) ||
!payload.startDate ||
!payload.endDate ||
!payload.startTime ||
!payload.endTime ||
(payload.energyLevelXid !== undefined &&
Number.isNaN(payload.energyLevelXid)) ||
Number.isNaN(payload.entryTypeXid) ||
(payload.groupCount !== undefined && Number.isNaN(payload.groupCount)) ||
Number.isNaN(payload.page) ||
Number.isNaN(payload.limit)
) {
throw new ApiError(
400,
'userLat, userLong, startDate, endDate, startTime, endTime, entryTypeXid, page and limit are required. energyLevelXid is optional.',
);
}
const result = await itineraryService.getMatchingBucketInterestedActivities(
userId,
payload,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Matching itinerary activities retrieved successfully',
data: result,
}),
};
});

View File

@@ -1,43 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const result = await itineraryService.getUserItineraryDetails(userId);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary details retrieved successfully',
data: result,
}),
};
});

View File

@@ -1,127 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const activities = Array.isArray(body.activities)
? body.activities
: body.itineraryActivityXid !== undefined
? [body]
: [];
if (!activities.length) {
throw new ApiError(400, 'activities is required and must be a non-empty array.');
}
const toOptionalId = (value: unknown) => {
if (value === undefined || value === null || value === '') {
return null;
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new ApiError(400, 'One or more selected option ids are invalid.');
}
return parsed;
};
const normalizedActivities = activities.map((activity: any, index: number) => {
const itineraryActivityXid = Number(activity.itineraryActivityXid);
if (!Number.isInteger(itineraryActivityXid) || itineraryActivityXid <= 0) {
throw new ApiError(400, `activities[${index}].itineraryActivityXid is required.`);
}
const selectedFoodTypeIds: number[] = Array.isArray(activity.selectedFoodTypeIds)
? Array.from(
new Set(
activity.selectedFoodTypeIds.map((id: unknown): number => Number(id)),
),
)
: [];
const selectedEquipmentIds: number[] = Array.isArray(activity.selectedEquipmentIds)
? Array.from(
new Set(
activity.selectedEquipmentIds.map((id: unknown): number => Number(id)),
),
)
: [];
if (selectedEquipmentIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(
400,
`activities[${index}].selectedEquipmentIds must contain valid ids.`,
);
}
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds must contain valid ids.`,
);
}
return {
itineraryActivityXid,
isFoodOpted: activity.isFoodOpted === undefined ? false : Boolean(activity.isFoodOpted),
selectedFoodTypeIds,
isTrainerOpted: activity.isTrainerOpted === undefined ? false : Boolean(activity.isTrainerOpted),
isInActivityNavigationOpted:
activity.isInActivityNavigationOpted === undefined
? false
: Boolean(activity.isInActivityNavigationOpted),
selectedNavigationModeXid: toOptionalId(activity.selectedNavigationModeXid),
selectedEquipmentIds,
};
});
const result = await itineraryService.saveItineraryActivitySelections(
userId,
normalizedActivities,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary activity selections saved successfully.',
data: result,
}),
};
});

View File

@@ -1,261 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const activities = Array.isArray(body.activities) ? body.activities : [];
if (!body.startDate || !body.endDate || !body.startTime || !body.endTime) {
throw new ApiError(
400,
'startDate, endDate, startTime and endTime are required.',
);
}
if (
body.startLocationAddress === undefined ||
body.startLocationAddress === null ||
body.startLocationLat === undefined ||
body.startLocationLat === null ||
body.startLocationLong === undefined ||
body.startLocationLong === null ||
body.endLocationAddress === undefined ||
body.endLocationAddress === null ||
body.endLocationLat === undefined ||
body.endLocationLat === null ||
body.endLocationLong === undefined ||
body.endLocationLong === null
) {
throw new ApiError(
400,
'startLocationAddress, startLocationLat, startLocationLong, endLocationAddress, endLocationLat and endLocationLong are required.',
);
}
if (
(typeof body.startLocationAddress === 'string' &&
!body.startLocationAddress.trim()) ||
(typeof body.endLocationAddress === 'string' &&
!body.endLocationAddress.trim())
) {
throw new ApiError(400, 'Location addresses cannot be empty.');
}
if (!activities.length) {
throw new ApiError(400, 'At least one activity is required.');
}
for (const activity of activities) {
const itineraryType =
typeof activity.itineraryType === 'string'
? activity.itineraryType.trim().toUpperCase().replace(/\s+/g, '_')
: 'ACTIVITY';
const isCustomItineraryType =
itineraryType === 'STAY' || itineraryType === 'FREE_TIME';
if (
!activity.modeOfTravel ||
activity.travelTimeBetweenPointsMins === undefined ||
activity.travelTimeBetweenPointsMins === null
) {
throw new ApiError(
400,
'Each itinerary item must include modeOfTravel and travelTimeBetweenPointsMins.',
);
}
if (isCustomItineraryType) {
const customStartDate = activity.startDate || activity.occurenceDate;
const customEndDate = activity.endDate || activity.occurenceDate;
if (
!customStartDate ||
!customEndDate ||
!activity.selectedStartTime ||
!activity.selectedEndTime
) {
throw new ApiError(
400,
`${itineraryType} items must include startDate, endDate, selectedStartTime and selectedEndTime.`,
);
}
if (itineraryType === 'STAY' && !activity.locationAddress) {
throw new ApiError(
400,
'STAY items must include locationAddress.',
);
}
continue;
}
if (!activity.activityXid || !activity.venueXid || !activity.scheduleHeaderXid) {
throw new ApiError(
400,
'ACTIVITY items must include activityXid, venueXid and scheduleHeaderXid.',
);
}
}
const payload = {
title: body.title,
startDate: body.startDate,
endDate: body.endDate,
startTime: body.startTime,
endTime: body.endTime,
startLocationAddress: body.startLocationAddress,
startLocationLat:
body.startLocationLat !== null && body.startLocationLat !== undefined
? Number(body.startLocationLat)
: undefined,
startLocationLong:
body.startLocationLong !== null && body.startLocationLong !== undefined
? Number(body.startLocationLong)
: undefined,
endLocationAddress: body.endLocationAddress,
endLocationLat:
body.endLocationLat !== null && body.endLocationLat !== undefined
? Number(body.endLocationLat)
: undefined,
endLocationLong:
body.endLocationLong !== null && body.endLocationLong !== undefined
? Number(body.endLocationLong)
: undefined,
activities: activities.map((activity: any) => {
const itineraryType =
typeof activity.itineraryType === 'string'
? activity.itineraryType.trim().toUpperCase().replace(/\s+/g, '_')
: 'ACTIVITY';
const isCustomItineraryType =
itineraryType === 'STAY' || itineraryType === 'FREE_TIME';
return {
activityXid:
!isCustomItineraryType &&
activity.activityXid !== undefined &&
activity.activityXid !== null
? Number(activity.activityXid)
: undefined,
venueXid:
!isCustomItineraryType &&
activity.venueXid !== undefined &&
activity.venueXid !== null
? Number(activity.venueXid)
: undefined,
scheduleHeaderXid:
!isCustomItineraryType &&
activity.scheduleHeaderXid !== undefined &&
activity.scheduleHeaderXid !== null
? Number(activity.scheduleHeaderXid)
: undefined,
modeOfTravel: activity.modeOfTravel,
travelTimeBetweenPointsMins: Number(
activity.travelTimeBetweenPointsMins,
),
kmForNextPoint:
activity.kmForNextPoint !== undefined &&
activity.kmForNextPoint !== null
? Number(activity.kmForNextPoint)
: undefined,
startDate: activity.startDate ?? activity.occurenceDate,
endDate: activity.endDate ?? activity.occurenceDate,
occurenceDate: activity.occurenceDate,
selectedStartTime: activity.selectedStartTime,
selectedEndTime: activity.selectedEndTime,
itineraryType,
paxCount:
activity.paxCount !== undefined && activity.paxCount !== null
? Number(activity.paxCount)
: undefined,
totalAmount:
activity.totalAmount !== undefined && activity.totalAmount !== null
? Number(activity.totalAmount)
: undefined,
locationLat:
activity.locationLat !== undefined && activity.locationLat !== null
? Number(activity.locationLat)
: undefined,
locationLong:
activity.locationLong !== undefined && activity.locationLong !== null
? Number(activity.locationLong)
: undefined,
locationAddress: activity.locationAddress,
};
}),
};
if (
payload.activities.some(
(activity) =>
(activity.activityXid !== undefined &&
Number.isNaN(activity.activityXid)) ||
(activity.venueXid !== undefined && Number.isNaN(activity.venueXid)) ||
(activity.scheduleHeaderXid !== undefined &&
Number.isNaN(activity.scheduleHeaderXid)) ||
Number.isNaN(activity.travelTimeBetweenPointsMins) ||
(activity.kmForNextPoint !== undefined &&
Number.isNaN(activity.kmForNextPoint)) ||
(activity.paxCount !== undefined && Number.isNaN(activity.paxCount)) ||
(activity.totalAmount !== undefined &&
Number.isNaN(activity.totalAmount)) ||
(activity.locationLat !== undefined &&
Number.isNaN(activity.locationLat)) ||
(activity.locationLong !== undefined &&
Number.isNaN(activity.locationLong)),
Number.isNaN(payload.startLocationLat) ||
Number.isNaN(payload.startLocationLong) ||
Number.isNaN(payload.endLocationLat) ||
Number.isNaN(payload.endLocationLong),
)
) {
throw new ApiError(400, 'One or more numeric itinerary values are invalid.');
}
const result = await itineraryService.saveUserItinerary(userId, payload);
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary saved successfully',
data: result,
}),
};
});

View File

@@ -1,95 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { PaymentService } from '../../services/payment.service';
const paymentService = new PaymentService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const amount = Number(body.amount);
if (!Number.isFinite(amount) || amount <= 0) {
throw new ApiError(400, 'amount is required and must be greater than 0.');
}
const currency =
typeof body.currency === 'string' && body.currency.trim()
? body.currency.trim().toUpperCase()
: 'INR';
const receipt =
typeof body.receipt === 'string' && body.receipt.trim()
? body.receipt.trim()
: undefined;
const notes =
body.notes && typeof body.notes === 'object' && !Array.isArray(body.notes)
? body.notes
: undefined;
const itineraryHeaderXid =
body.itineraryHeaderXid !== undefined && body.itineraryHeaderXid !== null
? Number(body.itineraryHeaderXid)
: undefined;
if (
itineraryHeaderXid !== undefined &&
(!Number.isInteger(itineraryHeaderXid) || itineraryHeaderXid <= 0)
) {
throw new ApiError(400, 'Invalid itineraryHeaderXid.');
}
const result = await paymentService.createOrder(userId, {
amount,
currency,
receipt,
notes: {
userId,
...(itineraryHeaderXid !== undefined ? { itineraryHeaderXid } : {}),
...(notes ?? {}),
},
itineraryHeaderXid,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Order created successfully',
data: result,
}),
};
});

View File

@@ -1,63 +0,0 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { PaymentService } from '../../services/payment.service';
const paymentService = new PaymentService(prismaClient);
function getHeaderValue(
headers: APIGatewayProxyEvent['headers'],
name: string,
): string | undefined {
const targetName = name.toLowerCase();
for (const [key, value] of Object.entries(headers ?? {})) {
if (key.toLowerCase() === targetName) {
return typeof value === 'string' ? value : undefined;
}
}
return undefined;
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const signature =
getHeaderValue(event.headers, 'x-razorpay-signature')?.trim() ?? '';
if (!signature) {
throw new ApiError(400, 'Missing Razorpay webhook signature.');
}
const rawBody = event.body
? event.isBase64Encoded
? Buffer.from(event.body, 'base64').toString('utf8')
: event.body
: '';
const result = await paymentService.handleWebhook({
rawBody,
signature,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Webhook received',
data: result,
}),
};
});

View File

@@ -1,71 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { PaymentService } from '../../services/payment.service';
const paymentService = new PaymentService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const paymentId =
typeof body.paymentId === 'string' ? body.paymentId.trim() : '';
const orderId =
typeof body.orderId === 'string' ? body.orderId.trim() : '';
const signature =
typeof body.signature === 'string' ? body.signature.trim() : '';
if (!paymentId || !orderId || !signature) {
throw new ApiError(
400,
'paymentId, orderId and signature are required.',
);
}
const result = await paymentService.verifyPayment(userId, {
paymentId,
orderId,
signature,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Payment verified and itinerary booked successfully',
data: result,
}),
};
});

View File

@@ -1,938 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
import {
ACTIVITY_AM_INTERNAL_STATUS,
ACTIVITY_INTERNAL_STATUS,
} from '../../../common/utils/constants/host.constant';
import config from '../../../config/config';
const bucket = config.aws.bucketName;
const attachPresignedUrl = async (file: string | null | undefined) => {
if (!file) return null;
const key = file.startsWith('http')
? new URL(file).pathname.replace(/^\/+/, '')
: file;
return getPresignedUrl(bucket, key);
};
const attachMediaWithPresignedUrl = async (mediaArr: any[] = []) => {
return Promise.all(
mediaArr.map(async (m) => ({
...m,
presignedUrl: await attachPresignedUrl(m.mediaFileName),
})),
);
};
@Injectable()
export class FilteredLandingPageService {
constructor(private readonly prisma: PrismaClient) { }
normalizeName = (name: string): string => {
return name
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, ' ')
.trim();
};
findOrCreateLocation = async (
countryName: string,
stateName: string,
cityName: string,
tx: any,
) => {
const normalizedCountry = this.normalizeName(countryName);
const normalizedState = this.normalizeName(stateName);
const normalizedCity = this.normalizeName(cityName);
let country = await tx.countries.findFirst({
where: {
countryName: { contains: normalizedCountry, mode: 'insensitive' },
},
});
if (!country) {
country = await tx.countries.create({
data: {
countryName: countryName.trim(),
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
});
}
let state = await tx.states.findFirst({
where: {
countryXid: country.id,
stateName: { contains: normalizedState, mode: 'insensitive' },
},
});
if (!state) {
state = await tx.states.create({
data: {
countryXid: country.id,
stateName: stateName.trim(),
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
});
}
let city = await tx.cities.findFirst({
where: {
stateXid: state.id,
cityName: { contains: normalizedCity, mode: 'insensitive' },
},
});
if (!city) {
city = await tx.cities.create({
data: {
stateXid: state.id,
cityName: cityName.trim(),
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
});
}
return {
countryXid: country.id,
stateXid: state.id,
cityXid: city.id,
};
};
// attachMediaWithPresignedUrl = async (mediaArr = []) => {
// return (
// await Promise.all(
// mediaArr.map(async (m) => {
// return {
// ...m,
// presignedUrl: await this.attachPresignedUrl(m.mediaFileName),
// };
// }),
// )
// );
// };
calculateDistance = (
lat1: number | null,
lon1: number | null,
lat2: number | null,
lon2: number | null,
) => {
if (!lat1 || !lon1 || !lat2 || !lon2) return null;
const R = 6371; // km
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c;
// return distance rounded to 2 decimals
return Number(distance.toFixed(2));
};
async rankAndPaginateActivities(
tx: any,
whereClause: any,
page: number,
limit: number,
connectionInterestMap: Map<number, number>,
) {
const skip = (page - 1) * limit;
// Get total count
const totalCount = await tx.activities.count({ where: whereClause });
// Fetch activities with ranking metadata
const activities = await tx.activities.findMany({
where: whereClause,
skip,
take: limit,
select: {
id: true,
activityTitle: true,
sustainabilityScore: true,
totalScore: true,
activityType: {
select: {
interestXid: true,
energyLevel: {
select: {
id: true,
energyLevelName: true,
energyColor: true,
energyIcon: true,
},
},
},
},
ActivitiesMedia: {
where: { isActive: true },
select: {
id: true,
mediaFileName: true,
mediaType: true,
},
},
// Fetch ranking metadata
ItineraryActivities: {
select: {
ActivityFeedbacks: {
select: { activityStars: true },
},
},
},
ActivityVenues: {
select: {
ActivityPrices: {
select: { sellPrice: true },
},
},
},
},
});
// Sort and format
const sortedActivities = activities
.map((act) => {
const feedbacks = act.ItineraryActivities.flatMap(
(ia) => ia.ActivityFeedbacks,
);
const totalStars = feedbacks.reduce(
(sum, f) => sum + f.activityStars,
0,
);
const avgRating =
feedbacks.length > 0 ? totalStars / feedbacks.length : 0;
const prices = act.ActivityVenues.flatMap((v) =>
v.ActivityPrices.map((p) => p.sellPrice),
).filter((p) => p !== null) as number[];
const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity;
return {
...act,
avgRating,
minPrice,
sustainabilityScore: act.sustainabilityScore ?? 0,
totalScore: act.totalScore ?? 0,
};
})
.sort((a, b) => {
if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating;
if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice;
if (b.sustainabilityScore !== a.sustainabilityScore)
return b.sustainabilityScore - a.sustainabilityScore;
return b.totalScore - a.totalScore;
});
const formattedActivities = await Promise.all(
sortedActivities.map(async (activity) => ({
interestXid: activity.activityType.interestXid,
activityId: activity.id,
connectionInterestedCount:
connectionInterestMap.get(activity.id) ?? 0,
activityTitle: activity.activityTitle,
sustainabilityScore: activity.sustainabilityScore,
cheapestPrice: activity.minPrice === Infinity ? null : activity.minPrice,
distance: 0,
rating: activity.avgRating,
energyLevel: activity.activityType.energyLevel
? {
...activity.activityType.energyLevel,
presignedUrl: await attachPresignedUrl(
activity.activityType.energyLevel.energyIcon
),
}
: null,
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
})),
);
return {
page,
limit,
totalCount,
hasMore: skip + limit < totalCount,
activities: formattedActivities,
};
}
async getFilteredLandingPageAllDetails(
userId: number,
page: number,
limit: number,
countryName: string,
stateName: string,
cityName: string,
userLat: string,
userLong: string,
activityTypeXids?: number[],
) {
const data = await this.prisma.$transaction(async (tx) => {
const userAddressDetails = await tx.userAddressDetails.findFirst({
where: { userXid: userId, isActive: true },
select: {
id: true,
address1: true,
address2: true,
pinCode: true,
locationName: true,
stateXid: true,
cityXid: true,
countryXid: true,
locationLat: true,
locationLong: true,
},
});
const userLatitude = userAddressDetails?.locationLat ?? null;
const userLongitude = userAddressDetails?.locationLong ?? null;
let effectiveLocation: {
countryXid?: number | null;
stateXid?: number | null;
cityXid?: number | null;
} | null = null;
const hasRequestLocation = countryName && stateName && cityName;
if (hasRequestLocation) {
effectiveLocation = await this.findOrCreateLocation(
countryName!,
stateName!,
cityName!,
tx,
);
} else if (userAddressDetails) {
effectiveLocation = {
countryXid: userAddressDetails.countryXid,
stateXid: userAddressDetails.stateXid,
cityXid: userAddressDetails.cityXid,
};
}
const effectiveCountryXid = effectiveLocation?.countryXid ?? null;
const effectiveStateXid = effectiveLocation?.stateXid ?? null;
// Get all activity types for user interests, filtered by selected activity types if provided
const activityTypeWhere: any = {
isActive: true,
};
if (activityTypeXids && activityTypeXids.length > 0) {
activityTypeWhere.id = { in: activityTypeXids };
}
const activityTypesWithInterests = await tx.activityTypes.findMany({
where: activityTypeWhere,
select: {
id: true,
activityTypeName: true,
interestXid: true,
interests: {
select: {
id: true,
interestName: true,
interestColor: true,
interestImage: true,
displayOrder: true,
},
},
},
});
if (!activityTypesWithInterests.length) {
return {
userAddressDetails,
interests: [],
activityTypes: [],
otherStatesActivities: null,
overSeasActivities: null,
};
}
const userBucketInterested = await tx.userBucketInterested.findMany({
where: {
userXid: userId,
isActive: true,
},
select: {
activityXid: true,
isBucket: true,
},
});
const userBucketActivityIds = userBucketInterested
.filter(u => u.isBucket)
.map(u => u.activityXid);
const userInterestedActivityIds = userBucketInterested
.filter(u => !u.isBucket)
.map(u => u.activityXid);
const allUserExcludedActivityIds = userBucketInterested.map(
u => u.activityXid,
);
const userConnectionDetails = await tx.connectDetails.findMany({
where: { userXid: userId, isActive: true },
select: {
id: true,
schoolCompanyXid: true,
}
});
const otherConnectionUsers = await tx.connectDetails.findMany({
where: { userXid: { notIn: [userId] }, isActive: true, schoolCompanyXid: { in: userConnectionDetails.map((u) => u.schoolCompanyXid) } },
select: {
id: true,
userXid: true,
}
});
const connectionUserIds =
otherConnectionUsers.length > 0
? otherConnectionUsers.map(u => u.userXid)
: [-1];
const connectionInterestByActivity = await tx.userBucketInterested.groupBy({
by: ['activityXid'],
where: {
userXid: { in: connectionUserIds },
isActive: true,
},
_count: {
activityXid: true,
},
});
const connectionInterestMap = new Map(
connectionInterestByActivity.map(item => [
item.activityXid,
item._count.activityXid,
])
);
const skip = (page - 1) * limit;
// Group activity types by interest
const activityTypesByInterest = activityTypesWithInterests.reduce((acc, at) => {
if (!acc[at.interestXid]) {
acc[at.interestXid] = {
interest: at.interests,
activityTypes: [],
};
}
acc[at.interestXid].activityTypes.push({
activityTypeId: at.id,
activityTypeName: at.activityTypeName,
});
return acc;
}, {} as any);
// Fetch activities for each activity type with excluded activities filter
const activitiesByActivityType = await Promise.all(
activityTypesWithInterests.map(async (activityType) => {
const activities = await tx.activities.findMany({
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: activityType.id,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
},
skip,
take: limit,
orderBy: { id: 'desc' },
select: {
id: true,
activityTitle: true,
activityDurationMins: true,
sustainabilityScore: true,
checkInLat: true,
checkInLong: true,
activityType: {
select: {
id: true,
activityTypeName: true,
interestXid: true,
energyLevel: {
select: {
id: true,
energyLevelName: true,
energyColor: true,
energyIcon: true,
},
},
},
},
ActivityVenues: {
select: {
ActivityPrices: {
select: {
sellPrice: true,
},
},
},
},
ActivitiesMedia: {
where: { isActive: true },
select: {
id: true,
mediaFileName: true,
mediaType: true,
},
},
},
});
const formattedActivities = await Promise.all(
activities.map(async (activity) => {
const cheapestPrice =
activity.ActivityVenues.flatMap((v) => v.ActivityPrices)
.map((p) => p.sellPrice)
.filter(Boolean)
.sort((a, b) => a - b)[0] ?? null;
const distance = this.calculateDistance(
userLatitude,
userLongitude,
activity.checkInLat,
activity.checkInLong,
);
return {
activityId: activity.id,
connectionInterestedCount:
connectionInterestMap.get(activity.id) ?? 0,
activityTitle: activity.activityTitle,
activityDurationMins: activity.activityDurationMins,
sustainabilityScore: activity.sustainabilityScore,
cheapestPrice,
distance,
rating: 0,
energyLevel: activity.activityType.energyLevel
? {
...activity.activityType.energyLevel,
presignedUrl: await attachPresignedUrl(
activity.activityType.energyLevel.energyIcon
),
}
: null,
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
};
}),
);
return {
activityTypeId: activityType.id,
activityTypeName: activityType.activityTypeName,
interestXid: activityType.interestXid,
activities: formattedActivities,
pagination: {
page,
limit,
hasMore: formattedActivities.length === limit,
},
};
}),
);
// Group by interests for the final structure
const interestsWithActivities = await Promise.all(
Object.values(activityTypesByInterest).map(async (interestGroup: any) => {
// collect all activities belonging to this interest
const activitiesForInterest = activitiesByActivityType
.filter(a => a.interestXid === interestGroup.interest.id)
.flatMap(a => a.activities);
return {
interestId: interestGroup.interest.id,
interestName: interestGroup.interest.interestName,
interestColor: interestGroup.interest.interestColor,
interestImage: interestGroup.interest.interestImage,
interestImagePresignedUrl: await attachPresignedUrl(
interestGroup.interest.interestImage
),
displayOrder: interestGroup.interest.displayOrder,
page,
limit,
hasMore: activitiesForInterest.length === limit,
activities: activitiesForInterest
};
})
);
// Most Hyped Activities with filtering
const mostHypedGrouped = await tx.userBucketInterested.groupBy({
by: ['activityXid'],
where: {
isActive: true,
isBucket: false,
activityXid: {
notIn: allUserExcludedActivityIds.length ? allUserExcludedActivityIds : [-1],
},
},
_count: {
activityXid: true,
},
orderBy: {
_count: {
activityXid: 'desc',
},
},
});
// Filter most hyped activities by activity type if provided
let filteredMostHypedActivityIds = mostHypedGrouped.map((a) => a.activityXid);
if (activityTypeXids && activityTypeXids.length > 0) {
const activitiesWithTypes = await tx.activities.findMany({
where: {
id: { in: filteredMostHypedActivityIds },
activityTypeXid: { in: activityTypeXids },
},
select: { id: true },
});
filteredMostHypedActivityIds = activitiesWithTypes.map(a => a.id);
}
const finalMostHypedGrouped = mostHypedGrouped
.filter(group => filteredMostHypedActivityIds.includes(group.activityXid))
.slice(skip, skip + limit);
const totalHypedActivities = filteredMostHypedActivityIds.length;
const mostHypedActivityIds = finalMostHypedGrouped.map((a) => a.activityXid);
const mostHypedActivitiesRaw = await tx.activities.findMany({
where: {
id: {
in: mostHypedActivityIds,
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
},
select: {
id: true,
activityTitle: true,
sustainabilityScore: true,
totalScore: true,
activityType: {
select: {
energyLevel: {
select: {
id: true,
energyLevelName: true,
energyColor: true,
energyIcon: true,
},
},
},
},
ActivitiesMedia: {
where: { isActive: true },
select: {
id: true,
mediaFileName: true,
mediaType: true,
},
},
ItineraryActivities: {
select: {
ActivityFeedbacks: {
select: { activityStars: true },
},
},
},
ActivityVenues: {
select: {
ActivityPrices: {
select: { sellPrice: true },
},
},
},
},
});
// Sort Most Hyped by the 4 criteria
const mostHypedSorted = mostHypedActivitiesRaw
.map((act) => {
const feedbacks = act.ItineraryActivities.flatMap(
(ia) => ia.ActivityFeedbacks,
);
const totalStars = feedbacks.reduce(
(sum, f) => sum + f.activityStars,
0,
);
const avgRating =
feedbacks.length > 0 ? totalStars / feedbacks.length : 0;
const prices = act.ActivityVenues.flatMap((v) =>
v.ActivityPrices.map((p) => p.sellPrice),
).filter((p) => p !== null) as number[];
const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity;
return {
...act,
avgRating,
minPrice,
sustainabilityScore: act.sustainabilityScore ?? 0,
totalScore: act.totalScore ?? 0,
hypeCount:
finalMostHypedGrouped.find((g) => g.activityXid === act.id)?._count
.activityXid ?? 0,
};
})
.sort((a, b) => {
if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating;
if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice;
if (b.sustainabilityScore !== a.sustainabilityScore)
return b.sustainabilityScore - a.sustainabilityScore;
return b.totalScore - a.totalScore;
});
const mostHypedActivities = await Promise.all(
mostHypedSorted.map(async (activity) => ({
activityId: activity.id,
activityTitle: activity.activityTitle,
connectionInterestedCount:
connectionInterestMap.get(activity.id) ?? 0,
hypeCount: activity.hypeCount,
distance: 0,
rating: 0,
energyLevel: activity.activityType.energyLevel
? {
...activity.activityType.energyLevel,
presignedUrl: await attachPresignedUrl(
activity.activityType.energyLevel.energyIcon
),
}
: null,
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
})),
);
const formattedMostHypedActivities = {
page,
limit,
totalCount: totalHypedActivities,
hasMore: skip + limit < totalHypedActivities,
activities: mostHypedActivities,
};
// New Arrivals with filtering
const newArrivalsWhere: any = {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
};
if (activityTypeXids && activityTypeXids.length > 0) {
newArrivalsWhere.activityTypeXid = { in: activityTypeXids };
}
const formattedNewArrivalsActivities = await this.rankAndPaginateActivities(
tx,
newArrivalsWhere,
page,
limit,
connectionInterestMap
);
// Other States Activities with filtering
const otherStatesWhere: any = {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
};
if (effectiveCountryXid) {
otherStatesWhere.checkInCountryXid = effectiveCountryXid;
}
if (effectiveStateXid) {
otherStatesWhere.checkInStateXid = { not: effectiveStateXid };
}
if (activityTypeXids && activityTypeXids.length > 0) {
otherStatesWhere.activityTypeXid = { in: activityTypeXids };
}
const formattedOtherStatesActivities = await this.rankAndPaginateActivities(
tx,
otherStatesWhere,
page,
limit,
connectionInterestMap
);
// Random Activities with filtering
const totalActiveCount = await tx.activities.count({
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
...(activityTypeXids && activityTypeXids.length > 0 && {
activityTypeXid: { in: activityTypeXids },
}),
},
});
let randomActivities: any[] = [];
if (totalActiveCount > 0) {
const takeCount = Math.min(5, totalActiveCount);
const randomOffsets = new Set<number>();
while (randomOffsets.size < takeCount) {
randomOffsets.add(Math.floor(Math.random() * totalActiveCount));
}
const randomFetched = await Promise.all(
Array.from(randomOffsets).map((offset) =>
tx.activities.findFirst({
skip: offset,
where: {
isActive: true,
activityInternalStatus:
ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus:
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
...(activityTypeXids && activityTypeXids.length > 0 && {
activityTypeXid: { in: activityTypeXids },
}),
},
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
mediaFileName: true,
},
},
},
}),
),
);
randomActivities = await Promise.all(
randomFetched
.filter(Boolean)
.map(async (activity) => {
const cover = activity!.ActivitiesMedia?.[0];
return {
activityId: activity!.id,
activityTitle: activity!.activityTitle,
coverImage: cover?.mediaFileName ?? null,
coverImagePresignedUrl: await attachPresignedUrl(
cover?.mediaFileName
),
};
}),
);
}
// Overseas Activities with filtering
const overseasWhere: any = {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
};
if (effectiveCountryXid) {
overseasWhere.checkInCountryXid = { not: effectiveCountryXid };
}
if (activityTypeXids && activityTypeXids.length > 0) {
overseasWhere.activityTypeXid = { in: activityTypeXids };
}
const formattedOverSeasActivities = await this.rankAndPaginateActivities(
tx,
overseasWhere,
page,
limit,
connectionInterestMap
);
return {
userAddressDetails,
experiencesLogged: 0,
citiesDiscovered: 0,
loggedInNetworkCount: 0,
citiesInNetworkCount: 0,
rating: 0,
interestedCount: userInterestedActivityIds.length,
bucketCount: userBucketActivityIds.length,
pagination: {
page,
limit,
},
randomActivities,
interests: interestsWithActivities,
otherStatesActivities: formattedOtherStatesActivities,
overSeasActivities: formattedOverSeasActivities,
newArrivalsActivities: formattedNewArrivalsActivities,
mostHypedActivities: formattedMostHypedActivities,
};
});
return data;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,395 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
import crypto from 'crypto';
import Razorpay from 'razorpay';
import ApiError from '../../../common/utils/helper/ApiError';
import config from '../../../config/config';
import { ItineraryService } from './itinerary.service';
const razorpay = new Razorpay({
key_id: config.RAZORPAY_KEY_ID,
key_secret: config.RAZORPAY_KEY_SECRET,
});
const buildUniqueReceipt = (input?: string) => {
const normalizedPrefix = (input?.trim() || 'receipt')
.replace(/[^a-zA-Z0-9_-]/g, '_')
.replace(/_+/g, '_')
.slice(0, 20);
const timePart = Date.now().toString(36);
const randomPart = crypto.randomBytes(4).toString('hex');
return `${normalizedPrefix}_${timePart}_${randomPart}`.slice(0, 100);
};
type RazorpayWebhookPayload = {
event?: string;
payload?: {
payment?: {
entity?: {
id?: string;
order_id?: string;
status?: string;
amount?: number;
currency?: string;
};
};
order?: {
entity?: {
id?: string;
status?: string;
};
};
};
};
@Injectable()
export class PaymentService {
constructor(private prisma: PrismaClient) {}
private signaturesMatch(expectedSignature: string, receivedSignature: string) {
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
const receivedBuffer = Buffer.from(receivedSignature, 'utf8');
if (expectedBuffer.length !== receivedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
}
async createOrder(
userXid: number,
payload: {
amount: number;
currency?: string;
receipt?: string;
notes?: Record<string, string | number | boolean | null>;
itineraryHeaderXid?: number;
},
) {
if (!process.env.RAZORPAY_KEY_ID || !process.env.RAZORPAY_KEY_SECRET) {
throw new ApiError(500, 'Razorpay credentials are not configured.');
}
if (!Number.isFinite(payload.amount) || payload.amount <= 0) {
throw new ApiError(400, 'amount must be a positive number.');
}
if (
payload.itineraryHeaderXid !== undefined &&
payload.itineraryHeaderXid !== null
) {
const itineraryHeader = await this.prisma.itineraryHeader.findFirst({
where: {
id: payload.itineraryHeaderXid,
ownerXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
});
if (!itineraryHeader) {
throw new ApiError(
404,
'Itinerary not found for the logged-in user.',
);
}
}
const amountInPaise = Math.round(payload.amount * 100);
const receipt = buildUniqueReceipt(payload.receipt);
const currency = payload.currency ?? 'INR';
const order = (await razorpay.orders.create({
amount: amountInPaise,
currency,
receipt,
notes: payload.notes ?? undefined,
} as any)) as any;
const paymentOrder = await this.prisma.paymentOrders.create({
data: {
userXid,
itineraryHeaderXid: payload.itineraryHeaderXid ?? null,
razorpayOrderId: order.id,
receipt: order.receipt ?? receipt,
amount: order.amount ?? amountInPaise,
currency: order.currency ?? currency,
paymentStatus: order.status ?? 'created',
notes: payload.notes
? (payload.notes as Prisma.InputJsonValue)
: null,
isActive: true,
},
});
return {
paymentOrderId: paymentOrder.id,
orderId: order.id,
amount: paymentOrder.amount,
currency: paymentOrder.currency,
receipt: paymentOrder.receipt,
status: paymentOrder.paymentStatus,
};
}
async verifyPayment(
userXid: number,
payload: {
paymentId: string;
orderId: string;
signature: string;
},
) {
if (!process.env.RAZORPAY_KEY_SECRET) {
throw new ApiError(500, 'Razorpay credentials are not configured.');
}
const paymentId = payload.paymentId?.trim();
const orderId = payload.orderId?.trim();
const signature = payload.signature?.trim();
if (!paymentId || !orderId || !signature) {
throw new ApiError(
400,
'paymentId, orderId and signature are required.',
);
}
const generatedSignature = crypto
.createHmac('sha256', process.env.RAZORPAY_KEY_SECRET)
.update(`${orderId}|${paymentId}`)
.digest('hex');
if (generatedSignature !== signature) {
throw new ApiError(400, 'Invalid signature.');
}
const itineraryService = new ItineraryService(this.prisma);
const { updatedPaymentOrder, bookingResult } = await this.prisma.$transaction(
async (tx) => {
const paymentOrder = await tx.paymentOrders.findFirst({
where: {
razorpayOrderId: orderId,
userXid,
isActive: true,
deletedAt: null,
},
});
if (!paymentOrder) {
throw new ApiError(404, 'Payment order not found.');
}
let updatedPayment = paymentOrder;
if (paymentOrder.paymentStatus === 'paid') {
if (
paymentOrder.razorpayPaymentId &&
paymentOrder.razorpayPaymentId !== paymentId
) {
throw new ApiError(
400,
'Payment order is already verified with a different payment id.',
);
}
} else {
updatedPayment = await tx.paymentOrders.update({
where: {
id: paymentOrder.id,
},
data: {
razorpayPaymentId: paymentId,
razorpaySignature: signature,
paymentStatus: 'paid',
verifiedAt: new Date(),
paidAt: new Date(),
},
});
}
let booking = null;
if (updatedPayment.itineraryHeaderXid) {
booking = await itineraryService.bookItineraryAfterPayment(
tx,
userXid,
updatedPayment.itineraryHeaderXid,
);
}
return {
updatedPaymentOrder: updatedPayment,
bookingResult: booking,
};
},
);
return {
paymentOrderId: updatedPaymentOrder.id,
orderId: updatedPaymentOrder.razorpayOrderId,
paymentId: updatedPaymentOrder.razorpayPaymentId,
status: updatedPaymentOrder.paymentStatus,
verifiedAt: updatedPaymentOrder.verifiedAt,
paidAt: updatedPaymentOrder.paidAt,
itineraryBooking: bookingResult,
};
}
async handleWebhook(payload: {
rawBody: string;
signature: string;
}) {
const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET?.trim();
if (!webhookSecret) {
throw new ApiError(
500,
'Razorpay webhook secret is not configured.',
);
}
const rawBody = payload.rawBody;
const signature = payload.signature?.trim();
if (!rawBody) {
throw new ApiError(400, 'Webhook body is required.');
}
if (!signature) {
throw new ApiError(400, 'Razorpay webhook signature is required.');
}
const generatedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(rawBody, 'utf8')
.digest('hex');
if (!this.signaturesMatch(generatedSignature, signature)) {
throw new ApiError(400, 'Invalid webhook signature.');
}
let body: RazorpayWebhookPayload;
try {
body = JSON.parse(rawBody) as RazorpayWebhookPayload;
} catch {
throw new ApiError(400, 'Invalid webhook JSON body.');
}
const eventType = body.event?.trim();
if (!eventType) {
throw new ApiError(400, 'Webhook event type is missing.');
}
const paymentEntity = body.payload?.payment?.entity;
const orderEntity = body.payload?.order?.entity;
const razorpayOrderId =
paymentEntity?.order_id?.trim() || orderEntity?.id?.trim();
const razorpayPaymentId = paymentEntity?.id?.trim();
if (!razorpayOrderId) {
throw new ApiError(400, 'Webhook payload is missing Razorpay order id.');
}
const paymentOrder = await this.prisma.paymentOrders.findFirst({
where: {
razorpayOrderId,
isActive: true,
deletedAt: null,
},
});
if (!paymentOrder) {
return {
eventType,
processed: false,
message: 'Payment order not found for webhook payload.',
};
}
if (
eventType === 'payment.captured' &&
paymentOrder.paymentStatus === 'paid' &&
paymentOrder.razorpayPaymentId === razorpayPaymentId
) {
return {
paymentOrderId: paymentOrder.id,
orderId: paymentOrder.razorpayOrderId,
paymentId: paymentOrder.razorpayPaymentId,
status: paymentOrder.paymentStatus,
verifiedAt: paymentOrder.verifiedAt,
paidAt: paymentOrder.paidAt,
eventType,
processed: true,
};
}
if (eventType === 'payment.captured' || eventType === 'order.paid') {
const updatedPaymentOrder = await this.prisma.paymentOrders.update({
where: {
id: paymentOrder.id,
},
data: {
...(razorpayPaymentId
? { razorpayPaymentId }
: {}),
paymentStatus: 'paid',
verifiedAt: new Date(),
paidAt: new Date(),
},
});
return {
paymentOrderId: updatedPaymentOrder.id,
orderId: updatedPaymentOrder.razorpayOrderId,
paymentId: updatedPaymentOrder.razorpayPaymentId,
status: updatedPaymentOrder.paymentStatus,
verifiedAt: updatedPaymentOrder.verifiedAt,
paidAt: updatedPaymentOrder.paidAt,
eventType,
processed: true,
};
}
if (eventType === 'payment.failed') {
const updatedPaymentOrder = await this.prisma.paymentOrders.update({
where: {
id: paymentOrder.id,
},
data: {
...(razorpayPaymentId
? { razorpayPaymentId }
: {}),
paymentStatus: 'failed',
},
});
return {
paymentOrderId: updatedPaymentOrder.id,
orderId: updatedPaymentOrder.razorpayOrderId,
paymentId: updatedPaymentOrder.razorpayPaymentId,
status: updatedPaymentOrder.paymentStatus,
verifiedAt: updatedPaymentOrder.verifiedAt,
paidAt: updatedPaymentOrder.paidAt,
eventType,
processed: true,
};
}
return {
paymentOrderId: paymentOrder.id,
orderId: paymentOrder.razorpayOrderId,
paymentId: paymentOrder.razorpayPaymentId,
status: paymentOrder.paymentStatus,
verifiedAt: paymentOrder.verifiedAt,
paidAt: paymentOrder.paidAt,
eventType,
processed: false,
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { verifyAnyToken } from '../../../common/middlewares/jwt/authForAny';
import { WebSocketService } from '../../../common/services/websocket.service';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
const wsService = new WebSocketService(prismaClient);
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const connectionId = event.requestContext.connectionId;
if (!connectionId) {
throw new ApiError(400, 'Missing connection ID');
}
const token =
event.queryStringParameters?.token ||
event.queryStringParameters?.['x-auth-token'] ||
event.headers?.['x-auth-token'] ||
event.headers?.['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'Token is required for WebSocket connection');
}
const userInfo = await verifyAnyToken(token);
const activityXidParam =
event.queryStringParameters?.activityXid ||
event.queryStringParameters?.activity_xid;
const activityXid = activityXidParam ? Number(activityXidParam) : null;
if (activityXidParam && (!activityXid || isNaN(activityXid))) {
throw new ApiError(400, 'Invalid activityXid');
}
await wsService.connect({
connectionId,
userXid: userInfo.id,
activityXid,
});
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'Connected',
}),
};
}
);

View File

@@ -1,17 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'Default route',
}),
};
}
);

View File

@@ -1,26 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { WebSocketService } from '../../../common/services/websocket.service';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
const wsService = new WebSocketService(prismaClient);
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const connectionId = event.requestContext.connectionId;
if (connectionId) {
await wsService.disconnect(connectionId);
}
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'Disconnected',
}),
};
}
);

View File

@@ -1,99 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import AWS from 'aws-sdk';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { ChatService } from '../../../common/services/chat.service';
import { WebSocketService } from '../../../common/services/websocket.service';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
const chatService = new ChatService(prismaClient);
const wsService = new WebSocketService(prismaClient);
function getApiGatewayEndpoint(event: APIGatewayProxyEvent): string {
const domainName = event.requestContext.domainName;
const stage = event.requestContext.stage;
const isLocal =
domainName?.includes('localhost') || domainName?.includes('127.0.0.1');
const protocol = isLocal ? 'http' : 'https';
return `${protocol}://${domainName}/${stage}`;
}
async function postToConnection(
api: AWS.ApiGatewayManagementApi,
connectionId: string,
payload: unknown
) {
await api
.postToConnection({
ConnectionId: connectionId,
Data: Buffer.from(JSON.stringify(payload)),
})
.promise();
}
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const connectionId = event.requestContext.connectionId;
if (!connectionId) {
throw new ApiError(400, 'Missing connection ID');
}
let body: any;
try {
body = JSON.parse(event.body || '{}');
} catch {
throw new ApiError(400, 'Invalid JSON in request body');
}
const activityXid = Number(body.activityXid ?? body.activity_xid);
const otherUserXid = Number(body.otherUserXid ?? body.other_user_xid);
const limit = body.limit ? Number(body.limit) : undefined;
if (!activityXid || isNaN(activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!otherUserXid || isNaN(otherUserXid)) {
throw new ApiError(400, 'Valid otherUserXid is required');
}
const connection = await wsService.getConnectionById(connectionId);
if (!connection) {
throw new ApiError(401, 'Unauthorized WebSocket connection');
}
const userXid = connection.userXid;
const messages = await chatService.getMessages({
activityXid,
userXid,
otherUserXid,
limit,
});
const endpoint = getApiGatewayEndpoint(event);
const api = new AWS.ApiGatewayManagementApi({ endpoint });
const payload = { type: 'messages', data: messages };
try {
await postToConnection(api, connectionId, payload);
} catch (err: any) {
if (err?.statusCode === 410) {
await wsService.disconnect(connectionId);
} else {
throw err;
}
}
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'Messages sent',
}),
};
}
);

View File

@@ -1,111 +0,0 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import AWS from 'aws-sdk';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { ChatService } from '../../../common/services/chat.service';
import { WebSocketService } from '../../../common/services/websocket.service';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
const chatService = new ChatService(prismaClient);
const wsService = new WebSocketService(prismaClient);
function getApiGatewayEndpoint(event: APIGatewayProxyEvent): string {
const domainName = event.requestContext.domainName;
const stage = event.requestContext.stage;
const isLocal =
domainName?.includes('localhost') || domainName?.includes('127.0.0.1');
const protocol = isLocal ? 'http' : 'https';
return `${protocol}://${domainName}/${stage}`;
}
async function postToConnection(
api: AWS.ApiGatewayManagementApi,
connectionId: string,
payload: unknown
) {
await api
.postToConnection({
ConnectionId: connectionId,
Data: Buffer.from(JSON.stringify(payload)),
})
.promise();
}
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const connectionId = event.requestContext.connectionId;
if (!connectionId) {
throw new ApiError(400, 'Missing connection ID');
}
let body: any;
try {
body = JSON.parse(event.body || '{}');
} catch {
throw new ApiError(400, 'Invalid JSON in request body');
}
const activityXid = Number(body.activityXid ?? body.activity_xid);
const receiverXid = Number(
body.receiverXid ?? body.receivedXid ?? body.received_xid
);
const message = body.message;
if (!activityXid || isNaN(activityXid)) {
throw new ApiError(400, 'Valid activityXid is required');
}
if (!receiverXid || isNaN(receiverXid)) {
throw new ApiError(400, 'Valid receiverXid is required');
}
const connection = await wsService.getConnectionById(connectionId);
if (!connection) {
throw new ApiError(401, 'Unauthorized WebSocket connection');
}
const senderXid = connection.userXid;
const saved = await chatService.sendMessage({
activityXid,
senderXid,
receiverXid,
message,
});
const endpoint = getApiGatewayEndpoint(event);
const api = new AWS.ApiGatewayManagementApi({ endpoint });
const [receiverConnections, senderConnections] = await Promise.all([
wsService.getConnectionsForUser({ userXid: receiverXid, activityXid }),
wsService.getConnectionsForUser({ userXid: senderXid, activityXid }),
]);
const targets = [...receiverConnections, ...senderConnections];
const payload = { type: 'message', data: saved };
for (const target of targets) {
try {
await postToConnection(api, target.connectionId, payload);
} catch (err: any) {
if (err?.statusCode === 410) {
await wsService.disconnect(target.connectionId);
continue;
}
throw err;
}
}
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'Message sent',
data: saved,
}),
};
}
);