24 Commits

Author SHA1 Message Date
50f93bbeae added the delete logo path of the main and parent company 2026-04-01 15:10:10 +05:30
3ce9d1d180 sending the food trainer pick up and navigation details in the get user itinerary api 2026-03-27 11:57:05 +05:30
cb819088a0 fixed the comments string issue 2026-03-25 16:29:13 +05:30
8f5f01c287 sending the date of birth of user under the host getbyid api 2026-03-25 15:16:49 +05:30
e4a2a04045 fixed the coverimage issue for random activities and addbucket issue also resolved and taking the group count 2026-03-25 13:34:12 +05:30
50ce8e39c5 sending checkin lat long in the user itinerary api and city state country details of the parent company in the getbyid api of host 2026-03-25 12:36:47 +05:30
19e57f0e7f fixed the new user condition in the user module 2026-03-25 11:33:57 +05:30
ad5e343b66 changed the draft-activity to draft 2026-03-24 17:38:07 +05:30
8c3ece6ebd checking the activity title should not be same 2026-03-24 14:42:09 +05:30
092f425bb3 changed the to review 2026-03-24 14:41:25 +05:30
b1a3afd3a1 Sending mail on the final submission of the pqp 2026-03-23 19:31:19 +05:30
97f431260d sending the admin email in the prepopulate 2026-03-23 15:04:05 +05:30
bf6d9ae00b sending created at 2026-03-20 15:15:40 +05:30
518ec4eb21 made save itinerary and get itinerary details apis 2026-03-18 19:49:04 +05:30
95b061b400 made searchConnectionPeople to invite in the itinerary API 2026-03-18 13:30:41 +05:30
92992797ab sending the filter details in the getMatchingBucketInterestedActivities API 2026-03-18 13:21:32 +05:30
c96e3b0c1a sending the energyLevel details also in the getUserItineraryDetails service 2026-03-18 12:45:59 +05:30
f23b93801c making the energylevelxid optional 2026-03-18 11:09:57 +05:30
f1801a3210 made getMatchingBucketInterestedActivities api 2026-03-17 16:22:03 +05:30
2588ca4317 fixed the path 2026-03-17 14:28:56 +05:30
e809ba4480 sending mail after host submits pqp questioniare and not sending the applications with new status in the host applications 2026-03-16 15:50:27 +05:30
678be7c905 fixed the issue of path 2026-03-16 15:27:20 +05:30
08b4231e5f Merge branch 'Split-lambda' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-16 11:30:53 +05:30
a3ab9db5a2 Made getUserItineraryDetails api for the prepopulate data of itinerary page in mobile 2026-03-15 20:33:45 +05:30
21 changed files with 2838 additions and 236 deletions

View File

