import { ROLE, ROLE_NAME, USER_STATUS, } from '@/common/utils/constants/common.constant'; import { ACTIVITY_AM_DISPLAY_STATUS, ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_DISPLAY_STATUS, ACTIVITY_INTERNAL_STATUS, HOST_STATUS_DISPLAY, HOST_STATUS_INTERNAL, STEPPER, } from '@/common/utils/constants/host.constant'; import { MINGLAR_INVITATION_STATUS, MINGLAR_STATUS_DISPLAY, MINGLAR_STATUS_INTERNAL, } from '@/common/utils/constants/minglar.constant'; import { Injectable } from '@nestjs/common'; import { User } from '@prisma/client'; import * as bcrypt from 'bcryptjs'; import { PrismaService } from '../../../common/database/prisma.service'; import ApiError from '../../../common/utils/helper/ApiError'; import { CreateMinglarDto, UpdateMinglarDto } from '../dto/minglar.dto'; import { sendAMEmailForHostAssign } from './AMEmail.service'; import { getPresignedUrl } from '@/common/middlewares/aws/getPreSignedUrl'; import config from '@/config/config'; import { PaginationOptions } from '@/common/utils/pagination/pagination.types'; @Injectable() export class MinglarService { constructor(private prisma: PrismaService) { } async createPassword(user_xid: number, password: string): Promise { // Find user by id const user = await this.prisma.user.findUnique({ where: { id: user_xid, isActive: true, userStatus: USER_STATUS.INVITED }, select: { id: true, emailAddress: true, userPassword: true }, }); const invitationDetails = await this.prisma.inviteDetails.findMany({ where: { userXid: user.id, isActive: true, isMinglarInvitation: true, }, }); if (invitationDetails.length > 0) { await this.prisma.inviteDetails.update({ where: { id: invitationDetails[0].id }, data: { invitation_status: MINGLAR_INVITATION_STATUS.ACCEPTED, accepted_on: new Date(), is_accepted: true, }, }); } if (!user) { throw new ApiError(404, 'User not found'); } // Check if password already exists if (user.userPassword) { throw new ApiError( 400, 'Password already exists. Use update password instead.', ); } // Hash the password const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10); const hashedPassword = await bcrypt.hash(password, saltRounds); // Update user with hashed password await this.prisma.user.update({ where: { id: user.id }, data: { userPassword: hashedPassword, userStatus: USER_STATUS.ACTIVE, isEmailVerfied: true, }, }); return true; } async generateHostRefNumber(tx: any, role_xid: number) { const lastrecord = await tx.user.findFirst({ orderBy: { id: 'desc', }, select: { id: true, }, }); let referenceId = ''; const nextId = lastrecord ? lastrecord.id + 1 : 1; if (role_xid === ROLE.ACCOUNT_MANAGER) { referenceId = `AM-${String(nextId).padStart(6, '0')}`; } else if (role_xid === ROLE.CO_ADMIN) { referenceId = `CA-${String(nextId).padStart(6, '0')}`; } return referenceId; } async createHost(data: CreateMinglarDto) { return this.prisma.user.create({ data }); } async getAllHosts() { return this.prisma.user.findMany({ where: { roleXid: ROLE.HOST } }); } async updateHost(id: number, data: UpdateMinglarDto) { return this.prisma.user.update({ where: { id }, data, }); } async deleteHost(id: number) { return this.prisma.user.delete({ where: { id } }); } async getHostByEmail(email: string): Promise { return this.prisma.user.findUnique({ where: { emailAddress: email } }); } async getUserDetails(id: number) { return await this.prisma.user.findUnique({ where: { id: id }, }); } async verifyHostOtp(email: string, otp: string): Promise { const user = await this.prisma.user.findUnique({ where: { emailAddress: email }, select: { id: true, emailAddress: true, UserOtp: { where: { isActive: true, isVerified: false }, orderBy: { createdAt: 'desc' }, take: 1, }, }, }); if (!user) { throw new ApiError(404, 'User not found.'); } const userOtp = user.UserOtp[0]; if (!userOtp) { throw new ApiError(400, 'No OTP found.'); } if (new Date() > userOtp.expiresOn) { throw new ApiError(400, 'OTP has expired.'); } const isMatch = await bcrypt.compare(otp, userOtp.otpCode); if (!isMatch) { throw new ApiError(400, 'Invalid OTP.'); } await this.prisma.userOtp.update({ where: { id: userOtp.id }, data: { isVerified: true, verifiedOn: new Date(), isActive: false, }, }); return true; } async loginForMinglar(emailAddress: string, userPassword: string) { const existingUser = await this.prisma.user.findUnique({ where: { emailAddress: emailAddress, isActive: true, userStatus: USER_STATUS.ACTIVE, }, }); if (!existingUser) { throw new ApiError(404, 'User not found'); } if ( existingUser.roleXid !== ROLE.MINGLAR_ADMIN && existingUser.roleXid !== ROLE.CO_ADMIN && existingUser.roleXid !== ROLE.ACCOUNT_MANAGER ) { throw new ApiError(403, 'Access denied.'); } const matchPassword = await bcrypt.compare( userPassword, existingUser.userPassword, ); if (!matchPassword) { throw new ApiError(401, 'Invalid credentials'); } return existingUser; } async checkUserExists(emailAddress: string) { return await this.prisma.user.findUnique({ where: { emailAddress: emailAddress, isActive: true }, }); } async createUserForInvite(emailAddress: string, roleXid: number) { return await this.prisma.user.create({ data: { emailAddress: emailAddress, roleXid: roleXid, userStatus: USER_STATUS.INVITED, }, }); } async getAllHostActivityForMinglar(search?: string, hostXid?: number) { const hostActivities = await this.prisma.activities.findMany({ where: { isActive: true, ...(hostXid ? { hostXid } : {}), // Add only if provided }, include: { ActivitiesMedia: { select: { id: true, mediaFileName: true, mediaType: true, displayOrder: true, }, }, ActivityAmDetails: { select: { accountManager: { select: { id: true, firstName: true, lastName: true, profileImage: true, emailAddress: true, roleXid: true, }, }, }, }, activityType: true, }, }); const bucket = config.aws.bucketName; // Process each activity for (const activity of hostActivities) { /** -------------------------- * 1️⃣ Process Activity Media * -------------------------- */ if (activity.ActivitiesMedia?.length) { for (const media of activity.ActivitiesMedia) { if (!media.mediaFileName) continue; // Extract S3 key if URL or keep raw key const key = media.mediaFileName.startsWith("http") ? media.mediaFileName.split(".com/")[1] : media.mediaFileName; media.mediaFileName = await getPresignedUrl(bucket, key); } } /** -------------------------- * 2️⃣ Process AM Profile Image * -------------------------- */ const am = activity.ActivityAmDetails?.[0]?.accountManager; if (am?.profileImage) { const key = am.profileImage.startsWith("http") ? am.profileImage.split(".com/")[1] : am.profileImage; am.profileImage = await getPresignedUrl(bucket, key); } } return hostActivities; } async createUserRevenue( userXid: number, isFixedSalary: boolean, perValue: number, ) { return await this.prisma.userRevenue.create({ data: { userXid: userXid, is_fixed_salary: isFixedSalary, per_value: perValue || 0, isActive: true, }, }); } async createInviteDetails( userXid: number, invitedBy: number, invitationStatus: string, ) { return await this.prisma.inviteDetails.create({ data: { userXid: userXid, is_invited: true, invited_by: invitedBy, invited_on: new Date(), is_accepted: false, invitation_status: invitationStatus, isActive: true, isMinglarInvitation: true, }, }); } /** * Invite teammate flow: checks existing user, creates user, revenue and invite details * All operations are performed inside a single DB transaction to avoid races. */ async inviteTeammate( emailAddress: string, roleXid: number, isFixedSalary: boolean, perValue: number, invitedBy: number, ) { return await this.prisma.$transaction(async (tx) => { // Check existing user const existingUser = await tx.user.findFirst({ where: { emailAddress: emailAddress, isActive: true }, }); if (existingUser) { throw new ApiError(400, 'User already exists.'); } const referenceNumber = await this.generateHostRefNumber(tx, roleXid); // Create user with INVITED status const user = await tx.user.create({ data: { emailAddress: emailAddress, roleXid: roleXid, userStatus: USER_STATUS.INVITED, userRefNumber: referenceNumber }, }); // Create revenue record await tx.userRevenue.create({ data: { userXid: user.id, is_fixed_salary: isFixedSalary, per_value: perValue || 0, isActive: true, }, }); // Create invite details await tx.inviteDetails.create({ data: { userXid: user.id, is_invited: true, invited_by: invitedBy, invited_on: new Date(), is_accepted: false, invitation_status: MINGLAR_INVITATION_STATUS.INVITED, isActive: true, isMinglarInvitation: true, }, }); return user; }); } async updateProfile( userId: number, userData: { firstName?: string; lastName?: string; mobileNumber?: string; dateOfBirth?: string; profileImage?: string; }, addressData: { address1?: string; address2?: string; stateXid?: number; countryXid?: number; cityXid?: number; pinCode?: string; }, documents: Array<{ fileName: string; filePath: string, documentTypeName?: string }>, ) { try { return await this.prisma.$transaction(async (tx) => { console.log('Starting transaction for user:', userId); // 1. Update User table (optimized) const userUpdateData: any = {}; const userFields = [ 'firstName', 'lastName', 'mobileNumber', 'dateOfBirth', 'profileImage', ]; userFields.forEach((field) => { if (userData[field as keyof typeof userData] !== undefined) { if (field === 'dateOfBirth' && userData.dateOfBirth) { userUpdateData[field] = new Date(userData.dateOfBirth); } else { userUpdateData[field] = userData[field as keyof typeof userData]; } } }); if (Object.keys(userUpdateData).length > 0) { console.log('Updating user data:', userUpdateData); await tx.user.update({ where: { id: userId }, data: userUpdateData, }); } // 2. Update or create UserAddressDetails if (Object.keys(addressData).length > 0) { console.log('Processing address data:', addressData); const existingAddress = await tx.userAddressDetails.findFirst({ where: { userXid: userId, isActive: true }, select: { id: true }, // Only select needed field }); const addressUpdateData: any = {}; const addressFields = [ 'address1', 'address2', 'stateXid', 'countryXid', 'cityXid', 'pinCode', ]; addressFields.forEach((field) => { if (addressData[field as keyof typeof addressData] !== undefined) { addressUpdateData[field] = addressData[field as keyof typeof addressData]; } }); if (existingAddress) { await tx.userAddressDetails.update({ where: { id: existingAddress.id }, data: addressUpdateData, }); } else { // Validate required fields const requiredFields = [ 'address1', 'stateXid', 'countryXid', 'cityXid', 'pinCode', ]; const missingFields = requiredFields.filter( (field) => !addressData[field as keyof typeof addressData], ); if (missingFields.length > 0) { throw new ApiError( 400, `Missing required address fields: ${missingFields.join(', ')}`, ); } await tx.userAddressDetails.create({ data: { userXid: userId, ...addressUpdateData, }, }); } } // 3. Handle documents more efficiently if (documents && documents.length > 0) { console.log('Processing documents:', documents.length); // Use deleteMany and createMany for better performance await tx.userDocuments.deleteMany({ where: { userXid: userId, isActive: true }, }); if (documents.length > 0) { await tx.userDocuments.createMany({ data: documents.map((doc) => ({ userXid: userId, documentTypeName: doc.documentTypeName, fileName: doc.filePath, isActive: true, })), }); } } // 4. Fetch updated user data efficiently const updatedUser = await tx.user.findUnique({ where: { id: userId }, select: { id: true, firstName: true, lastName: true, mobileNumber: true, dateOfBirth: true, profileImage: true, userAddressDetails: { where: { isActive: true }, take: 1, select: { id: true, address1: true, address2: true, stateXid: true, countryXid: true, cityXid: true, pinCode: true, }, }, userDocuments: { where: { isActive: true }, select: { id: true, fileName: true, documentTypeName: true, }, }, }, }); if (!updatedUser) { throw new ApiError(404, 'User not found after update'); } // 5. Calculate profile completion percentage let percentage = 0; // Profile Image: 15% if (updatedUser.profileImage) percentage += 15; // Name and Phone Number: 15% if ( updatedUser.firstName && updatedUser.lastName && updatedUser.mobileNumber ) { percentage += 15; } // Location Info: 25% if (updatedUser.userAddressDetails.length > 0) { const address = updatedUser.userAddressDetails[0]; if ( address.address1 && address.stateXid && address.countryXid && address.cityXid && address.pinCode ) { percentage += 25; } } // Documents: 45% if (updatedUser.userDocuments.length >= 2) { percentage += 45; } else if (updatedUser.userDocuments.length === 1) { percentage += 22.5; } const profilePercentage = Math.min(percentage, 100); // Update profile completion status if (profilePercentage > 80) { await tx.user.update({ where: { id: userId }, data: { isProfileUpdated: true }, }); } console.log('Transaction completed successfully'); return { user: { id: updatedUser.id, firstName: updatedUser.firstName, lastName: updatedUser.lastName, mobileNumber: updatedUser.mobileNumber, dateOfBirth: updatedUser.dateOfBirth, profileImage: updatedUser.profileImage, }, address: updatedUser.userAddressDetails[0] || null, documents: updatedUser.userDocuments, profileCompletionPercentage: profilePercentage, }; }); } catch (error) { console.error('Error in updateProfile transaction:', error); throw error; } } async getAllInvitationDetails( search?: string, paginationOptions?: PaginationOptions, ) { const filters: any = { isMinglarInvitation: true, isActive: true, }; if (search?.trim()) { const term = search.trim(); filters.user = { OR: [ { emailAddress: { contains: term, mode: 'insensitive' as const } }, { firstName: { contains: term, mode: 'insensitive' as const } }, { lastName: { contains: term, mode: 'insensitive' as const } }, { userRefNumber: { contains: term, mode: 'insensitive' as const } }, ], }; } const totalCount = await this.prisma.inviteDetails.count({ where: filters, }); const invitations = await this.prisma.inviteDetails.findMany({ where: filters, include: { user: { select: { id: true, firstName: true, lastName: true, emailAddress: true, mobileNumber: true, roleXid: true, userRefNumber: true, role: { select: { id: true, roleName: true, }, }, }, }, }, orderBy: { createdAt: 'desc', }, skip: paginationOptions?.skip ?? 0, take: paginationOptions?.limit ?? 10, }); return { data: invitations, totalCount, }; } // Update your MinglarService method async getAllHostApplications( userId: number, userRoleXid: number, search?: string, userStatus?: string, paginationOptions?: PaginationOptions, roleFilter?: string, ) { const filters: any = { isActive: true, user: { roleXid: { notIn: [ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER], }, }, }; /** ----------------------------------- * SEARCH FILTER (FIRST NAME, LAST NAME, USER REF NUMBER) * ----------------------------------- */ if (search?.trim()) { const term = search.trim(); filters.user = { ...filters.user, OR: [ { firstName: { contains: term, mode: 'insensitive' } }, { lastName: { contains: term, mode: 'insensitive' } }, { userRefNumber: { contains: term, mode: 'insensitive' } }, ], }; } /** ----------------------------------- * USER STATUS FILTER (NEW) * ----------------------------------- */ if ( userStatus && userStatus.trim().toLowerCase() === MINGLAR_STATUS_DISPLAY.NEW.toLowerCase() ) { filters.adminStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW; } /** ----------------------------------- * ROLE FILTER * ----------------------------------- */ if (roleFilter?.trim()) { const roleFilterLower = roleFilter.trim().toLowerCase(); let roleId: number | undefined; if (roleFilterLower === 'co-admin') { roleId = ROLE.CO_ADMIN; } else if (roleFilterLower === 'account-manager') { roleId = ROLE.ACCOUNT_MANAGER; } if (roleId) { filters.user.roleXid = roleId; } } /** ----------------------------------- * ROLE-BASED FILTER: * CO_ADMIN & ACCOUNT_MANAGER only see assigned hosts * ----------------------------------- */ if (userRoleXid === ROLE.CO_ADMIN || userRoleXid === ROLE.ACCOUNT_MANAGER) { filters.accountManagerXid = userId; } /** ----------------------------------- * COUNT TOTAL RECORDS * ----------------------------------- */ const totalCount = await this.prisma.hostHeader.count({ where: filters, }); /** ----------------------------------- * MAIN QUERY WITH PAGINATION * ----------------------------------- */ const results = await this.prisma.hostHeader.findMany({ where: filters, select: { id: true, hostStatusInternal: true, hostStatusDisplay: true, adminStatusDisplay: true, adminStatusInternal: true, createdAt: true, companyName: true, assignedOn: true, cities: { select: { id: true, cityName: true } }, states: { select: { id: true, stateName: true } }, countries: { select: { id: true, countryName: true } }, user: { select: { id: true, firstName: true, lastName: true, emailAddress: true, mobileNumber: true, userRefNumber: true, }, }, accountManager: { select: { id: true, firstName: true, lastName: true, emailAddress: true, mobileNumber: true, roleXid: true, }, }, }, orderBy: { createdAt: 'desc' }, skip: paginationOptions?.skip || 0, take: paginationOptions?.limit || 10, }); /** ----------------------------------- * TRANSFORM RESPONSE * ----------------------------------- */ const transformedData = results.map((h) => ({ hostId: h.id, host: h.user, hostStatusDisplay: h.hostStatusDisplay, hostStatusInternal: h.hostStatusInternal, adminStatusDisplay: h.adminStatusDisplay, adminStatusInternal: h.adminStatusInternal, submittedOn: h.createdAt, accountManager: h.accountManager || null, companyName: h.companyName || null, city: h.cities || null, state: h.states || null, country: h.countries || null, assignedOn: h.assignedOn || null, })); return { data: transformedData, totalCount, }; } async getAllOnboardingHostApplications() { const onBoardingHostApp = await this.prisma.hostHeader.findMany({ where: { isActive: true, hostStatusInternal: { notIn: [HOST_STATUS_INTERNAL.DRAFT] }, }, select: { id: true, companyName: true, adminStatusDisplay: true, assignedOn: true, accountManagerXid: true, createdAt: true, user: { select: { id: true, firstName: true, lastName: true, emailAddress: true, userRefNumber: true, mobileNumber: true, }, }, accountManager: { select: { id: true, firstName: true, lastName: true, profileImage: true, }, }, }, }); const bucket = config.aws.bucketName; /** --------------------------------- * Add presigned URL for AM profile * --------------------------------- */ for (const host of onBoardingHostApp) { const am = host.accountManager; if (am?.profileImage) { const key = am.profileImage.startsWith("http") ? am.profileImage.split(".com/")[1] : am.profileImage; am.profileImage = await getPresignedUrl(bucket, key); } } return onBoardingHostApp; } async getAllOnboardingHostApplications_New( paginationOptions?: PaginationOptions, ) { const where = { isActive: true, adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW, }; const totalCount = await this.prisma.hostHeader.count({ where }); const onBoardingHostApp = await this.prisma.hostHeader.findMany({ where, select: { id: true, companyName: true, adminStatusDisplay: true, assignedOn: true, accountManagerXid: true, createdAt: true, cities: { select: { id: true, cityName: true, }, }, countries: { select: { id: true, countryName: true, }, }, states: { select: { id: true, stateName: true, }, }, user: { select: { id: true, firstName: true, lastName: true, emailAddress: true, userRefNumber: true, }, }, accountManager: { select: { id: true, firstName: true, lastName: true, profileImage: true, }, }, }, orderBy: { createdAt: 'desc', }, skip: paginationOptions?.skip ?? 0, take: paginationOptions?.limit ?? 10, }); const bucket = config.aws.bucketName; /** --------------------------------- * Add presigned URL for AM profile * --------------------------------- */ for (const host of onBoardingHostApp) { const am = host.accountManager; if (am?.profileImage) { const key = am.profileImage.startsWith('http') ? am.profileImage.split('.com/')[1] : am.profileImage; am.profileImage = await getPresignedUrl(bucket, key); } } return { data: onBoardingHostApp, totalCount, }; } async getAllCoadminAndAM(search?: string) { // Build search filter if search term is provided const searchFilter = search ? { OR: [ { email: { contains: search, mode: 'insensitive' as const } }, { firstName: { contains: search, mode: 'insensitive' as const } }, { lastName: { contains: search, mode: 'insensitive' as const } }, { userRefNumber: { contains: search, mode: 'insensitive' as const } }, ], } : {}; // 1. Fetch all required users (Admin, Co-Admin, AM) const users = await this.prisma.user.findMany({ where: { roleXid: { in: [ ROLE.MINGLAR_ADMIN, // Admin ROLE.CO_ADMIN, // Co-Admin ROLE.ACCOUNT_MANAGER, // AM ], }, isActive: true, userStatus: USER_STATUS.ACTIVE, ...searchFilter, }, include: { role: { select: { id: true, roleName: true, }, }, }, }); if (!users.length) return []; const userIds = users.map((u) => u.id); // 2. Count assigned hosts for ANY user (Admin / Co-Admin / AM) const groupedHosts = await this.prisma.hostHeader.groupBy({ by: ['accountManagerXid'], where: { accountManagerXid: { in: userIds }, // assigned user isActive: true, }, _count: { id: true, }, }); // 3. Build quick lookup map: userId -> hostCount const hostCountMap: Record = {}; groupedHosts.forEach((g) => { const uid = Number(g.accountManagerXid); hostCountMap[uid] = g._count.id; }); // 4. Attach host counts to each user return users.map((user) => ({ ...user, assignedHostCount: hostCountMap[user.id] ?? 0, })); } async getAllInvitedCoadminAndAM( search?: string, paginationOptions?: PaginationOptions, ) { const baseFilters: any = { roleXid: { in: [ ROLE.MINGLAR_ADMIN, // Admin ROLE.CO_ADMIN, // Co-Admin ROLE.ACCOUNT_MANAGER, // AM ], }, isActive: true, userStatus: { not: USER_STATUS.DE_ACTIVATED, // Exclude DE_ACTIVATED status }, }; if (search?.trim()) { const term = search.trim(); baseFilters.OR = [ { emailAddress: { contains: term, mode: 'insensitive' as const } }, { firstName: { contains: term, mode: 'insensitive' as const } }, { lastName: { contains: term, mode: 'insensitive' as const } }, { userRefNumber: { contains: term, mode: 'insensitive' as const } }, ]; } const totalCount = await this.prisma.user.count({ where: baseFilters, }); const users = await this.prisma.user.findMany({ where: baseFilters, include: { role: { select: { id: true, roleName: true, }, }, }, orderBy: { createdAt: 'desc', }, skip: paginationOptions?.skip ?? 0, take: paginationOptions?.limit ?? 10, }); return { data: users, totalCount, }; } async assignAMToHost( userId: number, hostXid: number, accountManagerXid: number, ) { const hostDetails = await this.prisma.hostHeader.findFirst({ where: { id: hostXid }, }); if (!hostDetails) { throw new ApiError(404, 'Host not found'); } if (hostDetails.accountManagerXid !== null) { throw new ApiError(400, 'AM already assigned to this host'); } if ( hostDetails.adminStatusInternal !== MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED && hostDetails.adminStatusDisplay !== MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED ) { throw new ApiError(400, 'Invalid host status'); } await this.prisma.hostHeader.update({ where: { id: hostXid }, data: { accountManagerXid: accountManagerXid, assignedOn: new Date(), hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW, adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW, }, }); return true; } /** * Notify Account Manager by email after assignment. * Encapsulates lookup + email send so handlers can call a single method. */ async notifyAMOfAssignment(accountManagerXid: number): Promise { if (!accountManagerXid) return false; const amUser = await this.prisma.user.findUnique({ where: { id: accountManagerXid, isActive: true }, select: { emailAddress: true }, }); if (!amUser || !amUser.emailAddress) { console.warn( `AM notification skipped: user not found or missing email for id=${accountManagerXid}`, ); return false; } try { await sendAMEmailForHostAssign(amUser.emailAddress); return true; } catch (err) { console.error('Error sending AM assignment email', err); return false; } } async addHostSuggestion( hostXid: number, title: string, comments: string, reviewedByXid: number, ) { // Check if host exists const hostHeader = await this.prisma.hostHeader.findUnique({ where: { id: hostXid }, select: { id: true }, }); console.log(hostHeader); if (!hostHeader) { throw new ApiError(404, 'Host not found'); } // Create suggestion in host_suggestion table await this.prisma.hostSuggestion.create({ data: { hostXid: hostXid, title: title, comments: comments, isparent: false, isreviewed: false, reviewedByXid: reviewedByXid, reviewOn: null, isActive: true, }, }); return true; } async addPqqSuggestion( title: string, comments: string, activity_pqq_header_xid: number, reviewedByXid: number, ) { // Check if host exists const ActivityHeader = await this.prisma.activityPQQheader.findUnique({ where: { id: activity_pqq_header_xid, isActive: true }, select: { id: true }, }); if (!ActivityHeader) { throw new ApiError(404, 'Host not found'); } await this.prisma.activityPQQSuggestions.create({ data: { title: title, comments: comments, isReviewed: false, reviewedOn: new Date(), isActive: true, activityPqqHeaderXid: activity_pqq_header_xid, reviewedByXid: reviewedByXid, }, }); return true; } async getHostSuggestions(userId: number) { const hostDetail = await this.prisma.hostHeader.findFirst({ where: { userXid: userId, isActive: true }, }); const suggestions = await this.prisma.hostSuggestion.findMany({ where: { hostXid: hostDetail.id, isreviewed: false, isActive: true }, select: { id: true, title: true, comments: true, isparent: true, isreviewed: true, reviewOn: true, }, orderBy: { id: 'asc', }, }); return suggestions; } async editAgreementDetails( host_xid: number, agreementStartDate: string, duration: number, isCommisionBase: boolean, commisionPer: number, amountPerBooking: number, durationFrequency: string, payoutDurationNum: number, payoutDurationFrequency: string, ) { return await this.prisma.hostHeader.update({ where: { id: host_xid }, data: { durationNumber: Number(duration), durationFrequency: durationFrequency, agreementStartDate: new Date(agreementStartDate), isCommisionBase: isCommisionBase, commisionPer: commisionPer ? Number(commisionPer) : null, // Convert to number if exists amountPerBooking: amountPerBooking ? Number(amountPerBooking) : null, // Convert to number if exists payoutDurationNum: Number(payoutDurationNum), // Convert to number payoutDurationFrequency: payoutDurationFrequency, }, }); } async acceptHostApplication(host_xid: number, user_xid: number) { return await this.prisma.$transaction(async (tx) => { await this.prisma.hostHeader.update({ where: { id: host_xid, hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW, adminStatusDisplay: MINGLAR_STATUS_DISPLAY.TO_REVIEW, }, data: { hostStatusInternal: HOST_STATUS_INTERNAL.APPROVED, hostStatusDisplay: HOST_STATUS_DISPLAY.APPROVED, adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_APPROVED, adminStatusDisplay: MINGLAR_STATUS_DISPLAY.APPROVED, stepper: STEPPER.COMPANY_DETAILS_APPROVED, }, }); await this.prisma.hostTrack.create({ data: { hostXid: host_xid, updatedByRole: ROLE_NAME.ACCOUNT_MANAGER, updatedByXid: user_xid, trackStatus: MINGLAR_STATUS_INTERNAL.AM_APPROVED, }, }); }); } async acceptHostApplicationMinglarAdmin(host_xid: number, user_xid: number) { return await this.prisma.$transaction(async (tx) => { await tx.hostHeader.update({ where: { id: host_xid, hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW, adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW, }, data: { isApproved: true, hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED, adminStatusDisplay: MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED, }, }); await this.prisma.hostTrack.create({ data: { hostXid: host_xid, updatedByRole: ROLE_NAME.MINGLAR_ADMIN, updatedByXid: user_xid, trackStatus: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED, }, }); }); } async rejectHostApplication(host_xid: number, user_xid: number) { await this.prisma.$transaction(async (tx) => { const hostDetails = await tx.hostHeader.findFirst({ where: { id: host_xid }, select: { id: true, userXid: true }, }); if (!hostDetails) { throw new Error('Host not found'); } await tx.hostHeader.update({ where: { id: host_xid, hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, }, data: { hostStatusInternal: HOST_STATUS_INTERNAL.REJECTED, hostStatusDisplay: HOST_STATUS_DISPLAY.REJECTED, adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED, adminStatusDisplay: MINGLAR_STATUS_DISPLAY.REJECTED, }, }); await this.prisma.hostTrack.create({ data: { hostXid: hostDetails.id, updatedByRole: ROLE_NAME.MINGLAR_ADMIN, updatedByXid: user_xid, trackStatus: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED, }, }); await tx.user.update({ where: { id: hostDetails.userXid }, data: { userStatus: USER_STATUS.REJECTED, }, }); }); } async rejectHostApplicationAM(host_xid: number, user_xid: number) { return await this.prisma.$transaction(async (tx) => { const hostDetails = await this.prisma.hostHeader.findFirst({ where: { id: host_xid }, select: { id: true, userXid: true }, }); if (!hostDetails) { throw new Error('Host not found'); } await this.prisma.hostHeader.update({ where: { id: host_xid, hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, }, data: { hostStatusInternal: HOST_STATUS_INTERNAL.HOST_TO_UPDATE, hostStatusDisplay: HOST_STATUS_DISPLAY.ENHANCING, adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_REJECTED, adminStatusDisplay: MINGLAR_STATUS_DISPLAY.ENHANCING, }, }); await this.prisma.hostTrack.create({ data: { hostXid: hostDetails.id, updatedByRole: ROLE_NAME.ACCOUNT_MANAGER, updatedByXid: user_xid, trackStatus: MINGLAR_STATUS_INTERNAL.AM_REJECTED, }, }); }); } async getAMdetailById(id: number) { const user = await this.prisma.user.findUnique({ where: { id: id, isActive: true, userStatus: USER_STATUS.ACTIVE }, include: { userAddressDetails: { select: { id: true, userXid: true, address1: true, address2: true, locationAddress: true, locationLat: true, locationLong: true, locationName: true, } }, userDocuments: { select: { id: true, fileName: true, } }, userRevenues: { select: { id: true, is_fixed_salary: true, per_value: true } }, }, }); const bucket = config.aws.bucketName; if (user.userDocuments?.length) { for (const media of user.userDocuments) { if (!media.fileName) continue; // Extract S3 key if URL or keep raw key const key = media.fileName.startsWith("http") ? media.fileName.split(".com/")[1] : media.fileName; media.fileName = await getPresignedUrl(bucket, key); } } if (user.profileImage) { const key = user.profileImage.startsWith('http') ? user.profileImage.split('.com/')[1] : user.profileImage; user.profileImage = await getPresignedUrl(bucket, key); } return user; } async getBasicUserDetails(user_xid) { return await this.prisma.user.findFirst({ where: { id: user_xid }, select: { id: true, firstName: true, lastName: true, emailAddress: true, userStatus: true, isProfileUpdated: true, roleXid: true, role: true, } }) } async rejectPQQbyAM(activityId: number) { return await this.prisma.activities.update({ where: { id: activityId, isActive: true }, data: { activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQQ_TO_UPDATE, activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.ENHANCING, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQQ_REJECTED, amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.ENHANCING } }) } async getHostDetailsById(host_xid) { const host = await this.prisma.hostHeader.findFirst({ where: { id: host_xid }, include: { hostParent: { include: { HostParenetDocuments: { select: { id: true, filePath: true, documentName: true, documentTypeXid: true, documentType: true } } } }, HostBankDetails: true, HostDocuments: { include: { documentType: true, }, }, user: { select: { id: true, emailAddress: true, firstName: true, lastName: true, mobileNumber: true, profileImage: true, userStatus: true, userRefNumber: true, } }, HostSuggestion: true, HostTrack: true, countries: true, currencies: true, states: true, cities: true, }, }); const bucket = config.aws.bucketName; if (host.HostDocuments?.length) { for (const doc of host.HostDocuments) { if (doc.filePath) { const filePath = doc.filePath; // If full URL is saved, extract only key const key = filePath.startsWith('http') ? filePath.split('.com/')[1] : filePath; (doc as any).presignedUrl = await getPresignedUrl(bucket, key); } } } if (host.logoPath) { const key = host.logoPath.startsWith('http') ? host.logoPath.split('.com/')[1] : host.logoPath; host.logoPath = await getPresignedUrl(bucket, key); } if (host.user.profileImage) { const key = host.user.profileImage.startsWith("http") ? host.user.profileImage.split(".com/")[1] : host.user.profileImage; host.user.profileImage = await getPresignedUrl(bucket, key); } if (host.hostParent?.length) { const parent = host.hostParent[0]; // since you allow only 1 parent // Parent company logo if (parent.logoPath) { const key = parent.logoPath.startsWith("http") ? parent.logoPath.split(".com/")[1] : parent.logoPath; parent.logoPath = await getPresignedUrl(bucket, key); } // Parent documents if (parent.HostParenetDocuments?.length) { for (const doc of parent.HostParenetDocuments) { if (doc.filePath) { const key = doc.filePath.startsWith("http") ? doc.filePath.split(".com/")[1] : doc.filePath; (doc as any).presignedUrl = await getPresignedUrl(bucket, key); } } } } return host; } }