diff --git a/src/modules/host/services/activityScheduling.service.ts b/src/modules/host/services/activityScheduling.service.ts index b228df1..9bfbed6 100644 --- a/src/modules/host/services/activityScheduling.service.ts +++ b/src/modules/host/services/activityScheduling.service.ts @@ -41,6 +41,8 @@ export class SchedulingService { venues, earlyCheckInMins, bookingCutOffMins, + isLateCheckingAllowed, + isInstantBooking } = data; return this.prisma.$transaction(async (tx) => { @@ -90,8 +92,32 @@ export class SchedulingService { ---------------------------------- */ const createdHeaders: number[] = []; + if (isInstantBooking !== undefined || isLateCheckingAllowed !== undefined) { + await tx.activities.update({ + where: { id: activityXid, isActive: true }, + data: { isInstantBooking, isLateCheckingAllowed }, + }); + } + + if (listNow) { + await tx.activities.update({ + where: { id: activityXid, isActive: true }, + data: { + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_LISTED + } + }) + } + + for (const venue of venues) { + if (!venue.slots || venue.slots.length === 0) { + continue; + } + const header = await tx.scheduleHeader.create({ data: { activityXid, @@ -109,8 +135,20 @@ export class SchedulingService { // WEEKLY if (scheduleType === SCHEDULING_TYPE.WEEKLY) { + const uniqueWeekdays = [ + ...new Set( + venue.slots + .map(s => s.weekDay) + .filter((d): d is "MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY" | "SATURDAY" | "SUNDAY" => !!d) + ), + ]; + + if (!uniqueWeekdays.length) { + throw new ApiError(400, 'Weekly schedule requires weekDay in slots'); + } + await tx.scheduleRecurrence.createMany({ - data: rules.weekdays!.map(day => ({ + data: uniqueWeekdays.map(day => ({ scheduleHeaderXid: header.id, weekDay: day, isActive: true, @@ -118,10 +156,23 @@ export class SchedulingService { }); } + // MONTHLY if (scheduleType === SCHEDULING_TYPE.MONTHLY) { + const uniqueDays = [ + ...new Set( + venue.slots + .map(s => s.dayOfMonth) + .filter((d): d is number => d !== null && d !== undefined) + ), + ]; + + if (!uniqueDays.length) { + throw new ApiError(400, 'Monthly schedule requires dayOfMonth in slots'); + } + await tx.scheduleRecurrence.createMany({ - data: rules.monthDates!.map(day => ({ + data: uniqueDays.map(day => ({ scheduleHeaderXid: header.id, dayOfMonth: day, isActive: true, @@ -129,17 +180,27 @@ export class SchedulingService { }); } + // CUSTOM / ONCE if (scheduleType === SCHEDULING_TYPE.CUSTOM || scheduleType === SCHEDULING_TYPE.ONCE) { + const uniqueDates = [ + ...new Set( + venue.slots + .map(s => s.occurrenceDate) + .filter(Boolean) + ), + ]; + await tx.scheduleOccurences.createMany({ - data: rules.customDates!.map(d => ({ + data: uniqueDates.map(d => ({ scheduleHeaderXid: header.id, - occurenceDate: new Date(d), + occurenceDate: new Date(d!), isActive: true, })), }); } + // Slots for (const slot of venue.slots) { await tx.scheduleDetails.create({ @@ -155,18 +216,6 @@ export class SchedulingService { }, }); } - - if (listNow) { - await tx.activities.update({ - where: { id: activityXid, isActive: true }, - data: { - activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, - activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.ACTIVITY_LISTED, - amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, - amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_LISTED - } - }) - } } return { success: true, scheduleHeaderIds: createdHeaders }; @@ -330,6 +379,15 @@ export class SchedulingService { mediaType: true, }, }, + ScheduleHeader: { + where: { isActive: true }, + select: { + id: true, + scheduleType: true, + startDate: true + }, + orderBy: { createdAt: 'desc' } + } }, orderBy: { createdAt: 'desc', @@ -360,6 +418,8 @@ export class SchedulingService { activityInternalStatus: activity.activityInternalStatus, activityDisplayStatus: activity.activityDisplayStatus, media: activity.ActivitiesMedia, + scheduleType: activity.ScheduleHeader?.length ? activity.ScheduleHeader[0].scheduleType : null, + scheduleStartDate: activity.ScheduleHeader?.length ? activity.ScheduleHeader[0].startDate : null, })); } diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 7818279..5134deb 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -3263,7 +3263,7 @@ export class HostService { if (!isDraft && isChargeable && totalPrice <= 0) { throw new ApiError( 400, - 'transportTotalPrice must be > 0 when pickup/drop is chargeable', + 'Pick-up and drop-off price is required.', ); } diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index 5699696..fc594ef 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -7,6 +7,7 @@ import { ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_INTERNAL_STATUS } from '../../../ import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; import config from '@/config/config'; +import { isNotIn } from 'class-validator'; // function deg2rad(deg) { // return deg * (Math.PI / 180); // } @@ -28,6 +29,28 @@ import config from '@/config/config'; // return R * c; // } +const attachMediaWithPresignedUrl = async (mediaArr = []) => { + return ( + await Promise.all( + mediaArr.map(async (m) => { + if (!m?.mediaFileName) return null; + + const key = m.mediaFileName.startsWith('http') + ? new URL(m.mediaFileName).pathname.replace(/^\/+/, '') + : m.mediaFileName; + + return { + id: m.id, + mediaType: m.mediaType, + mediaFileName: m.mediaFileName, + presignedUrl: await getPresignedUrl(bucket, key), + }; + }) + ) + ).filter(Boolean); +}; + + const bucket = config.aws.bucketName; @@ -62,7 +85,7 @@ export class UserService { } async getAllInterestDetails() { - return await this.prisma.interests.findMany({ + const interests = await this.prisma.interests.findMany({ where: { isActive: true }, select: { id: true, @@ -72,6 +95,20 @@ export class UserService { displayOrder: true } }) + + for (const interest of interests) { + if (interest.interestImage) { + const key = interest.interestImage.startsWith('http') + ? new URL(interest.interestImage).pathname.replace(/^\/+/, '') + : interest.interestImage; + + (interest as any).presignedUrl = await getPresignedUrl(bucket, key); + } else { + (interest as any).presignedUrl = null; + } + } + + return interests; } @@ -250,6 +287,25 @@ export class UserService { async getLandingPageAllDetails(userId: number) { const data = await this.prisma.$transaction(async (tx) => { + const userAddressDetails = await tx.userAddressDetails.findFirst({ + where: { userXid: userId }, + select: { + id: true, + address1: true, + address2: true, + pinCode: true, + locationName: true, + stateXid: true, + cityXid: true, + countryXid: true, + locationLat: true, + locationLong: true, + } + }) + + const userStateXid = userAddressDetails?.stateXid ?? null; + const userCountryXid = userAddressDetails?.countryXid ?? null; + const userInterests = await tx.userInterests.findMany({ where: { userXid: userId, isActive: true }, select: { @@ -278,22 +334,6 @@ export class UserService { } }) - const userAddressDetails = await tx.userAddressDetails.findFirst({ - where: { userXid: userId }, - select: { - id: true, - address1: true, - address2: true, - pinCode: true, - locationName: true, - stateXid: true, - cityXid: true, - countryXid: true, - locationLat: true, - locationLong: true, - } - }) - if (!activitiyTypesOfUserInterests.length) { return { userAddressDetails, @@ -355,6 +395,92 @@ export class UserService { }, }); + const otherStatesActivities = await tx.activities.findMany({ + where: { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + + // ✅ Only user's interest types + activityTypeXid: { + in: activitiyTypesOfUserInterests.map(at => at.id), + }, + + // ✅ Exclude user's state + ...(userStateXid && { + checkInStateXid: { not: userStateXid }, + }), + ...(userCountryXid && { + checkInCountryXid: userCountryXid, + }), + }, + select: { + id: true, + activityTitle: true, + activityType: { + select: { + energyLevel: { + select: { + id: true, + energyLevelName: true, + energyColor: true, + energyIcon: true, + }, + }, + }, + }, + ActivitiesMedia: { + where: { isActive: true }, + select: { + id: true, + mediaFileName: true, + mediaType: true, + }, + }, + }, + }); + + const overSeasActivity = await tx.activities.findMany({ + where: { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + + // ✅ Only user's interest types + activityTypeXid: { + in: activitiyTypesOfUserInterests.map(at => at.id), + }, + + // ✅ Exclude user's state + ...(userCountryXid && { + checkInCountryXid: { not: userCountryXid }, + }), + }, + select: { + id: true, + activityTitle: true, + activityType: { + select: { + energyLevel: { + select: { + id: true, + energyLevelName: true, + energyColor: true, + energyIcon: true, + }, + }, + }, + }, + ActivitiesMedia: { + where: { isActive: true }, + select: { + id: true, + mediaFileName: true, + mediaType: true, + }, + }, + }, + }) const formattedActivities = await Promise.all( activities.map(async (activity) => { @@ -371,35 +497,39 @@ export class UserService { } } - const media = await Promise.all( - (activity.ActivitiesMedia ?? []).map(async (m) => { - if (!m?.mediaFileName) return null; - - const key = m.mediaFileName.startsWith('http') - ? new URL(m.mediaFileName).pathname.replace(/^\/+/, '') - : m.mediaFileName; - - return { - id: m.id, - mediaType: m.mediaType, - mediaFileName: m.mediaFileName, - presignedUrl: await getPresignedUrl(bucket, key), - }; - }) - ); - return { interestXid: activity.activityType.interestXid, activityTitle: activity.activityTitle, activityDurationMins: activity.activityDurationMins, sustainabilityScore: activity.sustainabilityScore, - energyLevel: activity.activityType?.energyLevel ?? null, cheapestPrice, - media: media.filter(Boolean), // ✅ IMPORTANT + rating: 4, + distanceFromUser: 2, + connectionsCount: 10, + energyLevel: activity.activityType?.energyLevel ?? null, + media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia), }; }) ); + const formattedOtherStatesActivities = await Promise.all( + otherStatesActivities.map(async (activity) => ({ + activityTitle: activity.activityTitle, + energyLevel: activity.activityType.energyLevel, + media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia), + })) + ); + + const formattedOverSeasActivities = await Promise.all( + overSeasActivity.map(async (activity) => ({ + activityTitle: activity.activityTitle, + energyLevel: activity.activityType.energyLevel, + media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia), + })) + ); + + + const interestsWithActivities = userInterests.map(ui => { const activitiesForInterest = formattedActivities.filter( act => act.interestXid === ui.interestXid @@ -419,7 +549,13 @@ export class UserService { return { userAddressDetails, + experiencesLogged: 25, + citiesDiscovered: 10, + loggedInNetworkCount: 0, + citiesInNetworkCount: 0, interests: interestsWithActivities, + otherStatesActivities: formattedOtherStatesActivities, + overSeasActivities: formattedOverSeasActivities }; })