475 Commits

Author SHA1 Message Date
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
3fa5b8a3dd Added distance and rating key to the user apis 2026-02-28 11:54:32 +05:30
414400f034 sending the isCover flag and safety instruction and cancellation 2026-02-28 11:30:22 +05:30
paritosh18
eaae2557c0 refactor: Update interest relation handling in generateActivityRefNumber function 2026-02-27 23:08:23 +05:30
paritosh18
f1829a6d14 fix: Correct import path for PrismaService and update interest relation in generateActivityRefNumber function 2026-02-27 19:48:29 +05:30
9d64759097 hbhj 2026-02-27 19:03:28 +05:30
5d879542a1 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-27 18:44:09 +05:30
ea9938a736 fixed the package version 2026-02-27 18:44:00 +05:30
paritosh18
84608cc025 feat: Initialize AWS S3 client in HostService 2026-02-27 18:43:37 +05:30
paritosh18
9921edfea7 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-02-27 17:25:59 +05:30
paritosh18
26a8e997f1 feat: Add agreement template and refactor agreement PDF generation in HostService 2026-02-27 17:25:48 +05:30
b98b8cf864 sending the connection interest count and generating the activity ref number 2026-02-27 17:24:39 +05:30
85437ebc2e made the function to create ref number of activity and commented the unused api code 2026-02-27 12:15:48 +05:30
2b7b2b240f Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-26 19:53:06 +05:30
0242799eb5 Cancellation 2026-02-26 19:50:25 +05:30
paritosh18
c7408711a4 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-02-26 19:45:30 +05:30
paritosh18
db2dc7532a feat: Enhance host agreement generation with AWS S3 integration and document creation capabilities using pdf-lib and docx libraries. Added new dependencies for document processing and improved address and duration handling. 2026-02-26 19:45:08 +05:30
485bdee063 removed the slotXid for opening the slot 2026-02-26 19:01:59 +05:30
0c9ff76fe4 fixed the cancellation issues 2026-02-26 18:55:34 +05:30
0edcf3d515 made host agreement table 2026-02-26 18:43:58 +05:30
eb34edaedb added interest code in the interest table 2026-02-26 18:19:45 +05:30
41d33b72a3 fixed the host ref number generating logic 2026-02-26 17:27:32 +05:30
paritosh18
f3076aaec3 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-02-26 11:06:10 +05:30
paritosh18
e49d08fb14 Refactor openCanceledSlot handler to improve readability and error handling. Updated body structure to support multiple cancellations and enhanced token validation. Adjusted imports for better organization. 2026-02-26 11:05:56 +05:30
paritosh18
23bbb39af3 feat: Trim OTP input and update verification logic in host and user services 2026-02-25 15:56:18 +05:30
paritosh18
c4fd797e31 change the nearby logic 2026-02-25 14:48:54 +05:30
paritosh18
3f96dd4ae1 feat: Add safety instructions and cancellations fields to activity details 2026-02-25 14:41:58 +05:30
paritosh18
89f1bf55bc feat: Add initial Prisma schema with core models and implement host activity creation functionality. 2026-02-25 13:34:03 +05:30
e736cdaa7b updating the stepper when admin rejects 2026-02-24 13:25:56 +05:30
7703dba2d9 Fixed the statuses for the workflow of resubmission of host application 2026-02-24 12:30:18 +05:30
a3e906a779 Fixed the status for admin rejection of host application for resubmit workflow 2026-02-24 12:02:10 +05:30
3145037238 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-24 11:01:18 +05:30
paritosh18
ced8bdcbad Add getNearbyActivities API to retrieve activities based on user location and radius. Implemented token validation and pagination support. Enhanced UserService with distance calculation and filtering logic for nearby activities. 2026-02-23 20:00:50 +05:30
b22324539a Made view more api for upper section and host pq redirectiion through mail 2026-02-23 19:49:05 +05:30
aa3e6fd3d1 seeding only required company types 2026-02-23 19:08:16 +05:30
paritosh18
5ec07d4480 Refactor user ID validation and optimize city search results. Updated user ID check to use Number.isNaN for better clarity. Added a comment indicating that city results are capped at 50 in the database query to reduce latency. 2026-02-23 18:56:08 +05:30
paritosh18
1fbcf1dddc Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-02-23 17:52:03 +05:30
c50c4b1c5a taking array of cancellations and seeded cities of india 2026-02-23 12:26:56 +05:30
paritosh18
ca4def4695 Add getRandomActiveActivity API to fetch random active activities with token validation 2026-02-23 12:05:52 +05:30
4f8217c95a Fixed the method of remove connect details api and fixed the issue of add school company details 2026-02-20 14:46:37 +05:30
81994d97ff added validation for entry count of the user's connect details and made removeConnectionDetails api 2026-02-19 20:41:00 +05:30
28b4145ce9 fixed the file names of the interest types in seed file 2026-02-19 20:15:59 +05:30
e351dbf4b9 made view more activities api 2026-02-19 20:05:33 +05:30
d8475de70f taking array of schoolCompanyXids 2026-02-19 17:52:43 +05:30
paritosh18
2b0c1f4ae4 Add token verification to user-related API handlers 2026-02-19 17:22:34 +05:30
23932be637 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-19 17:13:30 +05:30
84678bc00e made getActivityFromConnectionsInterest api 2026-02-19 17:11:34 +05:30
paritosh18
a905cc7aee Refactor addOrFindSchoolCompanyDetail to normalize school/company names and improve search logic 2026-02-19 17:11:29 +05:30
paritosh18
340d47a708 add school or company api 2026-02-19 16:25:43 +05:30
b0eb2863cd sending the city and state details in the get connection details of user api 2026-02-19 16:04:49 +05:30
paritosh18
b002077381 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-02-19 15:56:26 +05:30
paritosh18
a43cc1e36a Refactor code structure for improved readability and maintainability 2026-02-19 15:55:18 +05:30
3e711cee18 Made get all connection details of user and fixed the issues in the schoolCompany table 2026-02-19 15:50:48 +05:30
4d3d95f906 folder made 2026-02-19 15:24:50 +05:30
6bac257a75 made the connection tables 2026-02-19 15:23:32 +05:30
af891834a7 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-19 14:51:37 +05:30
77fee0cc04 Sending the venue images and transportaion modes and activity type name 2026-02-19 14:51:26 +05:30
paritosh18
95e1838303 Add generateAccessFromRefreshToken handler to issue new access tokens 2026-02-19 12:44:55 +05:30
paritosh18
f8b6f277a1 Added endDate to the availability details response in checkAvailabilityDetails handler 2026-02-17 19:39:35 +05:30
3f2e553947 Changed the host constant as per client requirement and sending the activity type name in the pqq endpoints 2026-02-17 16:27:40 +05:30
7569b54d60 sending presigned urls in the get by id of acitivity details and fixed the issue of check availability 2026-02-17 11:48:06 +05:30
0a11c78351 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-13 17:52:26 +05:30
b4af010316 fixed the path of specific search api 2026-02-13 17:52:17 +05:30
paritosh18
b35cb4e178 added presigned url 2026-02-13 17:51:53 +05:30
bf9055c10a Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-13 17:41:17 +05:30
d1e2c649c3 creating the sorting data for user on registration and added it to seeder also 2026-02-13 17:41:08 +05:30
paritosh18
9c45d924e6 feat: add searchActivities functionality to retrieve activities based on search criteria 2026-02-13 17:40:12 +05:30
9846d40bfb Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-13 11:55:03 +05:30
a0b015d900 Sending the presgined url of amenities 2026-02-13 11:34:55 +05:30
paritosh18
ebeb4e5d06 feat: implement user service for managing personal information, retrieving interests, and ranking activities. 2026-02-11 18:49:28 +05:30
036f7ab130 fixed the issue in user address detail 2026-02-11 15:04:09 +05:30
e154be70ad Added amenities in seed file 2026-02-11 11:49:18 +05:30
paritosh18
62e7379306 feat: Implement user passcode setting and verification functionality with new handler and service methods. 2026-02-10 15:14:11 +05:30
paritosh18
68facd1146 small chagnes 2026-02-09 15:50:56 +05:30
paritosh18
1dcce8084c Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-02-09 15:40:26 +05:30
paritosh18
6e0d795348 fix: rename variable 'rooms' to 'Venues' for consistency in checkAvailabilityDetails handler 2026-02-09 15:40:19 +05:30
b1d0e0f52e Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-09 15:36:22 +05:30
paritosh18
76fdc42428 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-02-09 15:36:20 +05:30
paritosh18
c216d128a6 feat: add checkAvailabilityDetails API endpoint and implement schedule details retrieval logic 2026-02-09 15:34:13 +05:30
5ccdcdedae feat: add getSurpriseMePageDetails API endpoint and implement user-specific activity retrieval logic 2026-02-09 15:27:10 +05:30
73c528d1cc feat: update activity retrieval logic to include activity ID and placeholder for rating and distance 2026-02-09 14:53:33 +05:30
171a3aded6 feat: add getActivityDetailsById API endpoint to retrieve activity details by ID 2026-02-06 19:27:37 +05:30
42c1f2a268 feat: enhance user service and landing page details API with location handling, pagination, and new activity retrieval logic 2026-02-06 15:55:20 +05:30
00d53adf3d feat: enhance scheduling service to support instant booking and late check-in options, and improve activity listing logic 2026-02-05 16:07:43 +05:30
93fb58f4f4 feat: enhance user and activity models with location relations and add landing page details API 2026-02-04 15:32:11 +05:30
paritosh18
1f53180b4e feat: update setUserLocationDetails to create new user address entry instead of updating 2026-02-02 17:42:20 +05:30
paritosh18
558cb214c0 feat: add setUserLocationDetails functionality and corresponding API endpoint 2026-02-02 17:09:42 +05:30
f414bc42f9 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-02-02 14:51:49 +05:30
e92eb230a0 refactor: remove redundant user ID and host ID validation in scheduling handlers 2026-02-02 14:51:32 +05:30
paritosh18
5ca8ca266a feat: add setUserInterests functionality to manage user interests 2026-02-02 14:50:23 +05:30
cb92fbe400 feat: refactor user ID handling and implement getHostIdByUserId method in SchedulingService 2026-02-02 13:33:07 +05:30
d861e402e9 feat: include userStatus in user lookup and user creation for active users 2026-02-02 12:46:54 +05:30
8ed0df7424 feat: update user passcode length, enhance date validation, and refactor personal info handling 2026-02-01 10:02:35 +05:30
745e4fa73f fix: correct userPasscode validation and remove unused import in setPasscodeForMobile 2026-01-30 15:35:57 +05:30
paritosh18
46c8432582 feat: implement setPasscodeForMobile functionality
- Added a new API endpoint to set a user passcode with validation.
- Created SetPasscodeDTO for passcode input validation.
- Implemented setUserPasscode method in UserService to handle passcode setting.
- Updated user.yml to include the new set-passcode endpoint.
- Created setPasscodeForMobile handler to manage the passcode setting logic.
- Removed unused test-stepper-handler.ts file.
- Deleted Dockerfile, docker-compose.yml, init.sql, and serverless.yml.backup files as part of cleanup.
2026-01-30 15:30:45 +05:30
paritosh18
9bbd8f0c9a Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-01-30 14:49:55 +05:30
07f0212b62 made openCanceledSlot api 2026-01-30 14:49:43 +05:30
paritosh18
50d2242b55 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-01-30 14:49:31 +05:30
3adbadc3ee added where: { isActive: true } condition 2026-01-30 11:32:43 +05:30
paritosh18
3f92c6ca30 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-01-30 11:30:09 +05:30
ef7564f5a4 fixed the service code and added validation 2026-01-29 18:39:01 +05:30
8be2eebaba made cancel slot api and modified get by id api to send all the data of scheduling and taking listNow flag in create scheduling api while deleting all the records before creating new ones 2026-01-29 17:47:48 +05:30
1d90675d19 sending the presigned url for activity media 2026-01-28 19:12:41 +05:30
855cafb30f Made the activity duration and venue get api 2026-01-28 17:58:05 +05:30
6bc68ddd04 fixed the condition and route path 2026-01-28 16:55:14 +05:30
paritosh18
003ce54465 Update activity status messages for consistency 2026-01-28 16:46:17 +05:30
paritosh18
4525e969e6 Add getActivitiesByStatus API and update activity status constants 2026-01-28 16:24:40 +05:30
68f0dfe124 made create scheduling api 2026-01-28 15:49:19 +05:30
112fdab040 Made mobile register and verify otp and submit personal info apis and added interest type images in the seeder 2026-01-23 17:56:46 +05:30
834d16a76e written all the activity types in the seeder 2026-01-16 18:47:22 +05:30
f20e4191ee added gender name column and interest color and image column added activity seeder data till gamecraft made register and add personal info api for user mobile endpoints lambda and service 2026-01-16 17:50:30 +05:30
6d377296fc fixed the order by id asc in the allowed entry types 2026-01-12 18:46:06 +05:30
8534ac6c7d sending the statuses in the getAllPQPDetailsForAM API 2026-01-10 16:48:50 +05:30
89ee19f35d added isCheckOutSame flag in the get API 2026-01-10 15:24:50 +05:30
paritosh18
b4db2a3bc2 as per requierment 2026-01-09 19:11:46 +05:30
5d93945729 fixed the null issue of venue name 2026-01-09 18:10:38 +05:30
aa00e1585c removed the new column because its already there 2026-01-09 13:34:19 +05:30
9c10833856 added a new key in_activity_navigation_available for navigation 2026-01-09 13:16:07 +05:30
paritosh18
905350406f Refactor HostService to enhance chargeability logic for trainers and pickup details. Implement checks for trainer and pickup transport chargeability, ensuring proper handling of amounts and tax calculations based on chargeable status. Update validation to enforce positive values only when chargeable. 2026-01-08 23:01:09 +05:30
paritosh18
bc0470cf52 Update service name to 'minglarDev' in serverless.yml, adjust transportTotalPrice validation to allow zero values in createActivity.schema.ts, and enhance document handling logic in HostService for improved pickup transport and detail management. 2026-01-08 20:17:01 +05:30
paritosh18
39e03ce4c7 Schema Change to Nulable 2026-01-08 16:38:41 +05:30
paritosh18
4953a179a6 Enhance validation logic in HostService for venue and equipment pricing. Implement checks for draft status to allow zero values in draft mode while enforcing positive values on submission. Update query to include unreviewed activities. 2026-01-08 15:18:06 +05:30
paritosh18
82ba980b6f Refactor HostService to improve validation logic and error handling for activity payloads. Added checks for draft status in various validation conditions, ensuring proper error messages are thrown for required fields and amounts. Cleaned up import statements for better organization. 2026-01-08 12:14:12 +05:30
paritosh18
5ec819b881 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-01-07 19:34:58 +05:30
f7cc925e6f Fixed the status in the create activity api submission 2026-01-07 19:34:41 +05:30
16c901b64b added approved filter and fixed the error messages 2026-01-07 19:13:36 +05:30
e65ed8babc sending the host id in the get pqq details for am and corrected the error message in the submit pqq answer 2026-01-07 16:03:27 +05:30
paritosh18
fa1359a1c9 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-01-07 16:00:28 +05:30
paritosh18
2c965bfb92 small change 2026-01-07 16:00:24 +05:30
da4e2547ce sending frequency also in get activity and venue details 2026-01-07 12:11:09 +05:30
c6b6608ca6 removed the conversion code of minutes in the activityDurationMins 2026-01-06 14:17:39 +05:30
4a6292d108 Fixed the create activity content type json error 2026-01-06 14:12:21 +05:30
45aab89747 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-01-05 15:46:58 +05:30
183614f45a made upload venue media to s3 function 2026-01-05 15:46:47 +05:30
paritosh18
3ddaad0a46 suggestion is reviwed true 2026-01-05 14:47:24 +05:30
e286cffa49 fixed the schema and the NA problem and multiple transport creation 2026-01-05 13:28:25 +05:30
fafb5d06a7 made upload and delete image from s3 2026-01-02 18:21:08 +05:30
5482599e96 sending the host ref number and company name and the energylevel in the list api 2026-01-02 16:19:33 +05:30
e0086393b4 fixed the isreviewed flase suggestion only send and the activityPqqHeaderId sending in every respective question 2025-12-31 19:26:51 +05:30
paritosh18
9161e224cc sanitize the file name 2025-12-31 17:05:53 +05:30
paritosh18
c2fa769220 file name change 2025-12-31 16:57:13 +05:30
paritosh18
aeea3d0ca8 folder Name Chagne in Actvity 2025-12-31 16:42:35 +05:30
paritosh18
c2d3ab9da2 navigation modes per price update 2025-12-31 14:42:30 +05:30
paritosh18
401734096d sml chng 2025-12-31 13:03:23 +05:30
paritosh18
ed3aaab961 small change 2025-12-31 13:01:08 +05:30
8199ce327a Fixed the spelling of Enhancing 2025-12-31 12:42:11 +05:30
5ce3dd3780 Added foodcost and venue taxes in the get api 2025-12-30 16:59:40 +05:30
paritosh18
245af71705 transpost chagnes 2025-12-30 14:22:18 +05:30
2c81e949ca fixed the get api of activity 2025-12-30 13:39:36 +05:30
738b2c6608 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2025-12-30 13:22:30 +05:30
paritosh18
999c5ff616 schema changes in Transport and pickup details 2025-12-30 12:46:21 +05:30
8dad4bfd2a fiexed the venue label issue 2025-12-30 12:15:16 +05:30
65fc472902 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2025-12-29 19:07:17 +05:30
paritosh18
6359d64b63 venue add 2025-12-29 19:03:11 +05:30
b3e051bd63 fixed the other documents issue 2025-12-29 17:07:48 +05:30
80c552a3d5 fixed the energylevel relation with new logic 2025-12-29 17:01:04 +05:30
paritosh18
e6d37e04f9 updated create activtiy hanlder 2025-12-24 19:04:51 +05:30
paritosh18
0c71ae3ccf Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-24 17:11:17 +05:30
716ce3363f made the age resttriction thing in single model 2025-12-24 17:10:54 +05:30
paritosh18
ddc2e46dd2 duration changes 2025-12-24 17:07:11 +05:30
paritosh18
927649b4f6 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-24 16:11:44 +05:30
paritosh18
4f9955d9f4 get and create suggestion api 2025-12-24 16:11:22 +05:30
745e827d22 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2025-12-24 15:12:30 +05:30
8c86e72346 Made reject activity application from AM api 2025-12-24 15:12:12 +05:30
paritosh18
9b3fcd8cbe allowed entry type 2025-12-24 15:11:37 +05:30
d4b5153814 Made accept activity application from AM api 2025-12-24 15:00:02 +05:30
paritosh18
6440fc2440 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-24 13:21:29 +05:30
paritosh18
95f823e749 update status 2025-12-24 13:21:12 +05:30
60ee1f5f21 Sending the presigned url for activity media and venue artifacts 2025-12-24 11:50:13 +05:30
bf16a31ae6 Made a get all details of activity with venue for the big form details of activity 2025-12-23 17:51:32 +05:30
paritosh18
1540688b7d small change in resposne message of Create Activtiy handler 2025-12-22 17:04:02 +05:30
cab4408dc8 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-22 13:33:11 +05:30
be65a6c021 Added seed for energy level 2025-12-22 13:33:10 +05:30
paritosh18
f13e90ce39 upated createNewActivity handler 2025-12-22 13:30:55 +05:30
paritosh18
5a223f126f creat activity handler 2025-12-21 17:28:08 +05:30
paritosh18
2e4f318684 Add CreateActivityDto and update createFullActivity method for activity creation 2025-12-18 20:05:30 +05:30
paritosh18
53785bd5f2 Add handler for creating full activity with related records 2025-12-18 19:37:32 +05:30
c9b507f969 Enhance rejection email content with clickable link for application access 2025-12-18 16:55:57 +05:30
91871d1f44 Enhance search filter to include company name and user reference number in host retrieval 2025-12-17 17:29:17 +05:30
05e48063c9 Add isActive check when retrieving user in loginForHost method 2025-12-17 16:57:18 +05:30
a906dc5635 Add getAddActivityPrePopulate handler and implement prepopulate data retrieval
- Introduced a new handler for adding activity prepopulation.
- Enhanced the PrePopulateService to fetch all necessary prepopulate data for the new activity.
- Updated the updateBankDetails handler to correctly inject host ID.
- Improved user data retrieval in HostService to include additional fields.
- Added validation for host ID in showSuggestionToAM handler.
2025-12-17 16:17:55 +05:30
8ec8cf4854 fixed the path 2025-12-16 19:11:15 +05:30
fab7642302 Implement parent document deletion and improve host retrieval logic in onboarding process 2025-12-16 17:34:10 +05:30
4d3796c5f3 Refactor companyTypes seeding and enhance host retrieval to include user email address 2025-12-16 16:36:15 +05:30
2f3c531c56 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-16 16:09:28 +05:30
a2907929d4 updated serverless version 2025-12-16 16:08:59 +05:30
paritosh18
ef2b23ef83 Update serverless configuration to use exported stack output for Prisma Lambda Layer ARN, ensuring proper deployment functionality. 2025-12-16 16:07:03 +05:30
2767d29d79 Update parent company validation to allow optional fields and handle null values in company details submission. 2025-12-16 15:38:03 +05:30
46daec00ce fixed the keytooloong issue 2025-12-16 14:37:38 +05:30
43e494780d Refactor document name handling in host service to sanitize input and ensure valid connections for city, state, and country IDs during host creation and updates. 2025-12-16 13:16:22 +05:30
0da18b18f7 Add getSuggestionsForAM function and corresponding handler for retrieving suggestions based on host assignments. Update serverless configuration to include new API endpoint. 2025-12-16 12:07:42 +05:30
b5304b3c26 Refactor OTP email content for host registration and resend functionality to improve clarity and tone. 2025-12-15 16:53:12 +05:30
82340c2918 Implement email notifications for host application acceptance and rejection, including host details retrieval and email content customization. 2025-12-10 14:59:16 +05:30
b8f5f92c98 Add validation for city ID in company details submission to ensure existence in the database 2025-12-09 17:38:44 +05:30
3652d851f7 Enhance email notifications for host application approvals and rejections by including the host's first name in the greeting. 2025-12-09 15:02:15 +05:30
paritosh18
6166075967 Update service name in serverless configuration to 'minglar' and add 'zod' dependency in package-lock.json 2025-12-09 13:50:30 +05:30
paritosh18
b6cb5831c2 Refactor Prisma client usage and enhance service integration for improved connection management 2025-12-09 13:49:20 +05:30
paritosh18
a39cc1c3c8 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-09 12:09:38 +05:30
paritosh18
ae76618f7a Add environment configuration template and update .gitignore for new env files 2025-12-09 12:09:21 +05:30
e957fc5c50 Add token blacklist check in JWT middlewares to enhance session management and security 2025-12-09 12:07:03 +05:30
paritosh18
856db687c3 Update service name in serverless configuration to 'minglarDev' for development environment 2025-12-08 17:58:38 +05:30
a020a28993 Update host application handlers to retrieve user details using hostXid instead of userInfo.id for accurate email notifications. 2025-12-08 17:31:55 +05:30
1a520ae9e1 Remove email parameter from resendOtpHelper and normalize email addresses to lowercase in the resend OTP handler for consistent processing. 2025-12-08 15:25:21 +05:30
d5d6951e64 Normalize email addresses to lowercase in OTP verification handler for consistent processing. 2025-12-08 15:19:31 +05:30
bbd55562af Add HOST_LINK environment variable and update related configurations. Normalize email addresses to lowercase in login and registration handlers. Enhance email notifications for approved and rejected applications with HOST_LINK. 2025-12-08 15:08:05 +05:30
9abadba8f5 Refactor database access by introducing prisma.lambda.service for centralized PrismaClient usage across modules. Update imports in various handlers and services to utilize the new service, ensuring consistent database interactions and improved maintainability. 2025-12-08 11:23:58 +05:30
747566497c Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-06 16:33:57 +05:30
6c3e5ccd60 Add multiple states, cities, and banks to seed data in prisma/seed.ts 2025-12-06 16:33:35 +05:30
ca9ba601ad Add additional states and cities to seed data in Prisma 2025-12-06 16:27:54 +05:30
eab6565e12 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-06 16:11:53 +05:30
6a84876518 Remove profile completion percentage calculation from user update in MinglarService 2025-12-06 12:55:07 +05:30
d8fb4b242d Add roleXid field to user selection in MinglarService 2025-12-06 12:39:21 +05:30
51b053310f Add validation to prevent duplicate host accounts in addPaymentDetails method 2025-12-06 12:06:04 +05:30
06010ef6e8 Refactor API paths for acceptPQByAM and rejectPQQByAM to remove activityId from URL and update request body parsing for activityId 2025-12-05 19:21:19 +05:30
7b9763c668 Enhance getAllHostActivityForMinglar method to exclude activities with DRAFT_PQ status 2025-12-05 18:51:50 +05:30
4c1a04d043 Update serverless configuration to comment out Prisma client dependencies and enhance MinglarService to exclude draft activities from host activity retrieval. 2025-12-05 18:50:14 +05:30
paritosh18
ecf45c3e7c Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-05 14:56:45 +05:30
paritosh18
20a931ab27 Add AWS Lambda Bundle Size Optimization Guide and update build scripts for Prisma layer 2025-12-05 14:56:43 +05:30
6a84fbc0c3 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-05 14:38:32 +05:30
9fc8336bd9 fixed the routes 2025-12-05 14:37:37 +05:30
b049146664 Refactor serverless configuration to rename service, add Prisma layer support, and include AM_INVITATION_LINK in environment variables. Update validation schema and email invitation content for improved user experience. 2025-12-05 13:45:49 +05:30
963f84681c Enhance HostService to retrieve and utilize existing logo paths for host and parent companies, ensuring consistency in logo display across company details. 2025-12-04 20:18:22 +05:30
ab9e02972e Refactor hostCompanyDetails validation schema to make address and location fields optional; update login handler to remove password comparison logic; enhance host service to include user status check and return specific user fields; clean up profile completion logic in MinglarService. 2025-12-04 20:01:09 +05:30
33b330a15b Merge branch 'pqq-optimized-logic' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into pqq-optimized-logic 2025-12-04 18:40:44 +05:30
d898dcd8ff Add S3 document deletion functionality in submitCompanyDetails handler; update host service to return activity ID with sorted categories; modify PQP details retrieval to use new admin host token for authentication. 2025-12-04 18:40:41 +05:30
paritosh18
ff18fcbf9f Merge branch 'pqq-optimized-logic' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into pqq-optimized-logic 2025-12-04 11:54:12 +05:30
paritosh18
a872ed89e4 Enhance MinglarService: Add id field to selection in PQQ data retrieval and ensure activityPqqHeaderId is set at the category level for better data integrity. 2025-12-04 11:53:37 +05:30
759eeb298c Update sorting logic for categories and subcategories to ensure proper order in responses. 2025-12-04 08:33:21 +05:30
1b72e65a71 Enhance PQQ data retrieval: Add comments and pqqAnswerXid fields to the selection in HostService and MinglarService. Update PQQAnswers structure to include all answer options for questions, improving data handling in responses. 2025-12-03 19:43:22 +05:30
4a7e5fbb1e Add PQQ functionality: Introduce new endpoints for creating activities and submitting answers, along with updates to the Minglar service for retrieving PQQ details. Update serverless configuration to include new function files. 2025-12-03 19:21:21 +05:30
ca5936d0db Update referencedBy field in HostService to default to null if undefined 2025-12-03 14:56:15 +05:30
c0d607a321 Add optional referencedBy field to hostCompanyDetails validation schema and update HostService to handle referencedBy data 2025-12-03 14:41:00 +05:30
1d684b7de6 Add referencedBy field to HostHeader model and update import paths for various handlers 2025-12-03 14:34:53 +05:30
16b16ac7ca Refactor import path for verifyMinglarAdminHostToken and update selection fields in getAllPQQQuesAndSubmittedAns method 2025-12-03 13:56:39 +05:30
0e0c63e31a path updated for the renamed file 2025-12-03 13:49:56 +05:30
822b425c1d Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-03 13:43:41 +05:30
42e2d7a579 taking the activity xid in get all submited ques ans 2025-12-03 13:43:30 +05:30
paritosh18
78f49b35dd Update API path for retrieving all PQQ questions and answers for AM 2025-12-03 13:37:01 +05:30
paritosh18
930ae708a1 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into sprint1 2025-12-03 13:20:38 +05:30
paritosh18
8fb3f16d18 Add endpoint to retrieve all PQQ questions and answers for AM 2025-12-03 13:20:21 +05:30
3d6226ddac added search 2025-12-03 13:18:28 +05:30
38c616a7af making entries in host track 2025-12-03 12:44:07 +05:30
c6ab3e57c0 fixed the condition of submit pq for review 2025-12-02 20:19:03 +05:30
paritosh18
776c03911e Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into sprint1 2025-12-02 20:11:19 +05:30
paritosh18
781058c443 Implement pagination for host activity retrieval in HostService and MinglarService 2025-12-02 20:10:10 +05:30
3b723e5d1f made accept pq by am api 2025-12-02 20:09:42 +05:30
paritosh18
56ebf44d37 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-02 19:58:52 +05:30
4f8274adb9 Add pinCode field to location selection in MinglarService 2025-12-02 19:00:30 +05:30
39f182b8e9 sending city state abd country in getbyidAM 2025-12-02 18:59:06 +05:30
d9f7cd9a0f fixed the optional chaining 2025-12-02 18:08:39 +05:30
4772c320ba Enhance HostService and MinglarService to include account manager details and process profile images. Updated getAllHostActivity to retrieve account manager information and generate presigned URLs for media and profile images. Refactored media processing logic for improved clarity and consistency. 2025-12-02 17:53:19 +05:30
paritosh18
f19f5e46c4 Remove commented-out validation for allowed titles in addPQQSuggestion handler 2025-12-02 17:33:30 +05:30
paritosh18
156aed2429 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2025-12-02 17:27:57 +05:30
paritosh18
fb77111e34 Refactor payment details handling: remove IFSC code validation, add currencyXid to DTO, and implement bank branch lookup for IFSC code retrieval. 2025-12-02 17:27:36 +05:30
6b673a173d Refactor host functions and constants for improved clarity and functionality. Renamed 'getAllActivityType' to 'prePopulateNewActivity' and updated its path. Added 'submitPQQForReview' handler for submitting PQQ for review. Enhanced error handling and response structure in 'submitPQQ_Answer' and 'submitPQQForReview' methods. Updated constants to standardize PQQ-related statuses. Improved S3 file handling logic in various handlers. 2025-12-02 17:24:01 +05:30
3c4b0db39f Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-02 13:42:27 +05:30
e72c260b18 Add ActivityTrack model to schema and update User and Activities models to include activityTracks relation. Modify seed data to reflect new interest names and activity types. Implement activity reference number generation in HostService for activity creation. 2025-12-02 13:42:14 +05:30
paritosh18
9a777eb3f9 Refactor addOrUpdateCompanyDetails method in HostService to streamline host status handling and improve document management logic for both new and existing companies. 2025-12-02 13:40:41 +05:30
paritosh18
c3f0a1d82a Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-02 13:19:16 +05:30
3f921febe0 Refactor host status determination logic in HostService to handle various submission cases, including updates and drafts. Update host suggestion review process to only mark suggestions as reviewed when not in draft state. 2025-12-01 20:25:23 +05:30
paritosh18
4bc5eb8d4d Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 19:56:43 +05:30
f3f0b2a81b Refactor HOST_SUGGESTION_TITLES to comments in minglar.constant.ts, update HostService to filter HostSuggestion by active and reviewed status, and modify addSuggestion handler to remove title validation against HOST_SUGGESTION_TITLES. Enhance MinglarService to include profile image handling and add city, country, and state selections in user queries. 2025-12-01 19:56:21 +05:30
paritosh18
6f0cdb4e0a Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 17:58:14 +05:30
ce9c8174d8 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-12-01 17:57:45 +05:30
c72e757bf3 Refactor Minglar admin functions: removed deprecated acceptHostApplicationMinglar handler, added editAgreementDetailsAndAccept handler, and updated related service methods for improved agreement processing. Cleaned up schema by removing isSubsidairy field and adjusted file handling logic in submitCompanyDetails. Enhanced suggestion handling by including isParent flag in addSuggestion. Updated host retrieval logic in getStepper. 2025-12-01 17:57:08 +05:30
paritosh18
6aaf49bf72 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 17:07:59 +05:30
paritosh18
1b31ca4a83 Implement pagination in getAllOnboardingHostApplications handler and update MinglarService to support pagination options. Enhance response structure to include total count of applications. 2025-12-01 17:05:08 +05:30
140f70615c Added adminStatusInternal field to the MinglarService for enhanced data handling. 2025-12-01 15:48:42 +05:30
32f4c7ce42 added filter in get host am api 2025-12-01 15:43:06 +05:30
paritosh18
067d1a1f1b Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 13:45:51 +05:30
paritosh18
470298a3fb Add search functionality to getAllOnboardingHostApplications and getAllOnboardingNewApplications handlers; update MinglarService to support search queries 2025-12-01 13:45:39 +05:30
1d7d0749b3 Refactor authentication: replaced instances of verifyHostToken with verifyMinglarAdminHostToken in prepopulate handlers for improved security and consistency. 2025-12-01 13:42:10 +05:30
paritosh18
d3fb1c85fb Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 13:32:29 +05:30
e723e680ab sending the internal admin status to frontend 2025-12-01 13:32:09 +05:30
paritosh18
0e50b8b187 Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-12-01 13:29:46 +05:30
paritosh18
bbe725dd9e Update HTTP methods to PATCH for rejectHostApplication endpoints and implement pagination in getAllInvitationDetails and getAllInvitedCoadminAndAM handlers 2025-12-01 13:29:10 +05:30
e5861654e9 Enhance company types management: updated schema to include display order and relationships, modified validation to use company type XID, and seeded initial company types data. Updated services to reflect new structure and ensure proper data handling. 2025-12-01 13:26:06 +05:30
paritosh18
b9c90b488f Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-29 19:02:27 +05:30
d1eef782a2 fixed the profileimage key 2025-11-29 19:00:50 +05:30
c16a7bb95e fixed the validation for submit company details 2025-11-29 18:57:57 +05:30
paritosh18
626ea34a63 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-29 18:00:40 +05:30
paritosh18
31e0b3ff2d Add search functionality to getAllHostApplicationForAM, getAllCoadminAndAM, getAllInvitationDetails, and getAllInvitedCoadminAndAM handlers 2025-11-29 18:00:15 +05:30
07f6f0159c access applied 2025-11-29 17:21:24 +05:30
b1a70acfa8 sending the presigned url in getAMDetailById 2025-11-29 16:24:15 +05:30
5ad17869be fixed the document type name inserting issue 2025-11-29 16:13:15 +05:30
8961e49dac Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into sprint1 2025-11-29 16:04:55 +05:30
8d8702dfb6 stroring the document type in the user documents table 2025-11-29 16:04:00 +05:30
paritosh18
576c749b34 add JWT expiration settings to environment variables in serverless configuration 2025-11-29 16:03:29 +05:30
paritosh18
649fccb81b update JWT access token expiration default to 1440 minutes 2025-11-29 15:54:33 +05:30
15b8afd9b2 fixed the profile image issue 2025-11-29 14:29:41 +05:30
84c4b1f2b9 sending the userrefNumber in all endpoints 2025-11-29 14:17:31 +05:30
paritosh18
933addd60f Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-29 13:55:48 +05:30
paritosh18
18ff254e8d enable reload handler for serverless-offline plugin 2025-11-29 13:55:23 +05:30
ae7b453abd made prisma config file in root 2025-11-29 13:55:09 +05:30
7bec950096 adding a random reference number in user table 2025-11-29 13:48:55 +05:30
6b2b211990 updated cascade 2025-11-29 12:20:42 +05:30
2d36ca5dd7 fixed the function name 2025-11-29 12:14:28 +05:30
f75993f738 made the file name clear 2025-11-29 12:06:13 +05:30
264f2fa29c made a pagination service 2025-11-29 12:04:50 +05:30
e0b841b437 fixing the submit pqq answer api 2025-11-29 10:29:58 +05:30
54613534db sending the basic user details in the create password of minglar 2025-11-28 20:41:03 +05:30
6cac8fb163 sending the profile image in presigned url 2025-11-28 17:47:43 +05:30
6147b0f476 Refactor HostHeader and HostParent models in Prisma schema for consistency and clarity. Update submitCompanyDetails API to improve JSON field normalization and error handling. Enhance host and parent document management with presigned URL generation for S3 uploads. Implement upsert logic for host documents and parent company details, ensuring proper handling of existing records. 2025-11-28 17:20:27 +05:30
71e3f2a933 sending the user details in the getby id of host 2025-11-28 16:02:30 +05:30
174f13300c Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-28 15:57:37 +05:30
3f324bc1fc made get host details by id for minglar admin 2025-11-28 15:57:25 +05:30
paritosh18
27e19bd921 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-28 15:35:28 +05:30
paritosh18
82cbaddce1 feat: add serverless-offline plugin and new showSuggestion function
- Added serverless-offline plugin to package.json and serverless.yml for local development.
- Implemented new showSuggestion function in host.yml with appropriate memory size and event configuration.
- Removed deprecated getSuggestion function from minglaradmin.yml and updated related handlers.
- Enhanced safeHandler to convert Prisma errors to ApiError automatically, improving error handling.
- Introduced comprehensive Prisma error handling in ApiError.ts, mapping error codes to user-friendly messages and HTTP status codes.
2025-11-28 15:33:43 +05:30
80bd926e16 fixed the submit company details api 2025-11-28 15:27:28 +05:30
15c1458f02 made resend otp lambda 2025-11-28 12:23:08 +05:30
0cedcec109 Enhance activity status constants and update validation logic in submitCompanyDetails API. Added new statuses for PQQ and improved validation handling for draft submissions. Updated rejectPQQbyAM to reflect new status updates. 2025-11-28 12:07:45 +05:30
9e47d861a2 made the stepper updation in submit company details api 2025-11-28 11:41:35 +05:30
4f3adff517 submit company details api s3 uploading issue resolved and required docs removed 2025-11-27 20:51:04 +05:30
8ccbdc8f32 fixed the submit last pqq question api logic 2025-11-27 20:00:29 +05:30
75077c00da made reject pqq by am api 2025-11-27 19:47:04 +05:30
abe512b0c2 made updateSuggestionAsReviewed lambda api 2025-11-27 19:35:24 +05:30
326ea2f96b added transaction for calculating the pqq final answer score 2025-11-27 19:16:46 +05:30
b0bae33b6e Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-27 18:56:03 +05:30
030ec9225d updating the status to under review if the pqq overall score is more than 50% 2025-11-27 18:55:25 +05:30
paritosh18
83c3a39cc9 Refactor getAllHostActivityForAdmin handler: update paths and token verification method 2025-11-27 18:48:20 +05:30
paritosh18
6b0884b070 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-27 17:27:03 +05:30
paritosh18
9e0f12117f Refactor host and minglaradmin handlers: update paths, rename functions, and add prepopulateRole handler for role retrieval 2025-11-27 17:26:33 +05:30
67da5b57e6 fixed the registration api 2025-11-27 16:56:28 +05:30
paritosh18
dcc0fbbcf6 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-27 16:07:33 +05:30
fb392369a5 fixed the path of get score of pqq api 2025-11-27 16:07:20 +05:30
paritosh18
870aaca557 Refactor host and minglaradmin functions: rename handlers, update paths, and add new endpoints for activity and role management 2025-11-27 16:06:57 +05:30
62637ecc9a made minor fixes 2025-11-27 15:03:34 +05:30
f421a1e88a updated the path of import 2025-11-27 11:31:41 +05:30
7beebc16b3 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-26 19:31:23 +05:30
paritosh18
6fa96c3fb0 Increase memory size for submitCompanyDetails and update package patterns in host and minglaradmin functions; refactor JWT middleware to use shared Prisma client instance 2025-11-26 19:30:57 +05:30
e63e906f62 fixed the path in host yml file 2025-11-26 19:21:49 +05:30
a01a93680a fixed the path in serverless for minglar endpoints 2025-11-26 18:53:10 +05:30
paritosh18
7be03b6c27 Refactor host API endpoint and integrate Prisma client
- Changed HTTP method for the /host/add-company-details endpoint from POST to PATCH in host.yml.
- Introduced a new Prisma client setup in prisma.client.ts for database interactions.
- Updated authForHost middleware to utilize the new Prisma client instance.
2025-11-26 18:04:26 +05:30
paritosh18
abae9d9ac2 Update Prisma dependencies and refactor host onboarding handlers
- Updated Prisma client and adapter versions in package.json and package-lock.json.
- Refactored host onboarding handlers to improve structure and naming conventions.
- Added new handlers for onboarding processes including login, signup, and OTP verification.
- Implemented new functionality for managing bank details and company submissions.
- Enhanced error handling and validation across various handlers.
2025-11-26 17:31:08 +05:30
a0ed3b9faa minglaradmin handlers folder structure made 2025-11-26 13:14:37 +05:30
14ddc94765 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-26 12:00:59 +05:30
498c4cbe46 updated prisma version 2025-11-26 12:00:48 +05:30
paritosh18
d14948b344 smalll fixx of yml 2025-11-26 09:42:07 +05:30
paritosh18
c9b4269e9a Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-25 12:28:16 +05:30
930605e22e Enhance getAllOnboardingHostApplications to exclude hosts with DRAFT status 2025-11-25 12:26:01 +05:30
paritosh18
263d06949e Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-25 12:04:50 +05:30
8e25c5c4a2 made get all host applications for minglar admin and get all new host application for minglar admin 2025-11-25 12:02:15 +05:30
917a1f1ee9 made the pre populate of city and bank branch 2025-11-25 09:44:36 +05:30
paritosh18
089d022267 SMall FIx 2025-11-24 23:53:31 +05:30
paritosh18
d65f7f5368 resolved Merge 2025-11-24 23:25:20 +05:30
7056f32e24 made get score api for pqq 2025-11-24 23:19:18 +05:30
c4e470be05 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-24 19:19:34 +05:30
paritosh18
fca4991577 Add filtering for account manager in getAllHostApplications based on user role 2025-11-24 19:19:14 +05:30
2ab046fbd2 fixed minor issues 2025-11-24 19:19:02 +05:30
paritosh18
12420f6b51 Enhance getAllHostApplications to support userStatus filtering and include additional host details in response 2025-11-24 17:09:22 +05:30
paritosh18
fbd3b12937 Remove created activity data from response in addActivity handler 2025-11-22 21:10:17 +05:30
paritosh18
1c83cc5910 Add addActivity handler and createActivity method in HostService
- Implemented addActivity handler to create new activities with validation.
- Added createActivity method in HostService to handle activity creation logic.
2025-11-22 20:48:45 +05:30
d21dcacd7b added necessary packages 2025-11-22 20:20:25 +05:30
38d3b4ca6a fixed the file path 2025-11-22 20:15:01 +05:30
d0fd8e6691 Implement validation for required fields in PQQ handlers and enhance file upload logic in submitPqqAns. Added error handling for missing activity and question IDs, and improved S3 file management with delete functionality for existing files. Updated HostService methods for better file handling and header management. 2025-11-22 20:05:43 +05:30
6d48eeb25b Add new endpoint to retrieve the latest PQQ question details and implement corresponding handler. Update HostService to include method for fetching latest question details with active status filtering. 2025-11-22 19:47:04 +05:30
b1623343c8 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-22 19:37:30 +05:30
paritosh18
3fb6961e7c Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-22 19:36:32 +05:30
5e0ba4c8e8 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-22 19:31:40 +05:30
1377e0ba9a made the getbyid pqq question details api 2025-11-22 19:30:16 +05:30
paritosh18
d0b2de3f18 Add new endpoints for activity types and frequencies, and implement email notifications for AM assignments 2025-11-22 19:25:07 +05:30
3b1aac921f Add new handlers for accepting and rejecting host applications, including email notifications. Updated serverless configuration with new function timeouts and added missing handlers. Refactored profile update logic to improve file upload handling and added user detail retrieval methods in the Minglar service. 2025-11-22 19:22:34 +05:30
e7c94a1b19 formatted the files 2025-11-22 12:09:37 +05:30
d351a632bf Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-22 12:00:07 +05:30
0d858f5411 made getall document country city and replacing the files on s3 when updating the company details sending mail to am or admin 2025-11-22 11:59:48 +05:30
paritosh18
15c85686c6 Refactor getAllCoadminAndAM to include active user filtering and host count aggregation 2025-11-22 11:33:55 +05:30
976c9fc686 fixed the display order in PQQ getAll api and fixed the get All admin co admin and am api service 2025-11-22 08:00:59 +05:30
4665cb7ca4 fixed the serverless file 2025-11-21 16:23:38 +05:30
1fea4f18f6 storing the assigned on date and time when am is assigned to host 2025-11-21 15:56:40 +05:30
29cc335a07 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-21 15:54:51 +05:30
paritosh18
2ffb9ca242 Add companyName field and filter for profileImage in getAllHostApplications 2025-11-21 15:54:33 +05:30
577471e7f1 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-21 15:39:51 +05:30
e21ffd08f1 made the presigned url middleware 2025-11-21 14:53:53 +05:30
paritosh18
76e2bd542a Refactor MINGLAR constants to use consistent single quotes and expand HOST_SUGGESTION_TITLES with additional fields 2025-11-21 13:32:03 +05:30
926ea67e41 fixed code 2025-11-21 13:31:41 +05:30
ee9c0027de defined the apis in serverless file 2025-11-21 12:28:47 +05:30
8f25bb87e6 made accept host application and reject host application apis 2025-11-21 12:27:23 +05:30
e366bb4a70 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-21 12:11:44 +05:30
2c3b1af754 updating stepper on company details adding 2025-11-21 11:47:29 +05:30
paritosh18
4c17e1b269 Return initial step for non-existent host records instead of throwing an error 2025-11-20 18:37:26 +05:30
78592dafec removed unnecessary variable declaration 2025-11-20 17:45:15 +05:30
5dc0445a61 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-20 17:33:54 +05:30
2ba9b7778b fixed the varchar length and migrated the prisma 2025-11-20 17:33:41 +05:30
paritosh18
3e12419985 Refactor invite teammate handler to use a single service method encapsulating user creation, revenue, and invite details within a transaction 2025-11-20 17:14:23 +05:30
041c28ea99 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-20 17:00:16 +05:30
5f37455290 added varchar 2025-11-20 17:00:01 +05:30
paritosh18
00dd080ee0 Refactor registration handler to use a transaction for user creation and OTP generation; implement OTP storage and user role assignment 2025-11-20 16:57:10 +05:30
paritosh18
f61287a11d update transaction in invite teammates 2025-11-20 16:40:50 +05:30
paritosh18
1a0fe5768c Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-20 16:30:41 +05:30
e7234c29ad added some columns and fixed some code issues 2025-11-20 16:26:48 +05:30
6bbcb36b10 Add editAgreementDetails and acceptHostApplication handlers; update serverless.yml and Prisma schema 2025-11-20 15:23:15 +05:30
paritosh18
3a29e70b2b CHange path of of GetSuggestion 2025-11-19 18:47:00 +05:30
paritosh18
a3ac533d96 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-19 17:25:38 +05:30
paritosh18
5a9d0d6155 GetSuggstion 2025-11-19 17:20:05 +05:30
13ffee5f7e made submit pqq answer for host api 2025-11-19 16:55:54 +05:30
99cbe55a70 made getall pqq question prepopulate 2025-11-19 15:13:30 +05:30
paritosh18
5e061f33a1 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-19 13:33:54 +05:30
paritosh18
803a11cdda Add suggestion and getsuggestion 2025-11-19 13:33:32 +05:30
58053e061d fixed the category name in seeder 2025-11-19 13:32:57 +05:30
4ea9aa8afa made full excel data seeder for pqq 2025-11-19 13:25:47 +05:30
60e3a0e72e Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-19 13:01:11 +05:30
c544c2ac06 added more data in seeder 2025-11-19 13:01:00 +05:30
paritosh18
d65fc684db prisma 2025-11-19 13:00:25 +05:30
5fac8b0156 migration 2025-11-19 12:26:34 +05:30
4664eae896 made prepolulate api in serverless 2025-11-19 11:34:12 +05:30
844ebb4672 made 2 pqq categories seeder 2025-11-18 22:35:51 +05:30
74b1420b8a Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-18 20:53:30 +05:30
paritosh18
b1068e7106 Add displayOrder field to DocumentType model in schema.prisma 2025-11-18 20:17:05 +05:30
ee38391d1e add payment details modified and making seeder of pqq 2025-11-18 17:44:51 +05:30
05f97d33e7 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-18 14:37:54 +05:30
b2bfed756d made am assign api 2025-11-18 14:36:32 +05:30
5506b5d5cc Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-17 19:06:15 +05:30
paritosh18
94143bd4d0 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-17 19:06:12 +05:30
e42a98f723 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-17 19:05:44 +05:30
paritosh18
c402c76096 feat: add search functionality to getAllHostApplications 2025-11-17 19:05:39 +05:30
db65abf693 fixed the add company details api 2025-11-17 19:05:26 +05:30
paritosh18
a54f61e8c8 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-17 17:39:25 +05:30
7ed225a3f0 made get all co admin and am details 2025-11-17 15:28:22 +05:30
paritosh18
d24fe70046 refactor: update getAllHostApplications query to exclude specific roles 2025-11-17 15:08:41 +05:30
paritosh18
cb70b078e5 refactor: remove roleXid filter from getAllHostApplications query 2025-11-17 13:50:12 +05:30
paritosh18
508487c3a3 feat: enhance host application retrieval to filter by accepted Minglar invitations 2025-11-17 13:45:36 +05:30
paritosh18
399c520985 feat: add getAllHostApplication handler and service method for retrieving host applications 2025-11-17 13:33:51 +05:30
008bcf7c79 fixed the invitation status for minglar admin registration 2025-11-17 12:42:25 +05:30
ce4af2ea3b made getAllInvitation Details api 2025-11-17 12:17:06 +05:30
4ee0819051 integrated sending mail from brevo 2025-11-14 19:06:47 +05:30
paritosh18
54c024fc4f refactor: enhance inviteTeammate handler to utilize MinglarService for user management 2025-11-14 18:52:37 +05:30
e9496d3868 made email sending 2025-11-14 17:48:40 +05:30
paritosh18
d0e6629466 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 17:33:13 +05:30
paritosh18
50878e6374 add inviteTeammate function to handle teammate invitations 2025-11-14 17:33:02 +05:30
5d7ce4455e Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-14 17:32:37 +05:30
8e91fee8ee made update profile for minglar admin 2025-11-14 17:22:07 +05:30
paritosh18
7e0f5b3162 rename endpoint to retrieve roles for Coadmin and Account_manager 2025-11-14 17:01:09 +05:30
paritosh18
d2045ae0b8 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 16:58:57 +05:30
b2298e0fb1 fixed the middleware functions 2025-11-14 16:58:24 +05:30
paritosh18
f23ff39f2e Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 16:33:34 +05:30
paritosh18
d8f08bf564 add prepopulateTeammate function to retrieve roles for Coadmin and Account_manager 2025-11-14 16:33:14 +05:30
paritosh18
f8a405204f Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 15:30:47 +05:30
8dda773b90 fixed the condition in the registration of minglar admin 2025-11-14 15:30:42 +05:30
3e64e50385 fixed the condition for minglar login 2025-11-14 15:25:56 +05:30
paritosh18
ecb866c836 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 15:23:08 +05:30
0353982ff4 fixed the file location of stepper 2025-11-14 15:16:53 +05:30
8c48db854e fixed the stepper endpoint 2025-11-14 15:15:13 +05:30
paritosh18
73a1f0b375 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 15:04:44 +05:30
03c2e729a0 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-14 15:04:26 +05:30
43a2a1c2b1 fixed the getbyid host endpoint 2025-11-14 15:04:01 +05:30
paritosh18
096613f1e2 path changes 2025-11-14 15:03:28 +05:30
6b3d2ae7cf added cors 2025-11-14 14:44:04 +05:30
fb9a92984b fixed the schema issues 2025-11-14 14:08:47 +05:30
paritosh18
224e62ef45 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-13 20:41:13 +05:30
6f75d912eb added statuscode in safehandler 2025-11-13 20:37:07 +05:30
fb044a4535 added new condition for security 2025-11-13 19:45:28 +05:30
paritosh18
ac494455b5 id para change 2025-11-13 19:43:09 +05:30
8d1955f357 refactored serverless file and optimized it 2025-11-13 18:43:09 +05:30
8587016ad5 removed redundant aws package in unwanted functions 2025-11-13 18:10:58 +05:30
8f12151ef1 fixed some logics 2025-11-13 18:01:41 +05:30
477f8d229d Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-13 17:49:20 +05:30
3ac0a591df made add parent company details and upload supporting documents 2025-11-13 17:48:09 +05:30
paritosh18
2ed5435a15 resolvec 2025-11-13 16:04:20 +05:30
paritosh18
ceacdbddcb Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-13 16:00:18 +05:30
paritosh18
fb72feb20e add api 2025-11-13 15:56:13 +05:30
72f9e26ca6 made add company details api and moved it in the host folder 2025-11-13 15:53:35 +05:30
paritosh18
a14f1388f6 add host and minglar admin APIs for registration, login, and retrieval of host details 2025-11-13 14:59:50 +05:30
8e19bb566d made s3 uploader and apis 2025-11-12 19:59:54 +05:30
c0e58fe1ce made register and login apis for host 2025-11-12 16:03:57 +05:30
223 changed files with 37230 additions and 2004 deletions

