143 Commits

Author SHA1 Message Date
paritosh18
0d18a77ab5 changed logic form filter only with start date 2026-04-27 20:02:21 +05:30
paritosh18
e6ba52520d getmemberpermissions of co-admin , operator 2026-04-27 16:41:35 +05:30
paritosh18
e164e1c25a sending venue details in after booking api 2026-04-27 15:24:04 +05:30
paritosh18
ce9a9b6211 change afterbooking api 2026-04-27 14:51:24 +05:30
paritosh18
4e04781a06 check in and checkout otp activity venue 2026-04-27 13:44:20 +05:30
paritosh18
e77d9e50c9 added pagination in get-matching-bucket-interested-activities 2026-04-27 13:04:38 +05:30
paritosh18
be9780f9ec added api for scan qr and get info of person and actvity 2026-04-27 12:51:39 +05:30
paritosh18
3c56c45b01 api for operator get actvity 2026-04-27 12:24:04 +05:30
paritosh18
d1c4ad76ba message changed 2026-04-27 11:55:50 +05:30
paritosh18
75025b62d9 ADDDED BOOKING FORM CLANEDER 2026-04-24 16:40:18 +05:30
paritosh18
04ae88b239 added new verify otp for operator 2026-04-24 15:49:24 +05:30
paritosh18
b18b6bc468 change in response oprator 2026-04-23 19:20:48 +05:30
paritosh18
6f1504e93f remove serverless prefix 2026-04-23 19:07:57 +05:30
paritosh18
1a6411acdc added check 2026-04-23 19:03:00 +05:30
paritosh18
841539b8cc Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-04-23 18:41:39 +05:30
paritosh18
5fcff67916 add oprator apis 2026-04-23 18:41:37 +05:30
paritosh18
acd31725ed added reason in cancel api 2026-04-20 20:04:35 +05:30
paritosh18
0d96b1e67e payment serive 2026-04-20 18:57:46 +05:30
paritosh18
f98354a1c8 added cancel iteneray api 2026-04-20 17:25:49 +05:30
paritosh18
66d65c3b84 send role 2026-04-17 16:28:47 +05:30
paritosh18
eef9bbf368 sending presignedurl in get-all-host activtiy 2026-04-17 15:33:47 +05:30
paritosh18
2eac865c51 geteallinvitedCoadminAndOperator 2026-04-17 15:06:09 +05:30
f205dfedd6 made the role based access system for the host panel 2026-04-17 13:16:00 +05:30
22e2e8e1b7 sending the food, pick drop, navigation and other details in the verifyPayment api 2026-04-16 14:54:29 +05:30
paritosh18
ce1be2c94e sending activtiy details 2026-04-15 20:06:15 +05:30
paritosh18
d793852d63 added env in common 2026-04-15 12:41:48 +05:30
paritosh18
9b95149469 Sending itenary based on the start date and end date 2026-04-15 11:57:39 +05:30
paritosh18
1789084685 Email template changes 2026-04-14 13:28:46 +05:30
paritosh18
cf5bd02a04 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-04-14 13:27:58 +05:30
0be293288d fix isuue 2026-04-14 13:22:00 +05:30
paritosh18
0ba056af98 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-04-14 11:15:12 +05:30
9e8d9502ae sending the host parent company contact info 2026-04-13 16:06:32 +05:30
94454a6f25 sending the precise error 2026-04-13 15:55:40 +05:30
paritosh18
631ae79277 add websocket support with connection management and chat functionality 2026-04-13 15:35:39 +05:30
25b82bee31 sending precise error message 2026-04-13 15:33:27 +05:30
paritosh18
dcb2259c7d implement chat functionality for users and hosts with message sending and retrieval 2026-04-13 15:13:43 +05:30
c5dcc5b1f0 booking the itinerary after successful payment 2026-04-13 14:00:50 +05:30
b47e6271a3 saving multiple activities selections at once in the api 2026-04-13 13:38:17 +05:30
958a3e5cec made the get itinerary checkout details api 2026-04-13 13:19:50 +05:30
181f32b2e7 added the contact details of the host parent company 2026-04-13 12:13:11 +05:30
c5cad4fdce sending the activity sell price in the activity object 2026-04-10 18:48:50 +05:30
daff265584 sending the lowest price for activity and sending the venue prices 2026-04-10 17:55:16 +05:30
0f5061a129 sending the activity price and check in check out city same for the pick up and drop locations 2026-04-10 17:48:20 +05:30
5e87ab84d1 made the razorpay webhook api 2026-04-10 15:06:10 +05:30
54a4f22d2f added the new env vars for the razorpay in the config file and the serverless yml file 2026-04-10 11:20:01 +05:30
bb87e0ac05 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-04-09 14:03:20 +05:30
paritosh18
01569670b4 optinal comapny details 2026-04-09 13:58:45 +05:30
8fccc62f33 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-04-09 11:37:40 +05:30
fcdb813fb7 added the display order to send the itinerary activities in actual order 2026-04-08 19:15:47 +05:30
5239e04621 fixed the error handling for submit company details 2026-04-08 18:42:50 +05:30
fe0a2eb95b made the razorpay apis and model 2026-04-08 18:41:08 +05:30
paritosh18
16d075b5f7 refactor: update email templates for clarity and consistency across notifications 2026-04-08 16:10:36 +05:30
9df8c5443c saving the start stop details and sending it in the get api for the itinerary 2026-04-08 15:57:36 +05:30
be0667d8e9 sending all the details in the getUserItinerary details api 2026-04-08 14:57:49 +05:30
a44321044f made the api for storing the selections for the itinerary of the user 2026-04-07 19:13:06 +05:30
fcac64e0a9 fix the company logo path issue 2026-04-07 18:45:07 +05:30
6ea2ebe5e1 added the dataconsent flags 2026-04-07 13:36:11 +05:30
388f3079a1 changed teh occurance date to start and end date for the saveUserItinerary creation api and sending all the data of host in the get by id api 2026-04-07 12:00:34 +05:30
6a11c15f39 edited the mail templates 2026-04-04 19:55:48 +05:30
ea461b6056 sending welcome email to host and changed the email template for sending otp 2026-04-04 19:12:28 +05:30
6703dc784d fixed the end time issue to saveuseritinerary 2026-04-03 10:13:56 +05:30
e22c37bc65 fixed the issues of the saveuseritinerary file 2026-04-02 16:03:12 +05:30
d32915c865 made the total amount optional 2026-04-01 19:59:31 +05:30
1c7ad52d0e fixed the indentation 2026-04-01 19:57:42 +05:30
f1f1f199e8 made the total Amount optional 2026-04-01 19:57:08 +05:30
1c32c18e03 Fixed the pax and location address 2026-04-01 19:51:13 +05:30
0d3c71ab5a added the stay and free time in the saveUserItinerary api 2026-04-01 16:09:04 +05:30
50f93bbeae added the delete logo path of the main and parent company 2026-04-01 15:10:10 +05:30
3ce9d1d180 sending the food trainer pick up and navigation details in the get user itinerary api 2026-03-27 11:57:05 +05:30
cb819088a0 fixed the comments string issue 2026-03-25 16:29:13 +05:30
8f5f01c287 sending the date of birth of user under the host getbyid api 2026-03-25 15:16:49 +05:30
e4a2a04045 fixed the coverimage issue for random activities and addbucket issue also resolved and taking the group count 2026-03-25 13:34:12 +05:30
50ce8e39c5 sending checkin lat long in the user itinerary api and city state country details of the parent company in the getbyid api of host 2026-03-25 12:36:47 +05:30
19e57f0e7f fixed the new user condition in the user module 2026-03-25 11:33:57 +05:30
ad5e343b66 changed the draft-activity to draft 2026-03-24 17:38:07 +05:30
8c3ece6ebd checking the activity title should not be same 2026-03-24 14:42:09 +05:30
092f425bb3 changed the to review 2026-03-24 14:41:25 +05:30
b1a3afd3a1 Sending mail on the final submission of the pqp 2026-03-23 19:31:19 +05:30
97f431260d sending the admin email in the prepopulate 2026-03-23 15:04:05 +05:30
bf6d9ae00b sending created at 2026-03-20 15:15:40 +05:30
518ec4eb21 made save itinerary and get itinerary details apis 2026-03-18 19:49:04 +05:30
95b061b400 made searchConnectionPeople to invite in the itinerary API 2026-03-18 13:30:41 +05:30
92992797ab sending the filter details in the getMatchingBucketInterestedActivities API 2026-03-18 13:21:32 +05:30
c96e3b0c1a sending the energyLevel details also in the getUserItineraryDetails service 2026-03-18 12:45:59 +05:30
f23b93801c making the energylevelxid optional 2026-03-18 11:09:57 +05:30
f1801a3210 made getMatchingBucketInterestedActivities api 2026-03-17 16:22:03 +05:30
2588ca4317 fixed the path 2026-03-17 14:28:56 +05:30
e809ba4480 sending mail after host submits pqp questioniare and not sending the applications with new status in the host applications 2026-03-16 15:50:27 +05:30
678be7c905 fixed the issue of path 2026-03-16 15:27:20 +05:30
08b4231e5f Merge branch 'Split-lambda' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-16 11:30:53 +05:30
paritosh18
4a61e2c412 prepopulate change 2026-03-16 11:19:54 +05:30
a3ab9db5a2 Made getUserItineraryDetails api for the prepopulate data of itinerary page in mobile 2026-03-15 20:33:45 +05:30
paritosh18
7167eae07e fixed the path in the user yml file 2026-03-13 17:31:29 +05:30
paritosh18
4f3d7fd737 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into Split-lambda 2026-03-13 17:30:27 +05:30
paritosh18
199013b0f5 split lambda 2026-03-13 17:27:51 +05:30
0b81dbf7b1 made a new getAllBucketActivities api 2026-03-13 13:09:25 +05:30
paritosh18
9722e1988c Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into Split-lambda 2026-03-13 13:05:14 +05:30
cf2bbbf138 fixed the response structure in the specific api 2026-03-11 15:53:54 +05:30
paritosh18
2ca785248f add serverless-plugin-split-stacks and update serverless configuration; refactor host function definitions and improve updateHostProfile handler 2026-03-11 15:42:41 +05:30
0aa2b9b53e fixed the search api for the specific and sending by default 15 km radius activities in the nearby activity api 2026-03-11 13:41:15 +05:30
b5cdb20c4f sending the latestactivity image in the bucket 2026-03-10 21:30:43 +05:30
00e07113e5 sending the latest added activity image in the add to bucket api and fixed the presignedurl in the getFilteredLandingPageAllDetails api and fixed the logic to send the activities from the selected activityxids 2026-03-10 19:36:44 +05:30
c8f0f93792 sending the duration cheapprice per person and sustanibility score in the surprise me api 2026-03-10 18:12:26 +05:30
87779664d1 sending the activity type xids in the search api of I am specific 2026-03-10 15:26:57 +05:30
5b31e5f2a9 sending the activity count in the submit personal info 2026-03-09 18:49:40 +05:30
2a073c44a2 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-09 16:09:36 +05:30
paritosh18
220d309087 Added profile image upload functionality to updateHostProfile API, including multipart form data handling and S3 integration for image storage. 2026-03-09 16:09:20 +05:30
f45c33ba83 sending the random activities in the getconnection api 2026-03-09 16:00:31 +05:30
22b3593150 sending 5 random activities in the get surprise me api 2026-03-09 15:57:14 +05:30
d186681ee4 added isActive condition 2026-03-09 15:51:58 +05:30
8f428fc1cb fixed the i am specific new api issue 2026-03-09 15:18:59 +05:30
0b503cf8bb sending the checkIn and checkOut location 2026-03-06 19:59:31 +05:30
7110d0462c Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-06 17:37:56 +05:30
96648fe37e sending the isBukcet and isInterested flag in getbyid api and changed the logic in the getactivitiesfrom connections api 2026-03-06 17:36:14 +05:30
paritosh18
2095f8e124 filtered landing page for specific search api 2026-03-06 17:11:21 +05:30
21c8799502 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-06 15:49:46 +05:30
paritosh18
ad9e8e1a3f Added remove api from interested 2026-03-06 15:49:25 +05:30
b200e2cb94 sending the distance in the getbyid of activity 2026-03-06 15:49:13 +05:30
cae66237d2 fixed the check availability api and sending the interested and bucket count in the connection api 2026-03-06 15:40:21 +05:30
25be8a5647 sending the distance in connection activities api 2026-03-05 19:21:16 +05:30
7a4aecdd45 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-05 18:58:34 +05:30
5cced2981a sending the distance in the apis 2026-03-05 18:57:58 +05:30
paritosh18
b9fbab3717 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-03-05 18:52:32 +05:30
paritosh18
90c897ad48 remove deleteMany call to allow reuse of refresh tokens in generateAuthToken method 2026-03-05 18:44:57 +05:30
4a069cc67a if there is no slot then the venue will not be sent 2026-03-05 18:12:35 +05:30
paritosh18
5d046c4bcf update ActivityOtherDetails model to change string fields to text type for better data handling 2026-03-05 16:54:43 +05:30
accfc4b769 sending the safety instruction and cancellations in the getbyid api of activity in user 2026-03-05 16:15:15 +05:30
e149884f72 sending hostheader data also in the get latest agreement details api response 2026-03-05 13:05:56 +05:30
a31ec97640 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-05 12:49:44 +05:30
0c97412057 sending pincode in getbyid 2026-03-05 11:25:33 +05:30
paritosh18
b4ff39c0d7 removed logs 2026-03-04 18:59:37 +05:30
bb5da7647b sending the address details in the getbyid host api 2026-03-04 18:57:14 +05:30
3f19bb4087 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-04 18:45:44 +05:30
be8b9cef7d gbbb 2026-03-04 18:45:33 +05:30
paritosh18
77cef98091 Implement updateHostProfile endpoint and related service logic; remove navigation modes seeding; add logging for user activities 2026-03-04 17:04:14 +05:30
97f9c2b26e fixed the get in the getbyid 2026-03-04 12:23:29 +05:30
paritosh18
b93cd6b32c Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-03-02 19:07:20 +05:30
paritosh18
51319a69fc Enhance getCityByState handler to support optional search term with validation and update service method to filter cities accordingly. 2026-03-02 19:06:48 +05:30
5ad46309ef getting and storing the string for the navigation modes not the xids 2026-03-02 19:05:08 +05:30
paritosh18
781212277a ghfghf 2026-03-02 13:01:53 +05:30
6b0ee461c5 sending the hostId in the get stepper api 2026-03-02 12:35:51 +05:30
cc2fa3eb6b sending the 5 random users profile image in the getbyid api 2026-02-28 13:36:33 +05:30
fe6bb59cc7 Made add to bucket interested api and sending the count of bucket interested in the landing page api and surprise me api and sending the updated count in the add to bucket api 2026-02-28 13:11:26 +05:30
111 changed files with 16773 additions and 1420 deletions

View File