@@ -1728,8 +1728,8 @@ model ItineraryActivities {
travelMode String? @map("travel_mode") @db.VarChar(30) travelMode String? @map("travel_mode") @db.VarChar(30)
kmForNextPoint Float? @map("km_for_next_point") kmForNextPoint Float? @map("km_for_next_point")
timeForNextPointMins Int? @map("time_for_next_point_mins") timeForNextPointMins Int? @map("time_for_next_point_mins")
paxCount Int @map("pax_count") paxCount Int? @map("pax_count")
totalAmount Int @map("total_amount") totalAmount Int? @map("total_amount")
bookingStatus String @default("pending") @map("booking_status") @db.VarChar(30) bookingStatus String @default("pending") @map("booking_status") @db.VarChar(30)
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")

View File

@@ -1,5 +1,5 @@
# Legacy monolith config. For new deployments use serverless.*.yml files. # Legacy monolith config. For new deployments use serverless.*.yml files.
service: minglarDev service: minglar
useDotenv: true useDotenv: true

View File

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

View File

@@ -288,6 +288,21 @@ getActivityFromConnectionsInterest:
path: /connections/get-activity-from-connections-interest path: /connections/get-activity-from-connections-interest
method: get method: get
searchConnectionPeople:
handler: src/modules/user/handlers/connections/searchConnectionPeople.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/handlers/connections/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /connections/search-connection-people
method: get
viewMoreActivitiesByInterest: viewMoreActivitiesByInterest:
handler: src/modules/user/handlers/activities/viewMoreActivities.handler handler: src/modules/user/handlers/activities/viewMoreActivities.handler
memorySize: 384 memorySize: 384
@@ -406,4 +421,64 @@ getAllBucketActivities:
events: events:
- httpApi: - httpApi:
path: /activities/get-all-bucket-activities path: /activities/get-all-bucket-activities
method: get method: get
getUserItineraryDetails:
handler: src/modules/user/handlers/itinerary/getUserItineraryDetails.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/get-user-itinerary-details
method: get
saveUserItinerary:
handler: src/modules/user/handlers/itinerary/saveUserItinerary.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/save-user-itinerary
method: post
getAllUserSavedItineraries:
handler: src/modules/user/handlers/itinerary/getAllUserSavedItineraries.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/get-all-user-saved-itineraries
method: get
getMatchingBucketInterestedActivities:
handler: src/modules/user/handlers/itinerary/getMatchingBucketInterestedActivities.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/get-matching-bucket-interested-activities
method: post

View File

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

View File

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

View File

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

View File

@@ -142,6 +142,10 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const deletedFiles = normalizeJsonField(fields, "deletedFiles") || []; const deletedFiles = normalizeJsonField(fields, "deletedFiles") || [];
const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || []; const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || [];
const deleteCompanyLogo =
fields.deleteCompanyLogo === 'true' || fields.deleteCompanyLogo === true;
const deleteParentCompanyLogo =
fields.deleteParentCompanyLogo === 'true' || fields.deleteParentCompanyLogo === true;
/** 4) Extract and clean isDraft flag */ /** 4) Extract and clean isDraft flag */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true; const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
@@ -379,6 +383,63 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}); });
} }
/** DELETE EXISTING LOGO IF REQUESTED */
if (deleteCompanyLogo) {
const existingHost = await prismaClient.hostHeader.findFirst({
where: { userXid: userInfo.id },
select: { logoPath: true },
});
if (existingHost?.logoPath) {
try {
const s3Key = getS3KeyFromUrl(existingHost.logoPath);
await deleteFromS3(s3Key);
} catch (e) {
console.error('S3 delete failed for company logo:', existingHost.logoPath, e);
}
}
parsedCompany.logoPath = null;
}
/** DELETE EXISTING PARENT COMPANY LOGO IF REQUESTED */
if (deleteParentCompanyLogo && parsedCompany.isSubsidairy) {
const existingHost = await prismaClient.hostHeader.findFirst({
where: { userXid: userInfo.id },
select: {
id: true,
hostParent: {
select: {
id: true,
logoPath: true,
},
take: 1,
},
},
});
const existingParent = Array.isArray(existingHost?.hostParent)
? existingHost.hostParent[0]
: existingHost?.hostParent;
if (existingParent?.logoPath) {
try {
const s3Key = getS3KeyFromUrl(existingParent.logoPath);
await deleteFromS3(s3Key);
} catch (e) {
console.error('S3 delete failed for parent company logo:', existingParent.logoPath, e);
}
}
if (parsedParentCompany) {
parsedParentCompany.logoPath = null;
} else {
parsedParentCompany = {
logoPath: null,
};
}
}
/** UPLOAD LOGO (if provided) */ /** UPLOAD LOGO (if provided) */
const logoFile = files.find( const logoFile = files.find(
(f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile' (f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
@@ -449,6 +510,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
parsedParentCompany, parsedParentCompany,
uploadedParentDocs, uploadedParentDocs,
isDraft, isDraft,
{ deleteCompanyLogo, deleteParentCompanyLogo },
); );
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.'); if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');

View File