44
.env.example Normal file
View File

@@ -0,0 +1,44 @@
# Environment Configuration Template
# Copy this file to .env.dev, .env.test, or .env.uat and fill in the values
# Database
DATABASE_URL=
DB_USERNAME=
DB_PASSWORD=
DB_DATABASE_NAME=
DB_HOSTNAME=
DB_PORT=5432
# Email Bypass (set to true for dev/test, false for production-like environments)
BY_PASS_EMAIL=
BYPASS_OTP=
# Brevo Email Configuration
BREVO_EMAIL_API_KEY=
BREVO_API_BASEURL=https://api.brevo.com
BREVO_FROM_EMAIL=
BREVO_SMTP_HOST=smtp-relay.brevo.com
BREVO_SMTP_PORT=587
BREVO_SMTP_USER=
BREVO_SMTP_PASS=
# JWT Configuration
REFRESH_TOKEN_SECRET=
JWT_SECRET=
JWT_ACCESS_EXPIRATION_MINUTES=30
JWT_REFRESH_EXPIRATION_DAYS=7
JWT_RESET_PASSWORD_EXPIRATION_MINUTES=15
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES=15
# Security
SALT_ROUNDS=
NODE_ENV=
# AWS S3
S3_BUCKET_NAME=
# Admin Configuration
MINGLAR_ADMIN_NAME=
MINGLAR_ADMIN_EMAIL=
AM_INVITATION_LINK=
HOST_LINK=

7
.gitignore vendored
View File

@@ -40,10 +40,14 @@ lerna-debug.log*
.env.test.local
.env.production.local
.env.local
.env.dev
.env.test
.env.uat
# temp
.tmp
.temp
undefined/
# Runtime data
pids
@@ -86,6 +90,9 @@ web_modules/
# Optional npm cache directory
.npm
#package.lock.json
.package-lock.json
# Optional eslint cache
.eslintcache

115
CURL_EXAMPLE.md Normal file
View File

@@ -0,0 +1,115 @@
# CURL Command for Testing addCompanyDetails Lambda
## Prerequisites
1. Replace `YOUR_API_URL` with your actual API Gateway URL
2. Replace `YOUR_AUTH_TOKEN` with a valid JWT token
3. Replace file paths with actual document files on your system
## Form Data Structure
The endpoint expects:
- `companyDetails`: JSON string containing company information
- `documents`: JSON string containing array of document metadata
- File fields: One file field per document (e.g., `panFile`, `gstFile`, etc.)
## CURL Command
```bash
curl -X POST "YOUR_API_URL/minglaradmin/add-company-details" \
-H "x-auth-token: YOUR_AUTH_TOKEN" \
-F "companyDetails={\"companyName\":\"Test Company\",\"hostRefNumber\":\"HOST001\",\"address1\":\"123 Main St\",\"address2\":\"Suite 100\",\"cityXid\":1,\"stateXid\":1,\"countryXid\":1,\"pinCode\":\"12345\",\"isSubsidairy\":false,\"registrationNumber\":\"REG123456\",\"panNumber\":\"ABCDE1234F\",\"gstNumber\":\"27ABCDE1234F1Z5\",\"formationDate\":\"2020-01-01\",\"companyType\":\"Private Limited\",\"currencyXid\":1}" \
-F "documents=[{\"documentTypeXid\":1,\"documentName\":\"pan.pdf\",\"fieldName\":\"panFile\"},{\"documentTypeXid\":2,\"documentName\":\"gst.pdf\",\"fieldName\":\"gstFile\"},{\"documentTypeXid\":3,\"documentName\":\"registration.pdf\",\"fieldName\":\"registrationFile\"},{\"documentTypeXid\":4,\"documentName\":\"aadhaar.pdf\",\"fieldName\":\"aadhaarFile\"}]" \
-F "panFile=@/path/to/pan.pdf" \
-F "gstFile=@/path/to/gst.pdf" \
-F "registrationFile=@/path/to/registration.pdf" \
-F "aadhaarFile=@/path/to/aadhaar.pdf"
```
## Example with Real Values
```bash
curl -X POST "https://abc123.execute-api.ap-south-1.amazonaws.com/minglaradmin/add-company-details" \
-H "x-auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-F "companyDetails={\"companyName\":\"Acme Corp\",\"hostRefNumber\":\"HOST001\",\"address1\":\"123 Business Park\",\"address2\":\"Building A\",\"cityXid\":1,\"stateXid\":1,\"countryXid\":1,\"pinCode\":\"400001\",\"isSubsidairy\":false,\"registrationNumber\":\"U12345MH2020PTC123456\",\"panNumber\":\"ABCDE1234F\",\"gstNumber\":\"27ABCDE1234F1Z5\",\"formationDate\":\"2020-01-15\",\"companyType\":\"Private Limited\",\"websiteUrl\":\"https://acme.com\",\"currencyXid\":1}" \
-F "documents=[{\"documentTypeXid\":1,\"documentName\":\"pan-certificate.pdf\",\"fieldName\":\"panFile\"},{\"documentTypeXid\":2,\"documentName\":\"gst-certificate.pdf\",\"fieldName\":\"gstFile\"},{\"documentTypeXid\":3,\"documentName\":\"registration-certificate.pdf\",\"fieldName\":\"registrationFile\"},{\"documentTypeXid\":4,\"documentName\":\"aadhaar.pdf\",\"fieldName\":\"aadhaarFile\"}]" \
-F "panFile=@./documents/pan.pdf" \
-F "gstFile=@./documents/gst.pdf" \
-F "registrationFile=@./documents/registration.pdf" \
-F "aadhaarFile=@./documents/aadhaar.pdf"
```
## Postman Setup
1. **Method**: POST
2. **URL**: `YOUR_API_URL/minglaradmin/add-company-details`
3. **Headers**:
- `x-auth-token`: `YOUR_AUTH_TOKEN`
4. **Body**: Select `form-data`
5. **Add Fields**:
- `companyDetails` (Text): JSON string with company details
- `documents` (Text): JSON array string with document metadata
- `panFile` (File): Select PDF file
- `gstFile` (File): Select PDF file
- `registrationFile` (File): Select PDF file
- `aadhaarFile` (File): Select PDF file
## Required Document Types
Based on `REQUIRED_DOC_TYPES`:
- `documentTypeXid: 1` - PAN
- `documentTypeXid: 2` - GST
- `documentTypeXid: 3` - REGISTRATION
- `documentTypeXid: 4` - AADHAAR
## Company Details JSON Structure
```json
{
"companyName": "string (required)",
"hostRefNumber": "string (required)",
"address1": "string (required)",
"address2": "string (optional)",
"cityXid": "number (required)",
"stateXid": "number (required)",
"countryXid": "number (required)",
"pinCode": "string (required, min 4 chars)",
"logoPath": "string (optional)",
"isSubsidairy": "boolean (required)",
"registrationNumber": "string (required)",
"panNumber": "string (required)",
"gstNumber": "string (optional)",
"formationDate": "string (required, ISO date)",
"companyType": "string (required)",
"websiteUrl": "string (optional, must be valid URL)",
"instagramUrl": "string (optional, must be valid URL)",
"facebookUrl": "string (optional, must be valid URL)",
"linkedinUrl": "string (optional, must be valid URL)",
"twitterUrl": "string (optional, must be valid URL)",
"currencyXid": "number (required)"
}
```
## Documents JSON Structure
```json
[
{
"documentTypeXid": 1,
"documentName": "pan.pdf",
"fieldName": "panFile"
},
{
"documentTypeXid": 2,
"documentName": "gst.pdf",
"fieldName": "gstFile"
}
]
```
## Notes
- All 4 required document types (PAN, GST, REGISTRATION, AADHAAR) must be provided
- File field names in the `documents` array must match the form field names
- Files can be PDF, images, or any other document type
- The endpoint supports both `multipart/form-data` and JSON (for backward compatibility)