@@ -1,40 +0,0 @@
# 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-js": "^5.2.0",
"@aws-sdk/client-s3": "^3.928.0",
"@aws-sdk/s3-request-presigner": "^3.310.0",
"@aws-sdk/s3-request-presigner": "^3.928.0",
"@aws/lambda-invoke-store": "^0.2.1",
"@nestjs/common": "^10.3.0",
"@nestjs/config": "^3.1.1",
@@ -31,18 +31,27 @@
"@types/http-status": "^1.1.2",
"ajv": "8.12.0",
"aws-lambda": "^1.0.7",
"aws-sdk": "^2.1692.0",
"bcrypt": "^6.0.0",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"date-fns": "^4.1.0",
"dayjs": "^1.11.19",
"docx": "^9.6.0",
"docxtemplater": "^3.68.3",
"fast-xml-parser": "^5.3.1",
"fs": "^0.0.1-security",
"helmet": "^7.1.0",
"http-status": "^2.1.0",
"moment": "^2.30.1",
"number-to-words": "^1.2.4",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"path": "^0.12.7",
"pdf-lib": "^1.17.1",
"pizzip": "^3.2.0",
"prisma": "^7.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
@@ -73,6 +82,7 @@
"prettier": "^3.2.5",
"serverless-esbuild": "^1.55.1",
"serverless-offline": "^14.4.0",
"serverless-plugin-split-stacks": "^1.14.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
@@ -4650,6 +4660,24 @@
"dev": true,
"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": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -5751,6 +5779,13 @@
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"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": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -6546,6 +6581,15 @@
"@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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -6624,6 +6668,16 @@
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -6964,6 +7018,19 @@
"dev": true,
"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": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -6992,6 +7059,13 @@
"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": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/aws-lambda/-/aws-lambda-1.0.7.tgz",
@@ -7257,6 +7331,16 @@
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@@ -8386,7 +8470,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cors": {
@@ -8539,6 +8622,12 @@
"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": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -8633,6 +8722,21 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -8763,6 +8867,50 @@
"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": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
@@ -9031,6 +9179,39 @@
"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": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
@@ -10022,6 +10203,12 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -10199,6 +10386,31 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
@@ -10437,6 +10649,16 @@
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -10499,6 +10721,20 @@
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/http-status/-/http-status-2.1.0.tgz",
@@ -10514,6 +10750,20 @@
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -10556,7 +10806,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"dev": true,
"license": "MIT"
},
"node_modules/import-fresh": {
@@ -10661,6 +10910,16 @@
"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": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -11986,7 +12245,6 @@
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dev": true,
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
@@ -11999,7 +12257,6 @@
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
@@ -12015,14 +12272,12 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
@@ -12149,7 +12404,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
@@ -12548,6 +12802,12 @@
"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": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
@@ -12698,6 +12958,24 @@
"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": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -12722,6 +13000,16 @@
"dev": true,
"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": {
"version": "13.5.6",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz",
@@ -12873,6 +13161,12 @@
"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": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
@@ -13118,6 +13412,40 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -13128,7 +13456,6 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true,
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
@@ -13219,6 +13546,16 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -13293,6 +13630,21 @@
"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": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -13304,6 +13656,24 @@
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
@@ -13438,6 +13808,21 @@
"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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -13689,11 +14074,19 @@
}
}
},
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT"
},
"node_modules/prompts": {
@@ -13756,6 +14149,36 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -14756,6 +15179,23 @@
"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": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
@@ -14792,7 +15232,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"dev": true,
"license": "MIT"
},
"node_modules/setprototypeof": {
@@ -14923,6 +15362,47 @@
"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": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
@@ -15504,6 +15984,13 @@
"dev": true,
"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": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -17076,6 +17563,30 @@
}
}
},
"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": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",

View File

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

View File

