diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 657a1c6..3620bf4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1036,6 +1036,8 @@ model ActivityOtherDetails { activityXid Int @map("activity_xid") activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) exclusiveNotes String? @map("exclusive_notes") @db.VarChar(500) + SafetyInstruction String? @map("safety_instruction") @db.VarChar(400) + Cancellations String? @map("cancellations") @db.VarChar(400) dosNotes String? @map("dos_notes") @db.VarChar(400) dontsNotes String? @map("donts_notes") @db.VarChar(400) tipsNotes String? @map("tips_notes") @db.VarChar(400) @@ -1074,6 +1076,7 @@ model ActivitiesMedia { activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade) mediaType String @map("media_type") @db.VarChar(30) mediaFileName String @map("media_file_name") @db.VarChar(400) + isCoverImage Boolean @default(false) @map("is_cover_image") displayOrder Int @map("display_order") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") diff --git a/src/modules/host/dto/createActivity.schema.ts b/src/modules/host/dto/createActivity.schema.ts index 385dc01..c03f0bb 100644 --- a/src/modules/host/dto/createActivity.schema.ts +++ b/src/modules/host/dto/createActivity.schema.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; export const MediaDto = z.object({ mediaType: z.string().optional(), mediaFileName: z.string(), + isCoverImage: z.boolean().optional().default(false), }); /* ================= PRICE ================= */ @@ -85,10 +86,12 @@ export const EligibilityDto = z.object({ /* ================= 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 ================= */ diff --git a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts index 1d840e4..b0b8a8e 100644 --- a/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts +++ b/src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.ts @@ -1,4 +1,3 @@ -import config from '../../../../../config/config'; import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; @@ -59,8 +58,23 @@ export const handler = safeHandler( 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; diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 183b23a..2179901 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -383,7 +383,9 @@ export class HostService { } async verifyHostOtp(email: string, otp: string): Promise { - const user = await this.prisma.user.findUnique({ + const trimmedOtp = (otp || '').toString().trim(); + + const user = await this.prisma.user.findFirst({ where: { emailAddress: email, isActive: true }, select: { id: true, @@ -410,7 +412,7 @@ export class HostService { throw new ApiError(400, 'OTP has expired.'); } - const isMatch = await bcrypt.compare(otp, userOtp.otpCode); + const isMatch = await bcrypt.compare(trimmedOtp, userOtp.otpCode); if (!isMatch) { throw new ApiError(400, 'Invalid OTP.'); @@ -2950,6 +2952,7 @@ export class HostService { activityXid, mediaType: m.mediaType ?? 'unknown', mediaFileName: m.mediaFileName, + isCoverImage: m.isCoverImage ?? false, displayOrder: index + 1, })), }); @@ -3498,6 +3501,22 @@ export class HostService { data: { activityXid, exclusiveNotes: payload.otherDetails.exclusiveNotes ?? null, + SafetyInstruction: (() => { + const s = payload.otherDetails.safetyInstruction ?? null; + if (s === undefined || s === null) return null; + if (typeof s !== 'string') { + throw new ApiError(400, 'safetyInstruction must be a string'); + } + return s; + })(), + Cancellations: (() => { + const c = payload.otherDetails.cancellations ?? null; + if (c === undefined || c === null) return null; + if (typeof c !== 'string') { + throw new ApiError(400, 'cancellations must be a string'); + } + return c; + })(), dosNotes: payload.otherDetails.dosNotes ?? null, dontsNotes: payload.otherDetails.dontsNotes ?? null, tipsNotes: payload.otherDetails.tipsNotes ?? null, diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index e76df5b..8ad88cb 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -23,10 +23,9 @@ import { import { PaginationOptions } from '@/common/utils/pagination/pagination.types'; import config from '@/config/config'; import { Injectable } from '@nestjs/common'; -import { User } from '@prisma/client'; +import { PrismaClient, User } from '@prisma/client'; import * as bcrypt from 'bcryptjs'; import { PrismaService } from '../../../common/database/prisma.service'; -import { PrismaClient } from '@prisma/client'; import ApiError from '../../../common/utils/helper/ApiError'; import { CreateMinglarDto, UpdateMinglarDto } from '../dto/minglar.dto'; import { sendAMEmailForHostAssign } from './AMEmail.service'; @@ -154,8 +153,10 @@ export class MinglarService { } async verifyHostOtp(email: string, otp: string): Promise { - const user = await this.prisma.user.findUnique({ - where: { emailAddress: email }, + const trimmedOtp = (otp || '').toString().trim(); + + const user = await this.prisma.user.findFirst({ + where: { emailAddress: email, isActive: true }, select: { id: true, emailAddress: true, @@ -181,7 +182,7 @@ export class MinglarService { throw new ApiError(400, 'OTP has expired.'); } - const isMatch = await bcrypt.compare(otp, userOtp.otpCode); + const isMatch = await bcrypt.compare(trimmedOtp, userOtp.otpCode); if (!isMatch) { throw new ApiError(400, 'Invalid OTP.'); diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index a54663b..49e9c2f 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -413,6 +413,8 @@ export class UserService { } async verifyHostOtp(mobileNumber: string, otp: string): Promise { + const trimmedOtp = (otp || '').toString().trim(); + const user = await this.prisma.user.findFirst({ where: { mobileNumber: mobileNumber, isActive: true }, select: { @@ -440,7 +442,7 @@ export class UserService { throw new ApiError(400, 'OTP has expired.'); } - const isMatch = await bcrypt.compare(otp, userOtp.otpCode); + const isMatch = await bcrypt.compare(trimmedOtp, userOtp.otpCode); if (!isMatch) { throw new ApiError(400, 'Invalid OTP.'); @@ -1896,6 +1898,39 @@ export class UserService { const skip = (page - 1) * limit; + // 0️⃣ Get user's interests and map to activity types + const userInterests = await this.prisma.userInterests.findMany({ + where: { userXid: userId, isActive: true }, + select: { interestXid: true }, + }); + + if (!userInterests.length) { + return { + page, + limit, + totalCount: 0, + hasMore: false, + activities: [], + }; + } + + const activityTypeIds = ( + await this.prisma.activityTypes.findMany({ + where: { interestXid: { in: userInterests.map((u) => u.interestXid) }, isActive: true }, + select: { id: true }, + }) + ).map((t) => t.id); + + if (!activityTypeIds.length) { + return { + page, + limit, + totalCount: 0, + hasMore: false, + activities: [], + }; + } + // Rough bounding box in degrees to reduce DB scan const earthRadiusKm = 6371; const latDelta = (radiusKm / earthRadiusKm) * (180 / Math.PI); @@ -1908,6 +1943,7 @@ export class UserService { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + activityTypeXid: { in: activityTypeIds }, checkInLat: { not: null, gte: userLat - latDelta,