// src/modules/host/services/host.service.ts import { Injectable } from '@nestjs/common'; import { PrismaService } from '../../../common/database/prisma.service'; import { AddPaymentDetailsDTO, CreateHostDto, UpdateHostDto, } from '../dto/host.dto'; import * as bcrypt from 'bcryptjs'; import ApiError from '../../../common/utils/helper/ApiError'; import { User } from '@prisma/client'; import { z } from 'zod'; import { hostCompanyDetailsSchema } from '@/common/utils/validation/host/hostCompanyDetails.validation'; 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_STATUS_DISPLAY, MINGLAR_STATUS_INTERNAL, } from '@/common/utils/constants/minglar.constant'; import { ROLE, ROLE_NAME, USER_STATUS, } from '@/common/utils/constants/common.constant'; import { getPresignedUrl } from '@/common/middlewares/aws/getPreSignedUrl'; import config from '@/config/config'; type HostCompanyDetailsInput = z.infer; // Document input after S3 upload (with S3 URL as filePath) interface HostDocumentInput { documentTypeXid: number; documentName: string; filePath: string; // S3 URL } export async function generateActivityRefNumber(tx: any) { const lastrecord = await tx.activities.findFirst({ orderBy: { id: 'desc', }, select: { id: true, }, }); const nextId = lastrecord ? lastrecord.id + 1 : 1; return `ACT-${String(nextId).padStart(6, '0')}`;; } function round2(value: number) { return Math.round(value * 100) / 100; } const bucket = config.aws.bucketName; @Injectable() export class HostService { constructor(private prisma: PrismaService) { } async createHost(data: CreateHostDto) { return this.prisma.user.create({ data }); } async getAllHosts() { return this.prisma.user.findMany({ where: { roleXid: 3 } }); } async getHostIdByUserXid(user_xid: number) { const host = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid }, select: { id: true, companyName: true, countryXid: true, stepper: true }, }); return host; } async getHostById(id: number) { const host = await this.prisma.hostHeader.findFirst({ where: { userXid: id }, include: { hostParent: { include: { HostParenetDocuments: { select: { id: true, filePath: true, documentName: true, documentTypeXid: true, documentType: true } } } }, HostBankDetails: true, HostDocuments: { include: { documentType: true, }, }, accountManager: { select: { id: true, firstName: true, lastName: true, emailAddress: true, mobileNumber: true, profileImage: true, userRefNumber: true, } }, user: { select: { id: true, emailAddress: true, firstName: true, lastName: true, mobileNumber: true, profileImage: true, userStatus: true, userRefNumber: true, } }, companyTypes: { select: { id: true, companyTypeName: true, }, }, HostSuggestion: { where: { isActive: true, isreviewed: false, }, select: { id: true, hostXid: true, title: true, comments: true, isparent: true, } }, countries: true, currencies: true, states: true, cities: true, }, }); if (!host) { // If host record doesn't exist yet, return stepper 1 (NOT_SUBMITTED) // so callers (like the stepper endpoint) can show initial step. return { stepper: STEPPER.NOT_SUBMITTED } as any; } 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.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.logoPath) { const key = host.logoPath.startsWith('http') ? host.logoPath.split('.com/')[1] : host.logoPath; host.logoPath = await getPresignedUrl(bucket, key); } if (host.accountManager.profileImage) { const key = host.accountManager.profileImage.startsWith('http') ? host.accountManager.profileImage.split('.com/')[1] : host.accountManager.profileImage; host.accountManager.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; } async updateHost(id: number, data: UpdateHostDto) { 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 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 loginForHost(emailAddress: string, userPassword: string) { const existingUser = await this.prisma.user.findUnique({ where: { emailAddress: emailAddress }, }); if (!existingUser) { throw new ApiError(404, 'User not found'); } if (existingUser.roleXid !== 4) { throw new ApiError(403, 'Access denied. Not a host user.'); } const matchPassword = await bcrypt.compare( userPassword, existingUser.userPassword, ); if (!matchPassword) { throw new ApiError(401, 'Invalid credentials'); } return existingUser; } async createMinglarUser(email: string) { const newUser = await this.prisma.user.create({ data: { emailAddress: email, roleXid: ROLE.HOST }, }); return newUser; } async createPassword(user_xid: number, password: string): Promise { // Find user by id const user = await this.prisma.user.findUnique({ where: { id: user_xid }, select: { id: true, emailAddress: true, userPassword: 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, isEmailVerfied: true, userStatus: USER_STATUS.ACTIVE, }, }); return true; } async addPaymentDetails(data: AddPaymentDetailsDTO) { return await this.prisma.$transaction(async (tx) => { const addedPaymentDetails = await tx.hostBankDetails.create({ data, }); if (!addedPaymentDetails) { throw new ApiError(400, 'Failed to add payment details'); } await tx.hostHeader.update({ where: { id: data.hostXid }, data: { stepper: STEPPER.BANK_DETAILS_UPDATED, currencyXid: data.currencyXid }, }); }); } async getAllHostActivity(search?: string, user_xid?: number) { const hostDetails = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid, isActive: true } }) const hostAllActivities = await this.prisma.activities.findMany({ where: { isActive: true, hostXid: hostDetails.id, }, include: { ActivitiesMedia: true, ActivityAmDetails: { select: { accountManager: { select: { id: true, firstName: true, lastName: true, profileImage: true, emailAddress: true, roleXid: true, }, }, }, }, activityType: true, }, }); for (const activity of hostAllActivities) { /** 1️⃣ Process Activity Media */ const processedMedia = []; for (const media of activity.ActivitiesMedia || []) { const key = media.mediaFileName?.startsWith("http") ? media.mediaFileName.split(".com/")[1] : media.mediaFileName; const presignedUrl = key ? await getPresignedUrl(bucket, key) : null; processedMedia.push({ ...media, presignedUrl, }); } activity.ActivitiesMedia = processedMedia; /** 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; const presignedUrl = await getPresignedUrl(bucket, key); activity.ActivityAmDetails[0].accountManager = { ...am, profileImage: presignedUrl, }; } } return hostAllActivities; } async acceptMinglarAgreement(user_xid: number) { const hostDetails = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid }, select: { id: true, userXid: true, }, }); await this.prisma.hostHeader.update({ where: { id: hostDetails.id }, data: { stepper: STEPPER.AGREEMENT_ACCEPTED, isApproved: true, agreementAccepted: true, }, }); } async getPQQQuestionDetail(question_xid: number, activity_xid: number) { const detailsOfQuestion = await this.prisma.activityPQQheader.findFirst({ where: { activityXid: activity_xid, pqqQuestionXid: question_xid, isActive: true, }, select: { pqqQuestionXid: true, pqqAnswerXid: true, ActivityPQQSupportings: { select: { id: true, activityPqqHeaderXid: true, mediaFileName: true, mediaType: true } }, ActivityPQQSuggestions: { where: { isActive: true, isReviewed: false }, select: { id: true, title: true, comments: true, } }, }, }); if (detailsOfQuestion.ActivityPQQSupportings?.length) { for (const doc of detailsOfQuestion.ActivityPQQSupportings) { if (doc.mediaFileName) { const filePath = doc.mediaFileName; // 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); } } } return detailsOfQuestion; } async getLatestQuestionDetailsPQQ(activity_xid: number) { return await this.prisma.activityPQQheader.findFirst({ where: { activityXid: activity_xid, isActive: true }, select: { pqqQuestionXid: true, pqqAnswerXid: true, pqqQuestions: { select: { pqqSubCategoryXid: true, pqqSubCategories: { select: { categoryXid: true, }, }, }, }, }, orderBy: { id: 'desc' }, }); } async addOrUpdateCompanyDetails( user_xid: number, companyData: HostCompanyDetailsInput, documents: HostDocumentInput[], parentCompanyData?: any | null, parentDocuments?: HostDocumentInput[], isDraft: boolean = false, ) { return await this.prisma.$transaction(async (tx) => { // Check if host already has a company const existingHostCompany = await tx.hostHeader.findFirst({ where: { userXid: user_xid }, include: { hostParent: true }, }); let hostStatusInternal; let hostStatusDisplay; let minglarStatusInternal; let minglarStatusDisplay; if (existingHostCompany) { hostStatusInternal = existingHostCompany.hostStatusInternal; hostStatusDisplay = existingHostCompany.hostStatusDisplay; minglarStatusInternal = existingHostCompany.adminStatusInternal; minglarStatusDisplay = existingHostCompany.adminStatusDisplay; } // CASE 1: Host was asked to update AND is submitting final if ( existingHostCompany && existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE && !isDraft ) { hostStatusInternal = HOST_STATUS_INTERNAL.HOST_SUBMITTED; hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW; minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW; minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW; } // CASE 2: Host was asked to update BUT saving draft else if ( existingHostCompany && existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE && isDraft ) { // keep original hostStatusInternal = existingHostCompany.hostStatusInternal; hostStatusDisplay = existingHostCompany.hostStatusDisplay; minglarStatusInternal = existingHostCompany.adminStatusInternal; minglarStatusDisplay = existingHostCompany.adminStatusDisplay; } // CASE 3: Normal create or update else { hostStatusInternal = isDraft ? HOST_STATUS_INTERNAL.DRAFT : HOST_STATUS_INTERNAL.HOST_SUBMITTED; hostStatusDisplay = isDraft ? HOST_STATUS_DISPLAY.DRAFT : HOST_STATUS_DISPLAY.UNDER_REVIEW; minglarStatusInternal = isDraft ? MINGLAR_STATUS_INTERNAL.DRAFT : MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW; minglarStatusDisplay = isDraft ? MINGLAR_STATUS_DISPLAY.DRAFT : MINGLAR_STATUS_DISPLAY.NEW; } const stepper = isDraft ? STEPPER.NOT_SUBMITTED : STEPPER.UNDER_REVIEW; // ------------------------------------------------------- // CREATE FLOW // ------------------------------------------------------- if (!existingHostCompany) { if (!isDraft) { const existingByPan = await tx.hostHeader.findFirst({ where: { panNumber: companyData.panNumber }, }); if (existingByPan) throw new ApiError(400, 'Company already exists with this pan/bin number'); } const createdHost = await tx.hostHeader.create({ data: { user: { connect: { id: user_xid } }, companyName: companyData.companyName, address1: companyData.address1, address2: companyData.address2, cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined, states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined, countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined, pinCode: companyData.pinCode, logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, registrationNumber: companyData.registrationNumber, panNumber: companyData.panNumber, gstNumber: companyData.gstNumber || null, formationDate: companyData.formationDate ? new Date(companyData.formationDate as any) : null, companyTypes: companyData.companyTypeXid ? { connect: { id: companyData.companyTypeXid } } : undefined, websiteUrl: companyData.websiteUrl || null, instagramUrl: companyData.instagramUrl || null, facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, stepper, hostStatusInternal, hostStatusDisplay, adminStatusInternal: minglarStatusInternal, adminStatusDisplay: minglarStatusDisplay, }, }); // host documents if (documents?.length) { const docsData = documents.map((doc) => ({ hostXid: createdHost.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, })); await tx.hostDocuments.createMany({ data: docsData }); } // parent create if (companyData.isSubsidairy && parentCompanyData) { const createdParent = await tx.hostParent.create({ data: { host: { connect: { id: createdHost.id } }, companyName: parentCompanyData.companyName, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, cities: parentCompanyData.cityXid ? { connect: { id: parentCompanyData.cityXid } } : undefined, states: parentCompanyData.stateXid ? { connect: { id: parentCompanyData.stateXid } } : undefined, countries: parentCompanyData.countryXid ? { connect: { id: parentCompanyData.countryXid } } : undefined, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || null, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, formationDate: parentCompanyData.formationDate ? new Date(parentCompanyData.formationDate as any) : null, companyTypes: parentCompanyData.companyTypeXid ? { connect: { id: parentCompanyData.companyTypeXid } } : undefined, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, facebookUrl: parentCompanyData.facebookUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null, twitterUrl: parentCompanyData.twitterUrl || null, }, }); // parent docs if (parentDocuments?.length) { const parentDocsData = parentDocuments.map((doc) => ({ hostParentXid: createdParent.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, })); await tx.hostParenetDocuments.createMany({ data: parentDocsData }); } } // ⭐ FIX — TRACK USING createdHost (no null risk) await tx.hostTrack.create({ data: { hostXid: createdHost.id, updatedByRole: ROLE_NAME.HOST, updatedByXid: user_xid, trackStatus: createdHost.hostStatusInternal, }, }); return createdHost; } // ------------------------------------------------------- // UPDATE FLOW // ------------------------------------------------------- const updatedHost = await tx.hostHeader.update({ where: { id: existingHostCompany.id }, data: { companyName: companyData.companyName, address1: companyData.address1, address2: companyData.address2, cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined, states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined, countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined, pinCode: companyData.pinCode, logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, registrationNumber: companyData.registrationNumber, panNumber: companyData.panNumber, gstNumber: companyData.gstNumber || null, formationDate: companyData.formationDate ? new Date(companyData.formationDate as any) : null, companyTypes: companyData.companyTypeXid ? { connect: { id: companyData.companyTypeXid } } : undefined, websiteUrl: companyData.websiteUrl || null, instagramUrl: companyData.instagramUrl || null, facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, stepper, hostStatusInternal, hostStatusDisplay, adminStatusInternal: minglarStatusInternal, adminStatusDisplay: minglarStatusDisplay, }, }); // documents UPSERT if (documents?.length) { for (const doc of documents) { const existingDoc = await tx.hostDocuments.findFirst({ where: { hostXid: updatedHost.id, documentTypeXid: doc.documentTypeXid, }, }); if (existingDoc) { await tx.hostDocuments.update({ where: { id: existingDoc.id }, data: { filePath: doc.filePath, documentName: doc.documentName || existingDoc.documentName, }, }); } else { await tx.hostDocuments.create({ data: { hostXid: updatedHost.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, }, }); } } } // parent logic untouched if (companyData.isSubsidairy) { const parentRecords = existingHostCompany.hostParent; const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords; if (!parentRecord) { const createdParent = await tx.hostParent.create({ data: { host: { connect: { id: updatedHost.id } }, companyName: parentCompanyData.companyName, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, cities: parentCompanyData.cityXid ? { connect: { id: parentCompanyData.cityXid } } : undefined, states: parentCompanyData.stateXid ? { connect: { id: parentCompanyData.stateXid } } : undefined, countries: parentCompanyData.countryXid ? { connect: { id: parentCompanyData.countryXid } } : undefined, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || null, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, formationDate: parentCompanyData.formationDate ? new Date(parentCompanyData.formationDate as any) : null, companyTypes: parentCompanyData.companyTypeXid ? { connect: { id: parentCompanyData.companyTypeXid } } : undefined, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, facebookUrl: parentCompanyData.facebookUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null, twitterUrl: parentCompanyData.twitterUrl || null, }, }); if (parentDocuments?.length) { for (const doc of parentDocuments) { await tx.hostParenetDocuments.create({ data: { hostParentXid: createdParent.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, }, }); } } } else { await tx.hostParent.update({ where: { id: parentRecord.id }, data: { companyName: parentCompanyData.companyName, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, cities: parentCompanyData.cityXid ? { connect: { id: parentCompanyData.cityXid } } : undefined, states: parentCompanyData.stateXid ? { connect: { id: parentCompanyData.stateXid } } : undefined, countries: parentCompanyData.countryXid ? { connect: { id: parentCompanyData.countryXid } } : undefined, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || null, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, formationDate: parentCompanyData.formationDate ? new Date(parentCompanyData.formationDate as any) : null, companyTypes: parentCompanyData.companyTypeXid ? { connect: { id: parentCompanyData.companyTypeXid } } : undefined, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, facebookUrl: parentCompanyData.facebookUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null, twitterUrl: parentCompanyData.twitterUrl || null, }, }); if (parentDocuments?.length) { for (const doc of parentDocuments) { const existingParentDoc = await tx.hostParenetDocuments.findFirst({ where: { hostParentXid: parentRecord.id, documentTypeXid: doc.documentTypeXid, }, }); if (existingParentDoc) { await tx.hostParenetDocuments.update({ where: { id: existingParentDoc.id }, data: { filePath: doc.filePath, documentName: doc.documentName || existingParentDoc.documentName, }, }); } else { await tx.hostParenetDocuments.create({ data: { hostParentXid: parentRecord.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, }, }); } } } } } else { const previousParent = existingHostCompany.hostParent; let prevParentId = null; if (Array.isArray(previousParent) && previousParent.length) { prevParentId = previousParent[0].id; } else if (previousParent && typeof previousParent === 'object' && 'id' in previousParent) { prevParentId = previousParent.id; } if (prevParentId) { await tx.hostParenetDocuments.deleteMany({ where: { hostParentXid: prevParentId }, }); await tx.hostParent.delete({ where: { id: prevParentId } }); } } // ⭐ FIX — USE updatedHost instead of re-querying hostHeader await tx.hostTrack.create({ data: { hostXid: updatedHost.id, updatedByRole: ROLE_NAME.HOST, updatedByXid: user_xid, trackStatus: updatedHost.hostStatusInternal, }, }); // suggestion update unchanged if (!isDraft) { await tx.hostSuggestion.updateMany({ where: { hostXid: updatedHost.id, isActive: true, isreviewed: false }, data: { isreviewed: true, reviewedByXid: user_xid, reviewOn: new Date(), }, }); } return updatedHost; }); } async getSuggestionDetails(user_xid: number) { const hostDetails = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid, isActive: true }, include: { user: { select: { id: true, emailAddress: true, firstName: true, userRefNumber: true }, }, accountManager: { select: { id: true, emailAddress: true, firstName: true, }, }, }, }); if (!hostDetails) { return { hostSuggestionDetails: [], hostDetails: null }; } const hostSuggestionDetails = await this.prisma.hostSuggestion.findMany({ where: { hostXid: hostDetails.id, isActive: true, isreviewed: false }, }); if (hostSuggestionDetails) { await this.prisma.hostSuggestion.updateMany({ where: { hostXid: hostDetails.id, isActive: true, isreviewed: false }, data: { isreviewed: true, reviewedByXid: hostDetails.id, reviewOn: new Date(), }, }); } return { hostSuggestionDetails, hostDetails }; } // async createOrUpdateHeader( // activityXid: number, // pqqQuestionXid: number, // pqqAnswerXid: number, // comments: string | null // ) { // // find existing header // const existing = await this.prisma.activityPQQheader.findFirst({ // where: { activityXid, pqqQuestionXid, deletedAt: null } // }); // if (!existing) { // return await this.prisma.activityPQQheader.create({ // data: { // activityXid, // pqqQuestionXid, // pqqAnswerXid, // comments // } // }); // } // // mark old supportings deleted // await this.prisma.activityPQQSupportings.updateMany({ // where: { activityPqqHeaderXid: existing.id }, // data: { // isActive: false, // deletedAt: new Date() // } // }); // // update header // return await this.prisma.activityPQQheader.update({ // where: { id: existing.id }, // data: { // pqqAnswerXid, // comments // } // }); // } // async addSupportingFile( // headerId: number, // mimeType: string, // fileUrl: string // ) { // return await this.prisma.activityPQQSupportings.create({ // data: { // activityPqqHeaderXid: headerId, // mediaType: mimeType, // mediaFileName: fileUrl // } // }); // } async calculatePqqScoreForUser(activityXid: number) { return await this.prisma.$transaction(async (tx) => { // 1. Get all headers for this activity (user's answers) const answers = await this.prisma.activityPQQheader.findMany({ where: { activityXid, isActive: true }, include: { pqqQuestions: { include: { pqqSubCategories: { include: { category: true, }, }, }, }, pqqAnswers: true, }, }); if (!answers.length) { return { overallPercentage: 0, categoryWise: {}, }; } // Prepare accumulators let totalUserPoints = 0; let totalMaxPoints = 0; // For category-wise scoring const categories: Record< number, { categoryId: number; categoryName: string; userPoints: number; maxPoints: number; } > = {}; for (const item of answers) { const question = item.pqqQuestions; const answer = item.pqqAnswers; const maxPoints = question.maxPoints; const userPoints = answer.answerPoints; totalUserPoints += userPoints; totalMaxPoints += maxPoints; // Category const category = question.pqqSubCategories.category; const categoryId = category.id; if (!categories[categoryId]) { categories[categoryId] = { categoryId, categoryName: category.categoryName, userPoints: 0, maxPoints: 0, }; } categories[categoryId].userPoints += userPoints; categories[categoryId].maxPoints += maxPoints; } // Overall percent const overallPercentage = totalMaxPoints > 0 ? round2((totalUserPoints / totalMaxPoints) * 100) : 0; // ---------- 🔥 ONLY FIRST 2 CATEGORIES ---------- const categoryArray = Object.values(categories); // Sort by categoryId (or change to displayOrder if needed) categoryArray.sort((a, b) => a.categoryId - b.categoryId); // Take only first 2 categories const topTwo = categoryArray.slice(0, 2); const categoryWise: Record = {}; for (const c of topTwo) { categoryWise[c.categoryName] = c.maxPoints > 0 ? round2((c.userPoints / c.maxPoints) * 100) : 0; } await this.prisma.activities.update({ where: { id: activityXid }, data: { totalScore: round2(overallPercentage), sustainabilityScore: round2(categoryWise.Sustainability), safetyScore: round2(categoryWise.Safety), } }) // Return final score object return { overallPercentage, categoryWise, }; }); } async createHeader( activityXid: number, pqqQuestionXid: number, pqqAnswerXid: number, comments?: string | null, ) { return await this.prisma.activityPQQheader.create({ data: { activityXid, pqqQuestionXid, pqqAnswerXid, comments: comments || null, // Handle null comments }, }); } async findHeaderByCompositeKey( activityXid: number, pqqQuestionXid: number, ) { return await this.prisma.activityPQQheader.findFirst({ where: { activityXid, pqqQuestionXid, }, }); } async updateHeader(headerId: number, pqqAnswerXid: number, comments?: string | null) { return await this.prisma.activityPQQheader.update({ where: { id: headerId, }, data: { comments: comments || null, // Handle null comments pqqAnswerXid: pqqAnswerXid, updatedAt: new Date(), }, }); } async addSupportingFile(headerId: number, mimeType: string, fileUrl: string) { return await this.prisma.activityPQQSupportings.create({ data: { activityPqqHeaderXid: headerId, mediaType: mimeType, mediaFileName: fileUrl, }, }); } async getSupportingFilesByHeaderId(headerId: number) { return await this.prisma.activityPQQSupportings.findMany({ where: { activityPqqHeaderXid: headerId, }, orderBy: { id: 'asc', // Maintain consistent order }, }); } async submitpqqforreview(activity_xid: number) { const activity = await this.prisma.activities.findFirst({ where: { id: activity_xid, isActive: true }, select: { id: true, activityTitle: true, activityRefNumber: true, } }) if (!activity) { throw new ApiError(404, "Activity not found") } await this.prisma.activities.update({ where: { id: activity_xid }, data: { activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQ_SUBMITTED, activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.PQ_IN_REVIEW, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQ_TO_REVIEW, amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.NEW } }) } async updateSupportingFile( supportingFileId: number, mimeType: string, fileUrl: string, ) { return await this.prisma.activityPQQSupportings.update({ where: { id: supportingFileId, }, data: { mediaType: mimeType, mediaFileName: fileUrl, updatedAt: new Date(), }, }); } async deleteSupportingFile(supportingFileId: number) { return await this.prisma.activityPQQSupportings.delete({ where: { id: supportingFileId, }, }); } async markPQQSuggestionReviewed(user_xid: number, activityPqqHeaderXid: number, activityPQQSuggestionId: number) { return await this.prisma.activityPQQSuggestions.update({ where: { id: activityPQQSuggestionId, activityPqqHeaderXid: activityPqqHeaderXid, isActive: true, isReviewed: false }, data: { isReviewed: true, reviewedByXid: user_xid, reviewedOn: new Date() } }) } async getAllPQQQuesAndSubmittedAns() { return await this.prisma.activityPQQheader.findMany({ where: { isActive: true }, include: { pqqQuestions: { select: { questionName: true, maxPoints: true, displayOrder: true, pqqSubCategories: { select: { id: true, subCategoryName: true, displayOrder: true, category: { select: { id: true, categoryName: true, displayOrder: true, } } } } } }, ActivityPQQSuggestions: { select: { id: true, title: true, comments: true, isReviewed: true, reviewedBy: true, reviewedOn: true, } }, pqqAnswers: { select: { id: true, displayOrder: true, answerName: true, answerPoints: true } }, ActivityPQQSupportings: { select: { id: true, mediaFileName: true, mediaType: true, } }, }, }); } async getAllActivityTypesWithInterest(search?: string) { const where: any = { isActive: true, deletedAt: null, }; if (search && search.trim() !== '') { const q = search.trim(); where.OR = [ { activityTypeName: { contains: q, mode: 'insensitive' } }, { interests: { interestName: { contains: q, mode: 'insensitive' } } }, ]; } return await this.prisma.activityTypes.findMany({ where, select: { id: true, activityTypeName: true, interestXid: true, interests: { select: { id: true, interestName: true, displayOrder: true, }, }, }, orderBy: { activityTypeName: 'asc' }, }); } async createActivity( userId: number, activityTypeXid: number, frequenciesXid?: number, ) { return await this.prisma.$transaction(async (tx) => { // Fetch host const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true }, }); if (!host) throw new ApiError(404, 'Host not found for the user'); // Validate activityType const activityType = await tx.activityTypes.findUnique({ where: { id: activityTypeXid }, }); if (!activityType) throw new ApiError(404, 'Activity type not found'); // Validate frequency if (frequenciesXid) { const freq = await tx.frequencies.findUnique({ where: { id: frequenciesXid }, }); if (!freq) throw new ApiError(404, 'Frequency not found'); } // Generate reference number const referenceNumber = await generateActivityRefNumber(tx); // Create activity const created = await tx.activities.create({ data: { hostXid: host.id, activityTypeXid, frequenciesXid: frequenciesXid || null, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.DRAFT_PQ, activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.DRAFT_PQ, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.DRAFT_PQ, amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.DRAFT_PQ, activityRefNumber: referenceNumber, }, }); return created; }); } }