View File

@@ -1,62 +0,0 @@
# Multi-stage build for NestJS Serverless Application
FROM node:18-alpine AS builder
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY prisma ./prisma/
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy source code
COPY . .
# Generate Prisma client
RUN npx prisma generate
# Build the application
RUN npm run build
# Production stage
FROM node:18-alpine AS production
# Set working directory
WORKDIR /app
# Install serverless framework globally
RUN npm install -g serverless
# Copy package files
COPY package*.json ./
# Install production dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy built application
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
# Copy serverless configuration
COPY serverless*.yml ./
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001
# Change ownership
RUN chown -R nestjs:nodejs /app
USER nestjs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start the application
CMD ["npm", "run", "start:prod"]

View File

@@ -0,0 +1,490 @@
# AWS Lambda Bundle Size Optimization Guide
## Overview
This guide documents how to optimize AWS Lambda function bundle sizes when using:
- **Serverless Framework v4** (with built-in esbuild)
- **Prisma ORM** (with driver adapters)
- **NestJS** or any Node.js framework
## Problem
Lambda functions can become bloated (25+ MB) due to:
1. **Prisma binary engines** (~50MB uncompressed)
2. **AWS SDK v3** being bundled (~5-10MB)
3. **Dependencies copied to node_modules** instead of being bundled
## Solution Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Lambda Function │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Your Code (bundled by esbuild) ~50-500 KB │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Prisma Layer (shared) ~15 MB │ │
│ │ - @prisma/client │ │
│ │ - @prisma/adapter-pg │ │
│ │ - .prisma/client (generated) │ │
│ │ - pg driver │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ AWS Lambda Runtime │ │
│ │ - AWS SDK v3 (built-in for Node.js 18+) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## Step-by-Step Setup
### 1. Project Structure
```
your-project/
├── serverless.yml
├── package.json
├── prisma/
│ └── schema.prisma
├── layers/
│ └── prisma/
│ └── nodejs/
│ └── package.json
├── src/
│ └── ... your code
└── build-prisma-layer.ps1 (or .sh for Linux/Mac)
```
### 2. Prisma Schema Configuration
**`prisma/schema.prisma`**
```prisma
generator client {
provider = "prisma-client-js"
// For Prisma 7+ with driver adapters, no binary targets needed
// The WASM-based query engine is used automatically
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```
> **Note**: Prisma 7+ uses WASM-based query compiler instead of binary engines when using driver adapters, which is much smaller.
### 3. Layer Package.json
**`layers/prisma/nodejs/package.json`**
```json
{
"name": "prisma-layer",
"version": "1.0.0",
"description": "Lambda layer for Prisma with pg driver adapter",
"dependencies": {
"@prisma/client": "^7.0.0",
"@prisma/adapter-pg": "^7.0.0",
"pg": "^8.13.0"
}
}
```
### 4. Serverless Configuration
**`serverless.yml`**
```yaml
service: your-service-name
provider:
name: aws
runtime: nodejs22.x
region: your-region
memorySize: 512
# Apply Prisma layer to ALL functions
layers:
- !Ref PrismaLambdaLayer
environment:
DATABASE_URL: ${env:DATABASE_URL}
# ... other env vars
# esbuild configuration (Serverless v4 built-in)
build:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node20
platform: node
# Mark packages as external (not bundled into JS)
external:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma/client'
- '.prisma'
- 'pg'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
# Exclude from npm install in zip (CRITICAL!)
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
# Define the Prisma layer
layers:
prisma:
path: layers/prisma
name: ${self:service}-prisma-layer-${sls:stage}
description: Prisma client with pg driver adapter
compatibleRuntimes:
- nodejs22.x
retain: false
# Package configuration
package:
individually: true
patterns:
- '!node_modules/**'
- '!node_modules/@prisma/**'
- '!node_modules/.prisma/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!.git/**'
functions:
myFunction:
handler: src/handlers/myHandler.handler
events:
- httpApi:
path: /my-endpoint
method: get
plugins:
- serverless-offline
```
### 5. Build Script for Prisma Layer
**Windows (PowerShell) - `build-prisma-layer.ps1`**
```powershell
# Build Prisma Lambda Layer
$layerPath = "layers\prisma\nodejs"
Write-Host "Building Prisma Lambda Layer..." -ForegroundColor Cyan
# 1. Clean existing node_modules in layer
Write-Host "Cleaning layer node_modules..."
if (Test-Path "$layerPath\node_modules") {
Remove-Item -Recurse -Force "$layerPath\node_modules"
}
# 2. Install dependencies in layer
Write-Host "Installing layer dependencies..."
Push-Location $layerPath
npm install --omit=dev
Pop-Location
# 3. Generate Prisma client
Write-Host "Generating Prisma client..."
npx prisma generate
# 4. Copy .prisma/client to layer
Write-Host "Copying generated Prisma client to layer..."
$sourcePrisma = "node_modules\.prisma"
$destPrisma = "$layerPath\node_modules\.prisma"
if (Test-Path $sourcePrisma) {
if (Test-Path $destPrisma) {
Remove-Item -Recurse -Force $destPrisma
}
Copy-Item -Recurse $sourcePrisma $destPrisma
Write-Host "Copied .prisma/client successfully!" -ForegroundColor Green
} else {
Write-Host "ERROR: .prisma folder not found. Run 'npx prisma generate' first." -ForegroundColor Red
exit 1
}
# 5. Show layer size
$layerSize = (Get-ChildItem "$layerPath\node_modules" -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "`nTotal layer size: $([math]::Round($layerSize, 2)) MB" -ForegroundColor Yellow
Write-Host "Prisma layer built successfully!" -ForegroundColor Green
```
**Linux/Mac (Bash) - `build-prisma-layer.sh`**
```bash
#!/bin/bash
set -e
LAYER_PATH="layers/prisma/nodejs"
echo "Building Prisma Lambda Layer..."
# 1. Clean existing node_modules in layer
echo "Cleaning layer node_modules..."
rm -rf "$LAYER_PATH/node_modules"
# 2. Install dependencies in layer
echo "Installing layer dependencies..."
cd "$LAYER_PATH"
npm install --omit=dev
cd -
# 3. Generate Prisma client
echo "Generating Prisma client..."
npx prisma generate
# 4. Copy .prisma/client to layer
echo "Copying generated Prisma client to layer..."
if [ -d "node_modules/.prisma" ]; then
rm -rf "$LAYER_PATH/node_modules/.prisma"
cp -r "node_modules/.prisma" "$LAYER_PATH/node_modules/.prisma"
echo "Copied .prisma/client successfully!"
else
echo "ERROR: .prisma folder not found. Run 'npx prisma generate' first."
exit 1
fi
# 5. Show layer size
LAYER_SIZE=$(du -sm "$LAYER_PATH/node_modules" | cut -f1)
echo "Total layer size: ${LAYER_SIZE} MB"
echo "Prisma layer built successfully!"
```
### 6. Prisma Client Usage
**`src/common/database/prisma.client.ts`**
```typescript
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
// Connection pool for serverless
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 5, // Limit connections in Lambda
});
const adapter = new PrismaPg(pool);
// Single instance for Lambda warm starts
let prisma: PrismaClient;
export function getPrismaClient(): PrismaClient {
if (!prisma) {
prisma = new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
}
return prisma;
}
export { prisma };
```
---
## Deployment Workflow
```bash
# 1. Build the Prisma layer (run after schema changes)
./build-prisma-layer.ps1 # Windows
# or
./build-prisma-layer.sh # Linux/Mac
# 2. Deploy
npx serverless deploy --stage=dev
# 3. Deploy single function (faster, uses existing layer)
npx serverless deploy function -f myFunction --stage=dev
```
---
## Key Configuration Explained
### esbuild `external` vs `exclude`
| Property | Purpose |
|----------|---------|
| `external` | Tells esbuild NOT to bundle these into the JS file. They become `require()` calls. |
| `exclude` | Tells Serverless NOT to `npm install` these packages into the function zip. |
**Both are required!**
- `external` alone = esbuild doesn't bundle, but Serverless still installs to node_modules
- `exclude` alone = Serverless doesn't install, but esbuild bundles the code
### Layer Reference
```yaml
# This creates a CloudFormation reference to the layer defined in the same stack
layers:
- !Ref PrismaLambdaLayer
```
The `PrismaLambdaLayer` name comes from the layer key (`prisma`) converted to PascalCase + `LambdaLayer`.
### Why Exclude pg-* packages?
When `pg` is external, its dependencies still get installed:
- `pg-connection-string`
- `pg-pool`
- `pg-protocol`
- `pg-types`
- etc.
These must all be in the `exclude` list to prevent duplication.
---
## Expected Results
| Function Type | Before | After |
|---------------|--------|-------|
| Simple handlers | 25+ MB | **50-100 kB** |
| With validation (zod/yup) | 25+ MB | **300-500 kB** |
| With S3 uploads | 30+ MB | **1-2 MB** |
---
## Troubleshooting
### 1. "Cannot find module '@prisma/client'"
**Cause**: Layer doesn't have the generated `.prisma/client`
**Fix**: Run `build-prisma-layer.ps1` to regenerate the layer
### 2. Function size still large
**Debug**: Extract and inspect the zip:
```powershell
Expand-Archive ".serverless\build\your-function.zip" -DestinationPath "extracted"
Get-ChildItem "extracted\node_modules" -Directory
```
If you see `@prisma` or `pg` folders, the `exclude` config isn't working.
### 3. "Cannot resolve CloudFormation reference"
**Cause**: Using `${cf:...}` reference before first deploy
**Fix**: Use `!Ref PrismaLambdaLayer` instead (works on first deploy)
### 4. Cold starts still slow
Consider:
- **Provisioned Concurrency**: Pre-warm instances
- **Reduce memory**: Sometimes lower memory = same speed, lower cost
- **Connection pooling**: Use tools like PgBouncer for RDS
---
## Additional Optimizations
### 1. Remove duplicate validation libraries
Pick ONE of: `zod`, `yup`, or `class-validator`. Don't use all three.
### 2. Tree-shake NestJS
If not using full NestJS, import only what you need:
```typescript
// Instead of
import { Controller, Get } from '@nestjs/common';
// For Lambda handlers, you might not need NestJS at all
```
### 3. Use AWS SDK v3 selectively
```yaml
external:
- '@aws-sdk/*' # Exclude all
```
Then in code:
```typescript
// AWS SDK v3 is available in Lambda runtime (Node.js 18+)
import { S3Client } from '@aws-sdk/client-s3';
```
---
## Quick Reference
```yaml
# Minimal serverless.yml for Prisma + Lambda optimization
build:
esbuild:
bundle: true
minify: true
external:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma/client'
- '.prisma'
- 'pg'
- '@aws-sdk/*'
exclude:
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
- '@aws-sdk/*'
layers:
prisma:
path: layers/prisma
name: ${self:service}-prisma-${sls:stage}
compatibleRuntimes:
- nodejs22.x
provider:
layers:
- !Ref PrismaLambdaLayer
```
---
## Version Compatibility
| Tool | Tested Version |
|------|----------------|
| Serverless Framework | v4.x |
| Prisma | v7.x |
| Node.js | 20.x, 22.x |
| AWS Lambda Runtime | nodejs20.x, nodejs22.x |
---
*Last updated: December 2025*

51
build-prisma-layer.ps1 Normal file
View File

@@ -0,0 +1,51 @@
# Build Prisma Lambda Layer
# Run this script before deploying to ensure the layer has the generated client
$layerPath = "layers\prisma\nodejs"
Write-Host "Building Prisma Lambda Layer..." -ForegroundColor Cyan
# 1. Clean existing node_modules in layer
Write-Host "Cleaning layer node_modules..."
if (Test-Path "$layerPath\node_modules") {
Remove-Item -Recurse -Force "$layerPath\node_modules"
}
# 2. Install dependencies in layer
Write-Host "Installing layer dependencies..."
Push-Location $layerPath
npm install --omit=dev
Pop-Location
# 3. Generate Prisma client into the layer
Write-Host "Generating Prisma client into layer..."
# Set the output directory for Prisma client
$env:PRISMA_GENERATE_DATAPROXY = "false"
# Generate client - this creates .prisma/client
npx prisma generate
# 4. Copy .prisma/client to layer
Write-Host "Copying generated Prisma client to layer..."
$sourcePrisma = "node_modules\.prisma"
$destPrisma = "$layerPath\node_modules\.prisma"
if (Test-Path $sourcePrisma) {
if (Test-Path $destPrisma) {
Remove-Item -Recurse -Force $destPrisma
}
Copy-Item -Recurse $sourcePrisma $destPrisma
Write-Host "Copied .prisma/client successfully!" -ForegroundColor Green
} else {
Write-Host "ERROR: .prisma folder not found. Run 'npx prisma generate' first." -ForegroundColor Red
exit 1
}
# 5. Show layer size
Write-Host "`nLayer contents:"
Get-ChildItem "$layerPath\node_modules" -Directory | Select-Object Name
$layerSize = (Get-ChildItem "$layerPath\node_modules" -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "`nTotal layer size: $([math]::Round($layerSize, 2)) MB" -ForegroundColor Yellow
Write-Host "`nPrisma layer built successfully!" -ForegroundColor Green
Write-Host "Run 'serverless deploy' to deploy with the updated layer."

View File

@@ -1,86 +0,0 @@
version: '3.8'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: nestjs-postgres
restart: unless-stopped
environment:
POSTGRES_DB: nestjs_user_crud
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- nestjs-network
# Redis for caching (optional)
redis:
image: redis:7-alpine
container_name: nestjs-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- nestjs-network
# NestJS Application
app:
build:
context: .
dockerfile: Dockerfile
target: production
container_name: nestjs-app
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/nestjs_user_crud?schema=public
JWT_SECRET: docker-jwt-secret-key
JWT_EXPIRES_IN: 7d
API_PREFIX: api/v1
API_VERSION: 1.0.0
THROTTLE_TTL: 60
THROTTLE_LIMIT: 10
CORS_ORIGIN: http://localhost:3000
depends_on:
- postgres
- redis
networks:
- nestjs-network
volumes:
- ./src:/app/src
- ./prisma:/app/prisma
# Prisma Studio
prisma-studio:
image: node:18-alpine
container_name: nestjs-prisma-studio
restart: unless-stopped
ports:
- "5555:5555"
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/nestjs_user_crud?schema=public
working_dir: /app
volumes:
- .:/app
command: sh -c "npm install && npx prisma generate && npx prisma studio --hostname 0.0.0.0"
depends_on:
- postgres
networks:
- nestjs-network
volumes:
postgres_data:
redis_data:
networks:
nestjs-network:
driver: bridge

View File

@@ -1,19 +0,0 @@
-- Initialize database for NestJS Serverless Application
-- This file is executed when the PostgreSQL container starts
-- Create database if it doesn't exist
SELECT 'CREATE DATABASE nestjs_user_crud'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'nestjs_user_crud')\gexec
-- Connect to the database
\c nestjs_user_crud;
-- Create extensions if needed
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Set timezone
SET timezone = 'UTC';
-- Create a user for the application (optional)
-- CREATE USER nestjs_user WITH PASSWORD 'nestjs_password';
-- GRANT ALL PRIVILEGES ON DATABASE nestjs_user_crud TO nestjs_user;

53
insertCities.js Normal file
View File

@@ -0,0 +1,53 @@
import { PrismaClient } from '@prisma/client';
import fs from 'fs';
import path from 'path';
const prisma = new PrismaClient();
async function insertCities() {
try {
const statesFolder = path.join(process.cwd(), 'states-cities');
const files = fs.readdirSync(statesFolder);
for (const file of files) {
if (!file.endsWith('.json')) continue;
const stateName = file.replace('.json', '');
const state = await prisma.states.findFirst({
where: { stateName },
});
if (!state) {
console.log(`❌ State not found: ${stateName}`);
continue;
}
const filePath = path.join(statesFolder, file);
const citiesData = JSON.parse(
fs.readFileSync(filePath, 'utf-8')
);
await prisma.cities.createMany({
data: citiesData.map((city) => ({
stateXid: state.id,
cityName:
typeof city === 'string'
? city.trim()
: city.cityName.trim(),
})),
skipDuplicates: true,
});
console.log(`${stateName} cities inserted`);
}
console.log('🎉 All cities inserted successfully');
} catch (error) {
console.error('Error inserting cities:', error);
} finally {
await prisma.$disconnect();
}
}
insertCities();

217
layers/prisma/nodejs/package-lock.json generated Normal file
View File

@@ -0,0 +1,217 @@
{
"name": "prisma-layer",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prisma-layer",
"version": "1.0.0",
"dependencies": {
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"pg": "^8.13.0",
"zod": "^4.1.12"
}
},
"node_modules/@prisma/adapter-pg": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.2.0.tgz",
"integrity": "sha512-euIdQ13cRB2wZ3jPsnDnFhINquo1PYFPCg6yVL8b2rp3EdinQHsX9EDdCtRr489D5uhphcRk463OdQAFlsCr0w==",
"dependencies": {
"@prisma/driver-adapter-utils": "7.2.0",
"pg": "^8.16.3",
"postgres-array": "3.0.4"
}
},
"node_modules/@prisma/client": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.2.0.tgz",
"integrity": "sha512-JdLF8lWZ+LjKGKpBqyAlenxd/kXjd1Abf/xK+6vUA7R7L2Suo6AFTHFRpPSdAKCan9wzdFApsUpSa/F6+t1AtA==",
"dependencies": {
"@prisma/client-runtime-utils": "7.2.0"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24.0"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/@prisma/client-runtime-utils": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.2.0.tgz",
"integrity": "sha512-dn7oB53v0tqkB0wBdMuTNFNPdEbfICEUe82Tn9FoKAhJCUkDH+fmyEp0ClciGh+9Hp2Tuu2K52kth2MTLstvmA=="
},
"node_modules/@prisma/debug": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz",
"integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="
},
"node_modules/@prisma/driver-adapter-utils": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz",
"integrity": "sha512-gzrUcbI9VmHS24Uf+0+7DNzdIw7keglJsD5m/MHxQOU68OhGVzlphQRobLiDMn8CHNA2XN8uugwKjudVtnfMVQ==",
"dependencies": {
"@prisma/debug": "7.2.0"
}
},
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pg-types/node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/postgres-array": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/zod": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "prisma-layer",
"version": "1.0.0",
"description": "Lambda layer for Prisma 7 with pg driver adapter and zod",
"dependencies": {
"@prisma/client": "^7.0.1",
"@prisma/adapter-pg": "^7.0.1",
"pg": "^8.13.0",
"zod": "^4.1.12"
}
}

8294
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,9 +22,16 @@
"prisma:push": "prisma db push",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"prisma:seed": "ts-node prisma/seed.ts"
"prisma:seed": "ts-node prisma/seed.ts",
"seeder": "tsx prisma/seed.ts"
},
"dependencies": {
"@aws-crypto/crc32c": "^5.2.0",
"@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.928.0",
"@aws/lambda-invoke-store": "^0.2.1",
"@nestjs/common": "^10.3.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.3.0",
@@ -33,23 +40,44 @@
"@nestjs/platform-express": "^10.3.0",
"@nestjs/swagger": "^7.1.17",
"@nestjs/throttler": "^5.1.1",
"@prisma/client": "^5.8.1",
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@smithy/middleware-stack": "^4.2.5",
"@smithy/protocol-http": "^5.3.5",
"@smithy/types": "^4.9.0",
"@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",
"serverless": "4.17.0",
"serverless": "4.24.0",
"swagger-ui-express": "^5.0.0",
"yup": "^1.7.1"
"tslib": "^2.8.1",
"uuid": "^13.0.0",
"yup": "^1.7.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
@@ -69,14 +97,15 @@
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"prisma": "^5.8.1",
"serverless-esbuild": "^1.55.1",
"serverless-offline": "^14.4.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.6",
"typescript": "^5.3.3"
},
"jest": {

12
prisma.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
})

64
prisma/citiesSeeder.ts Normal file
View File