@@ -1,7 +1,8 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Add Linux target
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Lambda Node 18/20 (Amazon Linux) target
previewFeatures = ["multiSchema"]
engineType = "library"
}
datasource db {
@@ -31,6 +32,8 @@ model User {
isMobileVerfied Boolean? @default(false) @map("is_mobile_verified")
isProfileUpdated Boolean? @default(false) @map("is_profile_updated")
userRefNumber String? @unique @map("user_ref_number") @db.VarChar(20)
dataConsentAccepted Boolean? @default(false) @map("data_consent_accepted")
dataConsentAcceptedOn DateTime? @map("data_consent_accepted_on")
isActive Boolean? @default(true) @map("is_active")
createdAt DateTime? @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at")
@@ -58,8 +61,11 @@ model User {
ActivitySOSDetails ActivitySOSDetails[]
ActivityFeedbacks ActivityFeedbacks[]
ItineraryDetails ItineraryDetails[]
paymentOrders PaymentOrders[]
inviteDetails InviteDetails[] @relation("InvitedUser")
invitedInviteDetails InviteDetails[] @relation("InviterUser")
hostMembers HostMembers[] @relation("HostMemberUser")
invitedHostMembers HostMembers[] @relation("HostMemberInviter")
userRevenues UserRevenue[]
userInterests UserInterests[]
connectDetails ConnectDetails[]
@@ -75,6 +81,9 @@ model User {
// 🔹 Activities where this user is Account Manager
managedActivities Activities[] @relation("ActivityAccountManager")
activitySortings ActivitySorting[]
sentActivityMessages ActivityMessages[] @relation("ActivityMessageSender")
receivedActivityMessages ActivityMessages[] @relation("ActivityMessageReceiver")
chatConnections ChatConnections[]
@@map("users")
@@schema("usr")
@@ -554,20 +563,6 @@ model Frequencies {
@@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 {
id Int @id @default(autoincrement())
transportModeName String @unique @map("transport_mode_name") @db.VarChar(60)
@@ -684,6 +679,8 @@ model Roles {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
User User[]
hostMembers HostMembers[] @relation("HostMemberRole")
hostRolePermissionMasters HostRolePermissionMasters[] @relation("HostRolePermissionMasterRole")
@@map("roles")
@@schema("mst")
@@ -742,6 +739,22 @@ model Token {
@@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
model HostHeader {
@@ -800,6 +813,8 @@ model HostHeader {
HostBankDetails HostBankDetails[]
HostDocuments HostDocuments[]
HostSuggestion HostSuggestion[]
hostMembers HostMembers[]
hostRolePermissionMasters HostRolePermissionMasters[]
hostParent HostParent[]
HostTrack HostTrack[]
Activities Activities[]
@@ -831,13 +846,90 @@ model HostBankDetails {
@@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 {
id Int @id @default(autoincrement())
hostXid Int @map("host_xid")
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
documentTypeXid Int @map("document_type_xid")
documentType DocumentType @relation(fields: [documentTypeXid], references: [id], onDelete: Restrict)
documentName String @map("document_name") @db.VarChar(20)
documentName String @map("document_name") @db.VarChar(50)
filePath String @map("file_path") @db.VarChar(400)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
@@ -887,7 +979,10 @@ model HostParent {
id Int @id @default(autoincrement())
hostXid Int @map("host_xid")
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)
address2 String? @map("address_2") @db.VarChar(150)
cityXid Int? @map("city_xid")
@@ -1043,22 +1138,42 @@ model Activities {
activityCuisines ActivityCuisine[]
activityPickUpTransports ActivityPickUpTransport[]
userBucketInterests UserBucketInterested[]
activityMessages ActivityMessages[]
assignedHostMembers HostMemberActivities[]
@@map("activities")
@@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 {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
exclusiveNotes String? @map("exclusive_notes") @db.VarChar(500)
SafetyInstruction String? @map("safety_instruction") @db.VarChar(400)
Cancellations String? @map("cancellations") @db.VarChar(400)
dosNotes String? @map("dos_notes") @db.VarChar(400)
dontsNotes String? @map("donts_notes") @db.VarChar(400)
tipsNotes String? @map("tips_notes") @db.VarChar(400)
termsAndCondition String? @map("terms_and_condition") @db.VarChar(500)
exclusiveNotes String? @map("exclusive_notes") @db.Text
SafetyInstruction String? @map("safety_instruction") @db.Text
Cancellations String? @map("cancellations") @db.Text
dosNotes String? @map("dos_notes") @db.Text
dontsNotes String? @map("donts_notes") @db.Text
tipsNotes String? @map("tips_notes") @db.Text
termsAndCondition String? @map("terms_and_condition") @db.Text
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -1380,6 +1495,7 @@ model ActivityFoodTypes {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
itineraryActivitySelectionFoodTypes ItineraryActivitySelectionFoodType[]
@@map("activity_food_types")
@@schema("act")
@@ -1430,6 +1546,7 @@ model ActivityEquipments {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
itineraryActivitySelectionEquipments ItineraryActivitySelectionEquipment[]
@@map("activity_equipments")
@@schema("act")
@@ -1456,8 +1573,7 @@ model ActivityNavigationModes {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
navigationModeXid Int @map("navigation_mode_xid")
navigationMode NavigationModes @relation(fields: [navigationModeXid], references: [id], onDelete: Restrict)
navigationModeName String @map("navigation_mode_name") @db.VarChar(30)
isInActivityChargeable Boolean @default(false) @map("is_in_activity_chargeable")
navigationModesBasePrice Int @map("navigation_modes_base_price")
navigationModesTotalPrice Int @map("navigation_modes_total_price")
@@ -1466,6 +1582,7 @@ model ActivityNavigationModes {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityNavigationModesTaxes ActivityNavigationModesTaxes[]
ItineraryActivitySelections ItineraryActivitySelection[]
@@map("activity_navigation_modes")
@@schema("act")
@@ -1662,6 +1779,7 @@ model ItineraryHeader {
toDate DateTime @map("to_date")
toTime String @map("to_time") @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")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -1669,6 +1787,7 @@ model ItineraryHeader {
ItineraryMembers ItineraryMembers[]
ItineraryStartStopDetails ItineraryStartStopDetails[]
ItineraryActivities ItineraryActivities[]
paymentOrders PaymentOrders[]
@@map("itinerary_header")
@@schema("itn")
@@ -1692,6 +1811,7 @@ model ItineraryMembers {
User User? @relation(fields: [userId], references: [id])
userId Int?
ItineraryDetails ItineraryDetails[]
itineraryActivitySelections ItineraryActivitySelection[]
@@map("itinerary_members")
@@schema("itn")
@@ -1725,25 +1845,26 @@ model ItineraryActivities {
id Int @id @default(autoincrement())
itineraryHeaderXid Int @map("itinerary_header_xid")
itineraryHeader ItineraryHeader @relation(fields: [itineraryHeaderXid], references: [id], onDelete: Cascade)
displayOrder Int @default(0) @map("display_order")
itineraryType String @map("itinerary_type") @db.VarChar(30)
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Restrict)
scheduledHeaderXid Int @map("scheduled_header_xid")
scheduledHeader ScheduleHeader @relation(fields: [scheduledHeaderXid], references: [id], onDelete: Restrict)
activityXid Int? @map("activity_xid")
activity Activities? @relation(fields: [activityXid], references: [id], onDelete: Restrict)
scheduledHeaderXid Int? @map("scheduled_header_xid")
scheduledHeader ScheduleHeader? @relation(fields: [scheduledHeaderXid], references: [id], onDelete: Restrict)
occurenceDate DateTime @map("occurence_date")
startTime String @map("start_time") @db.VarChar(30)
endTime String @map("end_time") @db.VarChar(30)
endDate DateTime @map("end_date")
venueXid Int @map("venue_xid")
venue ActivityVenues @relation(fields: [venueXid], references: [id], onDelete: Restrict)
venueXid Int? @map("venue_xid")
venue ActivityVenues? @relation(fields: [venueXid], references: [id], onDelete: Restrict)
locationLat Float? @map("location_lat")
locationLong Float? @map("location_long")
locationAddress Json? @map("location_address")
travelMode String? @map("travel_mode") @db.VarChar(30)
kmForNextPoint Float? @map("km_for_next_point")
timeForNextPointMins Int? @map("time_for_next_point_mins")
paxCount Int @map("pax_count")
totalAmount Int @map("total_amount")
paxCount Int? @map("pax_count")
totalAmount Int? @map("total_amount")
bookingStatus String @default("pending") @map("booking_status") @db.VarChar(30)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
@@ -1752,11 +1873,93 @@ model ItineraryActivities {
ActivitySOSDetails ActivitySOSDetails[]
ActivityFeedbacks ActivityFeedbacks[]
ItineraryDetails ItineraryDetails[]
itineraryActivitySelections ItineraryActivitySelection[]
@@map("itinerary_activities")
@@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 {
id Int @id @default(autoincrement())
itineraryActivityXid Int @map("itinerary_activity_xid")
@@ -1809,8 +2012,8 @@ model ItineraryDetails {
activityStatus String @map("activity_status") @db.VarChar(30)
isChargeable Boolean @default(false) @map("is_chargeable")
baseAmount Int @map("base_amount")
totalAmount Int @map("total_amount")
itineraryStatus String @map("itinerary_status") @db.VarChar(30)
totalAmount Int? @map("total_amount")
itineraryStatus String? @map("itinerary_status") @db.VarChar(30)
isPaid Boolean @default(false) @map("is_paid")
paidByXid Int? @map("paid_by_xid")
paidBy User? @relation(fields: [paidByXid], references: [id], onDelete: Restrict)

View File

@@ -7,7 +7,73 @@ const prisma = new PrismaClient({
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() {
await seedHostPermissionMasters();
// ✅ Countries
// const india = await prisma.countries.upsert({
// where: { countryName: 'India' },
@@ -268,9 +334,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' },
});
const furfam = await prisma.interests.upsert({
where: { interestName: 'Fur Fam' },
where: { interestName: 'Pet space' },
update: {},
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' },
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' },
});
const dogoodfeelgood = await prisma.interests.upsert({
where: { interestName: 'Do Good, Feel Good' },
@@ -693,16 +759,6 @@ async function main() {
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
await prisma.transportModes.createMany({
data: [

View File

@@ -1,132 +1,13 @@
service: minglar-host
useDotenv: true
params:
dev:
stage: dev
test:
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/**'
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/host.yml)}
- ${file(./serverless/functions/pqq.yml)}
plugins:
- serverless-offline

View File

@@ -0,0 +1,13 @@
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)}

12
serverless.operator.yml Normal file
View File

@@ -0,0 +1,12 @@
service: minglar-operator
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/operator.yml)}

View File

@@ -1,133 +1,13 @@
service: minglar-prepopulate
useDotenv: true
params:
dev:
stage: dev
test:
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/**'
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/prepopulate.yml)}
plugins:
- serverless-offline

View File

@@ -1,4 +1,4 @@
service: minglar-layers
service: minglar-prisma-layer
useDotenv: true
@@ -15,9 +15,13 @@ provider:
runtime: nodejs22.x
region: ap-south-1
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
# Define layers (deployed once; other stacks reference via cf output)
layers:
prisma:
path: layers/prisma
@@ -26,5 +30,3 @@ layers:
compatibleRuntimes:
- nodejs22.x
retain: false
plugins: []

View File

@@ -1,133 +1,13 @@
service: minglar-user
useDotenv: true
params:
dev:
stage: dev
test:
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/**'
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/user.yml)}
plugins:
- serverless-offline

View File

@@ -1,3 +1,4 @@
# Legacy monolith config. For new deployments use serverless.*.yml files.
service: minglar
@@ -16,6 +17,11 @@ provider:
runtime: nodejs22.x
region: ap-south-1
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
memorySize: 512
# Apply Prisma layer to all functions
@@ -23,11 +29,13 @@ provider:
layers:
# 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.
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn}
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn, ''}
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
websocketsApiName: minglar-ws-${sls:stage}
websocketsApiRouteSelectionExpression: $request.body.action
environment:
DATABASE_URL: ${env:DATABASE_URL}
@@ -59,6 +67,9 @@ provider:
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
HOST_LINK: ${env:HOST_LINK}
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:
role:
@@ -72,17 +83,18 @@ provider:
Resource:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
custom:
serverless-offline:
reloadHandler: true
- Effect: Allow
Action:
- execute-api:ManageConnections
Resource:
- 'arn:aws:execute-api:${self:provider.region}:*:*/*/@connections/*'
build:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node22
target: node20
platform: node
# Mark as external so they're not bundled into the JS
external:
@@ -125,7 +137,6 @@ layers:
package:
individually: true
excludeDevDependencies: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
@@ -144,10 +155,22 @@ package:
# Import function definitions from separate files organized by module
functions:
- ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/operator.yml)}
- ${file(./serverless/functions/minglaradmin.yml)}
- ${file(./serverless/functions/prepopulate.yml)}
- ${file(./serverless/functions/pqq.yml)}
- ${file(./serverless/functions/user.yml)}
- ${file(./serverless/functions/websocket.yml)}
plugins:
- 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

@@ -1,5 +1,3 @@
service: minglar-admin
useDotenv: true
params:
@@ -15,12 +13,17 @@ provider:
runtime: nodejs22.x
region: ap-south-1
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
memorySize: 512
# Apply Prisma layer to all functions
# Reference the layer defined in the dedicated layer stack
layers:
- ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
httpApi:
id: ${cf:minglar-host-${sls:stage}.HttpApiId}
- ${cf:minglar-prisma-layer-${sls:stage}.PrismaLambdaLayerQualifiedArn}
apiGateway:
binaryMediaTypes:
- '*/*'
@@ -54,8 +57,12 @@ provider:
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
AM_INTERFACE_LINK: ${env:AM_INTERFACE_LINK}
HOST_LINK: ${env:HOST_LINK}
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:
role:
@@ -70,17 +77,14 @@ provider:
- '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
target: node20
platform: node
# Mark as external so they're not bundled into the JS
external:
- '@prisma/client'
- '.prisma/client'
@@ -91,6 +95,7 @@ build:
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
# Exclude prevents npm install of these packages in the zip
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
@@ -110,7 +115,6 @@ build:
package:
individually: true
excludeDevDependencies: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
@@ -126,8 +130,9 @@ package:
- '!.git/**'
- '!.github/**'
functions:
- ${file(./serverless/functions/minglaradmin.yml)}
plugins:
- serverless-offline
custom:
serverless-offline:
reloadHandler: true

View File

@@ -14,7 +14,7 @@ getHosts:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host
path: /
method: get
verifyOTP:
@@ -30,7 +30,7 @@ verifyOTP:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/verify-otp
path: /Host_Admin/onboarding/verify-otp
method: post
login:
@@ -46,7 +46,7 @@ login:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/login
path: /Host_Admin/onboarding/login
method: post
signUp:
@@ -62,7 +62,7 @@ signUp:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/registration
path: /Host_Admin/onboarding/registration
method: post
createPassword:
@@ -78,7 +78,7 @@ createPassword:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/create-password
path: /Host_Admin/onboarding/create-password
method: post
updateBankDetails:
@@ -94,7 +94,7 @@ updateBankDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/add-payment-details
path: /Host_Admin/onboarding/add-payment-details
method: post
saveActivity_ForPQQ:
@@ -110,7 +110,7 @@ saveActivity_ForPQQ:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/add-activity
path: /Activity_Hub/OnBoarding/add-activity
method: post
getHostById:
@@ -126,7 +126,7 @@ getHostById:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/getById
path: /getById
method: get
getPQQ_ByQuestionId:
@@ -142,7 +142,7 @@ getPQQ_ByQuestionId:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-pqq-question-details
path: /Activity_Hub/OnBoarding/get-pqq-question-details
method: get
getPQQ_LastUpdatedQuestion:
@@ -158,7 +158,7 @@ getPQQ_LastUpdatedQuestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-latest-pqq-question-details
path: /Activity_Hub/OnBoarding/get-latest-pqq-question-details
method: get
prePopulateNewActivity:
@@ -174,7 +174,7 @@ prePopulateNewActivity:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/prepopulate-new-activity
path: /Activity_Hub/OnBoarding/prepopulate-new-activity
method: get
createNewActivity:
@@ -191,7 +191,7 @@ createNewActivity:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/create-new-activity
path: /Activity_Hub/OnBoarding/create-new-activity
method: patch
showSuggestion:
@@ -207,7 +207,7 @@ showSuggestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/get-suggestion
path: /get-suggestion
method: get
getAllActivitySuggestion:
@@ -223,7 +223,7 @@ getAllActivitySuggestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/get-Activity-suggestion
path: /get-Activity-suggestion
method: get
getAllHostActivity:
@@ -239,7 +239,7 @@ getAllHostActivity:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-all-host-activity
path: /Activity_Hub/OnBoarding/get-all-host-activity
method: get
acceptAggrement:
@@ -255,9 +255,25 @@ acceptAggrement:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/accept-agreement
path: /Host_Admin/onboarding/accept-agreement
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:
handler: src/modules/host/handlers/getStepper.handler
memorySize: 384
@@ -276,6 +292,118 @@ getStepperInfo:
path: /stepper
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
getMemberPermissions:
handler: src/modules/host/handlers/settings/getMemberPermissions.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-permissions/{memberUserXid}
method: get
# Functions with S3/AWS SDK dependencies
submitCompanyDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
@@ -288,7 +416,7 @@ submitCompanyDetails:
- 'src/common/**'
events:
- httpApi:
path: /host/Host_Admin/onboarding/add-company-details
path: /Host_Admin/onboarding/add-company-details
method: patch
submitPQQ_Answer:
@@ -304,7 +432,7 @@ submitPQQ_Answer:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-pqq-answer
path: /Activity_Hub/OnBoarding/submit-pqq-answer
method: patch
updatePQQ_LastAnswer:
@@ -320,7 +448,7 @@ updatePQQ_LastAnswer:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer
path: /Activity_Hub/OnBoarding/submit-final-pqq-answer
method: post
submitPQQForReview:
@@ -336,7 +464,7 @@ submitPQQForReview:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-pqq-for-review
path: /Activity_Hub/OnBoarding/submit-pqq-for-review
method: patch
getAllPQQwithSubmittedAns:
@@ -351,7 +479,7 @@ getAllPQQwithSubmittedAns:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-all-pqq-ques-submited-ans
path: /Activity_Hub/OnBoarding/get-all-pqq-ques-submited-ans
method: get
getAllDetailsOfActivityAndVenue:
@@ -366,7 +494,7 @@ getAllDetailsOfActivityAndVenue:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-all-details-activity-venue/{activityXid}
path: /Activity_Hub/OnBoarding/get-all-details-activity-venue/{activityXid}
method: get
updateSuggestionAsReviewed:
@@ -381,7 +509,7 @@ updateSuggestionAsReviewed:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/update-suggestion-reviewed
path: /Activity_Hub/OnBoarding/update-suggestion-reviewed
method: patch
resendOTPmail:
@@ -399,7 +527,6 @@ resendOTPmail:
path: /resend-otp
method: post
mediaUploadTos3:
handler: src/modules/host/handlers/mediaUploadToS3.handler
memorySize: 512
@@ -415,7 +542,6 @@ mediaUploadTos3:
path: /media/upload/activity/{activityXid}
method: post
venueMediaUploadTos3:
handler: src/modules/host/handlers/mediaUploadForVenueToS3.handler
memorySize: 512
@@ -431,7 +557,6 @@ venueMediaUploadTos3:
path: /media/upload/venue/activity/{activityXid}
method: post
mediaDeleteFroms3:
handler: src/modules/host/handlers/mediaDeleteFromS3.handler
memorySize: 512
@@ -523,3 +648,33 @@ openCanceledSlotForActivity:
- httpApi:
path: /scheduling/open-canceled-slot
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}
events:
- httpApi:
path: /minglaradmin/registration
path: /registration
method: post
minglarLoginForAdmin:
@@ -28,7 +28,7 @@ minglarLoginForAdmin:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/login
path: /login
method: post
minglarCreatePassword:
@@ -43,7 +43,7 @@ minglarCreatePassword:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/create-password
path: /create-password
method: post
updateMinglarProfile:
@@ -60,7 +60,7 @@ updateMinglarProfile:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/update-profile
path: /update-profile
method: patch
prepopulateRole:
@@ -75,7 +75,7 @@ prepopulateRole:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/prepopulate-Roles
path: /prepopulate-Roles
method: get
getHostDetailsById:
@@ -90,7 +90,7 @@ getHostDetailsById:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/get-host-details/{host_xid}
path: /hosthub/hosts/get-host-details/{host_xid}
method: get
inviteTeammate:
@@ -105,7 +105,7 @@ inviteTeammate:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/invite-teammate
path: /settings/teammates/invite-teammate
method: post
getAllHostApplication:
@@ -121,7 +121,7 @@ getAllHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/get-all-host-applications-am
path: /hosthub/hosts/get-all-host-applications-am
method: get
getAllHostActivityForAdmin:
@@ -137,7 +137,7 @@ getAllHostActivityForAdmin:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/get-all-activity-of-host/{id}
path: /get-all-activity-of-host/{id}
method: get
getAllOnboardingHostApplications:
@@ -153,7 +153,7 @@ getAllOnboardingHostApplications:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin
path: /hosthub/onboarding/get-all-host-applications-admin
method: get
getAllOnboardingHostApplications_New:
@@ -169,7 +169,7 @@ getAllOnboardingHostApplications_New:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin-new
path: /hosthub/onboarding/get-all-host-applications-admin-new
method: get
getAllInvitationDetails:
@@ -184,7 +184,7 @@ getAllInvitationDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-invitation-details
path: /settings/teammates/get-all-invitation-details
method: get
addSuggestion:
@@ -200,7 +200,7 @@ addSuggestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-suggestion
path: /hosthub/hosts/add-suggestion
method: post
getAllCoadminAndAMDetails:
@@ -215,7 +215,7 @@ getAllCoadminAndAMDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-coadmin-am
path: /settings/teammates/get-all-coadmin-am
method: get
getAllInvitedCoadminAndAMDetails:
@@ -230,7 +230,7 @@ getAllInvitedCoadminAndAMDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-invited-coadmin-am
path: /settings/teammates/get-all-invited-coadmin-am
method: get
getAmDetailsbyId:
@@ -245,7 +245,7 @@ getAmDetailsbyId:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-am-details-by-id/{amXid}
path: /settings/teammates/get-am-details-by-id/{amXid}
method: get
assignAMToHost:
@@ -261,7 +261,7 @@ assignAMToHost:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/assign-am
path: /hosthub/onboarding/assign-am
method: patch
editAgreementDetailsAndAccept:
@@ -277,7 +277,7 @@ editAgreementDetailsAndAccept:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/edit-agreement-accept-host
path: /hosthub/onboarding/edit-agreement-accept-host
method: patch
getAllPqqQuesAnsForAM:
@@ -292,7 +292,7 @@ getAllPqqQuesAnsForAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-pqq-ques-ans-for-am
path: /hosthub/onboarding/get-all-pqq-ques-ans-for-am
method: get
acceptHostApplication:
@@ -308,7 +308,7 @@ acceptHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/accept-host-application
path: /hosthub/hosts/accept-host-application
method: patch
RejectPQQByAM:
@@ -324,7 +324,7 @@ RejectPQQByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-pq-by-am
path: /hosthub/hosts/reject-pq-by-am
method: patch
rejectActivityDetailsApplicationByAM:
@@ -340,7 +340,7 @@ rejectActivityDetailsApplicationByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-activity-application-by-am
path: /hosthub/hosts/reject-activity-application-by-am
method: patch
acceptPQByAM:
@@ -356,7 +356,7 @@ acceptPQByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/accept-pq-by-am
path: /hosthub/hosts/accept-pq-by-am
method: patch
acceptActivityDetailsApplicationByAM:
@@ -372,7 +372,7 @@ acceptActivityDetailsApplicationByAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/accept-activity-application-by-am
path: /hosthub/hosts/accept-activity-application-by-am
method: patch
rejectHostApplication:
@@ -388,7 +388,7 @@ rejectHostApplication:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/reject-host-application
path: /hosthub/onboarding/reject-host-application
method: patch
rejectHostApplicationAM:
@@ -404,7 +404,7 @@ rejectHostApplicationAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-host-application-am
path: /hosthub/hosts/reject-host-application-am
method: patch
addPQQSuggestion:
@@ -420,7 +420,7 @@ addPQQSuggestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-Pqq-suggestion
path: /hosthub/hosts/add-Pqq-suggestion
method: post
addActivitySuggestion:
@@ -436,7 +436,7 @@ addActivitySuggestion:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-Activity-suggestion
path: /hosthub/hosts/add-Activity-suggestion
method: post
getAllPQPDetailsForAM:
@@ -452,7 +452,7 @@ getAllPQPDetailsForAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/pqp/pqp-details-for-am/{activityXid}
path: /hosthub/pqp/pqp-details-for-am/{activityXid}
method: get
getSuggestionsForAM:
@@ -468,5 +468,5 @@ getSuggestionsForAM:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/show-suggestion-to-am/{hostXid}
path: /hosthub/onboarding/show-suggestion-to-am/{hostXid}
method: get

View File

@@ -0,0 +1,198 @@
# Operator Module Functions
operatorSignUp:
handler: src/modules/host/handlers/operator/signUp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${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: /signup
method: post
operatorVerifyOtp:
handler: src/modules/host/handlers/operator/verifyOtp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${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: /verify-otp
method: post
operatorCreatePassword:
handler: src/modules/host/handlers/operator/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${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: /create-password
method: post
operatorLogin:
handler: src/modules/host/handlers/operator/login.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${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: /login
method: post
operatorVerifyPassword:
handler: src/modules/host/handlers/operator/verifyPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/common/**'
- ${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: /verify-password
method: post
operatorGetActivitiesByDate:
handler: src/modules/host/handlers/operator/getActivitiesByDate.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorActivity.service.ts'
- 'src/modules/host/dto/operator.activity.dto.ts'
- 'src/common/**'
- ${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-by-date
method: get
operatorGetReservationByCheckInCode:
handler: src/modules/host/handlers/operator/getReservationByCheckInCode.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorActivity.service.ts'
- 'src/modules/host/dto/operator.activity.dto.ts'
- 'src/common/**'
- ${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: /reservation-by-checkin-code
method: get
operatorSendOtpCheckIn:
handler: src/modules/host/handlers/operator/sendOtpCheckIn.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorActivity.service.ts'
- 'src/modules/host/dto/operator.activity.dto.ts'
- 'src/common/**'
- ${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: /send-otp-checkin
method: post
operatorSendOtpCheckout:
handler: src/modules/host/handlers/operator/sendOtpCheckout.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorActivity.service.ts'
- 'src/modules/host/dto/operator.activity.dto.ts'
- 'src/common/**'
- ${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: /send-otp-checkout
method: post
operatorVerifyOtpCheckIn:
handler: src/modules/host/handlers/operator/verifyOtpCheckIn.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorActivity.service.ts'
- 'src/modules/host/dto/operator.activity.dto.ts'
- 'src/common/**'
- ${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: /verify-otp-checkin
method: post
operatorVerifyOtpCheckout:
handler: src/modules/host/handlers/operator/verifyOtpCheckout.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorActivity.service.ts'
- 'src/modules/host/dto/operator.activity.dto.ts'
- 'src/common/**'
- ${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: /verify-otp-checkout
method: post

View File

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

View File

@@ -13,7 +13,7 @@ registerUser:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/register
path: /register
method: post
submitPersonalInfo:
@@ -28,7 +28,7 @@ submitPersonalInfo:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/submit-personal-info
path: /submit-personal-info
method: post
verifyOtpForUser:
@@ -43,7 +43,7 @@ verifyOtpForUser:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/verify-otp
path: /verify-otp
method: post
generateAccessFromRefreshToken:
@@ -58,7 +58,7 @@ generateAccessFromRefreshToken:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/generate-access-from-refresh
path: /generate-access-from-refresh
method: post
@@ -74,7 +74,7 @@ setPasscodeForMobile:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/set-passcode
path: /set-passcode
method: post
@@ -90,7 +90,7 @@ verifyPasscode:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/verify-passcode
path: /verify-passcode
method: post
setUserInterest:
@@ -105,7 +105,7 @@ setUserInterest:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/set-interests
path: /set-interests
method: post
setUserLocationss:
@@ -120,7 +120,7 @@ setUserLocationss:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/set-location-user
path: /set-location-user
method: post
getLandingPageDetails:
@@ -135,7 +135,7 @@ getLandingPageDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/get-landing-page-details
path: /activities/get-landing-page-details
method: get
getSurpriseMePageDetails:
@@ -150,7 +150,7 @@ getSurpriseMePageDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/get-surprise-me-page-details
path: /activities/get-surprise-me-page-details
method: get
getActivityDetailsById:
@@ -165,7 +165,7 @@ getActivityDetailsById:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/get-activity-details-by-id/{activity_xid}
path: /activities/get-activity-details-by-id/{activity_xid}
method: get
checkAvailabilityDetails:
@@ -180,7 +180,7 @@ checkAvailabilityDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/check-availability/{activity_xid}
path: /activities/check-availability/{activity_xid}
method: get
searchActivities:
@@ -195,7 +195,7 @@ searchActivities:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/specific-search
path: /activities/specific-search
method: get
searchSchoolsAndCompanies:
@@ -210,7 +210,7 @@ searchSchoolsAndCompanies:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/connections/search-schools-companies
path: /connections/search-schools-companies
method: get
searchCities:
@@ -225,7 +225,7 @@ searchCities:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/connections/search-cities
path: /connections/search-cities
method: get
addSchoolCompanyDetail:
@@ -240,7 +240,7 @@ addSchoolCompanyDetail:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/connections/add-school-company
path: /connections/add-school-company
method: post
removeConnectionDetails:
@@ -255,7 +255,7 @@ removeConnectionDetails:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/connections/remove-connection-details
path: /connections/remove-connection-details
method: delete
getAllConnectionOfUser:
@@ -270,7 +270,7 @@ getAllConnectionOfUser:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/connections/get-all-connections-details
path: /connections/get-all-connections-details
method: get
getActivityFromConnectionsInterest:
@@ -285,7 +285,22 @@ getActivityFromConnectionsInterest:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/connections/get-activity-from-connections-interest
path: /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
viewMoreActivitiesByInterest:
@@ -300,7 +315,7 @@ viewMoreActivitiesByInterest:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/view-more-activities
path: /activities/view-more-activities
method: get
viewMoreActivitiesUpperSection:
@@ -315,7 +330,7 @@ viewMoreActivitiesUpperSection:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/view-more-activities-upper-section
path: /activities/view-more-activities-upper-section
method: get
getRandomActiveActivity:
@@ -330,7 +345,7 @@ getRandomActiveActivity:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/get-random-active-activity
path: /activities/get-random-active-activity
method: get
getNearbyActivities:
@@ -345,5 +360,230 @@ getNearbyActivities:
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/get-nearby-activities
path: /activities/get-nearby-activities
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: 1024
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
afterBookingFromCalendar:
handler: src/modules/user/handlers/itinerary/afterBookingFromCalendar.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/after-booking-from-calendar
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

@@ -0,0 +1,64 @@
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

@@ -0,0 +1,78 @@
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,10 +1,10 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '@/common/utils/constants/common.constant';
import { NextFunction, Request, Response } from 'express';
import httpStatus from 'http-status';
import jwt from 'jsonwebtoken';
import config from '../../../config/config';
import { prisma } from '../../database/prisma.client';
import ApiError from '../../utils/helper/ApiError';
interface DecodedToken {
id?: number;
@@ -29,6 +29,68 @@ declare module 'express-serve-static-core' {
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
/**
* Verifies JWT and validates Operator user (role_xid = 5)
*/
export async function verifyOperatorToken(token: string): Promise<{ id: 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');
}
// ✅ Fetch user from Prisma (Operator user only)
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');
}
// ✅ Check if user is active
if (user.isActive === false) {
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
}
// ✅ Check Operator role (role_xid = 5)
if (user.roleXid !== ROLE.OPERATOR) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
return { id: user.id, 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.');
}
}
export async function verifyHostToken(token: string): Promise<{ id: number; role?: string }> {
if (!token) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');

View File

@@ -0,0 +1,112 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { NextFunction, Request, Response } from 'express';
import { prisma } from '../../database/prisma.client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '../../utils/constants/common.constant';
interface DecodedToken {
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
}
interface UserPayload {
id: string;
role?: string;
}
declare module 'express-serve-static-core' {
interface Request {
user?: UserPayload;
}
}
export async function verifyOperatorToken(
token: string,
): Promise<{ id: 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.OPERATOR) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
return { id: user.id, 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.');
}
}
const verifyCallback = async (
req: Request,
resolve: (value?: unknown) => void,
reject: (reason?: Error) => void,
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
try {
const userInfo = await verifyOperatorToken(token);
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
return reject(error as Error);
}
};
const authForOperator =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForOperator;

View File

@@ -0,0 +1,150 @@
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

@@ -0,0 +1,58 @@
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_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_DRAFT: 'Draft',
ACTIVITY_IN_REVIEW: 'In Review',
ACTIVITY_TO_REVIEW: 'Re-submitted',
NOT_LISTED: 'Not Listed',
@@ -94,7 +94,7 @@ export const ACTIVITY_AM_DISPLAY_STATUS = {
PQ_APPROVED: 'PQ Approved',
REVISED: 'Revised',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_DRAFT: 'Draft',
ACTIVITY_NEW: 'New',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_ENHANCING: 'Enhancing',

View File

@@ -12,7 +12,7 @@ export interface OtpResult {
export async function resendOtpHelper(
prisma: any,
userId: number,
emailPurpose: "Register" | "Login" | "ForgotPassword",
emailPurpose: string,
otpLength: 4 | 6 = 4,
expiryMinutes: number = 5
): Promise<OtpResult> {

View File

@@ -16,7 +16,7 @@ export async function generateOtpHelper(
prisma: any, // ⭐ Inject prisma
userId: number,
email: string,
emailPurpose: "Register" | "Login" | "ForgotPassword",
emailPurpose: string,
otpLength: 4 | 6 = 4,
expiryMinutes: number = 5
): Promise<OtpResult> {

View File

@@ -46,6 +46,18 @@ export const parentCompanySchema = z.object({
companyTypeXid: z.number()
.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(),
instagramUrl: z.string().nullable().optional(),
facebookUrl: z.string().nullable().optional(),

View File

@@ -83,8 +83,12 @@ const envVarsSchema = yup
BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'),
// Email links
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_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);
@@ -165,6 +169,11 @@ function getConfig() {
AM_INVITATION_LINK: envVars.AM_INVITATION_LINK,
HOST_LINK: envVars.HOST_LINK,
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: {
// appID: envVars.ONESIGNAL_APPID,
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,

View File

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

View File

@@ -0,0 +1,113 @@
export class GetActivitiesByDateRequestDTO {
activityDate?: string; // ISO date format: YYYY-MM-DD (optional, defaults to today)
}
export class GetReservationByCheckInCodeRequestDTO {
checkInCode!: string;
}
export class OperatorReservationVerificationOtpRequestDTO {
checkInCode!: string;
}
export class OperatorReservationVerifyOtpRequestDTO {
checkInCode!: string;
otp!: string;
}
export class DateBreakdownDTO {
date: string;
count: number;
}
export class ActivitySummaryDTO {
activityName: string;
activityImage: string | null;
activityImagePreSignedUrl: string | null;
count: number;
dateBreakdown: DateBreakdownDTO[]; // Booking count for each scheduled date
}
export class GetActivitiesByDateResponseDTO {
success: boolean;
message: string;
data: {
date: string;
activities: ActivitySummaryDTO[];
totalCount: number;
};
}
export class OperatorReservationPersonalDetailsDTO {
fullName: string;
firstName: string | null;
lastName: string | null;
role: string | null;
mobileNumber: string | null;
profileImage: string | null;
profileImagePreSignedUrl: string | null;
tags: string[];
}
export class OperatorReservationBookingInformationDTO {
activityName: string | null;
slot: string | null;
startTime: string | null;
endTime: string | null;
track: string | null;
trackLabel: string | null;
date: string | null;
dateLabel: string | null;
bookedOn: string | null;
bookedOnLabel: string | null;
}
export class OperatorReservationBookingIncludedDTO {
food: string;
selectedFoodTypes: string[];
equipment: string;
selectedEquipments: string[];
trainerOrGuide: string;
pickupLocation: string | null;
}
export class OperatorReservationByCheckInCodeDTO {
itineraryHeaderXid: number;
itineraryActivityXid: number;
bookingId: string | null;
checkInCode: string | null;
reservationStatus: string | null;
personalDetails: OperatorReservationPersonalDetailsDTO;
bookingInformation: OperatorReservationBookingInformationDTO;
bookingIncluded: OperatorReservationBookingIncludedDTO;
}
export class GetReservationByCheckInCodeResponseDTO {
success: boolean;
message: string;
data: OperatorReservationByCheckInCodeDTO;
}
export class OperatorReservationVerificationOtpResponseDTO {
itineraryActivityXid: number;
itineraryMemberXid: number;
activityName: string | null;
checkInCode: string;
verificationType: 'check-in' | 'checkout';
requestedChannel: 'email' | 'mobile';
deliveryChannel: 'email';
destination: string;
reservationStatus: string | null;
expiresInMinutes: number;
deliveryNote: string | null;
}
export class OperatorReservationVerifyOtpResponseDTO {
itineraryActivityXid: number;
itineraryMemberXid: number;
activityName: string | null;
checkInCode: string;
verificationType: 'check-in' | 'checkout';
verified: boolean;
reservationStatus: string;
}

View File

@@ -8,6 +8,7 @@ import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHo
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { sendPQPEmailToAM } from '../../../services/sendHostResubmitEmailToAM.service';
const hostService = new HostService(prismaClient);
@@ -177,6 +178,15 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
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
return {

View File

@@ -13,6 +13,40 @@ const hostService = new HostService(prismaClient);
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 getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
@@ -122,22 +156,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
bb.on("field", (fieldname, val) => {
console.log(`FIELD RAW: ${fieldname} =`, 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;
}
}
fields[fieldname] = parseMultipartFieldValue(val);
});
bb.on("close", () => resolve());
@@ -154,7 +173,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = fields.comments || null;
const comments = normalizeComments(fields.comments);
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");

View File

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

View File

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

View File

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

View File

@@ -142,6 +142,10 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const deletedFiles = normalizeJsonField(fields, "deletedFiles") || [];
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 */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
@@ -172,14 +176,46 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
if (fields.userProfile) {
const userProfileRaw = normalizeJsonField(fields, 'userProfile');
if (userProfileRaw) {
const { firstName, lastName, mobileNumber } = userProfileRaw;
const firstName =
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({
where: { id: userInfo.id },
data: {
...(firstName && { firstName }),
...(lastName && { lastName }),
...(mobileNumber && { mobileNumber }),
...(firstName !== undefined && { firstName }),
...(lastName !== undefined && { lastName }),
...(mobileNumber !== undefined && { mobileNumber }),
},
});
}
@@ -379,6 +415,63 @@ 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) */
const logoFile = files.find(
(f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
@@ -449,6 +542,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
parsedParentCompany,
uploadedParentDocs,
isDraft,
{ deleteCompanyLogo, deleteParentCompanyLogo },
);
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');

View File

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

View File

@@ -0,0 +1,58 @@
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 { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForOperator';
import { OperatorAuthService } from '../../services/operatorAuth.service';
const operatorAuthService = new OperatorAuthService(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 verifyOperatorToken(token);
let body: { password?: string; confirmPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { password, confirmPassword } = body;
if (!password || !confirmPassword) {
throw new ApiError(400, 'Password and confirm password are required');
}
if (password !== confirmPassword) {
throw new ApiError(400, 'Password and confirm password do not match');
}
if (password.length < 8) {
throw new ApiError(400, 'Password must be at least 8 characters long');
}
await operatorAuthService.createOperatorPassword(userInfo.id, password);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Password created successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,59 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { GetActivitiesByDateRequestDTO } from '../../dto/operator.activity.dto';
import { OperatorActivityService } from '../../services/operatorActivity.service';
const operatorActivityService = new OperatorActivityService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
try {
// 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 operator info
const operatorInfo = await verifyOperatorToken(token);
const operatorId = Number(operatorInfo.id);
if (!operatorId || isNaN(operatorId)) {
throw new ApiError(400, 'Invalid operator ID');
}
// Get activityDate from query parameters
const { activityDate } = event.queryStringParameters || {};
const requestDTO: GetActivitiesByDateRequestDTO = {
activityDate: activityDate?.trim(),
};
// Fetch activities by date and operator
const result = await operatorActivityService.getActivitiesByDate(
operatorId,
requestDTO.activityDate,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Activities fetched successfully',
data: result,
}),
};
} catch (error) {
// Error will be handled by safeHandler
throw error;
}
});

View File

@@ -0,0 +1,65 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { GetReservationByCheckInCodeRequestDTO } from '../../dto/operator.activity.dto';
import { OperatorActivityService } from '../../services/operatorActivity.service';
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
const operatorId = Number(operatorInfo.id);
if (!operatorId || Number.isNaN(operatorId)) {
throw new ApiError(400, 'Invalid operator ID');
}
const requestDTO: GetReservationByCheckInCodeRequestDTO = {
checkInCode:
event.queryStringParameters?.checkInCode?.trim() ||
event.queryStringParameters?.offlineCode?.trim() ||
'',
};
if (!requestDTO.checkInCode) {
throw new ApiError(400, 'checkInCode is required.');
}
const result = await operatorActivityService.getReservationByCheckInCode(
operatorId,
requestDTO.checkInCode,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Reservation details fetched successfully',
data: result,
}),
};
},
);

View File

@@ -0,0 +1,55 @@
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 { GetHostLoginResponseDTO } from '../../dto/host.dto';
import { OperatorAuthService } from '../../services/operatorAuth.service';
import { TokenService } from '../../services/token.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: { emailAddress?: string; userPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, userPassword } = body;
if (!emailAddress || !userPassword) {
throw new ApiError(400, 'Email and password are required');
}
const operator = await operatorAuthService.loginForOperator(
emailAddress.trim().toLowerCase(),
userPassword,
);
const generatedToken = await tokenService.generateAuthToken(operator.id);
const response = new GetHostLoginResponseDTO(
operator,
generatedToken.access.token,
generatedToken.refresh.token,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Login successful',
data: response,
}),
};
});

View File

@@ -0,0 +1,70 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { OperatorReservationVerificationOtpRequestDTO } from '../../dto/operator.activity.dto';
import { OperatorActivityService } from '../../services/operatorActivity.service';
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
const operatorId = Number(operatorInfo.id);
if (!operatorId || Number.isNaN(operatorId)) {
throw new ApiError(400, 'Invalid operator ID');
}
let body: OperatorReservationVerificationOtpRequestDTO;
try {
body = event.body ? JSON.parse(event.body) : { checkInCode: '' };
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const requestDTO: OperatorReservationVerificationOtpRequestDTO = {
checkInCode: body?.checkInCode?.trim() || '',
};
if (!requestDTO.checkInCode) {
throw new ApiError(400, 'checkInCode is required.');
}
const result = await operatorActivityService.sendOtpCheckIn(
operatorId,
requestDTO.checkInCode,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Check-in OTP sent successfully',
data: result,
}),
};
},
);

View File

@@ -0,0 +1,70 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { OperatorReservationVerificationOtpRequestDTO } from '../../dto/operator.activity.dto';
import { OperatorActivityService } from '../../services/operatorActivity.service';
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
const operatorId = Number(operatorInfo.id);
if (!operatorId || Number.isNaN(operatorId)) {
throw new ApiError(400, 'Invalid operator ID');
}
let body: OperatorReservationVerificationOtpRequestDTO;
try {
body = event.body ? JSON.parse(event.body) : { checkInCode: '' };
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const requestDTO: OperatorReservationVerificationOtpRequestDTO = {
checkInCode: body?.checkInCode?.trim() || '',
};
if (!requestDTO.checkInCode) {
throw new ApiError(400, 'checkInCode is required.');
}
const result = await operatorActivityService.sendOtpCheckout(
operatorId,
requestDTO.checkInCode,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Checkout OTP sent successfully',
data: result,
}),
};
},
);

View File

@@ -0,0 +1,53 @@
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 { OperatorAuthService } from '../../services/operatorAuth.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: {
firstName?: string;
lastName?: string;
emailAddress?: string;
isdCode?: string;
mobileNumber?: string;
};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { firstName, lastName, emailAddress, isdCode, mobileNumber } = body;
if (!emailAddress && !mobileNumber) {
throw new ApiError(400, 'Email address or mobile number is required');
}
const signupResponse = await operatorAuthService.signUpOperator({
firstName,
lastName,
emailAddress,
isdCode,
mobileNumber,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP sent successfully',
data: signupResponse,
}),
};
});

View File

@@ -0,0 +1,51 @@
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 { OperatorAuthService } from '../../services/operatorAuth.service';
import { TokenService } from '../../services/token.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: { emailAddress?: string; mobileNumber?: string; otp?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, mobileNumber, otp } = body;
if ((!emailAddress && !mobileNumber) || !otp) {
throw new ApiError(400, 'Email address or mobile number and OTP are required');
}
const operator = await operatorAuthService.verifyOperatorOtp({
emailAddress,
mobileNumber,
otp,
});
const generatedToken = await tokenService.generateAuthToken(operator.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP verified successfully',
accessToken: generatedToken.access.token,
refreshToken: generatedToken.refresh.token,
data: null,
}),
};
});

View File

@@ -0,0 +1,72 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { OperatorReservationVerifyOtpRequestDTO } from '../../dto/operator.activity.dto';
import { OperatorActivityService } from '../../services/operatorActivity.service';
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
const operatorId = Number(operatorInfo.id);
if (!operatorId || Number.isNaN(operatorId)) {
throw new ApiError(400, 'Invalid operator ID');
}
let body: OperatorReservationVerifyOtpRequestDTO;
try {
body = event.body ? JSON.parse(event.body) : { checkInCode: '', otp: '' };
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const requestDTO: OperatorReservationVerifyOtpRequestDTO = {
checkInCode: body?.checkInCode?.trim() || '',
otp: body?.otp?.trim() || '',
};
if (!requestDTO.checkInCode || !requestDTO.otp) {
throw new ApiError(400, 'checkInCode and otp are required.');
}
const result = await operatorActivityService.verifyOtpCheckIn(
operatorId,
requestDTO.checkInCode,
requestDTO.otp,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Check-in OTP verified successfully',
data: result,
}),
};
},
);

View File

@@ -0,0 +1,72 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { OperatorReservationVerifyOtpRequestDTO } from '../../dto/operator.activity.dto';
import { OperatorActivityService } from '../../services/operatorActivity.service';
const operatorActivityService = new OperatorActivityService(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 operatorInfo = await verifyOperatorToken(token);
const operatorId = Number(operatorInfo.id);
if (!operatorId || Number.isNaN(operatorId)) {
throw new ApiError(400, 'Invalid operator ID');
}
let body: OperatorReservationVerifyOtpRequestDTO;
try {
body = event.body ? JSON.parse(event.body) : { checkInCode: '', otp: '' };
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const requestDTO: OperatorReservationVerifyOtpRequestDTO = {
checkInCode: body?.checkInCode?.trim() || '',
otp: body?.otp?.trim() || '',
};
if (!requestDTO.checkInCode || !requestDTO.otp) {
throw new ApiError(400, 'checkInCode and otp are required.');
}
const result = await operatorActivityService.verifyOtpCheckout(
operatorId,
requestDTO.checkInCode,
requestDTO.otp,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Checkout OTP verified successfully',
data: result,
}),
};
},
);

View File

@@ -0,0 +1,44 @@
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 { OperatorAuthService } from '../../services/operatorAuth.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: { emailAddress?: string; userPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, userPassword } = body;
if (!emailAddress || !userPassword) {
throw new ApiError(400, 'Email and password are required');
}
const isPasswordValid = await operatorAuthService.verifyPasswordForOperator(
emailAddress.trim().toLowerCase(),
userPassword,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: isPasswordValid ? 'Password is valid' : 'Password is invalid',
data: { isValid: isPasswordValid },
}),
};
});

View File

@@ -0,0 +1,61 @@
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

@@ -0,0 +1,58 @@
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);
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 memberUserXid = event.pathParameters?.memberUserXid;
if (!memberUserXid) {
throw new ApiError(400, 'memberUserXid is required.');
}
const memberUserId = parseInt(memberUserXid, 10);
if (isNaN(memberUserId)) {
throw new ApiError(400, 'Invalid memberUserXid. Must be a number.');
}
const result = await hostRolePermissionService.getMemberPermissions({
hostUserXid: userInfo.id,
memberUserXid: memberUserId,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Member permissions retrieved successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,59 @@
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

@@ -0,0 +1,60 @@
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

@@ -0,0 +1,111 @@
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

@@ -0,0 +1,81 @@
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

@@ -0,0 +1,344 @@
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,6 +362,9 @@ export class SchedulingService {
isActive: true,
startDate: { lte: date },
OR: [{ endDate: null }, { endDate: { gte: date } }],
ScheduleDetails: {
some: {}
}
},
include: {
activityVenue: {

View File

@@ -177,8 +177,8 @@ function computeBasePriceAndTaxes(
return { basePrice, taxDetails };
}
const normalize = (v?: string | null) =>
v ? v.trim().toLowerCase() : null;
const normalize = (v?: string | null, maxLength: number = 50) =>
v ? v.trim().toLowerCase().substring(0, maxLength) : null;
async function renderAgreementPdf(vars: {
effectiveDate: string;
@@ -338,9 +338,11 @@ const findOrCreateState = async (
) => {
if (!stateName || !countryXid) return null;
const trimmedStateName = stateName.trim().substring(0, 50);
const state = await tx.states.findFirst({
where: {
stateName: { equals: stateName.trim(), mode: 'insensitive' },
stateName: { equals: trimmedStateName, mode: 'insensitive' },
countryXid,
isActive: true,
},
@@ -350,7 +352,7 @@ const findOrCreateState = async (
const created = await tx.states.create({
data: {
stateName: stateName.trim(),
stateName: trimmedStateName,
countryXid,
},
});
@@ -365,9 +367,11 @@ const findOrCreateCity = async (
) => {
if (!cityName || !stateXid) return null;
const trimmedCityName = cityName.trim().substring(0, 50);
const city = await tx.cities.findFirst({
where: {
cityName: { equals: cityName.trim(), mode: 'insensitive' },
cityName: { equals: trimmedCityName, mode: 'insensitive' },
stateXid,
isActive: true,
},
@@ -377,7 +381,7 @@ const findOrCreateCity = async (
const created = await tx.cities.create({
data: {
cityName: cityName.trim(),
cityName: trimmedCityName,
stateXid,
},
});
@@ -391,10 +395,74 @@ const s3 = new AWS.S3({
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()
export class HostService {
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) {
return this.prisma.user.create({ data });
}
@@ -415,8 +483,8 @@ export class HostService {
});
const user = await this.prisma.user.findUnique({
where: { id: user_xid },
select: { id: true, emailAddress: true },
where: { id: user_xid, isActive: true },
select: { id: true, emailAddress: true, userRefNumber: true },
});
return { host, user };
}
@@ -424,9 +492,86 @@ export class HostService {
async getHostById(id: number) {
const host = await this.prisma.hostHeader.findFirst({
where: { userXid: id },
include: {
select: {
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: {
include: {
select: {
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: {
select: {
id: true,
@@ -459,12 +604,46 @@ export class HostService {
select: {
id: true,
emailAddress: true,
dateOfBirth: true,
firstName: true,
lastName: true,
mobileNumber: true,
profileImage: true,
userStatus: 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: {
@@ -522,11 +701,15 @@ export class HostService {
}
if (host?.logoPath) {
const key = host.logoPath.startsWith('http')
? host.logoPath.split('.com/')[1]
: host.logoPath;
host.logoPath = await getPresignedUrl(bucket, key);
const resolvedLogoUrl = await this.getValidLogoUrl(
'hostHeader',
host.id,
host.logoPath,
);
if (!resolvedLogoUrl) {
host.logoPath = null;
}
(host as any).logoPresignedUrl = resolvedLogoUrl;
}
if (host.accountManager?.profileImage) {
@@ -542,11 +725,15 @@ export class HostService {
// Parent company logo
if (parent.logoPath) {
const key = parent.logoPath.startsWith('http')
? parent.logoPath.split('.com/')[1]
: parent.logoPath;
parent.logoPath = await getPresignedUrl(bucket, key);
const resolvedParentLogoUrl = await this.getValidLogoUrl(
'hostParent',
parent.id,
parent.logoPath,
);
if (!resolvedParentLogoUrl) {
parent.logoPath = null;
}
(parent as any).logoPresignedUrl = resolvedParentLogoUrl;
}
// Parent documents
@@ -577,6 +764,114 @@ export class HostService {
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> {
return this.prisma.user.findUnique({ where: { emailAddress: email } });
}
@@ -676,10 +971,10 @@ export class HostService {
return newUser;
}
async createPassword(user_xid: number, password: string): Promise<boolean> {
async createPassword(user_xid: number, password: string): Promise<Partial<User>> {
// Find user by id
const user = await this.prisma.user.findUnique({
where: { id: user_xid },
where: { id: user_xid, isActive: true },
select: { id: true, emailAddress: true, userPassword: true },
});
@@ -709,7 +1004,7 @@ export class HostService {
},
});
return true;
return user;
}
async getBankBranchById(bankBranchXid: number) {
@@ -847,6 +1142,23 @@ 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,
take: paginationOptions?.limit || 10,
@@ -873,11 +1185,31 @@ 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 {
paginationService,
} = require('@/common/utils/pagination/pagination.service');
return paginationService.createPaginatedResponse(
hostAllActivities,
hostActivitiesWithAssets,
totalCount,
paginationOptions || { page: 1, limit: 10, skip: 0 },
);
@@ -919,6 +1251,9 @@ export class HostService {
acceptDate,
};
let pdfUrl: string | null = null;
try {
const pdfBuffer = await renderAgreementPdf(agreementVars);
const existingCount = await this.prisma.hostAgreement.count({
@@ -940,7 +1275,18 @@ export class HostService {
})
.promise();
const pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`;
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
@@ -968,6 +1314,87 @@ export class HostService {
},
});
});
} 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({
where: { hostXid, isActive: true },
orderBy: { createdAt: 'desc' },
select: {
id: true,
hostXid: true,
filePath: true,
versionNumber: true,
createdAt: true,
updatedAt: true,
},
});
// ❌ 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) {
@@ -1095,6 +1522,10 @@ export class HostService {
parentCompanyData?: any | null,
parentDocuments?: HostDocumentInput[],
isDraft: boolean = false,
options?: {
deleteCompanyLogo?: boolean;
deleteParentCompanyLogo?: boolean;
},
) {
return await this.prisma.$transaction(async (tx) => {
// Check if host already has a company
@@ -1139,7 +1570,7 @@ export class HostService {
hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW;
minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW;
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW;
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.RE_SUBMITTED;
}
// CASE 2: Admin has rejected but host can resubmit
else if (
@@ -1264,6 +1695,9 @@ export class HostService {
data: {
host: { connect: { id: createdHost.id } },
companyName: parentCompanyData.companyName || null,
firstName: parentCompanyData.firstName || null,
lastName: parentCompanyData.lastName || null,
mobileNumber: parentCompanyData.mobileNumber || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
// Safely handle city connection - only connect if valid ID exists
@@ -1300,7 +1734,7 @@ export class HostService {
facebookUrl: parentCompanyData.facebookUrl || null,
linkedinUrl: parentCompanyData.linkedinUrl || null,
twitterUrl: parentCompanyData.twitterUrl || null,
},
} as any,
});
// parent docs
@@ -1355,7 +1789,11 @@ export class HostService {
? { connect: { id: Number(companyData.countryXid) } }
: undefined,
pinCode: companyData.pinCode,
logoPath: companyData.logoPath || existingHostCompany.logoPath,
logoPath: options?.deleteCompanyLogo
? null
: resolveIncomingLogoPath(companyData.logoPath) ??
existingHostCompany.logoPath ??
null,
isSubsidairy: companyData.isSubsidairy,
registrationNumber: companyData.registrationNumber,
panNumber: companyData.panNumber,
@@ -1476,6 +1914,9 @@ export class HostService {
data: {
host: { connect: { id: updatedHost.id } },
companyName: parentCompanyData.companyName || null,
firstName: parentCompanyData.firstName || null,
lastName: parentCompanyData.lastName || null,
mobileNumber: parentCompanyData.mobileNumber || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
cities:
@@ -1496,9 +1937,10 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined,
pinCode: parentCompanyData.pinCode || null,
logoPath:
parentCompanyData?.logoPath ||
existingParentCompany?.logoPath ||
logoPath: options?.deleteParentCompanyLogo
? null
: resolveIncomingLogoPath(parentCompanyData?.logoPath) ??
existingParentCompany?.logoPath ??
null,
registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null,
@@ -1514,7 +1956,7 @@ export class HostService {
facebookUrl: parentCompanyData.facebookUrl || null,
linkedinUrl: parentCompanyData.linkedinUrl || null,
twitterUrl: parentCompanyData.twitterUrl || null,
},
} as any,
});
if (parentDocuments?.length) {
@@ -1534,6 +1976,9 @@ export class HostService {
where: { id: parentRecord.id },
data: {
companyName: parentCompanyData.companyName || null,
firstName: parentCompanyData.firstName || null,
lastName: parentCompanyData.lastName || null,
mobileNumber: parentCompanyData.mobileNumber || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
cities:
@@ -1554,9 +1999,10 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined,
pinCode: parentCompanyData.pinCode || null,
logoPath:
parentCompanyData?.logoPath ||
existingParentCompany?.logoPath ||
logoPath: options?.deleteParentCompanyLogo
? null
: resolveIncomingLogoPath(parentCompanyData?.logoPath) ??
existingParentCompany?.logoPath ??
null,
registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null,
@@ -1572,7 +2018,7 @@ export class HostService {
facebookUrl: parentCompanyData.facebookUrl || null,
linkedinUrl: parentCompanyData.linkedinUrl || null,
twitterUrl: parentCompanyData.twitterUrl || null,
},
} as any,
});
// if (parentDocuments?.length) {
@@ -2374,15 +2820,9 @@ export class HostService {
},
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesTotalPrice: true,
navigationMode: {
select: {
id: true,
navigationModeName: true,
navigationModeIcon: true,
},
},
},
},
equipmentAvailable: true,
@@ -3087,6 +3527,34 @@ export class HostService {
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
* -------------------------------- */
@@ -3706,7 +4174,7 @@ export class HostService {
const navMode = await tx.activityNavigationModes.create({
data: {
activityXid,
navigationModeXid: mode.navigationModeXid,
navigationModeName: mode.navigationModeName,
isInActivityChargeable: isChargeable,
navigationModesBasePrice: basePrice,
navigationModesTotalPrice: totalPrice,

View File

@@ -0,0 +1,497 @@
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

@@ -0,0 +1,254 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { ROLE } from '../../../common/utils/constants/common.constant';
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,
};
});
}
async getMemberPermissions(input: {
hostUserXid: number;
memberUserXid: number;
}) {
return this.prisma.$transaction(async (tx) => {
// Find the host
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.');
}
// Find the host member
const hostMember = await tx.hostMembers.findFirst({
where: {
hostXid: host.id,
userXid: input.memberUserXid,
isActive: true,
deletedAt: null,
memberStatus: 'accepted',
},
select: {
id: true,
userXid: true,
roleXid: true,
role: {
select: {
id: true,
roleName: true,
},
},
hostRolePermissionMasterXid: true,
},
});
if (!hostMember) {
throw new ApiError(404, 'Host member not found or not active.');
}
// Check if role is operator or co-admin
if (hostMember.roleXid !== ROLE.CO_ADMIN && hostMember.roleXid !== ROLE.OPERATOR) {
throw new ApiError(400, 'Member is not an operator or co-admin.');
}
if (!hostMember.hostRolePermissionMasterXid) {
// Return empty permissions if no role permissions assigned
return {
host,
member: {
userXid: hostMember.userXid,
role: hostMember.role,
},
permissions: [],
};
}
// Get the role permissions
const rolePermissions = await tx.hostRolePermissionMasters.findFirst({
where: {
id: hostMember.hostRolePermissionMasterXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
permissionMasterXids: true,
},
});
if (!rolePermissions) {
return {
host,
member: {
userXid: hostMember.userXid,
role: hostMember.role,
},
permissions: [],
};
}
// Get the actual permissions
const permissionIds = rolePermissions.permissionMasterXids as number[];
const permissions = await tx.hostPermissionMasters.findMany({
where: {
id: { in: permissionIds },
isActive: true,
deletedAt: null,
},
select: {
id: true,
permissionKey: true,
permissionGroup: true,
permissionSection: true,
permissionAction: true,
displayLabel: true,
displayOrder: true,
},
orderBy: {
displayOrder: 'asc',
},
});
return {
host,
member: {
userXid: hostMember.userXid,
role: hostMember.role,
},
permissions,
};
});
}
}

View File

@@ -0,0 +1,992 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
import { brevoService } from '../../../common/email/brevoApi';
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
import ApiError from '../../../common/utils/helper/ApiError';
import { generateOtpHelper } from '../../../common/utils/helper/sendOtp';
import config from '../../../config/config';
import {
ActivitySummaryDTO,
OperatorReservationByCheckInCodeDTO,
OperatorReservationVerificationOtpResponseDTO,
OperatorReservationVerifyOtpResponseDTO,
} from '../dto/operator.activity.dto';
const formatDateOnly = (date: Date): string => date.toISOString().split('T')[0];
const formatReadableDateTime = (date: Date): string =>
new Intl.DateTimeFormat('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
}).format(date);
const formatReadableDate = (date: Date): string =>
new Intl.DateTimeFormat('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
const getDateLabel = (date: Date): string => {
const today = new Date();
return formatDateOnly(today) === formatDateOnly(date)
? 'Today'
: formatReadableDate(date);
};
const buildFullName = (
firstName?: string | null,
lastName?: string | null,
): string => `${firstName ?? ''} ${lastName ?? ''}`.trim();
type ReservationVerificationPurpose = 'CheckInVerify' | 'CheckOutVerify';
type ReservationVerificationRequestChannel = 'email' | 'mobile';
@Injectable()
export class OperatorActivityService {
constructor(private prisma: PrismaClient) {}
private async getAcceptedHostXidsForOperator(
operatorId: number,
): Promise<number[]> {
const hostMembers = await this.prisma.hostMembers.findMany({
where: {
userXid: operatorId,
isActive: true,
memberStatus: 'accepted',
deletedAt: null,
},
select: {
hostXid: true,
},
});
return hostMembers.map((member) => member.hostXid);
}
private maskEmailAddress(emailAddress: string): string {
const [localPart, domainPart = ''] = emailAddress.split('@');
if (!localPart) {
return emailAddress;
}
if (localPart.length <= 2) {
return `${localPart[0] ?? '*'}*@${domainPart}`;
}
return `${localPart[0]}${'*'.repeat(
Math.max(localPart.length - 2, 1),
)}${localPart[localPart.length - 1]}@${domainPart}`;
}
private async sendReservationVerificationOtpEmail(input: {
emailAddress: string;
otp: string;
checkInCode: string;
verificationType: 'check-in' | 'checkout';
firstName?: string | null;
}): Promise<void> {
const subject =
input.verificationType === 'check-in'
? 'Your Minglar check-in verification code'
: 'Your Minglar checkout verification code';
const greetingName = input.firstName?.trim() || 'there';
const htmlContent = `
<p>Hi ${greetingName},</p>
<p>Your ${input.verificationType} verification code for Minglar is:</p>
<p><strong>${input.otp}</strong></p>
<p>Check-in code: <strong>${input.checkInCode}</strong></p>
<p>This code is valid for the next 5 minutes.</p>
<p>If you did not request this code, you can ignore this email.</p>
<p>Warm regards,<br />Team Minglar</p>
`;
try {
await brevoService.sendEmail({
recipients: [{ email: input.emailAddress }],
subject,
htmlContent,
});
} catch (error) {
console.error('Reservation OTP email send failed:', error);
throw new ApiError(500, 'Failed to send OTP to the registered email address.');
}
}
private async findReservationForOperatorByCheckInCode(
operatorId: number,
checkInCode: string,
) {
const hostXids = await this.getAcceptedHostXidsForOperator(operatorId);
if (hostXids.length === 0) {
throw new ApiError(404, 'Reservation not found for this check-in code');
}
const reservation = await this.prisma.itineraryDetails.findFirst({
where: {
offlineCode: checkInCode,
itineraryKind: 'ACTIVITY',
isActive: true,
deletedAt: null,
itineraryActivity: {
itineraryType: 'ACTIVITY',
isActive: true,
deletedAt: null,
activity: {
hostXid: {
in: hostXids,
},
isActive: true,
deletedAt: null,
},
},
},
select: {
id: true,
itineraryMemberXid: true,
offlineCode: true,
activityStatus: true,
itineraryMember: {
select: {
id: true,
member: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
},
},
},
},
itineraryActivity: {
select: {
id: true,
bookingStatus: true,
activity: {
select: {
activityTitle: true,
},
},
},
},
},
});
if (!reservation) {
throw new ApiError(404, 'Reservation not found for this check-in code');
}
return reservation;
}
private async verifyReservationOtp(
operatorId: number,
checkInCode: string,
otp: string,
input: {
otpType: ReservationVerificationPurpose;
verificationType: 'check-in' | 'checkout';
nextStatus: 'checked_in' | 'checked_out';
},
): Promise<OperatorReservationVerifyOtpResponseDTO> {
const normalizedCheckInCode = checkInCode.trim();
const trimmedOtp = otp.trim();
if (!normalizedCheckInCode) {
throw new ApiError(400, 'checkInCode is required');
}
if (!trimmedOtp) {
throw new ApiError(400, 'otp is required');
}
const reservation = await this.findReservationForOperatorByCheckInCode(
operatorId,
normalizedCheckInCode,
);
const member = reservation.itineraryMember?.member;
if (!member?.id) {
throw new ApiError(404, 'User not found for this reservation');
}
const currentStatus = (
reservation.activityStatus ??
reservation.itineraryActivity.bookingStatus ??
''
)
.trim()
.toLowerCase();
if (input.verificationType === 'check-in') {
if (currentStatus === 'checked_in') {
throw new ApiError(400, 'This user is already checked in.');
}
if (currentStatus === 'checked_out') {
throw new ApiError(400, 'This user has already checked out.');
}
}
if (input.verificationType === 'checkout') {
if (currentStatus === 'checked_out') {
throw new ApiError(400, 'This user is already checked out.');
}
if (currentStatus !== 'checked_in') {
throw new ApiError(
400,
'Check-in verification must be completed before checkout.',
);
}
}
const latestOtp = await this.prisma.userOtp.findFirst({
where: {
userXid: member.id,
otpType: input.otpType,
isActive: true,
isVerified: false,
},
orderBy: {
createdAt: 'desc',
},
});
if (!latestOtp) {
throw new ApiError(400, 'No OTP found.');
}
if (new Date() > latestOtp.expiresOn) {
throw new ApiError(400, 'OTP has expired.');
}
const isMatch = await bcrypt.compare(trimmedOtp, latestOtp.otpCode);
if (!isMatch) {
throw new ApiError(400, 'Invalid OTP.');
}
await this.prisma.$transaction(async (tx) => {
await tx.userOtp.update({
where: {
id: latestOtp.id,
},
data: {
isVerified: true,
verifiedOn: new Date(),
isActive: false,
},
});
await tx.itineraryDetails.update({
where: {
id: reservation.id,
},
data: {
activityStatus: input.nextStatus,
updatedOn: new Date(),
},
});
});
return {
itineraryActivityXid: reservation.itineraryActivity.id,
itineraryMemberXid: reservation.itineraryMemberXid,
activityName: reservation.itineraryActivity.activity?.activityTitle ?? null,
checkInCode: normalizedCheckInCode,
verificationType: input.verificationType,
verified: true,
reservationStatus: input.nextStatus,
};
}
private async requestReservationVerificationOtp(
operatorId: number,
checkInCode: string,
input: {
otpType: ReservationVerificationPurpose;
verificationType: 'check-in' | 'checkout';
requestedChannel: ReservationVerificationRequestChannel;
},
): Promise<OperatorReservationVerificationOtpResponseDTO> {
const normalizedCheckInCode = checkInCode.trim();
if (!normalizedCheckInCode) {
throw new ApiError(400, 'checkInCode is required');
}
const reservation = await this.findReservationForOperatorByCheckInCode(
operatorId,
normalizedCheckInCode,
);
const member = reservation.itineraryMember?.member;
if (!member?.id) {
throw new ApiError(404, 'User not found for this reservation');
}
const emailAddress = member.emailAddress?.trim().toLowerCase() || null;
const mobileNumber = member.mobileNumber?.trim() || null;
if (!emailAddress && !mobileNumber) {
throw new ApiError(
400,
'No registered mobile number or email address found for this user.',
);
}
if (!emailAddress) {
throw new ApiError(
501,
'SMS OTP delivery is not configured yet for reservation verification. Please add a registered email address for this user.',
);
}
const deliveryNote =
input.requestedChannel === 'mobile'
? 'SMS OTP delivery is not configured yet, so the OTP was sent to the registered email address instead.'
: null;
const otpResult = await generateOtpHelper(
this.prisma,
member.id,
emailAddress,
input.otpType,
6,
5,
);
await this.sendReservationVerificationOtpEmail({
emailAddress,
otp: otpResult.otp,
checkInCode: normalizedCheckInCode,
verificationType: input.verificationType,
firstName: member.firstName,
});
return {
itineraryActivityXid: reservation.itineraryActivity.id,
itineraryMemberXid: reservation.itineraryMemberXid,
activityName: reservation.itineraryActivity.activity?.activityTitle ?? null,
checkInCode: normalizedCheckInCode,
verificationType: input.verificationType,
requestedChannel: input.requestedChannel,
deliveryChannel: 'email',
destination: this.maskEmailAddress(emailAddress),
reservationStatus:
reservation.activityStatus ?? reservation.itineraryActivity.bookingStatus,
expiresInMinutes: 5,
deliveryNote,
};
}
private async attachPresignedUrl(
fileName?: string | null,
): Promise<string | null> {
if (!fileName) {
return null;
}
try {
return await getPresignedUrl(config.aws.bucketName, fileName);
} catch (error) {
console.error(`Failed to generate presigned URL for ${fileName}:`, error);
return null;
}
}
async getActivitiesByDate(
operatorId: number,
activityDate?: string,
): Promise<{
date: string;
activities: ActivitySummaryDTO[];
totalCount: number;
}> {
try {
// Get operator's assigned hosts
const hostMembers = await this.prisma.hostMembers.findMany({
where: {
userXid: operatorId,
isActive: true,
memberStatus: 'accepted', // Only accepted memberships
},
select: {
hostXid: true,
},
});
if (hostMembers.length === 0) {
return {
date: new Date().toISOString().split('T')[0],
activities: [],
totalCount: 0,
};
}
const hostXids = hostMembers.map((m) => m.hostXid);
// Use today's date if not provided
const queryDate = activityDate ? new Date(activityDate) : new Date();
// Validate date format
if (isNaN(queryDate.getTime())) {
throw new ApiError(400, 'Invalid date format. Use YYYY-MM-DD format');
}
// Set time to start of day (UTC)
queryDate.setUTCHours(0, 0, 0, 0);
// Set end of day
const endOfDay = new Date(queryDate);
endOfDay.setUTCHours(23, 59, 59, 999);
// Get all schedule occurrences from today onwards (not just query date)
// This includes future dates to show all scheduled dates
const allScheduleOccurrences =
await this.prisma.scheduleOccurences.findMany({
where: {
occurenceDate: {
gte: queryDate,
},
isActive: true,
scheduleHeader: {
activity: {
hostXid: {
in: hostXids,
},
},
},
},
include: {
scheduleHeader: {
include: {
activity: {
select: {
id: true,
activityTitle: true,
isActive: true,
ActivitiesMedia: {
where: {
isCoverImage: true,
isActive: true,
},
select: {
mediaFileName: true,
},
take: 1,
},
},
},
},
},
},
orderBy: {
occurenceDate: 'asc',
},
});
if (allScheduleOccurrences.length === 0) {
return {
date: queryDate.toISOString().split('T')[0],
activities: [],
totalCount: 0,
};
}
// Group activities by activity ID with all scheduled dates
const activityMap = new Map<
number,
{
activityTitle: string;
coverImage: string | null;
coverImageUrl: string | null;
activityId: number;
scheduledDates: Date[];
}
>();
// Process all schedule occurrences to build activity list with all scheduled dates
for (const occurrence of allScheduleOccurrences) {
const activity = occurrence.scheduleHeader.activity;
if (!activityMap.has(activity.id)) {
let coverImage: string | null = null;
let coverImageUrl: string | null = null;
if (activity.ActivitiesMedia.length > 0) {
coverImage = activity.ActivitiesMedia[0].mediaFileName;
try {
// Generate presigned URL for the image
coverImageUrl = await getPresignedUrl(
config.aws.bucketName,
coverImage,
);
} catch (error) {
console.error(
`Failed to generate presigned URL for ${coverImage}:`,
error,
);
coverImageUrl = null;
}
}
activityMap.set(activity.id, {
activityTitle: activity.activityTitle || 'Unknown Activity',
coverImage,
coverImageUrl,
activityId: activity.id,
scheduledDates: [new Date(occurrence.occurenceDate)],
});
} else {
// Add to scheduled dates if not already present
const existingActivity = activityMap.get(activity.id)!;
const occurrenceDateStr = new Date(occurrence.occurenceDate)
.toISOString()
.split('T')[0];
const dateExists = existingActivity.scheduledDates.some(
(d) => d.toISOString().split('T')[0] === occurrenceDateStr,
);
if (!dateExists) {
existingActivity.scheduledDates.push(
new Date(occurrence.occurenceDate),
);
}
}
}
// Get all unique scheduled dates for all activities
const allScheduledDates = new Set<string>();
activityMap.forEach((activity) => {
activity.scheduledDates.forEach((date) => {
allScheduledDates.add(date.toISOString().split('T')[0]);
});
});
// Get all activity IDs
const activityIds = Array.from(activityMap.keys());
// Query all bookings for these activities from query date onwards
const allBookings = await this.prisma.itineraryActivities.findMany({
where: {
activityXid: {
in: activityIds,
},
occurenceDate: {
gte: queryDate,
},
isActive: true,
},
select: {
activityXid: true,
itineraryHeaderXid: true,
occurenceDate: true,
},
});
// Count bookings by activity and date
const bookingsByActivityAndDate = new Map<
number,
Map<string, Set<number>>
>();
allBookings.forEach((booking) => {
if (booking.activityXid) {
if (!bookingsByActivityAndDate.has(booking.activityXid)) {
bookingsByActivityAndDate.set(booking.activityXid, new Map());
}
const dateStr = new Date(booking.occurenceDate)
.toISOString()
.split('T')[0];
const dateMap = bookingsByActivityAndDate.get(booking.activityXid)!;
if (!dateMap.has(dateStr)) {
dateMap.set(dateStr, new Set());
}
dateMap.get(dateStr)!.add(booking.itineraryHeaderXid);
}
});
// Convert map to array format with date breakdown
const activities: ActivitySummaryDTO[] = Array.from(
activityMap.values(),
).map((activity) => {
const activityBookings = bookingsByActivityAndDate.get(
activity.activityId,
);
// Get bookings for each scheduled date
const dateBreakdown = Array.from(allScheduledDates)
.sort()
.map((dateStr) => {
const count = activityBookings?.get(dateStr)?.size || 0;
return {
date: dateStr,
count,
};
});
// Count for the query date only
const queryDateStr = queryDate.toISOString().split('T')[0];
const countForQueryDate =
activityBookings?.get(queryDateStr)?.size || 0;
return {
activityName: activity.activityTitle,
activityImage: activity.coverImage,
activityImagePreSignedUrl: activity.coverImageUrl,
count: countForQueryDate,
dateBreakdown,
};
});
// Total count is bookings for the requested date only
const queryDateStr = queryDate.toISOString().split('T')[0];
const totalCount = allBookings.filter(
(b) =>
new Date(b.occurenceDate).toISOString().split('T')[0] ===
queryDateStr,
).length;
return {
date: queryDateStr,
activities,
totalCount,
};
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
500,
error instanceof Error ? error.message : 'Error fetching activities',
);
}
}
async getReservationByCheckInCode(
operatorId: number,
checkInCode: string,
): Promise<OperatorReservationByCheckInCodeDTO> {
try {
const normalizedCheckInCode = checkInCode.trim();
if (!normalizedCheckInCode) {
throw new ApiError(400, 'checkInCode is required');
}
const hostMembers = await this.prisma.hostMembers.findMany({
where: {
userXid: operatorId,
isActive: true,
memberStatus: 'accepted',
deletedAt: null,
},
select: {
hostXid: true,
},
});
const hostXids = hostMembers.map((member) => member.hostXid);
if (hostXids.length === 0) {
throw new ApiError(404, 'Reservation not found for this check-in code');
}
const reservation = await this.prisma.itineraryDetails.findFirst({
where: {
offlineCode: normalizedCheckInCode,
itineraryKind: 'ACTIVITY',
isActive: true,
deletedAt: null,
itineraryActivity: {
itineraryType: 'ACTIVITY',
isActive: true,
deletedAt: null,
activity: {
hostXid: {
in: hostXids,
},
isActive: true,
deletedAt: null,
},
},
},
select: {
id: true,
itineraryMemberXid: true,
offlineCode: true,
activityStatus: true,
createdAt: true,
paidOn: true,
itineraryMember: {
select: {
id: true,
memberRole: true,
member: {
select: {
id: true,
firstName: true,
lastName: true,
mobileNumber: true,
profileImage: true,
},
},
},
},
itineraryActivity: {
select: {
id: true,
occurenceDate: true,
startTime: true,
endTime: true,
bookingStatus: true,
venue: {
select: {
id: true,
venueName: true,
venueLabel: true,
},
},
itineraryHeader: {
select: {
id: true,
itineraryNo: true,
createdAt: true,
},
},
activity: {
select: {
id: true,
activityTitle: true,
checkInAddress: true,
ActivityPickUpDetails: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
isPickUp: true,
locationAddress: true,
},
},
},
},
itineraryActivitySelections: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
itineraryMemberXid: true,
isFoodOpted: true,
isTrainerOpted: true,
selectedFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityFoodType: {
select: {
foodType: {
select: {
foodTypeName: true,
},
},
},
},
},
},
selectedEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityEquipment: {
select: {
equipmentName: true,
},
},
},
},
},
},
},
},
},
});
if (!reservation) {
throw new ApiError(404, 'Reservation not found for this check-in code');
}
const activitySelection =
reservation.itineraryActivity.itineraryActivitySelections.find(
(selection) =>
selection.itineraryMemberXid === reservation.itineraryMemberXid,
) ?? null;
const selectedFoodTypes = (activitySelection?.selectedFoodTypes ?? [])
.map(
(selectedFoodType) =>
selectedFoodType.activityFoodType.foodType.foodTypeName,
)
.filter(Boolean);
const selectedEquipments = (activitySelection?.selectedEquipments ?? [])
.map(
(selectedEquipment) =>
selectedEquipment.activityEquipment.equipmentName,
)
.filter(Boolean);
const pickupLocation =
reservation.itineraryActivity.activity.ActivityPickUpDetails.find(
(detail) => detail.isPickUp && detail.locationAddress,
)?.locationAddress ??
reservation.itineraryActivity.activity.ActivityPickUpDetails.find(
(detail) => detail.locationAddress,
)?.locationAddress ??
reservation.itineraryActivity.activity.checkInAddress ??
null;
const member = reservation.itineraryMember.member;
const fullName =
buildFullName(member.firstName, member.lastName) || 'Guest';
const profileImagePreSignedUrl = await this.attachPresignedUrl(
member.profileImage,
);
const occurenceDate = new Date(
reservation.itineraryActivity.occurenceDate,
);
const bookedOnDate =
reservation.paidOn ??
reservation.createdAt ??
reservation.itineraryActivity.itineraryHeader.createdAt;
return {
itineraryHeaderXid: reservation.itineraryActivity.itineraryHeader.id,
itineraryActivityXid: reservation.itineraryActivity.id,
bookingId: reservation.itineraryActivity.itineraryHeader.itineraryNo,
checkInCode: reservation.offlineCode,
reservationStatus:
reservation.activityStatus ??
reservation.itineraryActivity.bookingStatus,
personalDetails: {
fullName,
firstName: member.firstName,
lastName: member.lastName,
role: reservation.itineraryMember.memberRole,
mobileNumber: member.mobileNumber,
profileImage: member.profileImage,
profileImagePreSignedUrl,
tags: [],
},
bookingInformation: {
activityName: reservation.itineraryActivity.activity.activityTitle,
slot:
reservation.itineraryActivity.startTime &&
reservation.itineraryActivity.endTime
? `${reservation.itineraryActivity.startTime} - ${reservation.itineraryActivity.endTime}`
: null,
startTime: reservation.itineraryActivity.startTime,
endTime: reservation.itineraryActivity.endTime,
track: reservation.itineraryActivity.venue?.venueName ?? null,
trackLabel: reservation.itineraryActivity.venue?.venueLabel ?? null,
date: formatDateOnly(occurenceDate),
dateLabel: getDateLabel(occurenceDate),
bookedOn: bookedOnDate ? bookedOnDate.toISOString() : null,
bookedOnLabel: bookedOnDate
? formatReadableDateTime(bookedOnDate)
: null,
},
bookingIncluded: {
food:
selectedFoodTypes.length > 0 ? selectedFoodTypes.join(', ') : 'No',
selectedFoodTypes,
equipment: selectedEquipments.length > 0 ? 'Yes' : 'No',
selectedEquipments,
trainerOrGuide: activitySelection?.isTrainerOpted ? 'Yes' : 'No',
pickupLocation,
},
// description1: null,
// description2: null,
};
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
500,
error instanceof Error
? error.message
: 'Error fetching reservation details',
);
}
}
async sendOtpCheckIn(
operatorId: number,
checkInCode: string,
): Promise<OperatorReservationVerificationOtpResponseDTO> {
return this.requestReservationVerificationOtp(operatorId, checkInCode, {
otpType: 'CheckInVerify',
verificationType: 'check-in',
requestedChannel: 'email',
});
}
async sendOtpCheckout(
operatorId: number,
checkInCode: string,
): Promise<OperatorReservationVerificationOtpResponseDTO> {
return this.requestReservationVerificationOtp(operatorId, checkInCode, {
otpType: 'CheckOutVerify',
verificationType: 'checkout',
requestedChannel: 'mobile',
});
}
async verifyOtpCheckIn(
operatorId: number,
checkInCode: string,
otp: string,
): Promise<OperatorReservationVerifyOtpResponseDTO> {
return this.verifyReservationOtp(operatorId, checkInCode, otp, {
otpType: 'CheckInVerify',
verificationType: 'check-in',
nextStatus: 'checked_in',
});
}
async verifyOtpCheckout(
operatorId: number,
checkInCode: string,
otp: string,
): Promise<OperatorReservationVerifyOtpResponseDTO> {
return this.verifyReservationOtp(operatorId, checkInCode, otp, {
otpType: 'CheckOutVerify',
verificationType: 'checkout',
nextStatus: 'checked_out',
});
}
}

View File

@@ -0,0 +1,286 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant';
import ApiError from '../../../common/utils/helper/ApiError';
import { OtpGeneratorSixDigit } from '../../../common/utils/helper/OtpGenerator';
type OperatorSignupInput = {
firstName?: string;
lastName?: string;
emailAddress?: string;
isdCode?: string;
mobileNumber?: string;
};
@Injectable()
export class OperatorAuthService {
constructor(private prisma: PrismaClient) {}
async findInvitedOperator(input: {
emailAddress?: string;
mobileNumber?: string;
}) {
const emailAddress = input.emailAddress?.trim().toLowerCase();
const mobileNumber = input.mobileNumber?.trim();
if (!emailAddress && !mobileNumber) {
throw new ApiError(400, 'Email address or mobile number is required');
}
const invitedOperator = await this.prisma.user.findFirst({
where: {
roleXid: ROLE.OPERATOR,
isActive: true,
OR: [
...(emailAddress ? [{ emailAddress }] : []),
...(mobileNumber ? [{ mobileNumber }] : []),
],
},
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
isdCode: true,
mobileNumber: true,
roleXid: true,
userPassword: true,
isEmailVerfied: true,
userStatus: true,
},
});
if (!invitedOperator) {
throw new ApiError(
403,
'Operator record not found. Please contact your host.',
);
}
return invitedOperator;
}
async signUpOperator(input: OperatorSignupInput) {
const emailAddress = input.emailAddress?.trim().toLowerCase();
const mobileNumber = input.mobileNumber?.trim();
const invitedOperator = await this.findInvitedOperator({
emailAddress,
mobileNumber,
});
if (invitedOperator.userPassword) {
return {
userId: invitedOperator.id,
emailAddress: emailAddress || invitedOperator.emailAddress,
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
otp: null,
expiresOn: null,
isNewOperator: false,
};
}
const otp = OtpGeneratorSixDigit.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + 5 * 60 * 1000);
await this.prisma.$transaction(async (tx) => {
await tx.user.update({
where: { id: invitedOperator.id },
data: {
firstName: input.firstName?.trim() || invitedOperator.firstName || null,
lastName: input.lastName?.trim() || invitedOperator.lastName || null,
emailAddress: emailAddress || invitedOperator.emailAddress,
isdCode: input.isdCode?.trim() || invitedOperator.isdCode || null,
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
userStatus: USER_STATUS.INVITED,
},
});
await tx.userOtp.updateMany({
where: {
userXid: invitedOperator.id,
otpType: 'Register',
isActive: true,
},
data: {
isActive: false,
isVerified: true,
},
});
await tx.userOtp.create({
data: {
userXid: invitedOperator.id,
otpType: 'Register',
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
});
return {
isNewOperator: true,
};
}
async verifyOperatorOtp(input: { emailAddress?: string; mobileNumber?: string; otp: string }) {
const emailAddress = input.emailAddress?.trim().toLowerCase();
const mobileNumber = input.mobileNumber?.trim();
const otp = input.otp.trim();
const invitedOperator = await this.findInvitedOperator({
emailAddress,
mobileNumber,
});
const latestOtp = await this.prisma.userOtp.findFirst({
where: {
userXid: invitedOperator.id,
otpType: 'Register',
isActive: true,
isVerified: false,
},
orderBy: { createdAt: 'desc' },
});
if (!latestOtp) {
throw new ApiError(400, 'No OTP found.');
}
if (new Date() > latestOtp.expiresOn) {
throw new ApiError(400, 'OTP has expired.');
}
const isMatch = await bcrypt.compare(otp, latestOtp.otpCode);
if (!isMatch) {
throw new ApiError(400, 'Invalid OTP.');
}
await this.prisma.userOtp.update({
where: { id: latestOtp.id },
data: {
isVerified: true,
verifiedOn: new Date(),
isActive: false,
},
});
await this.prisma.user.update({
where: { id: invitedOperator.id },
data: {
isEmailVerfied: emailAddress ? true : invitedOperator.isEmailVerfied,
},
});
return invitedOperator;
}
async createOperatorPassword(userId: number, password: string) {
const user = await this.prisma.user.findFirst({
where: {
id: userId,
roleXid: ROLE.OPERATOR,
isActive: true,
},
select: {
id: true,
emailAddress: true,
userPassword: true,
},
});
if (!user) {
throw new ApiError(404, 'Operator not found');
}
if (user.userPassword) {
throw new ApiError(400, 'Password already created. Please login.');
}
const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10);
const hashedPassword = await bcrypt.hash(password, saltRounds);
await this.prisma.user.update({
where: { id: user.id },
data: {
userPassword: hashedPassword,
userStatus: USER_STATUS.ACTIVE,
isEmailVerfied: true,
},
});
return {
id: user.id,
emailAddress: user.emailAddress,
};
}
async loginForOperator(emailAddress: string, userPassword: string) {
const existingOperator = await this.prisma.user.findFirst({
where: {
emailAddress,
roleXid: ROLE.OPERATOR,
isActive: true,
userStatus: USER_STATUS.ACTIVE,
},
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
roleXid: true,
isActive: true,
userStatus: true,
userPassword: true,
},
});
if (!existingOperator) {
throw new ApiError(404, 'Operator not found');
}
const isPasswordMatched = await bcrypt.compare(
userPassword,
existingOperator.userPassword || '',
);
if (!isPasswordMatched) {
throw new ApiError(401, 'Invalid credentials');
}
delete existingOperator.userPassword;
return existingOperator;
}
async verifyPasswordForOperator(emailAddress: string, userPassword: string) {
const existingOperator = await this.prisma.user.findFirst({
where: {
emailAddress,
roleXid: ROLE.OPERATOR,
isActive: true,
userStatus: USER_STATUS.ACTIVE,
},
select: {
id: true,
userPassword: true,
},
});
if (!existingOperator) {
throw new ApiError(404, 'Operator not found');
}
const isPasswordMatched = await bcrypt.compare(
userPassword,
existingOperator.userPassword || '',
);
return isPasswordMatched;
}
}

View File

@@ -10,13 +10,16 @@ export async function resendOtpEmail(
// messageId: string
}> {
const subject = "New OTP from Minglar Team";
const subject = "Your Minglar Verification Code";
const htmlContent = `
<p>Dear ${role},</p>
<p>Your new OTP is: <strong>${otp}</strong></p>
<p>This code will be valid for the next 5 minutes.</p>
<p>Warm regards,<br/>Minglar Team</p>
<p>Hi ${role},</p>
<p>Here's your verification code to get started:</p>
<p><strong>${otp}</strong></p>
<p>This code is valid for the next 5 minutes.</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 {
@@ -37,3 +40,5 @@ export async function resendOtpEmail(
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}

View File

@@ -0,0 +1,51 @@
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,24 +1,40 @@
import { brevoService } from "@/common/email/brevoApi";
import ApiError from "@/common/utils/helper/ApiError";
import config from '../../../config/config';
export async function sendEmailToAM(
emailAddress: string,
amName: string,
hostCompanyName: string,
hostRefNumber: string
activityName: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = `Host Application Re-Submited : ${hostCompanyName}`;
const subject = `${hostCompanyName} Has Resubmitted Their Application`;
const htmlContent = `
<p>Dear ${amName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has re-submited the application with implimented suggestions.</p>
<p>Please review their appliaction and take the necessary action.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
<p>Hello ${amName},</p>
<p>
${hostCompanyName} has updated and re-submitted their pre-qualification details of ${activityName} for your review.
</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 {
const result = await brevoService.sendEmail({
@@ -49,13 +65,14 @@ export async function sendEmailToMinglarAdmin(
// messageId: string
}> {
const subject = `New Host Application Recieved : ${hostCompanyName}`;
const subject = "New Host Application Submitted for Review";
const htmlContent = `
<p>Dear ${minglarAdminName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has submited their application.</p>
<p>Please review their appliaction and take the necessary action.</p>
<p>Best regards,<br/>Minglar Team</p>
<p>Hi ${minglarAdminName},</p>
<p>${hostCompanyName} has submitted their application and is awaiting your review.</p>
<p>Reference number: <strong>${hostRefNumber}</strong></p>
<p>Please log in to your dashboard to review the submission and take the necessary action.</p>
<p>Thank you,<br/>Minglar Team</p>
`;
try {
@@ -76,3 +93,57 @@ export async function sendEmailToMinglarAdmin(
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,22 +1,24 @@
import { brevoService } from "@/common/email/brevoApi";
import ApiError from "@/common/utils/helper/ApiError";
import { brevoService } from '../../../common/email/brevoApi';
import ApiError from '../../../common/utils/helper/ApiError';
import config from '../../../config/config';
export async function sendOtpEmailForHost(
emailAddress: string,
otp: string | number
otp: string | number,
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "OTP for Host Registration";
const subject = 'Your Minglar Verification Code';
const htmlContent = `
<p>Dear Host,</p>
<p>Youre almost all set! 🎉</p>
<p>Enter <strong>${otp}</strong> to wrap your registration.</p>
<p>This code will be valid for the next 5 minutes.</p>
<p>Warm regards,<br/>Minglar Team</p>
<p>Hi there 👋</p>
<p>Heres your verification code to get started:</p>
<p><strong>${otp}</strong></p>
<p>This code is 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>Need help? Reach out to us at info@minglargroup.com.</p>
<p>Warm regards,<br />Team Minglar</p>
`;
try {
@@ -33,7 +35,67 @@ export async function sendOtpEmailForHost(
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
console.error('Brevo email send failed:', err);
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
);
await this.prisma.token.deleteMany({
where: { userXid: user_xid }
})
// Optionally keep existing refresh tokens alive instead of deleting
// Removed deleteMany call so the same refresh token can be used multiple
// times. If you want to limit refresh tokens later you can implement
// rotation or blacklist logic elsewhere.
await this.prisma.token.create({
data: {
token: refreshToken.token,

View File

@@ -0,0 +1,91 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { MinglarService } from '../services/minglar.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
import { HOST_SUGGESTION_TITLES } from '../../../common/utils/constants/minglar.constant';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
interface AddSuggestionBody {
hostXid: number;
title: string;
comments: string;
activity_pqq_header_xid:number
}
/**
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Get user details
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse request body
let body: AddSuggestionBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { title, comments , activity_pqq_header_xid} = body;
if (!title) {
throw new ApiError(400, 'Title is required');
}
if (!comments) {
throw new ApiError(400, 'Comments are required');
}
if(!activity_pqq_header_xid){
throw new ApiError(400 , "Activity Pqq HeaderXid Required");
}
// Validate title is one of the allowed types
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
if (!allowedTitles.includes(title)) {
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
}
// Add suggestion using service
await minglarService.addPqqSuggestion(title, comments, activity_pqq_header_xid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestion added successfully',
data: null,
}),
};
});

View File

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

View File

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

View File

@@ -2,19 +2,30 @@
import { brevoService } from '@/common/email/brevoApi';
import ApiError from '@/common/utils/helper/ApiError';
export async function sendAMEmailForHostAssign(emailAddress: string): Promise<{
export async function sendAMEmailForHostAssign(emailAddress: string, accountManagerName?: string): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = 'Minglar Admin: Host Assignment Notification';
const subject = "You've Been Assigned a New Host";
const displayName = accountManagerName?.trim() || "there";
const htmlContent = `
<p>Hi,</p>
<p>Hi ${displayName},</p>
<p>Youve been assigned the <strong>Host</strong> role by Minglar Admin.</p>
<p>
A new host has been assigned to you by the Minglar team.
</p>
<p>Best regards,<br/>Minglar Admin 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 {
const result = await brevoService.sendEmail({
@@ -35,3 +46,5 @@ export async function sendAMEmailForHostAssign(emailAddress: string): Promise<{
}
}
// ...existing code...

View File

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

View File

@@ -1,25 +1,46 @@
import { brevoService } from "../../../common/email/brevoApi";
import ApiError from "../../../common/utils/helper/ApiError";
import config from "../../../config/config";
import { brevoService } from '../../../common/email/brevoApi';
import ApiError from '../../../common/utils/helper/ApiError';
import config from '../../../config/config';
export async function sendEmailToHostForApprovedApplication(
emailAddress: string,
name: string
name: string,
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Approval for your application";
const subject = 'Host Onboarding Application Approved';
const htmlContent = `
<p>Dear ${name},</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>Hi ${emailAddress},</p>
<p>
Were pleased to inform you that your host onboarding application has been approved by our team.
</p>
<p>
You can now proceed with completing your activity pre-qualification process.
</p>
<p>
Please click the link below to log in to your account and continue:
</p>
<p>
<strong>Host Portal Login</strong><br/>
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
</p>
<p>
If you have any questions or need assistance, feel free to reach out to us at
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
</p>
<p>
Warm regards,<br/>
Minglar Team
</p>
`;
try {
const result = await brevoService.sendEmail({
@@ -28,15 +49,15 @@ export async function sendEmailToHostForApprovedApplication(
htmlContent,
});
console.log("📧 Email sent successfully:", result);
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.");
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
}
}
@@ -46,14 +67,16 @@ export async function sendEmailToHostForMinglarApproval(
sent: boolean;
// messageId: string
}> {
const subject = "Approval for your application";
const subject = 'Host Onboarding Application Approved';
const htmlContent = `
<p>Dear Host,</p>
<p>Congratulations, Your application to minglar admin has been approved by minglar admin.</p>
<p>Minglar admin will assign account manager to your application.</p>
<p>Best regards,<br/>Minglar Team</p>
<p>Hi there,</p>
<p>We're pleased to inform you that your host onboarding application has been approved by our team.</p>
<p>You can now proceed with completing your activity pre-qualification process.</p>
<p>Please click the link below to log in to your account and continue:</p>
<p><a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a></p>
<p>If you have any questions or need assistance, feel free to reach out to us at info@minglargroup.com.</p>
<p>Warm regards,<br/>Minglar Team</p>
`;
try {
@@ -63,36 +86,57 @@ export async function sendEmailToHostForMinglarApproval(
htmlContent,
});
console.log("📧 Email sent successfully:", result);
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.");
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
}
}
export async function sendAMPQQAcceptanceMailtoHost(
emailAddress: string,
name: string
name: string,
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Approval for your activity onboarding application";
const subject = 'Your Activity Has Been Qualified for Onboarding';
const htmlContent = `
<p>Dear ${name},</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>Hi ${name},</p>
<p>
Were pleased to inform you that your activity has been qualified on the Minglar platform.
</p>
<p>
You can now proceed to complete the details of your activity through the Host portal.
</p>
<p>
Please click the link below to log in to your account and continue:
</p>
<p>
<strong>Host Portal Login</strong><br/>
<a href="${config.HOST_LINK_PQ}" target="_blank">${config.HOST_LINK_PQ}</a>
</p>
<p>
If you have any questions or need assistance, feel free to reach out at
<a href="mailto:info@minglargroup.com">info@minglargroup.com</a>.
</p>
<p>
Warm regards,<br/>
Minglar Team
</p>
`;
try {
const result = await brevoService.sendEmail({
@@ -101,36 +145,60 @@ export async function sendAMPQQAcceptanceMailtoHost(
htmlContent,
});
console.log("📧 Email sent successfully:", result);
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.");
console.error('Brevo email send failed:', err);
throw new ApiError(500, 'Failed to send OTP to minglar admin via email.');
}
}
export async function sendActivityAcceptanceMailtoHost(
emailAddress: string,
name: string
name: string,
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Approval for your activity details application";
const subject =
'Onboarding Completed | You Can Now Set Up Your Activity Schedule and Listing';
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>Hi ${name},</p>
<p>Great news! 🎉</p>
<p>
You have successfully completed the onboarding process for your activity on Minglar.
</p>
<p>
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>
👉 <strong>Access your Host Portal:</strong><br/>
<a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a>
</p>
<p>
If you have any questions or need assistance while setting things up, our team is here to help at
<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({
@@ -139,14 +207,71 @@ export async function sendActivityAcceptanceMailtoHost(
htmlContent,
});
console.log("📧 Email sent successfully:", result);
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.");
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,23 +9,36 @@ export async function sendInvitationEmailForMinglarAdmin(
// messageId: string
}> {
const subject = "Minglar Admin: Your Team Invitation";
const subject = "Team Invitation: Account Manager at Minglar";
const htmlContent = `
<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>Hi ${emailAddress},</p>
<p><strong>Access Your Invitation:</strong><br/>
<a href="${link}">${link}</a></p>
<p>
Were happy to invite you to join the Minglar team as an Account Manager.
</p>
<p>If you have any questions or need assistance, feel free to reach out — were here to help.<br/>
We look forward to having you on board!</p>
<p>
To get started, please set up your account using the link below:
</p>
<p>Welcome aboard!<br/>
<strong>The Minglar Admin Team</strong></p>
<p>
<a href="${link}" target="_blank">${link}</a>
</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 {
const result = await brevoService.sendEmail({
@@ -45,3 +58,4 @@ We look forward to having you on board!</p>
throw new ApiError(500, "Failed to send invitation via email.");
}
}

View File

@@ -34,7 +34,7 @@ const bucket = config.aws.bucketName;
@Injectable()
export class MinglarService {
constructor(private prisma: PrismaService | PrismaClient) {}
constructor(private prisma: PrismaService | PrismaClient) { }
async createPassword(user_xid: number, password: string): Promise<boolean> {
// Find user by id
@@ -144,10 +144,10 @@ export class MinglarService {
async getUserDetails(id: number) {
const hostDetail = await this.prisma.hostHeader.findFirst({
where: { id: id },
where: { id: id, isActive: true },
});
const userDetails = await this.prisma.user.findUnique({
where: { id: hostDetail.userXid },
where: { id: hostDetail.userXid, isActive: true },
});
return userDetails;
}
@@ -314,6 +314,8 @@ export class MinglarService {
companyName: true,
user: {
select: {
firstName: true,
lastName: true,
userRefNumber: true,
},
},
@@ -375,11 +377,52 @@ export class MinglarService {
const {
paginationService,
} = 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,
totalCount,
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(
@@ -832,9 +875,9 @@ export class MinglarService {
internal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
display: MINGLAR_STATUS_DISPLAY.NEW,
},
To_Review: {
Re_Submitted: {
internal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
display: MINGLAR_STATUS_DISPLAY.TO_REVIEW,
display: MINGLAR_STATUS_DISPLAY.RE_SUBMITTED,
},
Enhancing: {
internal: MINGLAR_STATUS_INTERNAL.AM_REJECTED,
@@ -945,6 +988,7 @@ export class MinglarService {
const where: any = {
isActive: true,
hostStatusInternal: { notIn: [HOST_STATUS_INTERNAL.DRAFT] },
adminStatusInternal: { notIn: [MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW] },
};
if (search?.trim()) {
@@ -1367,7 +1411,7 @@ export class MinglarService {
const amUser = await this.prisma.user.findUnique({
where: { id: accountManagerXid, isActive: true },
select: { emailAddress: true },
select: { emailAddress: true, firstName: true, lastName: true },
});
if (!amUser || !amUser.emailAddress) {
@@ -1378,7 +1422,8 @@ export class MinglarService {
}
try {
await sendAMEmailForHostAssign(amUser.emailAddress);
const amName = [amUser.firstName, amUser.lastName].filter(Boolean).join(' ').trim();
await sendAMEmailForHostAssign(amUser.emailAddress, amName);
return true;
} catch (err) {
console.error('Error sending AM assignment email', err);
@@ -1711,6 +1756,7 @@ export class MinglarService {
isEmailVerfied: true,
isMobileVerfied: true,
isBiometric: true,
createdAt: true,
userAddressDetails: {
select: {
id: true,

View File

@@ -4,20 +4,35 @@ import config from "../../../config/config";
export async function sendEmailToHostForRejectedApplication(
emailAddress: string,
firstName: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "Rejection for your application";
const subject = "Action Needed: Host Onboarding Application";
const htmlContent = `
<p>Dear Host,</p>
<p>Sorry to say that, But your application to minglar admin has been rejected.</p>
<p>Please update your application and resubmit it.</p>
<p>If you have any questions please contact to minglar admin.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
<p>Hi ${firstName},</p>
<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>
<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 {
const result = await brevoService.sendEmail({
@@ -47,21 +62,14 @@ export async function sendAMRejectionMailtoHost(
// messageId: string
}> {
const subject = "Improvement of your application";
const subject = "Action Needed: Host Onboarding Application";
const htmlContent = `
<p>Dear ${name},</p>
<p> Your account manager has reviewed your application and provided some suggestions. <br/>
Please make the necessary improvements and re-submit your application to proceed with the onboarding process on Minglar.</p>
<p> You may access your application using the link below:<br/>
<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>
<p>Hi ${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><a href="${link}" target="_blank">${link}</a></p>
<p>We appreciate your interest in Minglar and look forward to reviewing your updated application.</p>
<p>Warm regards,<br/>Team Minglar</p>
`;
try {
@@ -92,23 +100,42 @@ export async function sendAMPQQRejectionMailtoHost(
// messageId: string
}> {
const subject = "Improvement of your activity onboarding application";
const subject = "Action Needed: Activity Pre-qualification";
const htmlContent = `
<p>Dear ${name},</p>
<p>Hi ${name},</p>
<p>Your account manager has reviewed your activity application and provided some suggestions.<br/>
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>
Thank you for taking the time to submit your activity pre-qualification details on the Minglar platform.
</p>
<p>You may access your activity onboarding application using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK}</p>
<p>
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.
</p>
<p>If you have any questions, please feel free to contact the Minglar Support Team.</p>
<p>
You can log in to the Host portal to review the feedback and continue updating your application:
</p>
<p>Best regards,<br/>
<strong>Minglar Team</strong></p>
<p>
<strong>Host Portal Login</strong><br/>
<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 {
const result = await brevoService.sendEmail({
@@ -138,23 +165,42 @@ export async function sendActivityRejectionMailtoHost(
// messageId: string
}> {
const subject = "Improvement of your activity onboarding application";
const subject = "Action Needed: Activity Onboarding";
const htmlContent = `
<p>Dear ${name},</p>
<p>Hi ${name},</p>
<p>Your account manager has reviewed your activity application and provided some suggestions.<br/>
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>
Thank you for submitting your activity for review.
</p>
<p>You may access your activity onboarding application using the link below:<br/>
<strong>Link:</strong> ${config.HOST_LINK}</p>
<p>
After evaluating the details provided, were unable to approve the listing at this stage. A few updates are required before we can proceed.
</p>
<p>If you have any questions, please feel free to contact the Minglar Support Team.</p>
<p>
Please log in to your Host Portal to review the feedback and make the necessary revisions.
</p>
<p>Best regards,<br/>
<strong>Minglar Team</strong></p>
<p>
👉 <strong>Access your Host Portal:</strong><br/>
<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 {
const result = await brevoService.sendEmail({
@@ -174,3 +220,58 @@ export async function sendActivityRejectionMailtoHost(
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,13 +9,16 @@ export async function sendOtpEmailForMinglarAdmin(
// messageId: string
}> {
const subject = "OTP for Minglar Admin Registration";
const subject = "Your Minglar Verification Code";
const htmlContent = `
<p>Dear User,</p>
<p>Your OTP for minglar admin registration is: <strong>${otp}</strong></p>
<p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
<p>Best regards,<br/>Minglar Team</p>
<p>Hi there,</p>
<p>To continue your Minglar Admin registration, please use the following One-Time Password (OTP):</p>
<p><strong>${otp}</strong></p>
<p>This code is valid for 5 minutes.</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 {
@@ -36,3 +39,4 @@ export async function sendOtpEmailForMinglarAdmin(
throw new ApiError(500, "Failed to send OTP to minglar admin via email.");
}
}

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
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,14 +50,22 @@ export const handler = safeHandler(async (
const activity = activityDetails.activity;
// Rooms: combine ActivityVenues with their respective slots for the selected date
const Venues = (activity.ActivityVenues || []).map((v: any) => {
const header = scheduleDetails.find((h: any) => h.activityVenue?.venueXid === v.id);
const Venues = (activity.ActivityVenues || [])
.map((v: any) => {
const header = scheduleDetails.find(
(h: any) => h.activityVenue?.venueXid === v.id
);
const roomSlots = (header?.slots || []).map((s: any) => {
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';
if (!header || !header.slots?.length) {
return null; // ❌ venue has no slots for selected date
}
const roomSlots = header.slots.map((s: any) => {
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,
@@ -81,33 +89,34 @@ export const handler = safeHandler(async (
venueMedia: (v.ActivityVenueArtifacts || []).map((media: any) => ({
id: media.id,
mediaType: media.mediaType,
mediaFileName: media.mediaFileName, // original S3 key / URL
presignedUrl: media.presignedUrl, // presigned URL
mediaFileName: media.mediaFileName,
presignedUrl: media.presignedUrl,
})),
};
});
})
.filter(Boolean); // ✅ removes null venues
// 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;
// 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 = {
const responsePayload = {
selectedDate,
Venues,
checkInTime,
checkOutTime,
};
};
return {
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({ success: true, data: responsePayload }),
};
};
});

View File

@@ -0,0 +1,74 @@
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

@@ -0,0 +1,46 @@
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 radiusParam = event.queryStringParameters?.radiusKm ?? event.queryStringParameters?.radius;
if (!latParam || !longParam || !radiusParam) {
throw new ApiError(400, 'lat, long and radiusKm (in km) are required as query parameters');
}
const userLat = latParam ? Number(latParam) : undefined;
const userLong = longParam ? Number(longParam) : undefined;
const radiusKm = radiusParam ? Number(radiusParam) : 15; // default 15km
const userLat = Number(latParam);
const userLong = Number(longParam);
const radiusKm = Number(radiusParam);
if (Number.isNaN(userLat) || Number.isNaN(userLong) || Number.isNaN(radiusKm)) {
throw new ApiError(400, 'lat, long and radiusKm must be valid numbers');
if (
(userLat !== undefined && Number.isNaN(userLat)) ||
(userLong !== undefined && Number.isNaN(userLong)) ||
Number.isNaN(radiusKm)
) {
throw new ApiError(400, 'Invalid lat/long values');
}
const page = Number(event.queryStringParameters?.page ?? 1);

View File

@@ -26,19 +26,11 @@ export const handler = safeHandler(async (
}
// Extract query parameters for search
const activityTitle = event.queryStringParameters?.activityTitle?.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
const result = await userService.searchActivities(
userId,
{ activityTitle, activityType, checkInCity }
activityType
);
return {

View File

@@ -0,0 +1,71 @@
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,9 +86,10 @@ export const handler = safeHandler(
body: JSON.stringify({
success: true,
message: 'Access token generated successfully',
data: {
accessToken: newAccessToken.access.token,
accessTokenExpires: newAccessToken.access.expires,
data: null,
},
}),
};
},

View File

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

View File

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,62 @@
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');
}
let body: {
itineraryHeaderXid?: number;
};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { itineraryHeaderXid } = body;
if (!itineraryHeaderXid) {
throw new ApiError(400, 'itineraryHeaderXid is required');
}
const result = await itineraryService.getActivityDetailsAfterBooking(
userId,
itineraryHeaderXid,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Activity details retrieved successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,71 @@
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

@@ -0,0 +1,106 @@
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;
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() !== '';
let startDate: Date | undefined;
if (hasStartDate) {
startDate = parseQueryDate(startDateRaw, 'startDate');
}
const result = await itineraryService.getAllUserSavedItineraries(
userId,
itineraryHeaderXid,
startDate,
);
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

@@ -0,0 +1,61 @@
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

@@ -0,0 +1,103 @@
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.',
);
}
if (!Number.isInteger(payload.page) || payload.page <= 0) {
throw new ApiError(400, 'page must be a positive integer.');
}
if (!Number.isInteger(payload.limit) || payload.limit <= 0) {
throw new ApiError(400, 'limit must be a positive integer.');
}
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

@@ -0,0 +1,43 @@
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

@@ -0,0 +1,127 @@
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

@@ -0,0 +1,261 @@
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

@@ -0,0 +1,95 @@
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,
}),
};
});

Some files were not shown because too many files have changed in this diff Show More