@@ -446,7 +446,46 @@ export class HostService {
where: { userXid: id }, where: { userXid: id },
include: { include: {
hostParent: { hostParent: {
include: { select: {
id: true,
logoPath: true,
companyName: true,
address1: true,
address2: true,
cities: {
select: {
id: true,
cityName: true
}
},
states: {
select: {
id: true,
stateName: true
}
},
countries: {
select: {
id: true,
countryName: true
}
},
pinCode: true,
registrationNumber: true,
panNumber: true,
gstNumber: true,
formationDate: true,
companyTypes: {
select: {
id: true,
companyTypeName: true
}
},
websiteUrl: true,
instagramUrl: true,
facebookUrl: true,
linkedinUrl: true,
twitterUrl: true,
HostParenetDocuments: { HostParenetDocuments: {
select: { select: {
id: true, id: true,
@@ -479,6 +518,7 @@ export class HostService {
select: { select: {
id: true, id: true,
emailAddress: true, emailAddress: true,
dateOfBirth: true,
firstName: true, firstName: true,
lastName: true, lastName: true,
mobileNumber: true, mobileNumber: true,
@@ -1351,6 +1391,10 @@ export class HostService {
parentCompanyData?: any | null, parentCompanyData?: any | null,
parentDocuments?: HostDocumentInput[], parentDocuments?: HostDocumentInput[],
isDraft: boolean = false, isDraft: boolean = false,
options?: {
deleteCompanyLogo?: boolean;
deleteParentCompanyLogo?: boolean;
},
) { ) {
return await this.prisma.$transaction(async (tx) => { return await this.prisma.$transaction(async (tx) => {
// Check if host already has a company // Check if host already has a company
@@ -1395,7 +1439,7 @@ export class HostService {
hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW; hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW;
minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW; minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW;
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW; minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.RE_SUBMITTED;
} }
// CASE 2: Admin has rejected but host can resubmit // CASE 2: Admin has rejected but host can resubmit
else if ( else if (
@@ -1611,7 +1655,9 @@ export class HostService {
? { connect: { id: Number(companyData.countryXid) } } ? { connect: { id: Number(companyData.countryXid) } }
: undefined, : undefined,
pinCode: companyData.pinCode, pinCode: companyData.pinCode,
logoPath: companyData.logoPath || existingHostCompany.logoPath, logoPath: options?.deleteCompanyLogo
? companyData.logoPath ?? null
: companyData.logoPath || existingHostCompany.logoPath,
isSubsidairy: companyData.isSubsidairy, isSubsidairy: companyData.isSubsidairy,
registrationNumber: companyData.registrationNumber, registrationNumber: companyData.registrationNumber,
panNumber: companyData.panNumber, panNumber: companyData.panNumber,
@@ -1752,10 +1798,11 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } } ? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined, : undefined,
pinCode: parentCompanyData.pinCode || null, pinCode: parentCompanyData.pinCode || null,
logoPath: logoPath: options?.deleteParentCompanyLogo
parentCompanyData?.logoPath || ? parentCompanyData?.logoPath ?? null
existingParentCompany?.logoPath || : parentCompanyData?.logoPath ||
null, existingParentCompany?.logoPath ||
null,
registrationNumber: parentCompanyData.registrationNumber || null, registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null, panNumber: parentCompanyData.panNumber || null,
gstNumber: parentCompanyData.gstNumber || null, gstNumber: parentCompanyData.gstNumber || null,
@@ -1810,10 +1857,11 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } } ? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined, : undefined,
pinCode: parentCompanyData.pinCode || null, pinCode: parentCompanyData.pinCode || null,
logoPath: logoPath: options?.deleteParentCompanyLogo
parentCompanyData?.logoPath || ? parentCompanyData?.logoPath ?? null
existingParentCompany?.logoPath || : parentCompanyData?.logoPath ||
null, existingParentCompany?.logoPath ||
null,
registrationNumber: parentCompanyData.registrationNumber || null, registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null, panNumber: parentCompanyData.panNumber || null,
gstNumber: parentCompanyData.gstNumber || null, gstNumber: parentCompanyData.gstNumber || null,
@@ -3337,6 +3385,34 @@ export class HostService {
throw new ApiError(404, 'Activity not found'); throw new ApiError(404, 'Activity not found');
} }
const normalizedActivityTitle =
typeof payload.activityTitle === 'string'
? payload.activityTitle.trim()
: '';
if (normalizedActivityTitle) {
payload.activityTitle = normalizedActivityTitle;
const duplicateActivity = await tx.activities.findFirst({
where: {
id: { not: existingActivity.id },
isActive: true,
activityTitle: {
equals: normalizedActivityTitle,
mode: 'insensitive',
},
},
select: { id: true },
});
if (duplicateActivity) {
throw new ApiError(
400,
'Same activity name already exists. Please choose a different name.',
);
}
}
/* -------------------------------- /* --------------------------------
* 3⃣ STATUS DECISION * 3⃣ STATUS DECISION
* -------------------------------- */ * -------------------------------- */

View File

@@ -76,3 +76,41 @@ export async function sendEmailToMinglarAdmin(
throw new ApiError(500, "Failed to send OTP to host via email."); throw new ApiError(500, "Failed to send OTP to host via email.");
} }
} }
export async function sendPQPEmailToAM(
emailAddress: string,
minglarAdminName: string,
hostCompanyName: string,
hostRefNumber: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = `New Pre-qualification Questionnaire from : ${hostCompanyName}`;
const htmlContent = `
<p>Dear ${minglarAdminName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has submited their pre-qualification questionnaire.</p>
<p>Please review their appliaction and take the necessary action.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}

View File

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

View File

@@ -140,7 +140,9 @@ export class PrePopulateService {
}), }),
]); ]);
return { documentDetails, countryDetails, stateDetails, companyTypeDetails }; const adminEmail = config.MinglarAdminEmail;
return { documentDetails, countryDetails, stateDetails, companyTypeDetails, adminEmail };
} }
async getAllFrequencies() { async getAllFrequencies() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1148,7 +1148,9 @@ export class UserService {
// 6⃣ RANDOM ACTIVITIES (5 ONLY - SIMPLE) // 6⃣ RANDOM ACTIVITIES (5 ONLY - SIMPLE)
// ===================================================== // =====================================================
const totalActiveCount = await tx.activities.count({ let randomActivities: any[] = [];
const eligibleRandomActivityIds = await tx.activities.findMany({
where: { where: {
isActive: true, isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -1157,53 +1159,44 @@ export class UserService {
id: { id: {
notIn: allUserExcludedActivityIds.length notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds ? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue : [-1],
}, },
ActivitiesMedia: {
some: {
isActive: true,
isCoverImage: true,
},
},
},
select: {
id: true,
}, },
}); });
let randomActivities: any[] = []; if (eligibleRandomActivityIds.length > 0) {
const takeCount = Math.min(5, eligibleRandomActivityIds.length);
const selectedIds = eligibleRandomActivityIds
.sort(() => Math.random() - 0.5)
.slice(0, takeCount)
.map((activity) => activity.id);
if (totalActiveCount > 0) { const randomFetched = await tx.activities.findMany({
const takeCount = Math.min(5, totalActiveCount); where: {
id: { in: selectedIds },
const randomOffsets = new Set<number>(); },
while (randomOffsets.size < takeCount) { select: {
randomOffsets.add(Math.floor(Math.random() * totalActiveCount)); id: true,
} activityTitle: true,
ActivitiesMedia: {
const randomFetched = await Promise.all( where: { isActive: true, isCoverImage: true },
Array.from(randomOffsets).map((offset) => orderBy: { displayOrder: 'asc' },
tx.activities.findFirst({ take: 1,
skip: offset,
where: {
isActive: true,
activityInternalStatus:
ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus:
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
},
},
select: { select: {
id: true, mediaFileName: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
mediaFileName: true,
},
},
}, },
}), },
), },
); });
randomActivities = await Promise.all( randomActivities = await Promise.all(
randomFetched randomFetched
@@ -1817,7 +1810,9 @@ export class UserService {
RANDOM ACTIVITIES (5 COVER IMAGES) RANDOM ACTIVITIES (5 COVER IMAGES)
===================================================== */ ===================================================== */
const totalActiveCount = await tx.activities.count({ let randomActivities: any[] = [];
const eligibleRandomActivityIds = await tx.activities.findMany({
where: { where: {
isActive: true, isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -1826,50 +1821,43 @@ export class UserService {
id: { id: {
notIn: safeExcludedIds, notIn: safeExcludedIds,
}, },
ActivitiesMedia: {
some: {
isActive: true,
isCoverImage: true,
},
},
...excludeUserInterestCondition, ...excludeUserInterestCondition,
}, },
select: {
id: true,
},
}); });
let randomActivities: any[] = []; if (eligibleRandomActivityIds.length > 0) {
const takeCount = Math.min(5, eligibleRandomActivityIds.length);
const selectedIds = eligibleRandomActivityIds
.sort(() => Math.random() - 0.5)
.slice(0, takeCount)
.map((activity) => activity.id);
if (totalActiveCount > 0) { const randomFetched = await tx.activities.findMany({
const takeCount = Math.min(5, totalActiveCount); where: {
id: { in: selectedIds },
const randomOffsets = new Set<number>(); },
select: {
while (randomOffsets.size < takeCount) { id: true,
randomOffsets.add(Math.floor(Math.random() * totalActiveCount)); activityTitle: true,
} ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
const randomFetched = await Promise.all( orderBy: { displayOrder: 'asc' },
Array.from(randomOffsets).map((offset) => take: 1,
tx.activities.findFirst({
skip: offset,
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: safeExcludedIds,
},
...excludeUserInterestCondition,
},
select: { select: {
id: true, mediaFileName: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
mediaFileName: true,
},
},
}, },
}), },
), },
); });
randomActivities = await Promise.all( randomActivities = await Promise.all(
randomFetched randomFetched
@@ -2863,6 +2851,111 @@ export class UserService {
}); });
} }
async searchConnectionPeople(userXid: number, searchQuery?: string) {
const userConnectionDetails = await this.prisma.connectDetails.findMany({
where: {
userXid,
isActive: true,
deletedAt: null,
},
select: {
schoolCompanyXid: true,
},
});
const schoolCompanyXids = [
...new Set(userConnectionDetails.map((item) => item.schoolCompanyXid)),
];
if (!schoolCompanyXids.length) {
return {
count: 0,
people: [],
};
}
const trimmedSearchQuery = searchQuery?.trim() ?? '';
const connectionPeople = await this.prisma.connectDetails.findMany({
where: {
isActive: true,
deletedAt: null,
schoolCompanyXid: { in: schoolCompanyXids },
userXid: { not: userXid },
user: {
isActive: true,
deletedAt: null,
...(trimmedSearchQuery
? {
OR: [
{
firstName: {
contains: trimmedSearchQuery,
mode: 'insensitive',
},
},
{
lastName: {
contains: trimmedSearchQuery,
mode: 'insensitive',
},
},
],
}
: {}),
},
},
distinct: ['userXid'],
orderBy: {
createdAt: 'desc',
},
take: 10,
select: {
userXid: true,
schoolCompany: {
select: {
id: true,
schoolCompanyName: true,
isSchool: true,
},
},
user: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
},
},
},
});
const people = await Promise.all(
connectionPeople.map(async (item) => {
const firstName = item.user.firstName?.trim() ?? '';
const lastName = item.user.lastName?.trim() ?? '';
const fullName = `${firstName} ${lastName}`.trim();
return {
userXid: item.user.id,
fullName,
firstName: item.user.firstName,
lastName: item.user.lastName,
profileImage: item.user.profileImage,
profileImagePresignedUrl: await attachPresignedUrl(
item.user.profileImage,
),
schoolCompany: item.schoolCompany,
};
}),
);
return {
count: people.length,
people,
};
}
async searchSchoolsAndCompanies(searchQuery: string, isSchool: boolean) { async searchSchoolsAndCompanies(searchQuery: string, isSchool: boolean) {
if (!searchQuery) { if (!searchQuery) {
throw new ApiError( throw new ApiError(
@@ -3529,7 +3622,9 @@ export class UserService {
RANDOM ACTIVITIES FROM CONNECTION USERS (5 COVER IMAGES) RANDOM ACTIVITIES FROM CONNECTION USERS (5 COVER IMAGES)
===================================================== */ ===================================================== */
const totalActiveCount = await tx.activities.count({ let randomActivities: any[] = [];
const eligibleRandomActivityIds = await tx.activities.findMany({
where: { where: {
id: { in: connectionActivityIds }, id: { in: connectionActivityIds },
isActive: true, isActive: true,
@@ -3537,47 +3632,42 @@ export class UserService {
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: { in: activityTypeIds }, activityTypeXid: { in: activityTypeIds },
deletedAt: null, deletedAt: null,
ActivitiesMedia: {
some: {
isActive: true,
isCoverImage: true,
},
},
},
select: {
id: true,
}, },
}); });
let randomActivities: any[] = []; if (eligibleRandomActivityIds.length > 0) {
const takeCount = Math.min(5, eligibleRandomActivityIds.length);
const selectedIds = eligibleRandomActivityIds
.sort(() => Math.random() - 0.5)
.slice(0, takeCount)
.map((activity) => activity.id);
if (totalActiveCount > 0) { const randomFetched = await tx.activities.findMany({
const takeCount = Math.min(5, totalActiveCount); where: {
id: { in: selectedIds },
const randomOffsets = new Set<number>(); },
select: {
while (randomOffsets.size < takeCount) { id: true,
randomOffsets.add(Math.floor(Math.random() * totalActiveCount)); activityTitle: true,
} ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
const randomFetched = await Promise.all( orderBy: { displayOrder: 'asc' },
Array.from(randomOffsets).map((offset) => take: 1,
tx.activities.findFirst({
skip: offset,
where: {
id: { in: connectionActivityIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: { in: activityTypeIds },
deletedAt: null,
},
select: { select: {
id: true, mediaFileName: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: "asc" },
take: 1,
select: {
mediaFileName: true,
},
},
}, },
}), },
), },
); });
randomActivities = await Promise.all( randomActivities = await Promise.all(
randomFetched randomFetched
@@ -4046,56 +4136,54 @@ export class UserService {
async getFiveRandomActivities() { async getFiveRandomActivities() {
return await this.prisma.$transaction(async (tx) => { return await this.prisma.$transaction(async (tx) => {
// Step 1: Count eligible activities const eligibleRandomActivityIds = await tx.activities.findMany({
const totalCount = await tx.activities.count({
where: { where: {
isActive: true, isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null, deletedAt: null,
ActivitiesMedia: {
some: {
isActive: true,
isCoverImage: true,
},
},
},
select: {
id: true,
}, },
}); });
if (totalCount === 0) return []; if (eligibleRandomActivityIds.length === 0) return [];
// Step 2: Generate 5 unique random offsets const takeCount = Math.min(5, eligibleRandomActivityIds.length);
const takeCount = Math.min(5, totalCount); const selectedIds = eligibleRandomActivityIds
const randomOffsets = new Set<number>(); .sort(() => Math.random() - 0.5)
.slice(0, takeCount)
.map((activity) => activity.id);
while (randomOffsets.size < takeCount) { const activities = await tx.activities.findMany({
randomOffsets.add(Math.floor(Math.random() * totalCount)); where: {
} id: { in: selectedIds },
},
// Step 3: Fetch activities using skip (efficient for small limit like 5) select: {
const activities = await Promise.all( id: true,
Array.from(randomOffsets).map((offset) => activityTitle: true,
tx.activities.findFirst({ ActivitiesMedia: {
skip: offset,
where: { where: {
isActive: true, isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, isCoverImage: true,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
}, },
orderBy: {
displayOrder: 'asc',
},
take: 1,
select: { select: {
id: true, mediaFileName: true,
activityTitle: true,
ActivitiesMedia: {
where: {
isActive: true,
},
orderBy: {
displayOrder: 'asc',
},
take: 1,
select: {
mediaFileName: true,
},
},
}, },
}) },
) },
); });
// Step 4: Attach presigned URLs // Step 4: Attach presigned URLs
const result = await Promise.all( const result = await Promise.all(
@@ -4138,7 +4226,7 @@ export class UserService {
} }
const existing = await this.prisma.userBucketInterested.findFirst({ const existing = await this.prisma.userBucketInterested.findFirst({
where: { userXid, activityXid }, where: { userXid, activityXid, isActive: true },
}); });
if (existing) { if (existing) {
@@ -4322,4 +4410,4 @@ export class UserService {
oneDay, oneDay,
}; };
} }
} }