@@ -0,0 +1,64 @@
import fs from 'fs'
import path from 'path'
import { PrismaClient } from '@prisma/client'
export async function seedCities(prisma: PrismaClient) {
const statesFolder = path.join(process.cwd(), 'states-cities')
const files = fs.readdirSync(statesFolder)
for (const file of files) {
if (!file.endsWith('.json')) continue
const stateName = file.replace('.json', '')
const state = await prisma.states.findFirst({
where: {
stateName: {
equals: stateName,
mode: 'insensitive',
},
},
})
if (!state) {
console.log(`❌ State not found: ${stateName}`)
continue
}
const filePath = path.join(statesFolder, file)
const rawData = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
if (!rawData.districts) {
console.log(`❌ Invalid structure in ${file}`)
continue
}
const allVillages: string[] = []
for (const district of rawData.districts) {
for (const sub of district.subDistricts || []) {
for (const village of sub.villages || []) {
if (village && village.trim()) {
allVillages.push(village.trim())
}
}
}
}
console.log(`📦 Total villages found in ${stateName}:`, allVillages.length)
const result = await prisma.cities.createMany({
data: allVillages.map((village) => ({
stateXid: state.id,
cityName: village,
})),
skipDuplicates: true,
})
console.log(`${stateName} inserted: ${result.count}`)
}
console.log('🎉 All states processed successfully!')
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

10
prisma/prisma.ts Normal file
View File

@@ -0,0 +1,10 @@
// prisma.ts
// Re-export from the main singleton for consistency
import { prisma } from '../src/common/database/prisma.client';
export { prisma };
process.on('SIGINT', async () => {
await prisma.$disconnect();
process.exit(0);
});

File diff suppressed because it is too large Load Diff

1679
prisma/seed.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,41 @@
service: minglarDev
useDotenv: true
params:
dev:
stage: dev
test:
stage: test
uat:
stage: uat
provider:
name: aws
runtime: nodejs20.x
runtime: nodejs22.x
region: ap-south-1
stage: ${opt:stage, 'dev'}
versionFunctions: false
memorySize: 512
# Apply Prisma layer to all functions
# Reference the layer defined in this stack using CloudFormation Ref
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, ''}
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}
@@ -18,56 +47,106 @@ provider:
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}
httpApi:
cors:
allowedOrigins: ['*']
allowedHeaders:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Auth-Token
allowCredentials: false
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: false
minify: true
sourcemap: false
exclude: ['aws-sdk']
target: node20
platform: node
concurrency: 10
outdir: dist
# Mark as external so they're not bundled into the JS
external:
- '@prisma/client'
- '.prisma/client'
- '.prisma'
- '@prisma/adapter-pg'
- 'pg'
- 'zod'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
# Exclude prevents npm install of these packages in the zip
exclude:
- 'aws-sdk'
- '@aws-sdk/*'
- '@smithy/*'
- '@aws-crypto/*'
- '@prisma/adapter-pg'
- '@prisma/client'
- '.prisma'
- '.prisma/client'
- 'pg'
- 'zod'
- 'pg-*'
- 'postgres-*'
- 'pgpass'
- 'split2'
- 'xtend'
# Define layers
layers:
prisma:
path: layers/prisma
name: ${self:service}-prisma-layer-${sls:stage}
description: Prisma 7 client with pg driver adapter (no binary engines)
compatibleRuntimes:
- nodejs22.x
retain: false
package:
individually: true
patterns:
- '!node_modules/**' # exclude all node_modules first
- '!**/*.spec.ts'
- '!**/*.test.ts'
- '!**/*.log'
- 'src/**' # include all source files
- 'common/**' # include common modules
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/**'
- 'prisma/schema.prisma'
- '!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/**'
# Import function definitions from separate files organized by module
functions:
# 👇 Example Lambda for Host Module
getHosts:
handler: src/modules/host/handlers/host.handler
package:
patterns:
- 'src/modules/host/**'
- 'common/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
- 'node_modules/@prisma/client/**'
- 'prisma/schema.prisma'
events:
- httpApi:
path: /host
method: get
- ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/minglaradmin.yml)}
- ${file(./serverless/functions/prepopulate.yml)}
- ${file(./serverless/functions/pqq.yml)}
- ${file(./serverless/functions/user.yml)}
plugins:
- serverless-offline

View File

@@ -0,0 +1,557 @@
# Host Module Functions
# All authentication and host management endpoints
getHosts:
handler: src/modules/host/handlers/host.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/host.*'
- '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
method: get
verifyOTP:
handler: src/modules/host/handlers/Host_Admin/onboarding/verifyOTP.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/verifyOtp.*'
- '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/Host_Admin/onboarding/verify-otp
method: post
login:
handler: src/modules/host/handlers/Host_Admin/onboarding/login.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/loginForHost.*'
- '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/Host_Admin/onboarding/login
method: post
signUp:
handler: src/modules/host/handlers/Host_Admin/onboarding/signUp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/registration.*'
- '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/Host_Admin/onboarding/registration
method: post
createPassword:
handler: src/modules/host/handlers/Host_Admin/onboarding/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/createPassword.*'
- '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/Host_Admin/onboarding/create-password
method: post
updateBankDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addPaymentDetails.*'
- '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/Host_Admin/onboarding/add-payment-details
method: post
saveActivity_ForPQQ:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/saveActivity_ForPQQ.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addActivity.*'
- '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/Activity_Hub/OnBoarding/add-activity
method: post
getHostById:
handler: src/modules/host/handlers/getbyidhandler.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getbyidhandler.*'
- '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/getById
method: get
getPQQ_ByQuestionId:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_ByQuestionId.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getByIdPQQ.*'
- '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/Activity_Hub/OnBoarding/get-pqq-question-details
method: get
getPQQ_LastUpdatedQuestion:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_LastUpdatedQuestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getLatestQuestionDetailsPQQ.*'
- '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/Activity_Hub/OnBoarding/get-latest-pqq-question-details
method: get
prePopulateNewActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllActivityType.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getActivityType.*'
- '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/Activity_Hub/OnBoarding/prepopulate-new-activity
method: get
createNewActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.handler
memorySize: 1024
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.*'
- '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/Activity_Hub/OnBoarding/create-new-activity
method: patch
showSuggestion:
handler: src/modules/host/handlers/Host_Admin/onboarding/showSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Host_Admin/onboarding/showSuggestion.handler.*'
- '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/get-suggestion
method: get
getAllActivitySuggestion:
handler: src/modules/host/handlers/Host_Admin/onboarding/getAllActvitySuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Host_Admin/onboarding/getAllActvitySuggestion.handler.*'
- '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/get-Activity-suggestion
method: get
getAllHostActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllHostActivity.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/getAllHostActivity.*'
- '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/Activity_Hub/OnBoarding/get-all-host-activity
method: get
acceptAggrement:
handler: src/modules/host/handlers/Host_Admin/onboarding/acceptAggrement.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/acceptAgreement.*'
- '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/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/Host_Admin/onboarding/get-latest-agreement
method: get
getStepperInfo:
handler: src/modules/host/handlers/getStepper.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getStepper.*'
- 'src/common/utils/handlers/safeHandler.*'
- 'src/common/database/**'
- '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: /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: /host/profile
method: patch
# Functions with S3/AWS SDK dependencies
submitCompanyDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
memorySize: 1024
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/addCompanyDetails.*'
- 'src/modules/host/services/**'
- 'src/common/**'
events:
- httpApi:
path: /host/Host_Admin/onboarding/add-company-details
method: patch
submitPQQ_Answer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQ_Answer.handler
memorySize: 1024
package:
patterns:
- 'src/modules/host/handlers/submitPqqAns.*'
- '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/Activity_Hub/OnBoarding/submit-pqq-answer
method: patch
updatePQQ_LastAnswer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQScore.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/submitPqqAns.*'
- '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/Activity_Hub/OnBoarding/submit-final-pqq-answer
method: post
submitPQQForReview:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.*'
- '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/Activity_Hub/OnBoarding/submit-pqq-for-review
method: patch
getAllPQQwithSubmittedAns:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/prepopulate/**'
- ${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/get-all-pqq-ques-submited-ans
method: get
getAllDetailsOfActivityAndVenue:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllDetailsOfActivityAndVenue.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/**'
- ${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/get-all-details-activity-venue/{activityXid}
method: get
updateSuggestionAsReviewed:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/updateSuggestionAsReviewed.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/**'
- ${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/update-suggestion-reviewed
method: patch
resendOTPmail:
handler: src/modules/host/handlers/resendOtp.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/resendOtp/**'
- ${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: /resend-otp
method: post
mediaUploadTos3:
handler: src/modules/host/handlers/mediaUploadToS3.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/mediaUploadToS3/**'
- ${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: /media/upload/activity/{activityXid}
method: post
venueMediaUploadTos3:
handler: src/modules/host/handlers/mediaUploadForVenueToS3.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/mediaUploadForVenueToS3/**'
- ${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: /media/upload/venue/activity/{activityXid}
method: post
mediaDeleteFroms3:
handler: src/modules/host/handlers/mediaDeleteFromS3.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/mediaDeleteFromS3/**'
- ${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: /media/delete
method: delete
createSchedulingForAct:
handler: src/modules/host/handlers/Activity_Hub/Scheduling/createSchedulingOfAct.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/Scheduling/**'
- ${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: /scheduling/create
method: post
getActivitiesByStatus:
handler: src/modules/host/handlers/Activity_Hub/Scheduling/getSchedulingOfAct.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/Scheduling/getSchedulingOfAct.*'
- '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: /scheduling/get-all-activities
method: get
getVenueDurationByAct:
handler: src/modules/host/handlers/Activity_Hub/Scheduling/getVenueDurationByAct.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/Scheduling/getVenueDurationByAct.*'
- '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: /scheduling/get-venue-duration/{activityXid}
method: get
cancelSlotForActivity:
handler: src/modules/host/handlers/Activity_Hub/Scheduling/cancelSlot.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/Scheduling/**'
- ${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: /scheduling/cancel-slot
method: post
openCanceledSlotForActivity:
handler: src/modules/host/handlers/Activity_Hub/Scheduling/openCanceledSlot.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/Scheduling/**'
- ${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: /scheduling/open-canceled-slot
method: patch

View File

@@ -0,0 +1,472 @@
# Minglar Admin Module Functions
# Admin dashboard and management endpoints
minglarRegistration:
handler: src/modules/minglaradmin/handlers/registration.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- ${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: /minglaradmin/registration
method: post
minglarLoginForAdmin:
handler: src/modules/minglaradmin/handlers/loginForMinglar.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- ${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: /minglaradmin/login
method: post
minglarCreatePassword:
handler: src/modules/minglaradmin/handlers/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- ${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: /minglaradmin/create-password
method: post
updateMinglarProfile:
handler: src/modules/minglaradmin/handlers/updateProfile.handler
memorySize: 1024
timeout: 30
package:
patterns:
- 'src/modules/minglaradmin/handlers/updateProfile.*'
- 'src/modules/minglaradmin/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: /minglaradmin/update-profile
method: patch
prepopulateRole:
handler: src/modules/minglaradmin/handlers/prepopulateRole.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- ${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: /minglaradmin/prepopulate-Roles
method: get
getHostDetailsById:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/getByIdHostDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- ${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: /minglaradmin/hosthub/hosts/get-host-details/{host_xid}
method: get
inviteTeammate:
handler: src/modules/minglaradmin/handlers/settings/teammates/inviteTeammate.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/teammates/**'
- ${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: /minglaradmin/settings/teammates/invite-teammate
method: post
getAllHostApplication:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/getAllHostApplicationForAM.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/get-all-host-applications-am
method: get
getAllHostActivityForAdmin:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/getAllActivityOfHost.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/getAllActivityOfHost.handler.*'
- 'src/modules/minglaradmin/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: /minglaradmin/get-all-activity-of-host/{id}
method: get
getAllOnboardingHostApplications:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/getAllOnboardingHosts.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin
method: get
getAllOnboardingHostApplications_New:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/getOnboardingNewApplications.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin-new
method: get
getAllInvitationDetails:
handler: src/modules/minglaradmin/handlers/settings/teammates/getAllInvitationDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/teammates**'
- ${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: /minglaradmin/settings/teammates/get-all-invitation-details
method: get
addSuggestion:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/addSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/add-suggestion
method: post
getAllCoadminAndAMDetails:
handler: src/modules/minglaradmin/handlers/settings/teammates/getAllCoadminAndAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/teammates/**'
- ${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: /minglaradmin/settings/teammates/get-all-coadmin-am
method: get
getAllInvitedCoadminAndAMDetails:
handler: src/modules/minglaradmin/handlers/settings/teammates/getAllInvitedCoadminAndAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/teammates/**'
- ${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: /minglaradmin/settings/teammates/get-all-invited-coadmin-am
method: get
getAmDetailsbyId:
handler: src/modules/minglaradmin/handlers/getAmDetail_ById.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/**'
- ${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: /minglaradmin/settings/teammates/get-am-details-by-id/{amXid}
method: get
assignAMToHost:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/assignAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/onboarding/assign-am
method: patch
editAgreementDetailsAndAccept:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/editAgreementDetailsAndAccept.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/onboarding/edit-agreement-accept-host
method: patch
getAllPqqQuesAnsForAM:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns**'
- ${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: /minglaradmin/hosthub/onboarding/get-all-pqq-ques-ans-for-am
method: get
acceptHostApplication:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/accept-host-application
method: patch
RejectPQQByAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/reject-pq-by-am
method: patch
rejectActivityDetailsApplicationByAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/rejectActivityApplicationByAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/rejectActivityApplicationByAM**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/reject-activity-application-by-am
method: patch
acceptPQByAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/acceptPQByAM**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/accept-pq-by-am
method: patch
acceptActivityDetailsApplicationByAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/acceptActivityApplicationByAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/acceptActivityApplicationByAM**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/accept-activity-application-by-am
method: patch
rejectHostApplication:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/rejectHostApplication.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/onboarding/reject-host-application
method: patch
rejectHostApplicationAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/reject-host-application-am
method: patch
addPQQSuggestion:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/addPQQSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/add-Pqq-suggestion
method: post
addActivitySuggestion:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/addActivtiySuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/hosts/add-Activity-suggestion
method: post
getAllPQPDetailsForAM:
handler: src/modules/minglaradmin/handlers/hosthub/pqp/getAllPQPDetailsForAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/pqp/getAllPQPDetailsForAM**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/pqp/pqp-details-for-am/{activityXid}
method: get
getSuggestionsForAM:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/showSuggestionToAM**'
- 'src/modules/minglaradmin/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: /minglaradmin/hosthub/onboarding/show-suggestion-to-am/{hostXid}
method: get

View File

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

View File

@@ -0,0 +1,109 @@
# Prepopulate Module Functions
# Reference data and lookup endpoints
getAllBankAndCurrencyDetails:
handler: src/modules/prepopulate/handlers/getAllBankDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/**'
- ${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: /prepopulate/get-all-bank-currency-details
method: get
getCityByState:
handler: src/modules/prepopulate/handlers/getCityByState.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/handlers/getCityByState.*'
- 'src/modules/prepopulate/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: /prepopulate/get-city-by-state
method: get
getBranchByBankXid:
handler: src/modules/prepopulate/handlers/getBranchByBank.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/handlers/getBranchByBank.*'
- 'src/modules/prepopulate/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: /prepopulate/get-branch-by-bank
method: get
getAllDocumentCountryStateCityDetails:
handler: src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.handler
memorySize: 512
package:
patterns:
- 'src/modules/prepopulate/**'
- ${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: /prepopulate/get-all-doc-country
method: get
getAllPqqQuesAns:
handler: src/modules/prepopulate/handlers/getAllPQQQuesWithAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/prepopulate/**'
- ${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: /prepopulate/get-all-pqq-ques-ans
method: get
getFrequenciesOfActivity:
handler: src/modules/prepopulate/handlers/getAllFrequencies.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/**'
- ${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: /prepopulate/get-all-Frequencies
method: get
getAddActivityPrePopulate:
handler: src/modules/prepopulate/handlers/getAddActivityPrePopulate.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/**'
- ${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: /prepopulate/get-add-activity-prepopulate
method: get

View File

@@ -0,0 +1,364 @@
# Prepopulate Module Functions
# Reference data and lookup endpoints
registerUser:
handler: src/modules/user/handlers/authentication/registration.handler
memorySize: 384
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: /user/register
method: post
submitPersonalInfo:
handler: src/modules/user/handlers/authentication/submitPersonalInfo.handler
memorySize: 384
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: /user/submit-personal-info
method: post
verifyOtpForUser:
handler: src/modules/user/handlers/authentication/verifyOtpForUser.handler
memorySize: 384
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: /user/verify-otp
method: post
generateAccessFromRefreshToken:
handler: src/modules/user/handlers/authentication/generateRefereshToAccess.handler
memorySize: 384
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: /user/generate-access-from-refresh
method: post
setPasscodeForMobile:
handler: src/modules/user/handlers/authentication/setPasscodeForMobile.handler
memorySize: 384
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: /user/set-passcode
method: post
verifyPasscode:
handler: src/modules/user/handlers/authentication/verifyPasscode.handler
memorySize: 384
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: /user/verify-passcode
method: post
setUserInterest:
handler: src/modules/user/handlers/authentication/SetuserInterest.handler
memorySize: 384
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: /user/set-interests
method: post
setUserLocationss:
handler: src/modules/user/handlers/authentication/SetLocationofUser.handler
memorySize: 384
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: /user/set-location-user
method: post
getLandingPageDetails:
handler: src/modules/user/handlers/activities/landingPageAllDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/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: /user/activities/get-landing-page-details
method: get
getSurpriseMePageDetails:
handler: src/modules/user/handlers/activities/surpriseMePage.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/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: /user/activities/get-surprise-me-page-details
method: get
getActivityDetailsById:
handler: src/modules/user/handlers/activities/getByIdActivityDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/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: /user/activities/get-activity-details-by-id/{activity_xid}
method: get
checkAvailabilityDetails:
handler: src/modules/user/handlers/activities/checkAvailabilityDetails.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: /user/activities/check-availability/{activity_xid}
method: get
searchActivities:
handler: src/modules/user/handlers/activities/getSpecificSearchApi.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: /user/activities/specific-search
method: get
searchSchoolsAndCompanies:
handler: src/modules/user/handlers/connections/getSchoolCompanyName.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: /user/connections/search-schools-companies
method: get
searchCities:
handler: src/modules/user/handlers/connections/searchCities.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: /user/connections/search-cities
method: get
addSchoolCompanyDetail:
handler: src/modules/user/handlers/connections/addSchoolCompanyDetail.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: /user/connections/add-school-company
method: post
removeConnectionDetails:
handler: src/modules/user/handlers/connections/removeConnectionDetails.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: /user/connections/remove-connection-details
method: delete
getAllConnectionOfUser:
handler: src/modules/user/handlers/connections/getAllConnectionDetailsOfUser.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: /user/connections/get-all-connections-details
method: get
getActivityFromConnectionsInterest:
handler: src/modules/user/handlers/connections/getActivityFromConnectionsInterest.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: /user/connections/get-activity-from-connections-interest
method: get
viewMoreActivitiesByInterest:
handler: src/modules/user/handlers/activities/viewMoreActivities.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: /user/activities/view-more-activities
method: get
viewMoreActivitiesUpperSection:
handler: src/modules/user/handlers/activities/viewMoreUpperSection.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: /user/activities/view-more-activities-upper-section
method: get
getRandomActiveActivity:
handler: src/modules/user/handlers/activities/getRandomActiveActivity.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: /user/activities/get-random-active-activity
method: get
getNearbyActivities:
handler: src/modules/user/handlers/activities/getNearbyActivities.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: /user/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: /user/activities/add-to-bucket-interested
method: post

View File

@@ -0,0 +1,18 @@
# AWS S3 SDK packaging patterns (merged from aws-s3.yml and aws-s3-full.yml)
# Comprehensive list of AWS S3 dependencies for Lambda packaging
pattern1: 'node_modules/@aws-sdk/client-s3/**'
pattern2: 'node_modules/@aws-sdk/s3-request-presigner/**'
pattern3: 'node_modules/@aws-sdk/types/**'
pattern4: 'node_modules/@aws-sdk/middleware-logger/**'
pattern5: 'node_modules/@aws-sdk/**'
pattern6: 'node_modules/@smithy/**'
pattern7: 'node_modules/tslib/**'
pattern8: 'node_modules/uuid/**'
pattern9: 'node_modules/@aws-crypto/**'
pattern10: 'node_modules/@aws/smithy-client/**'
pattern11: 'node_modules/@aws/util-uri-escape/**'
pattern12: 'node_modules/@aws/util-middleware/**'
pattern13: 'node_modules/@aws/lambda-invoke-store/**'
pattern14: 'node_modules/busboy/**'
pattern15: 'node_modules/lambda-multipart-parser/**'
pattern16: 'node_modules/fast-xml-parser/**'

View File

@@ -0,0 +1,9 @@
# Base packaging patterns shared across all functions
pattern1: 'src/common/**'
pattern2: 'common/**'
# REMOVED: Prisma is now provided by Lambda layer - DO NOT include in function packages
# pattern3: 'node_modules/@prisma/client/**'
# pattern4: 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
pattern3: '!node_modules/@prisma/**'
pattern4: '!node_modules/.prisma/**'
pattern5: '!node_modules/.prisma/client/libquery_engine*'

View File

@@ -0,0 +1,29 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
// Singleton pattern for Prisma client - prevents "Too many database connections" error
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
function createPrismaClient() {
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
return new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
});
}
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
// For serverless environments, always cache the client
if (process.env.IS_OFFLINE || process.env.AWS_LAMBDA_FUNCTION_NAME) {
globalForPrisma.prisma = prisma;
}

View File

@@ -0,0 +1,5 @@
// Re-export the singleton prisma client for Lambda handlers
// This ensures all Lambda functions use the same cached connection
import { prisma } from './prisma.client';
export const prismaClient = prisma;

View File

@@ -1,5 +1,5 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { PrismaService } from './prisma.service'; // correct export location
@Global()
@Module({

View File

@@ -1,17 +1,13 @@
import { Injectable, OnModuleInit, OnModuleDestroy, INestApplication } from '@nestjs/common';
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { prisma } from './prisma.client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
super({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
});
super();
// Use the singleton instance
Object.assign(this, prisma);
}
async onModuleInit() {
@@ -21,11 +17,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
async onModuleDestroy() {
await this.$disconnect();
}
async enableShutdownHooks(app: INestApplication) {
process.on('beforeExit', async () => {
await app.close();
});
}
}

View File

@@ -0,0 +1,46 @@
import axios, { AxiosInstance } from 'axios';
import config from '../../config/config';
interface EmailRecipient {
email: string;
name?: string;
}
interface EmailOptions {
recipients: EmailRecipient[];
subject: string;
htmlContent: string;
}
class BrevoService {
private readonly instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: config.email.BrevobaseURL,
headers: {
'api-key': config.email.api_key,
'Content-Type': 'application/json',
},
});
}
async sendEmail(options: EmailOptions): Promise<{ messageId: string }> {
const response = await this.instance.post('/smtp/email', {
sender: {
name: 'Minglar',
email: 'minglar.admin@minglargroup.com',
},
to: options.recipients,
subject: options.subject,
htmlContent: options.htmlContent,
replyTo: {
email: 'minglar.admin@minglargroup.com',
},
});
return response.data;
}
}
export const brevoService = new BrevoService();

View File

@@ -0,0 +1,18 @@
// common/utils/awsPresign.ts
import config from "@/config/config";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
region: config.aws.region, // e.g. ap-south-1
});
export const getPresignedUrl = async (bucket: string, key: string) => {
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
// URL valid for 1 hour
return await getSignedUrl(s3, command, { expiresIn: 3600 });
};

View File

@@ -1,14 +1,14 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
const prisma = new PrismaClient();
import { ROLE } from '@/common/utils/constants/common.constant';
import { prisma } from '../../database/prisma.client';
interface DecodedToken {
id: number;
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
@@ -26,7 +26,70 @@ declare module 'express-serve-static-core' {
}
/**
* Verifies JWT and validates Host user (role_xid = 3)
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
export async function verifyHostToken(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 (Host 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 Host role (role_xid = 4)
if (user.roleXid !== ROLE.HOST) {
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.');
}
}
/**
* Verifies JWT and validates Host user (role_xid = 4)
*/
const verifyCallback = async (
req: Request,
@@ -35,69 +98,29 @@ const verifyCallback = async (
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
if (!token) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
if (!decoded?.id) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
}
// ✅ Fetch user from Prisma (Host user only)
const user = await prisma.user.findUnique({
where: { id: decoded.id },
include: { role: true },
});
if (!user) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
}
// ✅ Check if user is active
if (!user.isActive) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
);
}
// ✅ Check Host role (role_xid = 3)
if (user.roleXid !== 3) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
);
}
const userInfo = await verifyHostToken(token);
// Attach user to request
req.user = { id: user.id.toString(), role: user.role?.roleName };
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return reject(
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
);
}
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
);
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const auth =
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default auth;
export default authForHost;

View File

@@ -1,14 +1,14 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
const prisma = new PrismaClient();
import { ROLE } from '@/common/utils/constants/common.constant';
import { prisma } from '../../database/prisma.client';
interface DecodedToken {
id: number;
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
@@ -26,7 +26,71 @@ declare module 'express-serve-static-core' {
}
/**
* Verifies JWT and validates Host user (role_xid = 3)
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
export async function verifyMinglarAdminToken(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 (Host 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 Minglar Roles (role_xid = 1, 2, 3)
if (![ROLE.MINGLAR_ADMIN, ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER].includes(user.roleXid)) {
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.');
}
}
/**
* Verifies JWT and validates Host user (role_xid = 1)
*/
const verifyCallback = async (
req: Request,
@@ -35,69 +99,29 @@ const verifyCallback = async (
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
if (!token) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
if (!decoded?.id) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
}
// ✅ Fetch user from Prisma (Host user only)
const user = await prisma.user.findUnique({
where: { id: decoded.id },
include: { role: true },
});
if (!user) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
}
// ✅ Check if user is active
if (!user.isActive) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
);
}
// ✅ Check Admin role (role_xid = 2)
if (user.roleXid !== 2) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
);
}
const userInfo = await verifyMinglarAdminToken(token);
// Attach user to request
req.user = { id: user.id.toString(), role: user.role?.roleName };
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return reject(
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
);
}
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
);
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const auth =
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default auth;
export default authForHost;

View File

@@ -0,0 +1,129 @@
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 { prisma } from '../../database/prisma.client';
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;
}
}
/**
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
export async function verifyMinglarAdminHostToken(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 (Host 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 Minglar Roles (role_xid = 1, 2, 3)
if (![ROLE.MINGLAR_ADMIN, ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER, ROLE.HOST].includes(user.roleXid)) {
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.');
}
}
/**
* Verifies JWT and validates Host user (role_xid = 1)
*/
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 verifyMinglarAdminHostToken(token);
// Attach user to request
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForHost;

View File

@@ -0,0 +1,127 @@
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 { prisma } from '../../database/prisma.client';
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;
}
}
/**
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
export async function verifyOnlyMinglarAdminToken(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 (Host 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 Minglar Roles (role_xid = 1)
if (user.roleXid !== ROLE.MINGLAR_ADMIN) {
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.');
}
}
/**
* Verifies JWT and validates Host user (role_xid = 1)
*/
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 verifyOnlyMinglarAdminToken(token);
// Attach user to request
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForHost;

View File

@@ -1,14 +1,15 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '@/common/utils/constants/common.constant';
const prisma = new PrismaClient();
import { prisma } from '../../database/prisma.client';
interface DecodedToken {
id: number;
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
@@ -26,7 +27,70 @@ declare module 'express-serve-static-core' {
}
/**
* Verifies JWT and validates Host user (role_xid = 3)
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
export async function verifyUserToken(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 (Host 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 Host role (role_xid = 6)
if (user.roleXid !== ROLE.USER) {
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.');
}
}
/**
* Verifies JWT and validates Host user (role_xid = 4)
*/
const verifyCallback = async (
req: Request,
@@ -35,69 +99,29 @@ const verifyCallback = async (
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
if (!token) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
if (!decoded?.id) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
}
// ✅ Fetch user from Prisma (Host user only)
const user = await prisma.user.findUnique({
where: { id: decoded.id },
include: { role: true },
});
if (!user) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
}
// ✅ Check if user is active
if (!user.isActive) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
);
}
// ✅ Check User role (role_xid = 1)
if (user.roleXid !== 1) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
);
}
const userInfo = await verifyUserToken(token);
// Attach user to request
req.user = { id: user.id.toString(), role: user.role?.roleName };
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return reject(
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
);
}
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
);
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const auth =
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default auth;
export default authForHost;

View File

@@ -0,0 +1,357 @@
export const AGREEMENT_TEMPLATE = `
MINGLAR HOST AGREEMENT
Effective Date: [EFFECTIVE_DATE]
BETWEEN
Minglar India Private Limited, a company incorporated under the Companies Act, 2013, having its registered office at 602, Aaradhya Avenue X Eve, Naidu Colony, Pant Nagar, Ghatkopar (East), Mumbai 400075 (hereinafter referred to as “Minglar India Private Limited”, which expression shall include its successors and permitted assigns);
AND
[HOST_LEGAL_NAME], a [COMPANY_TYPE], having its principal place of business at [FULL_ADDRESS] (hereinafter referred to as “Host”, which expression shall include its owners, partners, directors, employees, representatives, and permitted assigns).
Minglar India Private Limited and Host are individually referred to as a “Party” and collectively as the “Parties”.
1. PURPOSE AND RELATIONSHIP
1.1 Platform Overview
Minglar India Private Limited operates a curated digital marketplace under the brand “Minglar” that enables users (“Users”) to discover, review, and book experiential activities, events, workshops, tours, and related services (“Activities”) offered by independent Hosts.
1.2 Engagement
The Host desires to list and provide its Activities on the Minglar platform subject to the terms and standards set forth in this Agreement.
1.3 Independent Contractor
Nothing in this Agreement shall be construed as creating a partnership under the Indian Partnership Act, 1932, joint venture, agency, employment, franchise, or profit-sharing arrangement. The Host operates strictly as an independent contractor and shall have full operational control over execution of its Activities.
2. TERM
2.1 Duration
This Agreement shall remain valid for a fixed term of [DURATION_TEXT] and shall automatically expire on [EXPIRY_DATE], unless terminated earlier in accordance with Section 16.
2.2 Renewal
Renewal shall be subject to mutual agreement. Commission adjustments, if any, shall apply only at renewal due to inflation or additional expenses incurred by Minglar India Private Limited for platform upgrades or new features.
3. HOST RESPONSIBILITIES
3.1 Legal Compliance and Document Submission
3.1.1 Compliance
The Host shall obtain, maintain, and keep valid at all times all licenses, permits, approvals, registrations, certifications, insurance policies, and governmental permissions required under applicable laws for the lawful conduct of the Activities.
3.1.2 Document Upload
The Host shall, at the time of onboarding on the Minglar platform, upload true, complete, and legible copies of licenses, permits, registrations, tax certificates (including GST registration, where applicable), identity proof, business registration documents, insurance certificates, and any other documents reasonably required by Minglar India Private Limited for verification purposes.
3.1.3 Accuracy and Updates
The Host represents and warrants that all documents submitted are authentic, valid, and up to date. The Host shall promptly upload updated copies whenever any document expires, is renewed, modified, or replaced. Failure to provide valid documentation may result in suspension, delisting, or termination of this Agreement at the sole discretion of Minglar India Private Limited.
3.1.4 Verification
Minglar India Private Limited shall have the right to verify such documents and request additional documentation if required for regulatory, compliance, safety, or audit purposes.
3.2 Accurate Listing and Host Anonymity
3.2.1 Activity Details
The Host shall provide complete, accurate, and up-to-date descriptions for each Activity, including inclusions, exclusions, duration, safety requirements, pricing, and applicable tax percentages at the time of onboarding. The Host shall ensure that all information remains current throughout the term of this Agreement.
3.2.2 Host Content
Hosts retain ownership of original content they upload (such as activity descriptions, images, and videos).
However, by uploading content, the Host grants Minglar a:
- Worldwide
- Non-exclusive
- Royalty-free
- Transferable
- Sub-licensable
license to use, reproduce, modify, adapt, publish, translate, distribute and display such content for purposes of operating, marketing and promoting the platform.
This license continues for as long as the content remains on the platform.
3.2.3 Unique Activity Name
The Host shall provide a unique name for each Activity during onboarding. The Hosts actual company name, personal name, or brand shall not be visible to Users on the activity card or in any public-facing listing.
3.2.4 Prohibition on Branding and Contact
The Host shall not display, embed, or otherwise reveal its contact information, company name, logo, website, email, or other identifiable details in any photos, videos, descriptions, chat messages, or other content shared with Users via the Minglar platform.
3.2.5 Breach and Suspension
Any attempt to circumvent these provisions or display unauthorized branding or contact details shall be considered a material breach of this Agreement. Minglar India Private Limited reserves the right to suspend or delist the Activity immediately and take other remedial actions as necessary.
3.3 Taxes
3.3.1 Tax Responsibility
The total Activity price listed shall be inclusive of all applicable taxes. Minglar India Private Limited shall collect such taxes from Users and transfer them to the Host. The Host shall be solely responsible for depositing and complying with tax obligations before local authorities.
4. SAFETY AND OPERATIONAL STANDARDS
4.1 General Duty of Care
The Host shall conduct all Activities in a safe, hygienic, and controlled manner ensuring the well-being of Users at all times.
4.2 Risk Management
The Host shall conduct risk assessments, maintain standard operating procedures, provide safety briefings, and ensure trained and competent personnel supervise Activities.
4.3 Transportation
If the Host provides pick-up, drop-off, or in-Activity transportation, such transportation shall be safe, clean, reasonably comfortable, and legally compliant. The Host shall remain responsible for User safety during transport.
4.4 SOS Emergency Protocol
During execution of Activities, Minglars SOS feature shall be active. If activated by a User, the Host Operator shall receive immediate notification and live location details. The Host Operator shall immediately contact and reach the User. The emergency shall be cleared only after ensuring the User is safe. The Host Operator shall be the first point of contact for all emergencies.
4.5 Equipment Standards
The Host shall ensure that all equipment, tools, and materials used for conducting any Activity are:
1. Maintained in accordance with the manufacturers recommendations and operational guidelines.
2. Tested regularly to confirm they are safe and functional before each Activity.
3. Kept clean, hygienic, and in good working order at all times.
4. Adequate for the number of Users participating, ensuring no overuse or overcrowding that may compromise safety.
The Host shall be solely responsible for any incidents, accidents, or injuries caused due to faulty, poorly maintained, unhygienic, or unsafe equipment. Failure to comply may result in suspension, delisting, or immediate termination of this Agreement under Section 16.
5. QUALITY AND PUNCTUALITY
5.1 Quality Standards
The Host shall maintain consistently high service standards and continuously strive to improve user experience toward achieving five-star ratings.
5.2 Timeliness
The Host shall ensure timely check-in, commencement, and completion of Activities. Delays impacting Users onward travel or other bookings shall constitute service failure.
6. INSURANCE
The Host shall obtain, maintain, and keep valid insurance coverage appropriate for the risk associated with the respective Activities. Coverage shall be sufficient to protect Users, the Host, and Minglar India Private Limited against claims arising from accidents, injuries, fatalities, or property damage.
6.1 Risk-Based Coverage
The Host shall maintain Public Liability Insurance appropriate to the risk and scale of the respective Activities.
6.2 Coverage Amount
For high-risk or multi-participant Activities, coverage is recommended up to INR 5 Crores or an amount appropriate to cover potential claims arising from injury, death, or property damage.
6.3 Additional Insured
Policies shall name Minglar India Private Limited and Minglar Group Pte Ltd, Singapore as Additional Insured.
6.4 Proof
Valid insurance certificates must be submitted prior to onboarding and maintained throughout the Agreement term.
7. USER WAIVERS
The Host shall ensure that Users acknowledge and accept all standard waivers, terms, and risk disclosures prior to participation in any Activity, as provided by the Minglar platform.
8. INDEMNITY
The Host shall indemnify, defend, and hold harmless Minglar India Private Limited, its affiliates, employees, directors, and representatives from any claims, losses, damages, liabilities, fines, or expenses arising directly or indirectly from the Hosts failure to comply with laws, negligence, or breach of this Agreement.
9. LIMITATION OF LIABILITY
The total aggregate liability of Minglar India Private Limited arising out of or in connection with any claim relating to a specific Activity shall not exceed the total commission earned by Minglar India Private Limited from that particular Activity (or substantially similar activity category) conducted by the Host during the three (3) months immediately preceding the date of the claim.
If the Host lists multiple different Activities, the liability cap applies only to the commission earned from the specific Activity giving rise to the claim and does not include commission earned from other unrelated Activities.
Under no circumstances shall Minglar India Private Limited be liable for indirect, incidental, consequential, punitive, or special damages, including loss of profits, goodwill, reputation, or business opportunity.
10. PAYMENT AND COMMISSION
10.1 Commission
10.1.1 Standard Commission
A [COMMISSION_TEXT] commission shall be charged by Minglar India Private Limited on the Activity revenue (after deduction of applicable taxes).
10.1.2 Fixed During Term
The term of this agreement is [DURATION_TEXT]. This commission shall remain unchanged during the term of this Agreement.
10.1.3 Renewal Adjustment
At the time of renewal, Minglar India Private Limited reserves the right to adjust commission rates due to inflation or platform upgrades.
10.2 Host Payout
10.2.1 Timing
Minglar India Private Limited shall remit payments to the Host 24 hours after check-in completion or no-show.
10.2.2 Monthly Commission Invoice
Minglar India Private Limited shall issue a monthly invoice to the Host detailing the commission earned. GST shall be charged extra.
10.2.3 Banking and Gateway Charges
All banking, payment gateway, and transaction fees arising from normal bookings shall be borne by the Host.
11. BOOKING CANCELLATION
11.1 Host-Related Cancellation
If an Activity is cancelled due to host health, extreme weather, natural disasters, government restrictions, venue issues, equipment failure, team/partner unavailability, or other uncontrollable circumstances, the Host shall pay the applicable cancellation fee charged by the payment gateway. No payment will be remitted to the Host, and 100% of the booking amount including taxes shall be refunded to Users.
11.2 User-Initiated Cancellation
If Users cancel within the permitted period, the platform fee shall be deducted and the remaining amount refunded to Users. The Host will not receive payment for such cancellations.
12. PARTICIPATION OF MINGLAR ACCOUNT MANAGER FOR AUDIT
In case of low performance, low bookings, or safety/quality issues reported by Users that remain unresolved for more than one week, Minglar India Private Limited may send an Account Manager to audit the Activity at the Hosts location.
The date and slot for such an audit shall be coordinated by the Account Manager and the Host.
The participation of the Account Manager is free of charge. If the audit requires overnight stay or the Activity duration exceeds one day, the Host shall provide accommodation.
Transportation costs for the Account Manager shall be shared 50% by the Host.
Minglar India Private Limited reserves the right to send an Account Manager once per year or sooner in case of low performance, safety concerns, or user complaints with less than 2-star ratings.
13. DATA PROTECTION, PRIVACY & INFORMATION SECURITY
13.1 Compliance with Applicable Laws
The Parties acknowledge that Minglar Group Pte. Ltd. (Singapore) and/or Minglar India Private Limited (India) collects and processes personal data in compliance with applicable laws, including:
- The Personal Data Protection Act 2012 (“PDPA”); and
- The Digital Personal Data Protection Act 2023 (“DPDP Act”).
Each Party agrees to comply with all applicable privacy and data protection laws in the jurisdiction where the Activity is conducted.
13.2 Roles of the Parties
a) Minglar shall act as the Data Fiduciary / Data Controller for personal data collected via the Minglar platform.
b) The Host shall act as a Data Processor / Authorized Data Recipient when processing User personal data solely for the purpose of conducting booked Activities.
The Host shall not independently determine the purpose or means of processing User personal data without prior written authorization from Minglar.
PART A USER PERSONAL DATA
13.3 Categories of User Data Collected
The Host acknowledges that Minglar may collect and process the following categories of User personal data through the platform:
a) Full name
b) Date of birth
c) Mobile number and email address
d) Gender
e) Height and weight (where Activity eligibility or safety restrictions apply)
f) School and college information
g) Profile photograph
h) Home location and/or real-time GPS location
i) Check-in and check-out data
j) SOS or emergency alerts
k) Payment method details and transaction information
l) Spending preferences or monthly budget indicators
m) Biometric verification data (including facial verification, where applicable)
n) Medical conditions or health disclosures (Phase 2 implementation)
o) Attendance logs and activity participation records
13.4 Sensitive Personal Data
Certain categories of data may constitute sensitive personal data, including:
- Biometric data
- Medical or health information
- Real-time location data
- Financial/payment information
- Profile photographs capable of biometric identification
Such data shall:
a) Be processed only where necessary for safety, verification, compliance, or Activity execution;
b) Be accessed strictly on a need-to-know basis;
c) Be protected using enhanced security measures;
d) Not be downloaded, stored externally, or retained by the Host beyond the Activity duration unless legally required.
The Host shall not independently collect additional medical, biometric, or financial data outside the Minglar platform without prior written approval.
13.5 Confidentiality and Use Restrictions
The Host shall treat all User and platform data as strictly confidential, including attendance logs, SOS alerts, emergency records, and location data collected during Activities.
User data shall:
a) Be used solely for Activity execution and safety;
b) Not be shared with third parties without prior written consent from Minglar India Private Limited;
c) Not be used for independent marketing, profiling, or database creation;
d) Not be retained after completion of the Activity unless legally required.
The Host shall not contact Users outside the Minglar platform unless expressly authorized.
13.6 Security Measures
The Host shall implement reasonable technical and organizational measures to protect personal data from unauthorized access, alteration, misuse, or disclosure, including:
- Secure password-protected systems;
- Restricted employee access;
- Confidentiality undertakings from employees and operators;
- Secure handling of operator devices used for check-in/check-out;
- Immediate reporting of lost or compromised devices.
13.7 Data Breach Notification
Any actual or suspected breach involving personal data — including medical, biometric, financial, or location data — shall be reported to Minglar India Private Limited immediately and in any event within twenty-four (24) hours of discovery.
The Host shall fully cooperate in investigation, mitigation, and regulatory reporting obligations.
13.8 Retention and Deletion
The Host shall not retain personal data beyond the period necessary for conducting the Activity unless required by law.
Upon termination of this Agreement, the Host shall delete all User data in its possession and confirm deletion upon request.
13.9 User Responsibility for Medical Disclosures
Users remain responsible for the accuracy and completeness of any medical or health information voluntarily disclosed.
The Host may rely on such disclosures in good faith for safety and eligibility determinations.
Minglar shall not be liable for losses arising from incomplete or inaccurate medical information provided by Users.
PART B HOST PERSONAL DATA
13.10 Collection of Host Personal Data
The Host acknowledges that Minglar may collect and process personal data relating to the Host and its directors, partners, employees, and authorized representatives for:
- KYC verification and onboarding;
- Regulatory compliance;
- Risk assessment and fraud prevention;
- Payment processing and settlement;
- Audit and safety review;
- Enforcement of this Agreement.
13.11 Sharing and Cross-Border Transfers
Host personal data may be shared:
- Within Minglar Group entities;
- With banks, payment processors, verification agencies, insurers, auditors, and professional advisors;
- With regulatory or governmental authorities where legally required.
The Host acknowledges that personal data may be transferred between Singapore, India, and other jurisdictions where Minglar operates, subject to compliance with applicable cross-border transfer laws.
PART C ACTIVITY LOCATION & OPERATIONAL DATA
13.12 Collection of Activity Location and Operational Data
The Host agrees that Minglar may collect and process operational data relating to Activities, including:
- Venue address;
- Check-in and check-out locations;
- GPS coordinates;
- Pick-up and drop-off locations;
- Route and logistical details;
- Activity schedules.
Such data may include precise geo-location information.
13.13 Use and Display of Location Data
The Host expressly consents to Minglar:
- Displaying Activity venue, pick-up, and drop-off details on the platform;
- Using location data for navigation, safety monitoring, and emergency coordination;
- Using such data for fraud prevention and verification.
The Host warrants that all location data provided is accurate and lawful.
Minglar shall not be liable for losses arising from inaccurate or misleading information provided by the Host.
PART D ANALYTICS & PLATFORM OPTIMIZATION
13.14 Anonymized and Aggregated Data
Minglar may use anonymized, aggregated, or de-identified data derived from User or Host activity for:
- Platform improvement;
- Artificial intelligence systems;
- Recommendation engines;
- Safety analytics;
- Market research;
- Business strategy and investor reporting.
Such data shall not identify any individual User or Host.
All analytics models, algorithms, insights, and derived data shall remain the exclusive intellectual property of Minglar.
PART E LIABILITY & SURVIVAL
13.15 Indemnity
The Host shall indemnify and hold harmless Minglar Group Pte. Ltd. and Minglar India Private Limited against any losses, penalties, regulatory actions, claims, damages, or costs arising from:
- Unauthorized use of personal data;
- Data breaches attributable to the Host;
- Non-compliance with applicable data protection laws;
- Inaccurate operational or location data provided by the Host.
13.16 Survival
This Section 13 shall survive termination or expiry of this Agreement.
14. CONFIDENTIALITY
The Host shall maintain strict confidentiality regarding all platform operations, processes, user data, and commercial information. These obligations survive five (5) years post-termination. Exceptions only apply if required by law.
15. NON-EXCLUSIVITY
This Agreement does not restrict the Host from offering Activities on other platforms. Minglar India Private Limited does not claim exclusivity. The Host shall not interfere with other platform operations or solicit Users to bypass Minglar.
16. TERMINATION
16.1 Immediate Termination
Minglar India Private Limited may terminate immediately in case of breach, misrepresentation, failure to maintain safety or quality standards, or violation of this Agreement.
16.2 Termination with Notice
Either Party may terminate this Agreement by providing 30 days written notice.
16.3 Post-Termination Obligations
Upon termination, the Host must execute all bookings already made by Users. Minglar India Private Limited will block further bookings for the Hosts Activities.
16.4 Commission Adjustment at Renewal
Any change in commission shall only occur at renewal of this Agreement. No adjustments shall be made during the active term.
16.5 Non-Exclusivity
Termination or continuation of this Agreement does not prevent the Host from offering Activities elsewhere.
17. GOVERNING LAW
This Agreement shall be governed by the laws of India. Courts at the registered office jurisdiction of Minglar India Private Limited shall have exclusive jurisdiction.
18. ENTIRE AGREEMENT
This Agreement constitutes the complete understanding between the Parties and supersedes all prior communications.
19. HOST ACKNOWLEDGMENT & ELECTRONIC CONSENT
19.1 Electronic Acceptance
By clicking the “I Agree” button, the Host acknowledges they have read, understood, and accepted the terms of this Agreement and the Minglar Privacy Policy.
19.2 Binding Agreement
Electronic acceptance constitutes a legally binding contract, equivalent to a wet signature.
19.3 Host Obligations
- Provide accurate and lawful data
- Protect User data and follow emergency protocols
- Notify Minglar of breaches or unauthorized access
- Comply with applicable laws
19.4 Electronic Records
All electronic records and communications are valid and enforceable.
19.5 Updates
Continued use after updates constitutes acceptance of amended terms.
Signed Electronically by:
Minglar India Private Limited
Authorized Signatory
Host:
[HOST_LEGAL_NAME]
Date: [ACCEPT_DATE]
`;

View File

@@ -0,0 +1,29 @@
export const ROLE = {
MINGLAR_ADMIN: 1,
CO_ADMIN: 2,
ACCOUNT_MANAGER: 3,
HOST: 4,
OPERATOR: 5,
USER: 6
}
export const ROLE_NAME = {
MINGLAR_ADMIN: 'Minglar Admin',
CO_ADMIN: 'Co-admin',
ACCOUNT_MANAGER: 'Account manager',
HOST: 'Host',
OPERATOR: 'Operator',
USER: 'User'
}
export const USER_STATUS = {
INVITED: "Invited",
ACTIVE: "Active",
DE_ACTIVATED: "De-activated",
REJECTED: "Rejected"
}
export const RESTRICTION_NAME = {
ABOVE: "Above",
BELOW: "Below",
}

View File

@@ -0,0 +1,111 @@
export const HOST_STATUS_INTERNAL = {
HOST_SUBMITTED: 'Host Submitted',
HOST_TO_UPDATE: 'Host To Update',
REJECTED: 'Rejected',
APPROVED: 'Approved',
DRAFT: 'Draft',
};
export const HOST_STATUS_DISPLAY = {
DRAFT: 'Draft',
UNDER_REVIEW: 'Under Review',
ENHANCING: 'Enhancing',
REJECTED: 'Rejected',
APPROVED: 'Approved',
};
export const STEPPER = {
NOT_SUBMITTED: 1,
UNDER_REVIEW: 2,
COMPANY_DETAILS_APPROVED: 3,
BANK_DETAILS_UPDATED: 4,
AGREEMENT_ACCEPTED: 5,
REJECTED: 6,
};
export const ACTIVITY_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
PQ_TO_UPDATE: 'PQ To Update',
PQ_SUBMITTED: 'PQ Submitted',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_SUBMITTED: 'Activity Submitted',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_REJECTED: 'Activity Rejected',
ACTIVITY_APPROVED: 'Activity Approved',
ACTIVITY_LISTED: 'Activity Listed',
ACTIVITY_UNLISTED: 'Activity UnListed ',
ACTIVITY_NOT_LISTED: 'Activity Not Listed',
};
export const ACTIVITY_DISPLAY_STATUS = {
DRAFT_PQ: 'Draft',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enhancing',
PQ_IN_REVIEW: 'PQ In Review',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_IN_REVIEW: 'In Review',
ACTIVITY_TO_REVIEW: 'Re-submitted',
NOT_LISTED: 'Not Listed',
ACTIVITY_LISTED: 'Listed',
ACTIVITY_UNLISTED: 'Un Listed',
};
export const ACTIVITY_AM_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
PQ_REJECTED: 'PQ Rejected',
PQ_TO_REVIEW: 'PQ To Review',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_REJECTED: 'Activity Rejected',
ACTIVITY_APPROVED: 'Activity Approved',
ACTIVITY_LISTED: 'Activity Listed',
ACTIVITY_SUBMITED: 'Activity Submitted',
};
export const ACTIVITY_AM_DISPLAY_STATUS = {
DRAFT_PQ: 'Draft',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enhancing',
NEW: 'New',
PQ_APPROVED: 'PQ Approved',
REVISED: 'Revised',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_NEW: 'New',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_ENHANCING: 'Enhancing',
NOT_LISTED: 'Not Listed',
ACTIVITY_LISTED: 'Listed',
ACTIVITY_REVISED: 'Activity Revised'
};
export const SCHEDULING_TYPE = {
ONCE: 'ONCE',
WEEKLY: 'WEEKLY',
MONTHLY: 'MONTHLY',
CUSTOM: 'CUSTOM',
};

View File

@@ -0,0 +1,63 @@
export const MINGLAR_STATUS_INTERNAL = {
ADMIN_TO_REVIEW: 'Admin To Review',
ADMIN_REJECTED: 'Admin Rejected',
AM_NOT_ASSIGNED: 'AM Not Assigned',
AM_TO_REVIEW: 'AM To Review',
AM_REJECTED: 'AM Rejected',
AM_APPROVED: 'AM Approved',
DRAFT: 'Draft',
};
export const MINGLAR_STATUS_DISPLAY = {
NEW: 'New',
AM_NOT_ASSIGNED: 'AM Not Assigned',
TO_REVIEW: 'To Review',
ENHANCING: 'Enhancing',
APPROVED: 'Approved',
REJECTED: 'Rejected',
RE_SUBMITTED: 'Re-submitted',
DRAFT: 'Draft'
};
export const MINGLAR_INVITATION_STATUS = {
PENDING: 'Pending',
ACCEPTED: 'Accepted',
REJECTED: 'Rejected',
INVITED: 'Invited',
};
export const ACTIVITY_TRACK_TYPE = {
PQ: 'PQ',
ACTIVITY: 'Activity'
}
export const ACTIVITY_TRACK_STATUS = {
REJECTED_BY_AM: 'Rejected By AM',
ACCEPTED_BY_AM: 'Accepted By AM',
ENHANCING: 'Enhancing',
PQ_SUBMITTED: 'PQ Submitted',
UNDER_REVIEW:'Under Review',
SUBMITTED:'Activity Submitted',
DRAFT:'Activity Draft'
}
// export const HOST_SUGGESTION_TITLES = {
// COMPANY_DETAILS: 'Complete Details',
// COMPANY_DOCUMENTATION: 'Company documentataion',
// COMPANY_SOCIAL_PROOF: 'Social Proof',
// ACTIVITY_INFORMATION: 'Activity Information',
// ACTIVITY_LOCATION: 'Activity Location',
// PICKUP_DROP_LOCATION: 'Pickup-Drop Location',
// NUMBER_OF_PEOPLE: 'Number of People',
// INCLUSION: 'Inclusion',
// TAX_SETUP: 'Tax Setup',
// ENERGY_LEVEL: 'Energy Level',
// ELIGIBILITY_CRITERIA: 'Eligibility Criteria',
// AMENITIES: 'Amenities',
// EXLUSIVE_NOTES: 'Exclusive Notes',
// CANCELLATION_POLICY: 'Cancellation Policy',
// DOs_AND_DONTs: 'Dos and Donts',
// TIPS_FOR_USERS: 'Tips for Users',
// SUSTAINABILITY: 'Sustainability',
// TERMS_AND_CONDITION_FOR_USER: 'Terms and Conditions for User'
// };

View File

@@ -2,22 +2,68 @@
import { APIGatewayProxyEvent, Context, APIGatewayProxyResult } from 'aws-lambda';
import ApiError from '../helper/ApiError';
const stage = process.env.STAGE ?? 'dev';
export const safeHandler = (
handler: (event: APIGatewayProxyEvent, context?: Context) => Promise<APIGatewayProxyResult | undefined>
handler: (event: APIGatewayProxyEvent, context?: Context) => Promise<APIGatewayProxyResult | any>
): ((event: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyResult>) => {
return async (event, context) => {
try {
const result = await handler(event, context);
return (
result ?? {
// If handler returned null/undefined → return 204 response
if (!result) {
return {
statusCode: 204,
body: '',
}
);
body: JSON.stringify({
success: true,
statusCode: 204,
message: 'No content',
data: null,
}),
};
}
// If handler returned a structured Lambda response
if (result.statusCode && result.body) {
return {
statusCode: result.statusCode,
headers: result.headers || {},
body: injectStatusCodeIntoBody(result.body, result.statusCode),
};
}
// If handler returned plain data (not wrapped)
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'OK',
statusCode: 200,
data: result,
}),
};
} catch (error: any) {
console.error('Error occurred:', error);
console.error('Error occurred:', error);
// Convert Prisma errors to ApiError automatically
if (ApiError.isPrismaError(error)) {
const apiError = ApiError.fromPrismaError(error);
return {
statusCode: apiError.statusCode,
body: JSON.stringify({
success: false,
message: apiError.message,
statusCode: apiError.statusCode,
data: null,
error: {
code: apiError.code || apiError.statusCode,
description: apiError.message,
statusCode: apiError.statusCode,
...(apiError.meta && { meta: apiError.meta }),
...(process.env.STAGE !== 'prod' && { debug: apiError.stack }),
},
}),
};
}
if (error instanceof ApiError) {
return {
@@ -25,31 +71,53 @@ export const safeHandler = (
body: JSON.stringify({
success: false,
message: error.message,
statusCode: error.statusCode,
data: null,
error: {
code: error.statusCode,
code: error.code || error.statusCode,
description: error.message,
statusCode: error.statusCode,
...(process.env.STAGE !== 'prod' && { debug: error.stack ?? error.message }),
...(error.meta && { meta: error.meta }),
...(process.env.STAGE !== 'prod' && { debug: error.stack }),
},
}),
};
}
// Internal Server Error fallback
return {
statusCode: 500,
body: JSON.stringify({
success: false,
message: 'Internal server error',
statusCode: 500,
data: null,
error: {
code: 500,
description: 'Internal server error',
statusCode: 500,
...(process.env.STAGE !== 'prod' && { debug: error.stack ?? error.message }),
...(process.env.STAGE !== 'prod' && { debug: error.stack }),
},
}),
};
}
};
};
// Utility: safely inject statusCode into the JSON response body
function injectStatusCodeIntoBody(body: string, statusCode: number): string {
try {
const json = JSON.parse(body);
json.statusCode = statusCode;
return JSON.stringify(json);
} catch {
// If body is not JSON, wrap it
return JSON.stringify({
success: true,
statusCode,
message: body,
data: null,
});
}
}

View File

@@ -1,3 +1,75 @@
import { Prisma } from '@prisma/client';
/**
* Prisma error code mappings to user-friendly messages and HTTP status codes
*/
const PRISMA_ERROR_CODES: Record<string, { statusCode: number; message: string }> = {
// Common errors (P1xxx)
P1000: { statusCode: 500, message: 'Authentication failed against database server' },
P1001: { statusCode: 503, message: 'Database server is not reachable' },
P1002: { statusCode: 504, message: 'Database server timed out' },
P1003: { statusCode: 500, message: 'Database does not exist' },
P1008: { statusCode: 504, message: 'Database operation timed out' },
P1009: { statusCode: 409, message: 'Database already exists' },
P1010: { statusCode: 403, message: 'User was denied access to the database' },
P1011: { statusCode: 500, message: 'Error opening a TLS connection' },
P1012: { statusCode: 500, message: 'Schema validation error' },
P1013: { statusCode: 400, message: 'Invalid database connection string' },
P1014: { statusCode: 500, message: 'Underlying model does not exist' },
P1015: { statusCode: 500, message: 'Database schema uses unsupported features' },
P1016: { statusCode: 400, message: 'Raw query has incorrect number of parameters' },
P1017: { statusCode: 503, message: 'Database server has closed the connection' },
// Prisma Client Query Engine errors (P2xxx)
P2000: { statusCode: 400, message: 'Value too long for column' },
P2001: { statusCode: 404, message: 'Record not found' },
P2002: { statusCode: 409, message: 'Unique constraint violation' },
P2003: { statusCode: 409, message: 'Foreign key constraint violation' },
P2004: { statusCode: 400, message: 'Database constraint violation' },
P2005: { statusCode: 400, message: 'Invalid value stored in database' },
P2006: { statusCode: 400, message: 'Invalid value provided' },
P2007: { statusCode: 400, message: 'Data validation error' },
P2008: { statusCode: 400, message: 'Failed to parse the query' },
P2009: { statusCode: 400, message: 'Failed to validate the query' },
P2010: { statusCode: 500, message: 'Raw query failed' },
P2011: { statusCode: 400, message: 'Null constraint violation' },
P2012: { statusCode: 400, message: 'Missing required value' },
P2013: { statusCode: 400, message: 'Missing required argument' },
P2014: { statusCode: 409, message: 'Required relation violation' },
P2015: { statusCode: 404, message: 'Related record not found' },
P2016: { statusCode: 400, message: 'Query interpretation error' },
P2017: { statusCode: 400, message: 'Records for relation not connected' },
P2018: { statusCode: 404, message: 'Required connected records not found' },
P2019: { statusCode: 400, message: 'Input error' },
P2020: { statusCode: 400, message: 'Value out of range' },
P2021: { statusCode: 500, message: 'Table does not exist' },
P2022: { statusCode: 500, message: 'Column does not exist' },
P2023: { statusCode: 500, message: 'Inconsistent column data' },
P2024: { statusCode: 503, message: 'Connection pool timeout' },
P2025: { statusCode: 404, message: 'Record not found' },
P2026: { statusCode: 400, message: 'Unsupported database feature used in query' },
P2027: { statusCode: 500, message: 'Multiple database errors occurred' },
P2028: { statusCode: 500, message: 'Transaction API error' },
P2029: { statusCode: 400, message: 'Query parameter limit exceeded' },
P2030: { statusCode: 400, message: 'Fulltext index not found' },
P2031: { statusCode: 500, message: 'MongoDB requires replica set' },
P2033: { statusCode: 400, message: 'Number does not fit in 64 bit signed integer' },
P2034: { statusCode: 409, message: 'Transaction failed due to write conflict or deadlock' },
P2035: { statusCode: 500, message: 'Database assertion violation' },
P2036: { statusCode: 500, message: 'External connector error' },
P2037: { statusCode: 503, message: 'Too many database connections opened' },
};
interface PrismaErrorMeta {
target?: string[];
field_name?: string;
model_name?: string;
argument_name?: string;
constraint?: string;
cause?: string;
[key: string]: unknown;
}
class ApiError<T = unknown> extends Error {
statusCode: number;
data: T | null;
@@ -6,13 +78,17 @@ class ApiError<T = unknown> extends Error {
errors: Array<Error>;
isOperational: boolean;
stack?: string;
code?: string;
meta?: PrismaErrorMeta;
constructor(
statusCode: number,
message: string = 'Something went wrong',
errors: Array<Error> = [],
isOperational: boolean = true,
stack?: string
stack?: string,
code?: string,
meta?: PrismaErrorMeta
) {
super(message);
this.statusCode = statusCode;
@@ -21,6 +97,8 @@ class ApiError<T = unknown> extends Error {
this.success = false;
this.errors = errors;
this.isOperational = isOperational;
this.code = code;
this.meta = meta;
if (stack) {
this.stack = stack;
@@ -28,6 +106,159 @@ class ApiError<T = unknown> extends Error {
Error.captureStackTrace(this, this.constructor);
}
}
/**
* Creates an ApiError from a Prisma error
* Handles all Prisma error types: PrismaClientKnownRequestError, PrismaClientUnknownRequestError,
* PrismaClientRustPanicError, PrismaClientInitializationError, PrismaClientValidationError
*/
static fromPrismaError(error: unknown): ApiError {
// Handle PrismaClientKnownRequestError
if (error instanceof Prisma.PrismaClientKnownRequestError) {
const errorInfo = PRISMA_ERROR_CODES[error.code] || {
statusCode: 500,
message: 'Database operation failed',
};
let message = errorInfo.message;
const meta = error.meta as PrismaErrorMeta | undefined;
// Provide more specific messages based on error code and meta
switch (error.code) {
case 'P2002': {
const target = meta?.target;
if (target && Array.isArray(target)) {
message = `Unique constraint violation on field(s): ${target.join(', ')}`;
}
break;
}
case 'P2003': {
const fieldName = meta?.field_name;
if (fieldName) {
message = `Foreign key constraint failed on field: ${fieldName}`;
}
break;
}
case 'P2025': {
const cause = meta?.cause;
if (cause) {
message = `Record not found: ${cause}`;
}
break;
}
case 'P2011': {
const constraint = meta?.constraint;
if (constraint) {
message = `Null constraint violation on: ${constraint}`;
}
break;
}
case 'P2014': {
const modelName = meta?.model_name;
if (modelName) {
message = `Required relation violation on model: ${modelName}`;
}
break;
}
}
return new ApiError(
errorInfo.statusCode,
message,
[error],
true,
error.stack,
error.code,
meta
);
}
// Handle PrismaClientUnknownRequestError
if (error instanceof Prisma.PrismaClientUnknownRequestError) {
return new ApiError(
500,
'An unknown database error occurred',
[error],
true,
error.stack,
'UNKNOWN_REQUEST_ERROR'
);
}
// Handle PrismaClientRustPanicError
if (error instanceof Prisma.PrismaClientRustPanicError) {
return new ApiError(
500,
'A critical database error occurred. Please try again later.',
[error],
false,
error.stack,
'RUST_PANIC_ERROR'
);
}
// Handle PrismaClientInitializationError
if (error instanceof Prisma.PrismaClientInitializationError) {
const errorInfo = error.errorCode
? PRISMA_ERROR_CODES[error.errorCode] || { statusCode: 500, message: 'Database initialization failed' }
: { statusCode: 500, message: 'Database initialization failed' };
return new ApiError(
errorInfo.statusCode,
errorInfo.message,
[error],
false,
error.stack,
error.errorCode || 'INITIALIZATION_ERROR'
);
}
// Handle PrismaClientValidationError
if (error instanceof Prisma.PrismaClientValidationError) {
return new ApiError(
400,
'Invalid data provided for database operation',
[error],
true,
error.stack,
'VALIDATION_ERROR'
);
}
// Fallback for any other error
if (error instanceof Error) {
return new ApiError(500, error.message, [error], true, error.stack);
}
return new ApiError(500, 'An unexpected error occurred');
}
/**
* Check if an error is a Prisma error
*/
static isPrismaError(error: unknown): boolean {
return (
error instanceof Prisma.PrismaClientKnownRequestError ||
error instanceof Prisma.PrismaClientUnknownRequestError ||
error instanceof Prisma.PrismaClientRustPanicError ||
error instanceof Prisma.PrismaClientInitializationError ||
error instanceof Prisma.PrismaClientValidationError
);
}
/**
* Get a user-friendly message for a Prisma error code
*/
static getPrismaErrorMessage(code: string): string {
return PRISMA_ERROR_CODES[code]?.message || 'Database operation failed';
}
/**
* Get HTTP status code for a Prisma error code
*/
static getPrismaErrorStatusCode(code: string): number {
return PRISMA_ERROR_CODES[code]?.statusCode || 500;
}
}
export default ApiError;

View File

@@ -0,0 +1,37 @@
import * as crypto from 'crypto';
const algorithm = 'aes-256-cbc';
const secretKey = crypto.scryptSync('your-secret-password', 'salt', 32);
const ivLength = 16;
// Encrypt function
export function encryptUserId(id: string): string {
const iv = crypto.randomBytes(ivLength);
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
let encrypted = cipher.update(id, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}`;
}
// Decrypt function
export function decryptUserId(encryptedId: string): string | null {
try {
const parts = encryptedId.split(':');
if (parts.length !== 2) {
console.error('Invalid encryptedId format:', encryptedId);
return null;
}
const iv = Buffer.from(parts[0], 'hex');
const encryptedText = Buffer.from(parts[1], 'hex');
const decipher = crypto.createDecipheriv(algorithm, secretKey, iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString('utf8');
} catch (error) {
console.error('Decryption failed:', error);
return null;
}
}

View File

@@ -0,0 +1,18 @@
import config from '../../../config/config';
export class OtpGenerator {
static generateOtp(): string {
if (config.byPassOTP) {
return '1234';
}
return Math.floor(1000 + Math.random() * 9000).toString();
}
}
export class OtpGeneratorSixDigit {
static generateOtp(): string {
if (config.byPassOTP) {
return '123456';
}
return Math.floor(100000 + Math.random() * 900000).toString();
}
}

View File

@@ -0,0 +1,126 @@
import ApiError from './ApiError';
interface ParsedFormData {
fields: Record<string, string>;
files: Array<{
fieldName: string;
fileName: string;
contentType: string;
data: Buffer;
}>;
}
/**
* Parse multipart/form-data from Lambda event
* Supports both base64 encoded and binary body
*/
export function parseMultipartFormData(
eventBody: string | null,
contentType: string | undefined,
isBase64Encoded: boolean = false
): ParsedFormData {
if (!eventBody) {
throw new ApiError(400, 'Request body is required');
}
if (!contentType || !contentType.includes('multipart/form-data')) {
throw new ApiError(400, 'Content-Type must be multipart/form-data');
}
// Extract boundary from Content-Type header
const boundaryMatch = contentType.match(/boundary=([^;]+)/);
if (!boundaryMatch) {
throw new ApiError(400, 'Invalid multipart boundary');
}
const boundary = boundaryMatch[1].trim();
// Decode base64 body if needed (API Gateway sends base64 encoded for binary media types)
let bodyBuffer: Buffer;
try {
if (isBase64Encoded) {
bodyBuffer = Buffer.from(eventBody, 'base64');
} else {
// Try to detect if it's base64
if (eventBody.match(/^[A-Za-z0-9+/=]+$/)) {
bodyBuffer = Buffer.from(eventBody, 'base64');
} else {
bodyBuffer = Buffer.from(eventBody, 'binary');
}
}
} catch (error) {
throw new ApiError(400, 'Invalid request body encoding');
}
// Split by boundary
const parts = bodyBuffer.toString('binary').split(`--${boundary}`);
const fields: Record<string, string> = {};
const files: ParsedFormData['files'] = [];
for (const part of parts) {
if (!part || part.trim() === '' || part.trim() === '--') {
continue;
}
// Split headers and body
const [headers, ...bodyParts] = part.split('\r\n\r\n');
if (!headers || bodyParts.length === 0) {
continue;
}
const body = bodyParts.join('\r\n\r\n').trim();
if (!body) {
continue;
}
// Parse Content-Disposition header
const contentDispositionMatch = headers.match(/Content-Disposition:\s*form-data;\s*name="([^"]+)"/);
if (!contentDispositionMatch) {
continue;
}
const fieldName = contentDispositionMatch[1];
// Check if it's a file
const filenameMatch = headers.match(/filename="([^"]+)"/);
const contentTypeMatch = headers.match(/Content-Type:\s*([^\r\n]+)/);
if (filenameMatch) {
// It's a file
const fileName = filenameMatch[1];
const fileContentType = contentTypeMatch ? contentTypeMatch[1].trim() : 'application/octet-stream';
// Convert body to buffer (remove trailing boundary markers)
const fileData = Buffer.from(body.replace(/\r\n--$/, ''), 'binary');
files.push({
fieldName,
fileName,
contentType: fileContentType,
data: fileData,
});
} else {
// It's a regular field
fields[fieldName] = body.replace(/\r\n--$/, '').trim();
}
}
return { fields, files };
}
/**
* Parse JSON field from form data
*/
export function parseJsonField(fields: Record<string, string>, fieldName: string): any {
const value = fields[fieldName];
if (!value) {
return null;
}
try {
return JSON.parse(value);
} catch (error) {
throw new ApiError(400, `Invalid JSON in field: ${fieldName}`);
}
}

View File

@@ -0,0 +1,62 @@
import * as bcrypt from "bcryptjs";
import { OtpGenerator, OtpGeneratorSixDigit } from "./OtpGenerator";
import { encryptUserId } from "./CodeGenerator";
export interface OtpResult {
otp: string;
hashedOtp: string;
expiry: Date;
encryptedId: string;
}
export async function resendOtpHelper(
prisma: any,
userId: number,
emailPurpose: "Register" | "Login" | "ForgotPassword",
otpLength: 4 | 6 = 4,
expiryMinutes: number = 5
): Promise<OtpResult> {
// 1⃣ Deactivate previous OTPs
await prisma.userOtp.updateMany({
where: {
userXid: userId,
otpType: emailPurpose,
isActive: true,
},
data: {
isActive: false,
isVerified: true,
},
});
// 2⃣ Generate new OTP
const otp =
otpLength === 6
? OtpGeneratorSixDigit.generateOtp()
: OtpGenerator.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + expiryMinutes * 60000);
const encryptedId = encryptUserId(userId.toString());
// 3⃣ Insert new OTP into table
await prisma.userOtp.create({
data: {
userXid: userId,
otpType: emailPurpose,
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
// 4⃣ Return new OTP (email will use this)
return {
otp,
hashedOtp,
expiry,
encryptedId,
};
}

View File

@@ -0,0 +1,191 @@
import dotenv from 'dotenv';
import path from 'path';
import * as yup from 'yup';
dotenv.config({ path: path.join(__dirname, '../../.env') });
const envVarsSchema = yup
.object()
.shape({
NODE_ENV: yup
.string()
.oneOf(['production', 'development', 'test'])
.required(),
PORT: yup.number().default(3000),
// FRONTEND_URL: yup.string().required('Frontend URL is required'),
//JWT
JWT_SECRET: yup.string().required('JWT secret key is required'),
JWT_ACCESS_EXPIRATION_MINUTES: yup
.number()
.default(30)
.required('minutes after which access tokens expire'),
JWT_REFRESH_EXPIRATION_DAYS: yup
.number()
.default(30)
.required('days after which refresh tokens expire'),
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: yup
.number()
.default(10)
.required('minutes after which reset password token expires'),
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: yup
.number()
.default(10)
.required('minutes after which verify email token expires'),
AWS_REGION: yup.string().required('AWS region is required'),
S3_BUCKET_NAME: yup.string().required('S3 bucket name is required'),
//SMTP and BREVO
// BREVO_SMTP_HOST: yup
// .string()
// .nullable()
// .required('server that will send the emails'),
// BREVO_SMTP_PORT: yup
// .number()
// .nullable()
// .required('port to connect to the email server'),
// BREVO_SMTP_USER: yup
// .string()
// .nullable()
// .required('username for email server'),
// BREVO_SMTP_PASS: yup
// .string()
// .nullable()
// .required('password for email server'),
// BREVO_FROM_EMAIL: yup
// .string()
// .nullable()
// .required('the from field in the emails sent by the app'),
// BREVO_EMAIL_API_KEY: yup
// .string()
// .nullable()
// .required('the from field in the emails sent by the app api key'),
// BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'),
// //one signal
// ONESIGNAL_APPID: yup.string().required('One signal app id is required'),
// ONESIGNAL_REST_APIKEY: yup
// .string()
// .required('One signal api key is required'),
//branch IO
// BRANCH_IO_KEY: yup.string().required('Branch IO key is required'),
// DataBase
DB_USERNAME: yup.string().required('DB Username is required'),
DB_PASSWORD: yup.string().required('DB Password is required'),
DB_DATABASE_NAME: yup.string().required('Database name is required'),
DB_HOSTNAME: yup
.string()
.default('127.0.0.1')
.required('DB Hostname is required'),
DB_PORT: yup.number().default(3306).required('DB Port is required'),
//OTP Bypass
BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'),
})
.noUnknown(true);
// Validate and prepare the configuration
function getConfig() {
try {
// Validate the environment variables
const envVars = envVarsSchema.validateSync(process.env, {
abortEarly: false, // Validate all fields before throwing errors
stripUnknown: true, // Remove fields not in the schema
});
// Return the validated configuration
return {
env: envVars.NODE_ENV,
port: envVars.PORT,
jwt: {
secret: envVars.JWT_SECRET,
accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES,
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
resetPasswordExpirationMinutes:
envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
verifyEmailExpirationMinutes:
envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
},
database: {
development: {
host: envVars.DB_HOSTNAME,
port: envVars.DB_PORT,
username: envVars.DB_USERNAME,
password: envVars.DB_PASSWORD,
database: envVars.DB_DATABASE_NAME,
logging: false,
},
test: {
host: envVars.DB_HOSTNAME,
port: envVars.DB_PORT,
username: envVars.DB_USERNAME,
password: envVars.DB_PASSWORD,
database: envVars.DB_DATABASE_NAME,
logging: false,
socketPath: '/var/run/mysqld/mysqld.sock',
},
production: {
host: envVars.DB_HOSTNAME,
port: envVars.DB_PORT,
username: envVars.DB_USERNAME,
password: envVars.DB_PASSWORD,
database: envVars.DB_DATABASE_NAME,
logging: false,
socketPath: '/var/run/mysqld/mysqld.sock',
},
},
aws: {
region: envVars.AWS_REGION,
bucketName: envVars.S3_BUCKET_NAME,
},
byPassOTP: envVars.BYPASS_OTP,
// BaseURL: envVars.BASEURL,
// FRONTEND_URL: envVars.FRONTEND_URL,
// email: {
// smtp: {
// host: envVars?.BREVO_SMTP_HOST,
// port: envVars?.BREVO_SMTP_PORT,
// secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports
// auth: {
// user: envVars?.BREVO_SMTP_USER,
// pass: envVars?.BREVO_SMTP_PASS,
// },
// },
// from: envVars?.BREVO_FROM_EMAIL,
// api_key: envVars?.BREVO_EMAIL_API_KEY,
// BrevobaseURL: envVars?.BREVO_API_BASEURL,
// },
// oneSignal: {
// appID: envVars.ONESIGNAL_APPID,
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,
// },
// branchIO: {
// branchIOKey: envVars.BRANCH_IO_KEY,
// },
};
} catch (error: unknown) {
if (error instanceof yup.ValidationError) {
console.error('Validation Errors:', error.errors.join(', '));
} else {
console.error('Unexpected error during configuration validation:', error);
}
console.error(
'Server shut down due to incomplete environment variable configuration.'
);
process.exit(1); // Exit with error code 1
}
}
/**
* Created By : Angad Chauhan
* Created at : 31/1/25
* Use : For google login .env file global variable
*/
// export const googleConfig = {
// clientID: process.env.GOOGLE_CLIENT_ID!,
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
// callbackURL: process.env.GOOGLE_CALLBACK_URL!,
// };
// Validate and export configuration only if validation succeeds
const config = getConfig();
export default config;

View File

@@ -0,0 +1,62 @@
import * as bcrypt from "bcryptjs";
import { OtpGenerator, OtpGeneratorSixDigit } from "./OtpGenerator";
import { encryptUserId } from "./CodeGenerator";
export interface OtpResult {
otp: string;
hashedOtp: string;
expiry: Date;
encryptedId: string;
}
/**
* Generate OTP using Prisma instance passed from Lambda
*/
export async function generateOtpHelper(
prisma: any, // ⭐ Inject prisma
userId: number,
email: string,
emailPurpose: "Register" | "Login" | "ForgotPassword",
otpLength: 4 | 6 = 4,
expiryMinutes: number = 5
): Promise<OtpResult> {
const otp =
otpLength === 6
? OtpGeneratorSixDigit.generateOtp()
: OtpGenerator.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + expiryMinutes * 60000);
const encryptedId = encryptUserId(userId.toString());
// Delete previous active OTPs
await prisma.userOtp.deleteMany({
where: {
userXid: userId,
otpType: emailPurpose,
isActive: true,
},
});
// Create new OTP entry
await prisma.userOtp.create({
data: {
userXid: userId,
otpType: emailPurpose,
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
return {
otp,
hashedOtp,
expiry,
encryptedId,
};
}

View File

@@ -0,0 +1,66 @@
// common/utils/pagination/pagination.service.ts
import { PaginationOptions, PaginationParams, PaginatedResponse } from './pagination.types';
export class PaginationService {
private readonly DEFAULT_PAGE = 1;
private readonly DEFAULT_LIMIT = 10;
private readonly MAX_LIMIT = 100;
/**
* Parse and validate pagination parameters
*/
parsePaginationParams(params: PaginationParams): PaginationOptions {
let page = Number(params.page) || this.DEFAULT_PAGE;
let limit = Number(params.limit) || this.DEFAULT_LIMIT;
// Validate and constrain values
page = Math.max(1, page);
limit = Math.max(1, Math.min(limit, this.MAX_LIMIT));
const skip = (page - 1) * limit;
return {
page,
limit,
skip,
};
}
/**
* Create paginated response
*/
createPaginatedResponse<T>(
data: T[],
totalCount: number,
paginationOptions: PaginationOptions,
): PaginatedResponse<T> {
const { page, limit } = paginationOptions;
const totalPages = Math.ceil(totalCount / limit);
return {
data,
pagination: {
currentPage: page,
pageSize: limit,
totalCount,
totalPages,
hasNext: page < totalPages,
hasPrevious: page > 1,
},
};
}
/**
* Extract pagination params from API Gateway event
*/
getPaginationFromEvent(event: any): PaginationParams {
const queryParams = event.queryStringParameters || {};
return {
page: queryParams.page,
limit: queryParams.limit,
};
}
}
export const paginationService = new PaginationService();

View File

@@ -0,0 +1,23 @@
// common/utils/pagination/pagination.types.ts
export interface PaginationOptions {
page: number;
limit: number;
skip: number;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
currentPage: number;
pageSize: number;
totalCount: number;
totalPages: number;
hasNext: boolean;
hasPrevious: boolean;
};
}
export interface PaginationParams {
page?: string | number;
limit?: string | number;
}

View File

@@ -0,0 +1,35 @@
// validations/hostBankDetails.validation.ts
import { z } from "zod";
export const hostBankDetailsSchema = z.object({
accountNumber: z
.string()
.nonempty("Account number is required"),
accountHolderName: z
.string()
.nonempty("Account holder name is required")
.min(2, { message: "Account holder name must be at least 2 characters" }),
bankXid: z
.number()
.int({ message: "Bank ID must be an integer" })
.positive({ message: "Bank ID must be a positive number" }),
hostXid: z
.number()
.int({ message: "Host ID must be an integer" })
.positive({ message: "Host ID must be a positive number" }),
bankBranchXid: z
.number()
.int({ message: "Bank branch ID must be an integer" })
.positive({ message: "Bank branch ID must be a positive number" }),
currencyXid: z
.number()
.int({ message: "Currency ID must be an integer" })
.positive({ message: "Currency ID must be a positive number" }),
});
export type HostBankDetailsSchema = z.infer<typeof hostBankDetailsSchema>;

View File

@@ -0,0 +1,76 @@
import { z } from 'zod';
import { SCHEDULING_TYPE } from '../../constants/host.constant';
const WeekdayEnum = z.enum([
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
'SUNDAY',
]);
export const scheduleActivity = z.object({
activityXid: z.number(),
listNow: z.boolean(),
scheduleType: z.enum([
SCHEDULING_TYPE.ONCE,
SCHEDULING_TYPE.WEEKLY,
SCHEDULING_TYPE.MONTHLY,
SCHEDULING_TYPE.CUSTOM,
]),
dateRange: z.object({
startDate: z.string(),
endDate: z.string().nullable().optional(),
}),
rules: z.object({
weekdays: z.array(WeekdayEnum).optional(),
monthDates: z.array(z.number()).optional(),
customDates: z.array(z.string()).optional(),
}),
venues: z.array(
z.object({
venueXid: z.number(),
slots: z.array(
z.object({
startTime: z.string(),
endTime: z.string(),
weekDay: WeekdayEnum.nullable().optional(),
dayOfMonth: z.number().nullable().optional(),
occurrenceDate: z.string().nullable().optional(),
maxCapacity: z.number(),
})
).optional().default([]),
})
),
earlyCheckInMins: z.number().optional(),
bookingCutOffMins: z.number().optional(),
isLateCheckingAllowed: z.boolean().optional(),
isInstantBooking: z.boolean().optional(),
})
.superRefine((data, ctx) => {
if (data.scheduleType === 'WEEKLY' && !data.rules.weekdays?.length) {
ctx.addIssue({ path: ['rules', 'weekdays'], message: 'Weekdays required for WEEKLY schedule', code: 'custom' });
}
if (data.scheduleType === 'MONTHLY' && !data.rules.monthDates?.length) {
ctx.addIssue({ path: ['rules', 'monthDates'], message: 'Month dates required for MONTHLY schedule', code: 'custom' });
}
if (
(data.scheduleType === 'CUSTOM' || data.scheduleType === 'ONCE') &&
!data.rules.customDates?.length
) {
ctx.addIssue({ path: ['rules', 'customDates'], message: 'Custom dates required', code: 'custom' });
}
});
export type ScheduleActivityDTO = z.infer<typeof scheduleActivity>;

View File

@@ -0,0 +1,138 @@
import { z } from "zod";
export const parentCompanySchema = z.object({
companyName: z.string()
.max(100, "Parent company name cannot exceed 100 characters")
.optional(),
address1: z.string()
.max(150, "Address1 cannot exceed 150 characters")
.optional(),
address2: z.string()
.max(150, "Address2 cannot exceed 150 characters")
.optional(),
cityXid: z.number().optional(),
stateXid: z.number().optional(),
countryXid: z.number().optional(),
pinCode: z.string()
.max(30, "Pincode cannot exceed 30 characters")
.optional(),
logoPath: z.string()
.max(400, "Logo path cannot exceed 400 characters")
.optional(),
registrationNumber: z.string()
.max(30, "Registration number cannot exceed 30 characters")
.optional(),
panNumber: z.string()
.max(30, "PAN number cannot exceed 30 characters")
.optional(),
gstNumber: z.string()
.max(30, "GST number cannot exceed 30 characters")
.optional(),
formationDate: z.string()
.optional()
.refine((val) => !val || !isNaN(Date.parse(val)), {
message: "Formation date must be a valid date",
}),
companyTypeXid: z.number()
.optional(),
websiteUrl: z.string().nullable().optional(),
instagramUrl: z.string().nullable().optional(),
facebookUrl: z.string().nullable().optional(),
linkedinUrl: z.string().nullable().optional(),
twitterUrl: z.string().nullable().optional(),
});
// =======================================================
// HOST COMPANY DETAILS (Main Company)
// =======================================================
export const hostCompanyDetailsSchema = z.object({
companyName: z.string()
.min(1, "Company name is required")
.max(100, "Company name cannot exceed 100 characters"),
address1: z.string()
.min(1, "Address1 is required")
.max(150, "Address1 cannot exceed 150 characters"),
address2: z.string()
.max(150, "Address2 cannot exceed 150 characters")
.optional(),
cityXid: z.number().min(1, "City is required"),
stateXid: z.number().min(1, "State is required"),
countryXid: z.number().min(1, "Country is required"),
pinCode: z.string()
.min(4, "Pincode/Zipcode is required")
.max(30, "Pincode cannot exceed 30 characters"),
logoPath: z.string()
.max(400, "Logo path cannot exceed 400 characters")
.optional(),
isSubsidairy: z.boolean(),
registrationNumber: z.string()
.max(30, "Registration number cannot exceed 30 characters")
.optional(),
panNumber: z.string()
.min(1, "PAN number is required")
.max(30, "PAN number cannot exceed 30 characters"),
gstNumber: z.string()
.max(30, "GST number cannot exceed 30 characters")
.optional(),
formationDate: z.string()
.optional()
.refine((val) => !val || !isNaN(Date.parse(val)), {
message: "Formation date must be a valid date",
}),
companyTypeXid: z.number()
.int("Company type must be a valid integer")
.min(1, "Company type is required"),
referencedBy: z.string()
.optional(),
websiteUrl: z.string().nullable().optional(),
instagramUrl: z.string().nullable().optional(),
facebookUrl: z.string().nullable().optional(),
linkedinUrl: z.string().nullable().optional(),
twitterUrl: z.string().nullable().optional(),
parentCompany: parentCompanySchema.optional(),
});
// =======================================================
// DOCUMENTS VALIDATION
// =======================================================
export const hostDocumentsSchema = z.array(
z.object({
documentTypeXid: z.number(),
documentName: z.string().max(50, "Document name cannot exceed 50 characters"), // per parent max 50 / host max 20 — safe limit 50
fieldName: z.string(),
owner: z.enum(['host', 'parent']).optional(),
isOptional: z.boolean().optional(),
})
);

View File

@@ -0,0 +1,20 @@
// validations/hostBankDetails.validation.ts
import { z } from "zod";
export const loginForHostSchema = z.object({
emailAddress : z
.string()
.nonempty("Email is required"),
userPassword : z
.string()
.nonempty("Password is required")
.min(8, { message: "Password must be at least 8 characters" }),
});
export type loginForHostSchema = z.infer<typeof loginForHostSchema>;

View File

@@ -0,0 +1,26 @@
// validations/hostBankDetails.validation.ts
import { z } from "zod";
export const userPersonalInfoSchema = z.object({
firstName: z
.string()
.nonempty("First name is required"),
lastName: z
.string()
.optional(),
genderName: z
.string()
.nonempty("Gender is required"),
dateOfBirth: z
.string()
.nonempty("Date of birth is required")
.refine(val => !isNaN(Date.parse(val)), {
message: "Date of birth must be a valid ISO date (YYYY-MM-DD)",
}),
});
export type UserPersonalInfoSchema = z.infer<typeof userPersonalInfoSchema>;

View File

@@ -12,13 +12,12 @@ const envVarsSchema = yup
.oneOf(['production', 'development', 'test'])
.required(),
PORT: yup.number().default(3000),
BASEURL: yup.string().required('Base URL is required'),
// FRONTEND_URL: yup.string().required('Frontend URL is required'),
//JWT
JWT_SECRET: yup.string().required('JWT secret key is required'),
JWT_ACCESS_EXPIRATION_MINUTES: yup
.number()
.default(30)
.default(1440)
.required('minutes after which access tokens expire'),
JWT_REFRESH_EXPIRATION_DAYS: yup
.number()
@@ -32,32 +31,37 @@ const envVarsSchema = yup
.number()
.default(10)
.required('minutes after which verify email token expires'),
AWS_REGION: yup.string().required('AWS region is required'),
S3_BUCKET_NAME: yup.string().required('S3 bucket name is required'),
//SMTP and BREVO
// BREVO_SMTP_HOST: yup
// .string()
// .nullable()
// .required('server that will send the emails'),
// BREVO_SMTP_PORT: yup
// .number()
// .nullable()
// .required('port to connect to the email server'),
// BREVO_SMTP_USER: yup
// .string()
// .nullable()
// .required('username for email server'),
// BREVO_SMTP_PASS: yup
// .string()
// .nullable()
// .required('password for email server'),
// BREVO_FROM_EMAIL: yup
// .string()
// .nullable()
// .required('the from field in the emails sent by the app'),
// BREVO_EMAIL_API_KEY: yup
// .string()
// .nullable()
// .required('the from field in the emails sent by the app api key'),
// BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'),
BREVO_SMTP_HOST: yup
.string()
.nullable()
.required('server that will send the emails'),
BREVO_SMTP_PORT: yup
.number()
.nullable()
.required('port to connect to the email server'),
BREVO_SMTP_USER: yup
.string()
.nullable()
.required('username for email server'),
BREVO_SMTP_PASS: yup
.string()
.nullable()
.required('password for email server'),
BREVO_FROM_EMAIL: yup
.string()
.nullable()
.required('the from field in the emails sent by the app'),
BREVO_EMAIL_API_KEY: yup
.string()
.nullable()
.required('the from field in the emails sent by the app api key'),
BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'),
// Minglar Admin
MINGLAR_ADMIN_EMAIL: yup.string().required('Minglar admin email address is required.'),
MINGLAR_ADMIN_NAME: yup.string().required('Minglar admin name is required.'),
// //one signal
// ONESIGNAL_APPID: yup.string().required('One signal app id is required'),
// ONESIGNAL_REST_APIKEY: yup
@@ -77,6 +81,10 @@ const envVarsSchema = yup
DB_PORT: yup.number().default(3306).required('DB Port is required'),
//OTP Bypass
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'),
HOST_LINK: yup.string().required('Link to host panel is required'),
HOST_LINK_PQ: yup.string().required('Link to host panel pqp is required')
})
.noUnknown(true);
@@ -130,23 +138,33 @@ function getConfig() {
socketPath: '/var/run/mysqld/mysqld.sock',
},
},
aws: {
region: envVars.AWS_REGION,
bucketName: envVars.S3_BUCKET_NAME,
},
byPassOTP: envVars.BYPASS_OTP,
// BaseURL: envVars.BASEURL,
// FRONTEND_URL: envVars.FRONTEND_URL,
// email: {
// smtp: {
// host: envVars?.BREVO_SMTP_HOST,
// port: envVars?.BREVO_SMTP_PORT,
// secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports
// auth: {
// user: envVars?.BREVO_SMTP_USER,
// pass: envVars?.BREVO_SMTP_PASS,
// },
// },
// from: envVars?.BREVO_FROM_EMAIL,
// api_key: envVars?.BREVO_EMAIL_API_KEY,
// BrevobaseURL: envVars?.BREVO_API_BASEURL,
// },
email: {
smtp: {
host: envVars?.BREVO_SMTP_HOST,
port: envVars?.BREVO_SMTP_PORT,
secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports
auth: {
user: envVars?.BREVO_SMTP_USER,
pass: envVars?.BREVO_SMTP_PASS,
},
},
from: envVars?.BREVO_FROM_EMAIL,
api_key: envVars?.BREVO_EMAIL_API_KEY,
BrevobaseURL: envVars?.BREVO_API_BASEURL,
},
//Minglar admin
MinglarAdminEmail: envVars.MINGLAR_ADMIN_EMAIL,
MinglarAdminName: envVars.MINGLAR_ADMIN_NAME,
AM_INVITATION_LINK: envVars.AM_INVITATION_LINK,
HOST_LINK: envVars.HOST_LINK,
HOST_LINK_PQ: envVars.HOST_LINK_PQ,
// oneSignal: {
// appID: envVars.ONESIGNAL_APPID,
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,

View File

@@ -16,7 +16,7 @@ async function bootstrap() {
app.enableCors({
origin: [
'http://localhost:3000', // Local Swagger UI
'http://localhost:3006', // Local Frontend
'http://localhost:5173', // Local Frontend
'https://editor.swagger.io', // Swagger Editor
'https://klc-admin.wdiprojects.com',
'https://admin-uat.klc.betadelivery.com',

View File

@@ -0,0 +1,176 @@
import { z } from 'zod';
/* ================= MEDIA ================= */
export const MediaDto = z.object({
mediaType: z.string().optional(),
mediaFileName: z.string(),
isCoverImage: z.boolean().optional().default(false),
});
/* ================= PRICE ================= */
export const PriceDto = z.object({
noOfSession: z.number().int().optional().default(1),
isPackage: z.boolean().optional().default(false),
sessionValidity: z.number().int().optional().default(0),
sessionValidityFrequency: z.string().optional().default('Days'),
basePrice: z.number().int().optional().default(0),
sellPrice: z.number().int(),
});
/* ================= VENUE ================= */
export const VenueDto = z.object({
venueName: z.string(),
venueLabel: z.string(),
venueCapacity: z.number().int().optional().default(0),
availableSeats: z.number().int().optional().default(0),
isMinPeopleReqMandatory: z.boolean().optional().default(false),
minPeopleRequired: z.number().int().nullable().optional(),
minReqfullfilledBeforeMins: z.number().int().nullable().optional(),
venueDescription: z.string().optional(),
media: z.array(MediaDto).optional().default([]),
prices: z.array(PriceDto).optional().default([]),
});
/* ================= PICKUP / DROP ================= */
export const PickupDetailDto = z.object({
isPickUp: z.boolean().optional().default(false),
locationLat: z.number().nullable().optional(),
locationLong: z.number().nullable().optional(),
locationAddress: z.string().nullable().optional(),
transportTotalPrice: z.number().int().min(0),
});
export const PickupTransportDto = z.object({
transportModeXid: z.number().int(),
});
/* ================= EQUIPMENT ================= */
export const EquipmentDto = z.object({
equipmentName: z.string(),
isEquipmentChargeable: z.boolean().optional(),
equipmentBasePrice: z.number().int().optional().default(0),
equipmentTotalPrice: z.number().int().optional().default(0),
});
/* ================= NAVIGATION MODE ================= */
export const NavigationModeDto = z.object({
navigationModeName: z.string().optional(),
isChargeable: z.boolean().optional(),
totalPrice: z.number().int().optional().default(0),
});
/* ================= ELIGIBILITY ================= */
export const EligibilityDto = z.object({
isAgeRestriction: z.boolean().optional().default(false),
ageRestrictionName: z.string().nullable().optional(),
ageEntered: z.number().int().nullable().optional(),
ageIn: z.string().nullable().optional(),
minAge: z.number().int().nullable().optional(),
maxAge: z.number().int().nullable().optional(),
isWeightRestriction: z.boolean().optional().default(false),
weightRestrictionName: z.string().nullable().optional(),
weightEntered: z.number().int().nullable().optional(),
weightIn: z.string().nullable().optional(),
minWeight: z.number().int().nullable().optional(),
maxWeight: z.number().int().nullable().optional(),
isHeightRestriction: z.boolean().optional().default(false),
heightRestrictionName: z.string().nullable().optional(),
heightEntered: z.number().int().nullable().optional(),
heightIn: z.string().nullable().optional(),
minHeight: z.number().int().nullable().optional(),
maxHeight: z.number().int().nullable().optional(),
});
/* ================= OTHER DETAILS ================= */
export const OtherDetailsDto = z.object({
exclusiveNotes: z.string().optional(),
safetyInstruction: z.string().optional(),
dosNotes: z.string().optional(),
dontsNotes: z.string().optional(),
tipsNotes: z.string().optional(),
termsAndCondition: z.string().optional(),
cancellations: z.string().optional(),
});
/* ================= CREATE ACTIVITY ================= */
export const CreateActivityDto = z.object({
activityXid: z.number().int(),
activityTypeXid: z.number().int().optional(),
frequenciesXid: z.number().int().nullable().optional(),
activityTitle: z.string().optional(),
activityDescription: z.string().optional(),
checkInLat: z.number().nullable().optional(),
checkInLong: z.number().nullable().optional(),
checkInAddress: z.string().nullable().optional(),
isCheckOutSame: z.boolean().optional().default(true),
checkOutLat: z.number().nullable().optional(),
checkOutLong: z.number().nullable().optional(),
checkOutAddress: z.string().nullable().optional(),
checkInStateName: z.string().nullable().optional(),
checkInCityName: z.string().nullable().optional(),
checkInCountryName: z.string().nullable().optional(),
checkOutStateName: z.string().nullable().optional(),
checkOutCityName: z.string().nullable().optional(),
checkOutCountryName: z.string().nullable().optional(),
energyLevelXid: z.number().int().nullable().optional(),
durationDays: z.number().int().optional(),
durationHours: z.number().int().optional(),
durationMins: z.number().int().optional(),
foodAvailable: z.boolean().nullable().optional(),
foodIsChargeable: z.boolean().optional().default(false),
alcoholAvailable: z.boolean().nullable().optional(),
trainerAvailable: z.boolean().nullable().optional(),
trainerIsChargeable: z.boolean().optional().default(false),
pickUpDropAvailable: z.boolean().nullable().optional(),
pickUpDropIsChargeable: z.boolean().optional().default(false),
inActivityAvailable: z.boolean().nullable().optional(),
inActivityIsChargeable: z.boolean().optional().default(false),
equipmentAvailable: z.boolean().nullable().optional(),
equipmentIsChargeable: z.boolean().optional().default(false),
cancellationAvailable: z.boolean().nullable().optional(),
cancellationAllowedBeforeMins: z.number().int().nullable().optional(),
currencyXid: z.number().int().nullable().optional(),
sustainabilityScore: z.number().int().nullable().optional(),
safetyScore: z.number().int().nullable().optional(),
isInstantBooking: z.boolean().optional().default(false),
taxXids: z.array(z.number().int()).optional().default([]),
media: z.array(MediaDto).optional().default([]),
venues: z.array(VenueDto).optional().default([]),
foodTypeIds: z.array(z.number().int()).optional().default([]),
cuisineIds: z.array(z.number().int()).optional().default([]),
pickupTransports: z.array(PickupTransportDto).optional().default([]),
pickupDetails: z.array(PickupDetailDto).optional().default([]),
navigationModes: z
.array(NavigationModeDto)
.optional()
.default([]),
equipments: z.array(EquipmentDto).optional().default([]),
amenitiesIds: z.array(z.number().int()).optional().default([]),
foodTotalAmount: z.number().int().optional().default(0),
eligibility: EligibilityDto.optional(),
otherDetails: OtherDetailsDto.optional(),
allowedEntryTypes: z.array(z.number().int()).optional().default([]),
trainerTotalAmount: z.number().int().optional().default(0),
});
export type CreateActivityInput = z.infer<typeof CreateActivityDto>;

View File

@@ -0,0 +1,39 @@
export interface ScheduleSlotDTO {
startTime: string;
endTime: string;
weekDay?: 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY' | 'SUNDAY' | null;
dayOfMonth?: number | null; // 131
occurrenceDate?: string | null;
maxCapacity: number;
}
export interface ScheduleVenueDTO {
venueXid: number;
slots: ScheduleSlotDTO[];
}
// export interface ScheduleActivityDTO {
// activityXid: number;
// scheduleType: 'ONCE' | 'WEEKLY' | 'MONTHLY' | 'CUSTOM';
// dateRange: {
// startDate: string;
// endDate?: string | null;
// };
// rules: {
// weekdays?: (
// 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' |
// 'THURSDAY' | 'FRIDAY' | 'SATURDAY' | 'SUNDAY'
// )[];
// monthDates?: number[];
// customDates?: string[];
// };
// venues: ScheduleVenueDTO[];
// earlyCheckInMins?: number;
// bookingCutOffMins?: number;
// isLateCheckingAllowed?: boolean;
// isInstantBooking?: boolean;
// }

View File

@@ -21,7 +21,7 @@ export class CreateHostDto {
@IsOptional()
@IsString()
userPasscode?: string;
userPassword?: string;
@IsOptional()
@IsInt()
@@ -49,3 +49,47 @@ export class UpdateHostDto {
@IsBoolean()
isActive?: boolean;
}
export class GetHostLoginResponseDTO {
id: number;
firstName: string | null;
lastName: string | null;
emailAddress: string;
mobileNumber: string | null;
isActive: boolean;
roleXid: number;
accessToken: string;
refreshToken: string;
constructor(user: any, accessToken: string, refreshToken: string) {
this.id = user.id;
this.firstName = user.firstName;
this.lastName = user.lastName;
this.emailAddress = user.emailAddress;
this.mobileNumber = user.mobileNumber;
this.isActive = user.isActive;
this.roleXid = user.roleXid;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
export class AddPaymentDetailsDTO {
bankXid: number;
bankBranchXid: number;
accountNumber: string;
accountHolderName: string;
ifscCode: string;
hostXid: number;
currencyXid: number;
constructor(bankXid: number, bankBranchXid: number, accountNumber: string, accountHolderName: string, ifscCode: string, hostXid: number, currencyXid: number) {
this.bankXid = bankXid;
this.bankBranchXid = bankBranchXid;
this.accountNumber = accountNumber;
this.accountHolderName = accountHolderName;
this.ifscCode = ifscCode;
this.hostXid = hostXid;
this.currencyXid = currencyXid;
}
}

View File

@@ -0,0 +1,117 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } 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 {
CreateActivityDto,
CreateActivityInput,
} from '../../../dto/createActivity.schema';
import { HostService } from '../../../services/host.service';
const hostService = new HostService(prismaClient);
export const handler = safeHandler(
async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
/* 1⃣ AUTH */
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'Missing auth token');
}
const userInfo = await verifyHostToken(token);
/* 2⃣ PARSE JSON BODY */
if (!event.body) {
throw new ApiError(400, 'Request body is required');
}
let body: any;
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
const {
activity,
media = [],
isDraft = false,
} = body;
if (!activity) {
throw new ApiError(400, 'activity payload is required');
}
/* 3⃣ NORMALIZE ACTIVITY ID */
if (activity.activityXid) {
activity.activityXid = Number(activity.activityXid);
}
/* 4⃣ ATTACH ACTIVITY MEDIA (S3 URLs) */
if (!Array.isArray(media)) {
throw new ApiError(400, 'media must be an array');
}
activity.media = media.map((m: any) => ({
mediaType: m.mediaType ?? 'image',
mediaFileName: m.mediaFileName,
isCoverImage: m.isCoverImage ?? false,
}));
/* 4.1️⃣ ATTACH SAFETY INSTRUCTIONS (string only) */
if (activity.safetyInstruction !== undefined && activity.safetyInstruction !== null) {
if (typeof activity.safetyInstruction !== 'string') {
throw new ApiError(400, 'safetyInstruction must be a string');
}
}
/* 4.2️⃣ ATTACH CANCELLATIONS (string only) */
if (activity.cancellations !== undefined && activity.cancellations !== null) {
if (typeof activity.cancellations !== 'string') {
throw new ApiError(400, 'cancellations must be a string');
}
}
/* 5⃣ VALIDATION */
let parsedDto: CreateActivityInput;
if (!isDraft) {
const parsed = CreateActivityDto.safeParse(activity);
if (!parsed.success) {
throw new ApiError(
400,
parsed.error.issues.map((i) => i.message).join(', ')
);
}
parsedDto = parsed.data;
} else {
parsedDto = activity as CreateActivityInput;
}
/* 6⃣ SAVE TO DB */
const result = await hostService.createOrUpdateActivity(
userInfo.id,
parsedDto,
isDraft
);
/* 7⃣ RESPONSE */
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: isDraft
? 'Activity saved as draft successfully'
: 'Activity submitted successfully',
data: result,
}),
};
}
);

View File

@@ -0,0 +1,61 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
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);
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 verifyHostToken(token);
let body: any = {};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (err) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityTypeXid, frequenciesXid } = body;
if (!activityTypeXid || !frequenciesXid) {
throw new ApiError(400, 'activityType and frequency ID is required');
}
// Get all host applications from service based on user role
const createdData = await hostService.createActivityAndAllQuestionsEntry(userInfo.id, activityTypeXid, frequenciesXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Activity created successfully',
data: createdData
}),
};
},
);

View File

@@ -0,0 +1,50 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
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';
import { PrePopulateService } from '../../../../prepopulate/services/prepopulate.service';
const hostService = new HostService(prismaClient);
const prePopulateService = new PrePopulateService(prismaClient);
/**
* 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
await verifyHostToken(token);
// Read optional search query (supports ?search= or ?q=)
const search = event.queryStringParameters?.search || event.queryStringParameters?.q || undefined;
const data = await hostService.getAllActivityTypesWithInterest(search);
const frequencies = await prePopulateService.getAllFrequencies();
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data,
frequencies
}),
};
});

View File

@@ -0,0 +1,47 @@
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);
/**
* 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
await verifyMinglarAdminHostToken(token);
const activityXid = event.pathParameters?.activityXid
if (!activityXid) {
throw new ApiError(400, 'activityXid is required in path parameters');
}
const data = await hostService.getAllDetailsOfActivityAndVenue(Number(activityXid));
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data,
}),
};
});

View File

@@ -0,0 +1,57 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
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';
import { paginationService } from '../../../../../common/utils/pagination/pagination.service';
const hostService = new HostService(prismaClient);
/**
* 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 verifyHostToken(token);
// Get pagination params from event
const paginationParams = paginationService.getPaginationFromEvent(event);
const paginationOptions = paginationService.parsePaginationParams(paginationParams);
// Read optional search query (supports ?search= or ?q=)
const search = event.queryStringParameters?.search || event.queryStringParameters?.q || undefined;
const result = await hostService.getAllHostActivity(
search ? String(search) : undefined,
Number(userInfo.id),
paginationOptions
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
...result,
}),
};
});

View File

@@ -0,0 +1,46 @@
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';
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
const hostService = new HostService(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 the shared authForHost function
await verifyMinglarAdminHostToken(token);
const activity_xid = event.queryStringParameters?.activity_xid
? Number(event.queryStringParameters.activity_xid)
: null;
if (!activity_xid) {
throw new ApiError(409, "Activity ID is required")
}
const result = await hostService.getAllPQQQuesAndSubmittedAns(Number(activity_xid));
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

@@ -0,0 +1,204 @@
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
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';
const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
// Extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(bucketBaseUrl, '');
}
// Delete file from S3
async function deleteFromS3(s3Key: string): Promise<void> {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: s3Key,
}).promise();
console.log(`✅ Deleted from S3: ${s3Key}`);
} catch (err) {
console.error(`❌ Failed to delete from S3: ${s3Key}`, err);
}
}
// Upload new file
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string): Promise<string> {
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
const s3Key = `${prefix}/${uniqueKey}`;
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private'
}).promise();
console.log(`✅ Uploaded to S3: ${s3Key}`);
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// AUTH
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token');
const user = await verifyHostToken(token);
// Content-Type
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
if (!contentType?.startsWith("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
if (!event.isBase64Encoded) throw new ApiError(400, "Body must be base64 encoded");
const bodyBuffer = Buffer.from(event.body!, "base64");
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// Parse multipart
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
if (!filename) return file.resume();
const chunks: Buffer[] = [];
let size = 0;
file.on('data', chunk => {
size += chunk.length;
if (size > 5 * 1024 * 1024)
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit`));
chunks.push(chunk);
});
file.on('end', () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname
});
}
});
});
bb.on('field', (fieldname, val) => {
try { fields[fieldname] = JSON.parse(val); }
catch { fields[fieldname] = val; }
});
bb.on('close', resolve);
bb.on('error', err => reject(new ApiError(400, err.message)));
bb.end(bodyBuffer);
});
// Required fields
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = fields.comments || null;
if (!activityXid || !pqqQuestionXid || !pqqAnswerXid)
throw new ApiError(400, "Missing required fields");
// UPSERT header
const existingHeader = await hostService.findHeaderByCompositeKey(activityXid, pqqQuestionXid);
let header;
if (existingHeader) {
header = await hostService.updateHeader(existingHeader.id, pqqAnswerXid, comments);
} else {
header = await hostService.createHeader(activityXid, pqqQuestionXid, pqqAnswerXid, comments);
}
// SCORE
const score = await hostService.calculatePqqScoreForUser(activityXid);
// Existing supporting files
const existingSupportingFiles = await hostService.getSupportingFilesByHeaderId(header.id);
// Read deletedFiles from frontend
const deletedFiles = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
const deleteResults = [];
const addResults = [];
// DELETE explicitly requested files (Case 3)
if (deletedFiles.length > 0) {
for (const del of deletedFiles) {
const id = Number(del.id);
const record = existingSupportingFiles.find(f => f.id === id);
if (!record) continue;
// Delete from S3
if (record.mediaFileName) {
const key = getS3KeyFromUrl(record.mediaFileName);
await deleteFromS3(key);
}
// Delete from DB
await hostService.deleteSupportingFile(record.id);
deleteResults.push({ id: record.id, deleted: true });
}
}
// ADD new uploaded files (Case 1 + Case 3 new files)
if (files.length > 0) {
for (const file of files) {
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`
);
const newRec = await hostService.addSupportingFile(header.id, file.mimeType, url);
addResults.push(newRec);
}
}
const getAllUpdatedQuestionResponse = await hostService.getAllPQUpdatedResponse(activityXid)
// CASE 2 — NO deletion & NO new files => DO NOTHING to existing files
return {
statusCode: 200,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
body: JSON.stringify({
success: true,
message: existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully",
data: {
headerId: header.id,
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments,
score,
getAllUpdatedQuestionResponse
}
})
};
} catch (err: any) {
console.error("❌ Error:", err);
throw err;
}
});

View File

@@ -0,0 +1,46 @@
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);
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 verifyMinglarAdminHostToken(token);
const userId = Number(userInfo.id);
const question_xid = Number(event.queryStringParameters?.question_xid);
const activity_xid = Number(event.queryStringParameters?.activity_xid);
if (!question_xid || !activity_xid) {
throw new ApiError(400, "Question and activity xid are required.")
}
// Fetch user with their HostHeader stepper info
const pqqQuestionDetails = await hostService.getPQQQuestionDetail(question_xid, activity_xid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Stepper information retrieved successfully',
data: pqqQuestionDetails,
}),
};
});

View File

@@ -0,0 +1,58 @@
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 { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { HostService } from '../../../services/host.service';
const hostService = new HostService(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 verifyHostToken(token);
const userId = Number(userInfo.id);
const activity_xid = event.queryStringParameters?.activity_xid
? Number(event.queryStringParameters.activity_xid)
: null;
// Validate it
if (!activity_xid || isNaN(activity_xid)) {
throw new ApiError(400, "Activity id is required and must be a number.");
}
let result = null;
// Fetch user with their HostHeader stepper info
const pqqQuestionDetails = await hostService.getLatestQuestionDetailsPQQ(activity_xid);
if (pqqQuestionDetails) {
result = {
pqqQuestionXid: pqqQuestionDetails.pqqQuestionXid,
pqqAnswerXid: pqqQuestionDetails.pqqAnswerXid || null,
pqqSubCategoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategoryXid || null,
categoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategories.categoryXid || null
}
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Latest information retrieved successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,50 @@
// import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
// 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);
// 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(401, 'This is a protected route. Please provide a valid token.');
// const userInfo = await verifyHostToken(token);
// let body: any = {};
// try {
// body = event.body ? JSON.parse(event.body) : {};
// } catch (err) {
// throw new ApiError(400, 'Invalid JSON in request body');
// }
// const { activityTypeXid, frequenciesXid } = body;
// if (!activityTypeXid) {
// throw new ApiError(400, 'activityTypeXid is required');
// }
// await hostService.createActivity(
// userInfo.id,
// Number(activityTypeXid),
// frequenciesXid ? Number(frequenciesXid) : undefined,
// );
// return {
// statusCode: 201,
// headers: {
// 'Content-Type': 'application/json',
// 'Access-Control-Allow-Origin': '*',
// },
// body: JSON.stringify({
// success: true,
// message: 'Activity created successfully',
// data: null,
// }),
// };
// });

View File

@@ -0,0 +1,300 @@
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
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';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
// Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(bucketBaseUrl, '');
}
// Function to delete file from S3
async function deleteFromS3(s3Key: string): Promise<void> {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: s3Key,
}).promise();
console.log(`✅ File deleted from S3: ${s3Key}`);
} catch (error) {
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
// continue — we don't want S3 deletion failure to crash the whole request
}
}
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
// We intentionally do NOT reuse old key. If existingUrl is provided we delete old file and create a new random key.
if (existingUrl) {
try {
const oldKey = getS3KeyFromUrl(existingUrl);
await deleteFromS3(oldKey);
} catch (err) {
console.warn('Warning deleting existingUrl before upload', err);
}
}
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
const s3Key = `${prefix}/${uniqueKey}`;
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private'
}).promise();
console.log(`✅ File uploaded to S3: ${s3Key}`);
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
// 2) Content-Type check
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
if (!contentType?.includes("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
// 3) Body decoding
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body!, "base64")
: Buffer.from(event.body!, "binary");
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 4) Parse multipart data
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
if (!filename) {
file.resume();
return;
}
const chunks: Buffer[] = [];
let size = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on("data", (chunk) => {
size += chunk.length;
if (size > MAX_SIZE) {
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
return;
}
chunks.push(chunk);
});
file.on("end", () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
}
});
file.on("error", (err) =>
reject(new ApiError(400, `File upload error: ${err.message}`))
);
});
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;
}
}
});
bb.on("close", () => resolve());
bb.on("error", (err) =>
reject(new ApiError(400, `Multipart parsing error: ${err.message}`))
);
// IMPORTANT FIX for HTTP API
bb.write(bodyBuffer);
bb.end();
});
// 5) Extract required fields
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = fields.comments || null;
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Please provide a valid activity");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Please select a valid question");
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Please select a valid answer");
// 6) UPSERT header
const existingHeader = await hostService.findHeaderByCompositeKey(
activityXid,
pqqQuestionXid,
);
let header;
if (existingHeader) {
console.log("🔄 Updating existing PQQ header");
header = await hostService.updateHeader(
existingHeader.id,
pqqAnswerXid,
comments
);
} else {
console.log("🆕 Creating new PQQ header");
header = await hostService.createHeader(
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments
);
}
// 7) Get existing supporting files
const existingSupportingFiles = await hostService.getSupportingFilesByHeaderId(header.id);
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
// 8) Parse incoming control fields
// fields.deletedFiles should be array like [{ id: number, url: string }, ...] or null
const deletedFiles: Array<{ id: number; url?: string }> = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
// fields.existingFiles can be an array of urls; we accept it but do not require it
const existingFilesFromFront: string[] = Array.isArray(fields.existingFiles) ? fields.existingFiles : [];
// Prepare response trackers
const deletedResults: Array<{ id: number; success: boolean; reason?: string }> = [];
const addedResults: Array<any> = [];
// 9) Handle explicit deletions (ONLY delete ids provided in deletedFiles)
if (deletedFiles.length > 0) {
console.log(`🗑️ Processing ${deletedFiles.length} explicit deletions`);
// Build a map of existing supporting files by id for quick lookup
const existingById = new Map<number, any>();
for (const f of existingSupportingFiles) {
existingById.set(f.id, f);
}
for (const del of deletedFiles) {
const id = Number(del.id);
if (!id || !existingById.has(id)) {
deletedResults.push({ id, success: false, reason: 'Not found or invalid id' });
continue;
}
const record = existingById.get(id);
try {
// delete from s3
if (record.mediaFileName) {
const s3Key = getS3KeyFromUrl(record.mediaFileName);
await deleteFromS3(s3Key);
}
// delete DB record
await hostService.deleteSupportingFile(record.id);
deletedResults.push({ id: record.id, success: true });
console.log(`🗑️ Deleted supporting file record ${record.id}`);
} catch (err: any) {
console.error(`❌ Failed to delete supporting file id ${id}`, err);
deletedResults.push({ id, success: false, reason: err.message || 'delete failed' });
}
}
} else {
console.log(' No explicit deletions requested (deletedFiles empty)');
}
// 10) Handle new uploaded files (these are ALWAYS added as new rows)
if (files.length > 0) {
console.log(`📤 Processing ${files.length} uploaded new file(s)`);
for (const file of files) {
try {
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`
);
// create DB record
const supporting = await hostService.addSupportingFile(
header.id,
file.mimeType,
url
);
addedResults.push(supporting);
console.log(`🆕 Created new supporting file record: ${supporting.id}`);
} catch (err: any) {
console.error('❌ Error uploading/creating supporting file', err);
// push failure result but continue processing other files
addedResults.push({ success: false, reason: err.message || 'upload/create failed' });
}
}
} else {
console.log('📭 No new files uploaded in request');
}
// NOTE: We DO NOT delete or modify existing supporting files that were not listed in deletedFiles.
// This satisfies your Case 2: "if no files are provided, do not touch existing supporting files".
const allPQPQuestionAnswerResponse = await hostService.getAllPQUpdatedResponse(activityXid)
// 11) Compose response
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: responseMessage,
data: {
responseOfUpdatedData: allPQPQuestionAnswerResponse,
operation: existingHeader ? 'updated' : 'created',
// summary label for UI convenience:
fileOperation: (deletedResults.length > 0 || addedResults.length > 0) ? 'modified' : 'unchanged'
}
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

@@ -0,0 +1,40 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult } 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 pqqService = new HostService(prismaClient);
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
const activity_xid = event.queryStringParameters?.activity_xid
? Number(event.queryStringParameters.activity_xid)
: null;
await pqqService.submitpqqforreview(Number(activity_xid), Number(user.id))
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: "Your PQQ has been submitted for review.",
data: null
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

@@ -0,0 +1,298 @@
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
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';
const pqqService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
// Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(bucketBaseUrl, '');
}
// Function to delete file from S3
async function deleteFromS3(s3Key: string): Promise<void> {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: s3Key,
}).promise();
console.log(`✅ File deleted from S3: ${s3Key}`);
} catch (error) {
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
// continue — we don't want S3 deletion failure to crash the whole request
}
}
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
// We intentionally do NOT reuse old key. If existingUrl is provided we delete old file and create a new random key.
if (existingUrl) {
try {
const oldKey = getS3KeyFromUrl(existingUrl);
await deleteFromS3(oldKey);
} catch (err) {
console.warn('Warning deleting existingUrl before upload', err);
}
}
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
const s3Key = `${prefix}/${uniqueKey}`;
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private'
}).promise();
console.log(`✅ File uploaded to S3: ${s3Key}`);
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
// 2) Content-Type check
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
if (!contentType?.includes("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
// 3) Body decoding
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body!, "base64")
: Buffer.from(event.body!, "binary");
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 4) Parse multipart data
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
if (!filename) {
file.resume();
return;
}
const chunks: Buffer[] = [];
let size = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on("data", (chunk) => {
size += chunk.length;
if (size > MAX_SIZE) {
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
return;
}
chunks.push(chunk);
});
file.on("end", () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
}
});
file.on("error", (err) =>
reject(new ApiError(400, `File upload error: ${err.message}`))
);
});
bb.on("field", (fieldname, val) => {
if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null;
else {
try {
fields[fieldname] = JSON.parse(val);
} catch {
fields[fieldname] = val;
}
}
});
bb.on("close", () => resolve());
bb.on("error", (err) =>
reject(new ApiError(400, `Multipart parsing error: ${err.message}`))
);
// IMPORTANT FIX for HTTP API
bb.write(bodyBuffer);
bb.end();
});
// 5) Extract required fields
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = fields.comments || null;
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Please provide a valid activity");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Please select a valid question");
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Please select a valid answer");
// 6) UPSERT header
const existingHeader = await pqqService.findHeaderByCompositeKey(
activityXid,
pqqQuestionXid,
);
let header;
if (existingHeader) {
console.log("🔄 Updating existing PQQ header");
header = await pqqService.updateHeader(
existingHeader.id,
pqqAnswerXid,
comments
);
} else {
console.log("🆕 Creating new PQQ header");
header = await pqqService.createHeader(
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments
);
}
// 7) Get existing supporting files
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
// 8) Parse incoming control fields
// fields.deletedFiles should be array like [{ id: number, url: string }, ...] or null
const deletedFiles: Array<{ id: number; url?: string }> = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
// fields.existingFiles can be an array of urls; we accept it but do not require it
const existingFilesFromFront: string[] = Array.isArray(fields.existingFiles) ? fields.existingFiles : [];
// Prepare response trackers
const deletedResults: Array<{ id: number; success: boolean; reason?: string }> = [];
const addedResults: Array<any> = [];
// 9) Handle explicit deletions (ONLY delete ids provided in deletedFiles)
if (deletedFiles.length > 0) {
console.log(`🗑️ Processing ${deletedFiles.length} explicit deletions`);
// Build a map of existing supporting files by id for quick lookup
const existingById = new Map<number, any>();
for (const f of existingSupportingFiles) {
existingById.set(f.id, f);
}
for (const del of deletedFiles) {
const id = Number(del.id);
if (!id || !existingById.has(id)) {
deletedResults.push({ id, success: false, reason: 'Not found or invalid id' });
continue;
}
const record = existingById.get(id);
try {
// delete from s3
if (record.mediaFileName) {
const s3Key = getS3KeyFromUrl(record.mediaFileName);
await deleteFromS3(s3Key);
}
// delete DB record
await pqqService.deleteSupportingFile(record.id);
deletedResults.push({ id: record.id, success: true });
console.log(`🗑️ Deleted supporting file record ${record.id}`);
} catch (err: any) {
console.error(`❌ Failed to delete supporting file id ${id}`, err);
deletedResults.push({ id, success: false, reason: err.message || 'delete failed' });
}
}
} else {
console.log(' No explicit deletions requested (deletedFiles empty)');
}
// 10) Handle new uploaded files (these are ALWAYS added as new rows)
if (files.length > 0) {
console.log(`📤 Processing ${files.length} uploaded new file(s)`);
for (const file of files) {
try {
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`
);
// create DB record
const supporting = await pqqService.addSupportingFile(
header.id,
file.mimeType,
url
);
addedResults.push(supporting);
console.log(`🆕 Created new supporting file record: ${supporting.id}`);
} catch (err: any) {
console.error('❌ Error uploading/creating supporting file', err);
// push failure result but continue processing other files
addedResults.push({ success: false, reason: err.message || 'upload/create failed' });
}
}
} else {
console.log('📭 No new files uploaded in request');
}
// NOTE: We DO NOT delete or modify existing supporting files that were not listed in deletedFiles.
// This satisfies your Case 2: "if no files are provided, do not touch existing supporting files".
// 11) Compose response
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: responseMessage,
data: {
headerId: header.id,
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments,
files: {
added: addedResults,
deleted: deletedResults,
existingKeptCount: (existingSupportingFiles.length - deletedResults.filter(d => d.success).length)
},
operation: existingHeader ? 'updated' : 'created',
// summary label for UI convenience:
fileOperation: (deletedResults.length > 0 || addedResults.length > 0) ? 'modified' : 'unchanged'
}
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

@@ -0,0 +1,50 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
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);
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(401, 'This is a protected route. Please provide a valid token.');
const userInfo = await verifyHostToken(token);
let body: any = {};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (err) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityPqqHeaderXid, activityPQQSuggestionId } = body;
if (!activityPqqHeaderXid) {
throw new ApiError(400, 'activityPqqHeaderXid is required');
}
await hostService.markPQQSuggestionReviewed(
userInfo.id,
Number(activityPqqHeaderXid),
Number(activityPQQSuggestionId)
);
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestion reviewed successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,100 @@
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 { SchedulingService } from '../../../services/activityScheduling.service';
import { HostService } from '../../../services/host.service';
const schedulingService = new SchedulingService(prismaClient);
const hostService = new HostService(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 the shared authForHost function
const userInfo = await verifyHostToken(token);
const hostId = userInfo.id;
if (Number.isNaN(hostId)) {
throw new ApiError(400, 'Host id must be a number');
}
const host = await hostService.getHostIdByUserXid(hostId);
if (!host) {
throw new ApiError(404, 'Host not found');
}
let body: {
activityXid: number;
venueXid: number;
cancellations: {
scheduleHeaderXid: number;
occurenceDate: string;
startTime: string;
endTime: string;
cancellationReason: string
}[]
};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch {
throw new ApiError(400, 'Invalid JSON payload');
}
if (!body.activityXid || !body.venueXid || !Array.isArray(body.cancellations) || body.cancellations.length === 0) {
throw new ApiError(400, 'Missing required fields');
}
const activity = await schedulingService.getActivityByXid(body.activityXid);
if (!activity) {
throw new ApiError(404, "Activity not found");
}
const venueExists = await schedulingService.getVenueFromVenueXid(
body.venueXid,
body.activityXid
);
if (!venueExists) {
throw new ApiError(
404,
`Venue not found for this activity`
)
}
await schedulingService.cancelMultipleSlotsForActivity(
body.cancellations.map((item: any) => ({
scheduleHeaderXid: Number(item.scheduleHeaderXid),
occurenceDate: item.occurenceDate,
startTime: item.startTime,
endTime: item.endTime,
cancellationReason: item.cancellationReason
}))
);
const result = await schedulingService.getVenueDurationByAct(Number(body.activityXid), Number(hostId));
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Slot blocked successfully',
data: result
}),
};
});

View File

@@ -0,0 +1,89 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { SchedulingService } from '../../../services/activityScheduling.service';
import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { scheduleActivity } from '../../../../../common/utils/validation/host/createSchedulingOfAct.validation';
import { z } from 'zod';
const schedulingService = new SchedulingService(prismaClient);
const hostService = new HostService(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 the shared authForHost function
const userInfo = await verifyHostToken(token);
const hostId = userInfo.id;
if (Number.isNaN(hostId)) {
throw new ApiError(400, 'Host id must be a number');
}
const host = await hostService.getHostIdByUserXid(hostId);
if (!host) {
throw new ApiError(404, 'Host not found');
}
let body: unknown;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch {
throw new ApiError(400, 'Invalid JSON payload');
}
// ✅ Validate payload using Zod
const parsed = scheduleActivity.safeParse(body);
if (!parsed.success) {
const msg = parsed.error.issues
.map(e => e.message)
.join(', ');
throw new ApiError(400, `Validation failed: ${msg}`);
}
const activity = await schedulingService.getActivityByXid(parsed.data.activityXid);
if (!activity) {
throw new ApiError(404, "Activity not found");
}
if (parsed.data.venues && parsed.data.venues.length > 0) {
for (const venue of parsed.data.venues) {
const venueExists = await schedulingService.getVenueFromVenueXid(
venue.venueXid,
parsed.data.activityXid
);
if (!venueExists) {
throw new ApiError(
404,
`Venue with xid ${venue.venueXid} not found for this activity`
);
}
}
}
await schedulingService.addSchedulingForActivity(parsed.data);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Scheduling details updated successfully',
}),
};
});

View File

@@ -0,0 +1,64 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { SchedulingService } from '../../../services/activityScheduling.service';
import { ACTIVITY_INTERNAL_STATUS } from '../../../../../common/utils/constants/host.constant';
const schedulingService = new SchedulingService(prismaClient);
/**
* GET /activities
* Query Parameters:
* - status: Listed | Unlisted | Not_Listed (optional - if not provided, returns all)
* - hostId: ID of host (required from token)
*
* Returns activities based on status filter
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Get and verify token
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 verifyMinglarAdminHostToken(token);
const userId = Number(userInfo.id);
// Get status filter from query parameters
const status = event.queryStringParameters?.status as string | undefined;
const hostId = await schedulingService.getHostIdByUserId(userId);
// Validate status if provided
const validStatuses = [ACTIVITY_INTERNAL_STATUS.ACTIVITY_APPROVED, ACTIVITY_INTERNAL_STATUS.ACTIVITY_UNLISTED, ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED];
if (status && !validStatuses.includes(status)) {
throw new ApiError(400, `Invalid status. Must be one of: ${validStatuses.join(', ')}`);
}
// Get activities from service
const activities = await schedulingService.getActivitiesByStatus(hostId, status);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Activities retrieved successfully',
data: {
total: activities.length,
activities: activities,
filter: {
status: status || 'All',
},
},
}),
};
});

View File

@@ -0,0 +1,52 @@
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 { SchedulingService } from '../../../services/activityScheduling.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
const schedulingService = new SchedulingService(prismaClient);
/**
* GET /activities
* Query Parameters:
* - status: Listed | Unlisted | Not_Listed (optional - if not provided, returns all)
* - hostId: ID of host (required from token)
*
* Returns activities based on status filter
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Get and verify token
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 userId = Number(userInfo.id);
const activityXid = event.pathParameters?.activityXid
if (!activityXid) {
throw new ApiError(400, 'activityXid is required in path parameters');
}
const hostId = await schedulingService.getHostIdByUserId(userId);
const result = await schedulingService.getVenueDurationByAct(Number(activityXid), Number(hostId));
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: '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 { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { SchedulingService } from '../../../services/activityScheduling.service';
import { HostService } from '../../../services/host.service';
const schedulingService = new SchedulingService(prismaClient);
const hostService = new HostService(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 the shared authForHost function
const userInfo = await verifyHostToken(token);
const hostId = userInfo.id;
if (Number.isNaN(hostId)) {
throw new ApiError(400, 'Host id must be a number');
}
const host = await hostService.getHostIdByUserXid(hostId);
if (!host) {
throw new ApiError(404, 'Host not found');
}
let body: {
activityXid: number;
venueXid: number;
cancellations: { cancellationXid: number; }[];
};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch {
throw new ApiError(400, 'Invalid JSON payload');
}
if (
!body.activityXid ||
!body.venueXid ||
!Array.isArray(body.cancellations) ||
body.cancellations.length === 0
) {
throw new ApiError(400, 'Missing required fields');
}
const activity = await schedulingService.getActivityByXid(body.activityXid);
if (!activity) {
throw new ApiError(404, 'Activity not found');
}
const venueExists = await schedulingService.getVenueFromVenueXid(
body.venueXid,
body.activityXid,
);
if (!venueExists) {
throw new ApiError(404, `Venue not found for this activity`);
}
await schedulingService.openCanceledSlot(
body.cancellations.map((item: any) => ({
cancellationXid: Number(item.cancellationXid),
})),
);
const result = await schedulingService.getVenueDurationByAct(
Number(body.activityXid),
Number(hostId),
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Slot opened successfully',
data: result,
}),
};
},
);

View File

@@ -0,0 +1,46 @@
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';
const hostService = new HostService(prismaClient);
/**
* 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 verifyHostToken(token);
// Accept agreement and get dynamic fields and PDF URL
const result = await hostService.acceptMinglarAgreement(userInfo.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Application accepted successfully',
data: {
filePath: result.filePath,
dynamicFields: result.dynamicFields,
},
}),
};
});

View File

@@ -0,0 +1,64 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
const hostService = new HostService(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 the shared authForHost function
const userInfo = await verifyHostToken(token);
const user_xid = userInfo.id;
// Parse request body
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');
}
// Validate password match
if (password !== confirmPassword) {
throw new ApiError(400, 'Password and confirm password do not match');
}
// Validate password length
if (password.length < 8) {
throw new ApiError(400, 'Password must be at least 8 characters long');
}
await hostService.createPassword(user_xid, 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,54 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { MinglarService } from '../../../../minglaradmin/services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
const minglarService = new MinglarService(prismaClient);
/**
* Get suggestions handler
* Retrieves suggestions based on user's role and host assignments
*/
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 extract user info
const userInfo = await verifyMinglarAdminHostToken(token);
// ✅ Extract activityXid from query parameters
const activityXidParam = event.queryStringParameters?.activityXid;
if (!activityXidParam) {
throw new ApiError(400, 'Missing required query parameter: activityXid');
}
const activityXid = Number(activityXidParam);
if (isNaN(activityXid) || activityXid <= 0) {
throw new ApiError(400, 'Invalid activityXid provided. Must be a positive number.');
}
// ✅ Fetch suggestions from the service
const suggestions = await minglarService.getHostSuggestionsForActivity(activityXid);
// ✅ Return response
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestions retrieved successfully',
data: suggestions,
}),
};
});

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

@@ -0,0 +1,70 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { HostService } from '../../../services/host.service';
import { TokenService } from '../../../services/token.service';
import { GetHostLoginResponseDTO } from '../../../dto/host.dto';
import ApiError from '../../../../../common/utils/helper/ApiError';
const hostService = new HostService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse request body
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 emailToLowerCase = emailAddress.toLowerCase()
const loginForHost = await hostService.loginForHost(emailToLowerCase, userPassword);
if (!loginForHost) {
throw new ApiError(400, 'Failed to login');
}
if (!loginForHost.userPassword) {
throw new ApiError(401, 'Invalid credentials');
}
const generateTokenForHost = await tokenService.generateAuthToken(
loginForHost.id
);
if (!generateTokenForHost) {
throw new ApiError(500, 'Failed to generate token');
}
const loginForHostResponse = new GetHostLoginResponseDTO(
loginForHost,
generateTokenForHost.access.token,
generateTokenForHost.refresh.token
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Login successful',
data: loginForHostResponse,
}),
};
});

View File

@@ -0,0 +1,42 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { MinglarService } from '../../../../minglaradmin/services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
const minglarService = new MinglarService(prismaClient);
/**
* Get suggestions handler
* Retrieves suggestions based on user's role and host assignments
*/
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 verifyHostToken(token);
// Get suggestions using service
const suggestions = await minglarService.getHostSuggestions(userInfo.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestions retrieved successfully',
data: suggestions,
}),
};
});

View File

@@ -0,0 +1,121 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import * as bcrypt from 'bcryptjs';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
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: {
id: 'desc',
},
select: {
id: true,
},
});
const nextId = lastrecord ? lastrecord.id + 1 : 1;
return `076-H-${String(nextId).padStart(6, '0')}`;;
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse request body
let body: { email?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { email } = body;
if (!email) {
throw new ApiError(400, 'Email is required');
}
const emailToLowerCase = email.toLowerCase()
// 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 },
});
if (user && user.userPassword) {
throw new ApiError(409, 'User is already registered. Please login.');
}
let newUserLocal;
const referenceNumber = await generateHostRefNumber(tx);
if (user && !user.userPassword) {
// reuse existing invited user record
newUserLocal = user;
} else {
// create new user record within the transaction
newUserLocal = await tx.user.create({
data: { emailAddress: emailToLowerCase, roleXid: ROLE.HOST, userRefNumber: referenceNumber },
});
}
// Generate OTP (6-digit) and store within the same transaction
const otp = OtpGeneratorSixDigit.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes
// delete old active OTPs for this user/purpose
await tx.userOtp.deleteMany({
where: { userXid: Number(newUserLocal.id), otpType: 'Register', isActive: true },
});
await tx.userOtp.create({
data: {
userXid: Number(newUserLocal.id),
otpType: 'Register',
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
const encryptedId = encryptUserId(String(newUserLocal.id));
return { newUser: newUserLocal, otp, encryptedId };
});
if (!transactionResult || !transactionResult.otp) {
throw new ApiError(500, 'Failed to generate OTP');
}
// Send OTP email outside the DB transaction
await sendOtpEmailForHost(transactionResult.newUser.emailAddress, transactionResult.otp);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP sent successfully.',
data: {},
}),
};
});

View File

@@ -0,0 +1,498 @@
// modules/host/handlers/addCompanyDetails.ts
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
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 {
hostCompanyDetailsSchema,
hostDocumentsSchema,
parentCompanySchema,
} from '../../../../../common/utils/validation/host/hostCompanyDetails.validation';
import { HostService } from '../../../services/host.service';
import { sendEmailToAM, sendEmailToMinglarAdmin } from '../../../services/sendHostResubmitEmailToAM.service';
const hostService = new HostService(prismaClient);
function getExtensionFromMime(mimeType: string) {
const map: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'application/pdf': 'pdf',
'image/webp': 'webp',
};
return map[mimeType] || 'bin';
}
const s3 = new AWS.S3({
region: config.aws.region,
});
function normalizeJsonField(fields: any, key: string) {
if (!fields[key]) return undefined;
const val = fields[key];
if (typeof val === 'object') return val;
if (typeof val === 'string') {
try {
return JSON.parse(val);
} catch (err) {
throw new ApiError(400, `Invalid JSON in field: ${key}`);
}
}
throw new ApiError(400, `Invalid input: ${key} must be object or JSON string.`);
}
function cleanEmptyStrings(obj: any) {
if (!obj || typeof obj !== 'object') return obj;
const cleaned: any = {};
for (const key of Object.keys(obj)) {
if (obj[key] === '') cleaned[key] = undefined;
else if (typeof obj[key] === 'object') cleaned[key] = cleanEmptyStrings(obj[key]);
else cleaned[key] = obj[key];
}
return cleaned;
}
function getS3KeyFromUrl(url: string): string {
const base = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(base, "");
}
async function deleteFromS3(key: string) {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: key
}).promise();
console.log("✅ Deleted from S3:", key);
} catch (err) {
console.error("❌ Failed to delete from S3:", key, err);
}
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
/** 1) AUTH */
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);
/** 2) CHECK CONTENT TYPE */
const contentType = event.headers['content-type'] || event.headers['Content-Type'];
if (!contentType?.includes('multipart/form-data')) {
throw new ApiError(400, 'Content-Type must be multipart/form-data.');
}
/** 3) HANDLE BODY */
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body as string, 'base64')
: Buffer.from(event.body as string, 'binary');
const fields: Record<string, any> = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
const chunks: Buffer[] = [];
let totalSize = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on('data', (chunk) => {
totalSize += chunk.length;
if (totalSize > MAX_SIZE) {
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
return;
}
chunks.push(chunk);
});
file.on('end', () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType: mimeType || 'application/octet-stream',
fileName: filename || 'unknown',
fieldName: fieldname,
});
}
});
file.on('error', (error) => reject(new ApiError(400, `File upload error: ${error.message}`)));
});
bb.on('field', (fieldname, val) => (fields[fieldname] = val));
bb.on('close', () => resolve());
bb.on('error', (error) => reject(new ApiError(400, `Multipart parsing error: ${error.message}`)));
bb.write(bodyBuffer);
bb.end();
});
const deletedFiles = normalizeJsonField(fields, "deletedFiles") || [];
const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || [];
/** 4) Extract and clean isDraft flag */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
/** 5) PROCESS companyDetails ONCE ONLY (IMPORTANT FIX) */
let companyDetailsRaw = normalizeJsonField(fields, 'companyDetails');
if (!companyDetailsRaw) throw new ApiError(400, 'companyDetails is required.');
if (isDraft) {
companyDetailsRaw = cleanEmptyStrings(companyDetailsRaw);
// IMPORTANT: also clean parent company nested fields
if (companyDetailsRaw.parentCompany) {
companyDetailsRaw.parentCompany = cleanEmptyStrings(companyDetailsRaw.parentCompany);
}
}
if (
companyDetailsRaw.parentCompany &&
Object.values(companyDetailsRaw.parentCompany).every(
(v) => v === undefined || v === null
)
) {
companyDetailsRaw.parentCompany = null;
}
/** 6) Profile update if provided */
if (fields.userProfile) {
const userProfileRaw = normalizeJsonField(fields, 'userProfile');
if (userProfileRaw) {
const { firstName, lastName, mobileNumber } = userProfileRaw;
await prismaClient.user.update({
where: { id: userInfo.id },
data: {
...(firstName && { firstName }),
...(lastName && { lastName }),
...(mobileNumber && { mobileNumber }),
},
});
}
}
/** 7) VALIDATION - SKIPPED IF DRAFT */
let parsedCompany: any = companyDetailsRaw;
if (!isDraft) {
const validate = hostCompanyDetailsSchema.safeParse(companyDetailsRaw);
if (!validate.success) {
const message = validate.error.issues.map((i) => i.message).join(', ');
throw new ApiError(400, `Validation failed: ${message}`);
}
parsedCompany = validate.data;
}
/** 8) DOCUMENT METADATA */
const documentsMetadataRaw = normalizeJsonField(fields, 'documents');
if (!Array.isArray(documentsMetadataRaw)) throw new ApiError(400, 'documents must be an array.');
if (!isDraft) {
const docsParse = hostDocumentsSchema.safeParse(documentsMetadataRaw);
if (!docsParse.success) {
const message = docsParse.error.issues.map((i) => i.message).join(', ');
throw new ApiError(400, `Documents validation failed: ${message}`);
}
}
const documentsMetadata = documentsMetadataRaw.map((d: any) => ({
...d,
owner: d.owner || 'host',
}));
const documentMetadata = documentsMetadata.map((doc: any) => {
const file = files.find((f) => f.fieldName === doc.fieldName);
// In DRAFT mode → allow missing documents
if (!file) {
return { ...doc, file: null };
}
return { ...doc, file };
});
/** 9) SPLIT host & parent docs */
const hostDocs = documentMetadata.filter((d) => d.owner === 'host');
const parentDocs = documentMetadata.filter((d) => d.owner === 'parent');
/** 10) VALIDATE PARENT COMPANY (ONLY IN FINAL SUBMISSION) */
let parsedParentCompany: any = null;
if (!isDraft && parsedCompany.isSubsidairy) {
if (!parsedCompany.parentCompany) {
throw new ApiError(400, 'isSubsidairy is true but parentCompany object is missing.');
}
const parentCheck = parentCompanySchema.safeParse(parsedCompany.parentCompany);
if (!parentCheck.success) {
const message = parentCheck.error.issues.map((i) => i.message).join(', ');
throw new ApiError(400, `Parent company validation failed: ${message}`);
}
parsedParentCompany = parentCheck.data;
} else {
parsedParentCompany = parsedCompany.parentCompany || null;
}
/** 9.5) DELETE DOCUMENTS IF REQUESTED **/
if (Array.isArray(deletedFiles) && deletedFiles.length > 0) {
console.log(`🗑️ Deleting ${deletedFiles.length} documents...`);
for (const del of deletedFiles) {
const id = Number(del.id);
const url = del.url;
if (!id || !url) {
console.log("❌ Invalid delete entry:", del);
continue;
}
// Extract S3 key
const s3Key = getS3KeyFromUrl(url);
// Delete from S3
await deleteFromS3(s3Key);
// Delete from DB
await prismaClient.hostDocuments.delete({
where: { id }
});
console.log(`🗑️ Deleted host document ID ${id}`);
}
}
/** 9.6) DELETE PARENT DOCUMENTS **/
if (parsedCompany.isSubsidairy && Array.isArray(parentDeletedFiles) && parentDeletedFiles.length > 0) {
console.log(`🗑️ Deleting ${parentDeletedFiles.length} PARENT documents...`);
for (const del of parentDeletedFiles) {
const id = Number(del.id);
const url = del.url;
if (!id || !url) {
console.log("⚠️ Invalid parent delete entry:", del);
continue;
}
const s3Key = getS3KeyFromUrl(url);
// Delete S3
await deleteFromS3(s3Key);
// Delete DB
await prismaClient.hostParenetDocuments.delete({
where: { id }
});
console.log(`🗑️ Deleted PARENT document ID ${id}`);
}
}
/** 11) UPLOAD DOCUMENTS */
async function uploadToS3(buffer, mimeType, originalName, folderType, documentTypeXid?, fieldName?) {
// const ext = originalName.split('.').pop() || 'jpg';
const ext = getExtensionFromMime(mimeType);
let s3Key = '';
if (folderType === 'logo') {
s3Key = `Documents/Host/${userInfo.id}/logo/company_logo.${ext}`;
}
else if (folderType === 'parent_company_logo') {
s3Key = `Documents/Host/${userInfo.id}/parent_company/logo/parent_company_logo.${ext}`;
}
else if (folderType === 'documents') {
s3Key = `Documents/Host/${userInfo.id}/documents/${documentTypeXid}_${fieldName}.${ext}`;
}
else if (folderType === 'parent_company') {
s3Key = `Documents/Host/${userInfo.id}/parent_company/${documentTypeXid}_${fieldName}.${ext}`;
}
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}`;
}
/** Upload host docs */
const uploadedHostDocs: Array<any> = [];
for (const doc of hostDocs) {
if (!doc.file) continue;
const path = await uploadToS3(
doc.file.buffer,
doc.file.mimeType,
doc.file.fileName,
'documents',
doc.documentTypeXid,
doc.fieldName,
);
uploadedHostDocs.push({
documentTypeXid: doc.documentTypeXid,
documentName: doc.fieldName,
filePath: path,
});
}
/** Upload parent docs */
const uploadedParentDocs: Array<any> = [];
for (const doc of parentDocs) {
if (!doc.file) continue; // skip missing files in draft mode
const path = await uploadToS3(
doc.file.buffer,
doc.file.mimeType,
doc.file.fileName,
'parent_company',
doc.documentTypeXid,
doc.fieldName,
);
uploadedParentDocs.push({
documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName,
filePath: path,
});
}
/** UPLOAD LOGO (if provided) */
const logoFile = files.find(
(f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
);
if (logoFile && logoFile.buffer && logoFile.fileName) {
const logoUrl = await uploadToS3(
logoFile.buffer,
logoFile.mimeType,
logoFile.fileName,
'logo'
);
parsedCompany.logoPath = logoUrl;
}
/** UPLOAD PARENT COMPANY LOGO (if provided) */
const parentLogoFile = files.find(
(f) => f.fieldName === 'parentCompanyLogo'
);
if (parentLogoFile && parentLogoFile.buffer && parentLogoFile.mimeType) {
// 🔒 Only upload when an actual file is present
const parentLogoUrl = await uploadToS3(
parentLogoFile.buffer,
parentLogoFile.mimeType,
parentLogoFile.fileName, // safe here because it's a real file
'parent_company_logo',
);
if (parsedParentCompany) {
parsedParentCompany.logoPath = parentLogoUrl;
} else {
parsedParentCompany = {
logoPath: parentLogoUrl,
};
}
}
if (parsedCompany.cityXid) {
const city = await prismaClient.cities.findUnique({
where: { id: Number(parsedCompany.cityXid) }
});
if (!city) {
throw new ApiError(400, `City with ID ${parsedCompany.cityXid} not found`);
}
}
if (!parsedCompany.isSubsidairy) {
const parentDocuments = await hostService.getParentDocumentsByHostId(userInfo.id);
if (parentDocuments.length > 0) {
for (const doc of parentDocuments) {
try {
const s3Key = getS3KeyFromUrl(doc.filePath);
await deleteFromS3(s3Key);
} catch (e) {
console.error("S3 delete failed:", doc.filePath, e);
}
}
}
await hostService.deleteExistingParentRecords(userInfo.id)
}
/** 12) SAVE / UPDATE HOST ENTRY */
const createdOrUpdated = await hostService.addOrUpdateCompanyDetails(
userInfo.id,
parsedCompany,
uploadedHostDocs,
parsedParentCompany,
uploadedParentDocs,
isDraft,
);
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');
/** 13) SEND EMAIL ONLY IN FINAL SUBMISSION */
if (!isDraft) {
const details = await hostService.getSuggestionDetails(userInfo.id);
if (details.hostDetails.accountManagerXid) {
await sendEmailToAM(
details.hostDetails.accountManager.emailAddress,
details.hostDetails.accountManager.firstName,
details.hostDetails.companyName,
details.hostDetails.user.userRefNumber,
);
} else {
await sendEmailToMinglarAdmin(
config.MinglarAdminEmail,
config.MinglarAdminName,
details.hostDetails.companyName,
details.hostDetails.user.userRefNumber,
);
}
}
/** RESPONSE */
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: isDraft ? 'Company details saved as draft successfully.' : 'Company details uploaded successfully.',
data: {
id: createdOrUpdated.id,
isDraft,
},
}),
};
} catch (error: any) {
console.error('❌ Error in addCompanyDetails:', error);
throw error;
}
});

View File

@@ -0,0 +1,78 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { hostBankDetailsSchema } from '../../../../../common/utils/validation/host/addPaymentDetails.validation';
const hostService = new HostService(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 the shared authForHost function
const userInfo = await verifyHostToken(token);
const hostId = userInfo.id;
if (Number.isNaN(hostId)) {
throw new ApiError(400, 'Host id must be a number');
}
const host = await hostService.getHostIdByUserXid(hostId);
if (!host) {
throw new ApiError(404, 'Host not found');
}
// Parse request body
let body: { bankXid?: number; bankBranchXid?: number; accountNumber?: string; confirmAccountNumber?: string; accountHolderName?: string; currencyXid?: number };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
// ✅ Validate payload using Zod
const validationResult = hostBankDetailsSchema.safeParse({
...(body as object),
hostXid: host.host.id, // inject hostId from token (not from user input)
});
if (!validationResult.success) {
const errorMessages = validationResult.error.issues.map(e => e.message).join(', ');
throw new ApiError(400, `Validation failed: ${errorMessages}`);
}
const validatedData = validationResult.data;
// Fetch IFSC code from bank branch
const bankBranch = await hostService.getBankBranchById(validatedData.bankBranchXid);
if (!bankBranch) {
throw new ApiError(404, 'Bank branch not found');
}
await hostService.addPaymentDetails({
...validatedData,
ifscCode: bankBranch.ifscCode,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Payment details added successfully',
}),
};
});

View File

@@ -0,0 +1,53 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { HostService } from '../../../services/host.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { TokenService } from '../../../services/token.service';
const hostService = new HostService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse request body
let body: { email?: string; otp?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { email, otp } = body;
if (!email || !otp) {
throw new ApiError(400, 'Email and OTP are required');
}
const emailToLowerCase = email.toLowerCase();
await hostService.verifyHostOtp(emailToLowerCase, otp);
const user = await hostService.getHostByEmail(emailToLowerCase);
const generateTokenForHost = await tokenService.generateAuthToken(
user.id
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP verified successfully',
accessToken: generateTokenForHost.access.token,
refreshToken: generateTokenForHost.refresh.token,
data: null,
}),
};
});

View File

@@ -0,0 +1,47 @@
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 { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
import { HostService } from '../services/host.service';
const hostService = new HostService(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 verifyHostToken(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 host = await hostService.getHostIdByUserXid(userId);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Stepper information retrieved successfully',
data: {
stepper: host?.host?.stepper || null,
emailAddress: host.user?.emailAddress || null,
hostId: host.user?.userRefNumber || null,
},
}),
};
});

View File

@@ -0,0 +1,49 @@
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);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Get host ID from path parameters
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 verifyMinglarAdminHostToken(token);
const id = Number(userInfo.id)
if (!id) {
throw new ApiError(400, 'Host ID is required');
}
if (isNaN(id)) {
throw new ApiError(400, 'Invalid host ID format');
}
const hostDetails = await hostService.getHostById(id);
if (!hostDetails) {
throw new ApiError(404, 'Host not found');
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host details retrieved successfully',
data: hostDetails,
}),
};
});

View File

@@ -1,8 +1,8 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaClient } from '@prisma/client';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
const prisma = new PrismaClient();
const prisma = prismaClient;
export const handler = safeHandler(async (
event: APIGatewayProxyEvent
@@ -11,7 +11,6 @@ export const handler = safeHandler(async (
const result = await prisma.hostHeader.findMany({
select: {
hostParent: true,
hostRefNumber: true,
hostStatusDisplay: true,
accountManager: true,
},

View File

@@ -0,0 +1,107 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
import config from '../../../config/config';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
const s3 = new S3Client({ region: config.aws.region });
function extractS3Key(input: string): string {
if (input.startsWith('s3://')) {
return input.replace(`s3://${config.aws.bucketName}/`, '');
}
if (input.startsWith('https://')) {
const url = new URL(input);
return url.pathname.replace(/^\/+/, '');
}
return input;
}
export const handler: APIGatewayProxyHandler = async (event) => {
try {
/* ---------------- AUTH ---------------- */
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
await verifyHostToken(token);
/* ---------------- BODY ---------------- */
const body = JSON.parse(event.body || '{}');
const { key, mediaSource, mediaId } = body;
if (mediaSource && mediaId) {
if (!['ACTIVITY', 'VENUE'].includes(mediaSource)) {
throw new ApiError(400, 'Invalid mediaSource');
}
/* ---------------- DB DELETE ---------------- */
if (mediaSource === 'ACTIVITY') {
const media = await prismaClient.activitiesMedia.findUnique({
where: { id: Number(mediaId) },
});
if (!media) throw new ApiError(404, 'Activity media not found');
await prismaClient.activitiesMedia.delete({
where: { id: media.id },
});
}
if (mediaSource === 'VENUE') {
const media = await prismaClient.activityVenueArtifacts.findUnique({
where: { id: Number(mediaId) },
});
if (!media) throw new ApiError(404, 'Venue media not found');
await prismaClient.activityVenueArtifacts.delete({
where: { id: media.id },
});
}
}
const s3Key = extractS3Key(key);
/* ---------------- PATH SAFETY ---------------- */
const allowedPrefixes = ['ActivityOnboarding/'];
if (!allowedPrefixes.some((p) => s3Key.startsWith(p))) {
throw new ApiError(403, 'Unauthorized delete path');
}
/* ---------------- S3 DELETE ---------------- */
await s3.send(
new DeleteObjectCommand({
Bucket: config.aws.bucketName!,
Key: s3Key,
}),
);
return response(200, {
success: true,
message: 'Media deleted from DB and S3 successfully',
});
} catch (err: any) {
console.error('ERROR:', err);
if (err instanceof ApiError) {
return response(err.statusCode, err.message);
}
return response(500, 'Internal server error');
}
};
function response(statusCode: number, body: any) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(body),
};
}

View File

@@ -0,0 +1,104 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { v4 as uuid } from 'uuid';
import ApiError from '../../../common/utils/helper/ApiError';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { HostService } from '../services/host.service';
import config from '../../../config/config';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
const s3 = new S3Client({ region: config.aws.region });
const hostService = new HostService(prismaClient);
export const handler: APIGatewayProxyHandler = async (event) => {
try {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
await verifyHostToken(token);
const body = JSON.parse(event.body || '{}');
const { files, venueTempId } = body;
if (!venueTempId) {
throw new ApiError(400, 'venueTempId is required');
}
if (!Array.isArray(files) || files.length === 0) {
throw new ApiError(400, 'files array is required');
}
const activityXid = event.pathParameters?.activityXid;
if (!activityXid) {
throw new ApiError(400, 'activityXid is required in path parameters');
}
const activityDetails = await hostService.getActivityDetailsById(Number(activityXid));
if (!activityDetails) {
throw new ApiError(404, 'Activity not found');
}
const results: Array<any> = [];
for (const file of files) {
const { fileName, mimeType } = file;
if (!fileName || !mimeType) {
throw new ApiError(400, 'Each file must have fileName and mimeType');
}
const safeFileName = fileName
.trim()
.replace(/\s+/g, '_')
.replace(/[^a-zA-Z0-9._-]/g, '')
.toLowerCase();
const key = `ActivityOnboarding/Activity_${activityXid}/Venues/${venueTempId}/${uuid()}_${safeFileName}`;
const command = new PutObjectCommand({
Bucket: config.aws.bucketName!,
Key: key,
ContentType: mimeType,
});
const uploadUrl = await getSignedUrl(s3, command, {
expiresIn: 300, // 5 minutes
});
results.push({
uploadUrl,
key,
fileUrl: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${key}`,
});
}
return response(200, {
venueTempId,
files: results,
});
} catch (err: any) {
console.error('ERROR:', err);
// If it's your ApiError, return its status & message
if (err instanceof ApiError) {
return response(err.statusCode, err.message);
}
// Fallback for unknown errors
return response(500, 'Internal server error');
}
};
function response(statusCode: number, body: any) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(body),
};
}

View File

@@ -0,0 +1,98 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { v4 as uuid } from 'uuid';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { HostService } from '../services/host.service';
import ApiError from '../../../common/utils/helper/ApiError';
import config from '../../../config/config';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
const s3 = new S3Client({ region: config.aws.region });
const hostService = new HostService(prismaClient);
export const handler: APIGatewayProxyHandler = async (event) => {
try {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
await verifyHostToken(token);
const body = JSON.parse(event.body || '{}');
const { files } = body;
if (!Array.isArray(files) || files.length === 0) {
throw new ApiError(400, 'files array is required');
}
const activityXid = event.pathParameters?.activityXid;
if (!activityXid) {
throw new ApiError(400, 'activityXid is required in path parameters');
}
const activityDetails = await hostService.getActivityDetailsById(Number(activityXid));
if (!activityDetails) {
throw new ApiError(404, 'Activity not found');
}
const results = [];
for (const file of files) {
const { fileName, mimeType } = file;
if (!fileName || !mimeType) {
throw new ApiError(400, 'Each file must have fileName and mimeType');
}
const safeFileName = fileName
.trim()
.replace(/\s+/g, '_')
.replace(/[^a-zA-Z0-9._-]/g, '')
.toLowerCase();
const key = `ActivityOnboarding/Activity_${activityXid}/Artifacts/${uuid()}_${safeFileName}`;
const command = new PutObjectCommand({
Bucket: config.aws.bucketName!,
Key: key,
ContentType: mimeType,
});
const uploadUrl = await getSignedUrl(s3, command, {
expiresIn: 300,
});
results.push({
uploadUrl,
key,
fileUrl: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${key}`,
});
}
return response(200, { files: results });
} catch (err: any) {
console.error('ERROR:', err);
// If it's your ApiError, return its status & message
if (err instanceof ApiError) {
return response(err.statusCode, err.message);
}
// Fallback for unknown errors
return response(500, 'Internal server error');
}
};
function response(statusCode: number, body: any) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(body),
};
}

View File

@@ -0,0 +1,82 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } 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 { resendOtpHelper } from "../../../common/utils/helper/resendOtpHelper";
import { resendOtpEmail } from "../services/resendOTPEmail.service";
const prisma = prismaClient;
// allowed purposes
const ALLOWED_PURPOSES = ["Register", "Login", "ForgotPassword"] as const;
type OtpPurpose = typeof ALLOWED_PURPOSES[number];
export const handler = safeHandler(
async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
// parse body safely
let body: { email?: string; purpose?: string } = {};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch {
throw new ApiError(400, "Invalid JSON in request body");
}
// allow passing purpose via query string too (useful for GET requests)
const qsPurpose = event.queryStringParameters?.purpose;
const purposeRaw = (body.purpose || qsPurpose || "").trim();
if (!purposeRaw) {
throw new ApiError(400, "purpose is required. Allowed values: Register, Login, ForgotPassword");
}
if (!ALLOWED_PURPOSES.includes(purposeRaw as OtpPurpose)) {
throw new ApiError(
400,
`Invalid purpose '${purposeRaw}'. Allowed values: ${ALLOWED_PURPOSES.join(", ")}`
);
}
const purpose = purposeRaw as OtpPurpose;
const email = (body.email || "").trim();
if (!email) throw new ApiError(400, "Email is required");
const emailToLowerCase = email.toLowerCase();
// find user (you can adapt the isActive / userStatus checks per your rules)
const user = await prisma.user.findUnique({
where: { emailAddress: emailToLowerCase, isActive: true },
select: { id: true, emailAddress: true, role: true },
});
if (!user) {
throw new ApiError(404, "User not found");
}
const role = user.role.roleName
// call resend helper (old OTPs become inactive + verified, new OTP gets created)
const otpResult = await resendOtpHelper(
prisma,
user.id,
purpose,
6, // 6-digit OTP
5 // expires in 5 minutes
);
// send email (use appropriate template based on 'purpose' inside the email service)
await resendOtpEmail(user.emailAddress, otpResult.otp, role);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
success: true,
message: "OTP resent successfully.",
data: { purpose },
}),
};
}
);

View File

@@ -0,0 +1,244 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import dayjs from 'dayjs';
import { z } from 'zod';
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 { ROLE } from '../../../common/utils/constants/common.constant';
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(),
// 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();
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 }) {
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 (!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 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,
});
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: result,
}),
};
});

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