// 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 { 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 } from '@/common/utils/constants/common.constant'; type HostCompanyDetailsInput = z.infer; // Document input after S3 upload (with S3 URL as filePath) interface HostDocumentInput { documentTypeXid: number; documentName: string; filePath: string; // S3 URL } @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: true, HostBankDetails: true, HostDocuments: true, HostSuggestion: true, HostTrack: true, } }); if (!host) { throw new ApiError(404, 'Host record not found.'); } 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 }, }); return true; } async addPaymentDetails(data: AddPaymentDetailsDTO): Promise { const addedPaymentDetails = await this.prisma.hostBankDetails.create({ data, }); if (!addedPaymentDetails) { throw new ApiError(400, 'Failed to add payment details'); } return addedPaymentDetails; } async addOrUpdateCompanyDetails( user_xid: number, companyData: HostCompanyDetailsInput, documents: HostDocumentInput[], parentCompanyData?: any | null, parentDocuments?: HostDocumentInput[] ) { 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 }, }); // CREATE if (!existingHostCompany) { // Optionally check unique registration number const existingByReg = await tx.hostHeader.findFirst({ where: { registrationNumber: companyData.registrationNumber }, }); if (existingByReg) throw new ApiError(400, 'Company already exists with this registration number'); const refNumber = await this.generateHostRefNumber(tx); const createdHost = await tx.hostHeader.create({ data: { userXid: user_xid, companyName: companyData.companyName, hostRefNumber: refNumber, address1: companyData.address1, address2: companyData.address2, cityXid: companyData.cityXid, stateXid: companyData.stateXid, countryXid: companyData.countryXid, pinCode: companyData.pinCode, logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, registrationNumber: companyData.registrationNumber, panNumber: companyData.panNumber, gstNumber: companyData.gstNumber || null, formationDate: new Date(companyData.formationDate), companyType: companyData.companyType, websiteUrl: companyData.websiteUrl || null, instagramUrl: companyData.instagramUrl || null, facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, currencyXid: companyData.currencyXid, stepper: STEPPER.UNDER_REVIEW, hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED, hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW, adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW, adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW, }, }); // Create 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 company and its docs (if present) if (companyData.isSubsidairy && parentCompanyData) { const createdParent = await tx.hostParent.create({ data: { hostXid: createdHost.id, companyName: parentCompanyData.companyName, address1: parentCompanyData.address1, address2: parentCompanyData.address2 || null, cityXid: parentCompanyData.cityXid, stateXid: parentCompanyData.stateXid, countryXid: parentCompanyData.countryXid, pinCode: parentCompanyData.pinCode, logoPath: parentCompanyData.logoPath || null, isSubsidairy: false, registrationNumber: parentCompanyData.registrationNumber, panNumber: parentCompanyData.panNumber, gstNumber: parentCompanyData.gstNumber || null, formationDate: new Date(parentCompanyData.formationDate), companyType: parentCompanyData.companyType, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, facebookUrl: parentCompanyData.facebookUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null, twitterUrl: parentCompanyData.twitterUrl || null, }, }); 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 }); } } return createdHost; } // UPDATE existing // Prevent changing hostRefNumber const updatedHost = await tx.hostHeader.update({ where: { id: existingHostCompany.id }, data: { companyName: companyData.companyName, address1: companyData.address1, address2: companyData.address2, cityXid: companyData.cityXid, stateXid: companyData.stateXid, countryXid: companyData.countryXid, pinCode: companyData.pinCode, logoPath: companyData.logoPath || null, isSubsidairy: companyData.isSubsidairy, registrationNumber: companyData.registrationNumber, panNumber: companyData.panNumber, gstNumber: companyData.gstNumber || null, formationDate: new Date(companyData.formationDate), companyType: companyData.companyType, websiteUrl: companyData.websiteUrl || null, instagramUrl: companyData.instagramUrl || null, facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, currencyXid: companyData.currencyXid, // hostRefNumber: DO NOT UPDATE }, }); // Replace host documents (delete old, insert new) await tx.hostDocuments.deleteMany({ where: { hostXid: updatedHost.id } }); if (documents?.length) { const docsData = documents.map((doc) => ({ hostXid: updatedHost.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, })); await tx.hostDocuments.createMany({ data: docsData }); } // Parent company create/update and replace parent docs if (companyData.isSubsidairy) { // existingHostCompany.hostParent may be array or single object depending on Prisma schema let parentRecord = (existingHostCompany as any).hostParent; if (Array.isArray(parentRecord)) parentRecord = parentRecord[0]; if (!parentRecord) { // create const createdParent = await tx.hostParent.create({ data: { hostXid: updatedHost.id, companyName: parentCompanyData.companyName, address1: parentCompanyData.address1, address2: parentCompanyData.address2 || null, cityXid: parentCompanyData.cityXid, stateXid: parentCompanyData.stateXid, countryXid: parentCompanyData.countryXid, pinCode: parentCompanyData.pinCode, logoPath: parentCompanyData.logoPath || null, isSubsidairy: false, registrationNumber: parentCompanyData.registrationNumber, panNumber: parentCompanyData.panNumber, gstNumber: parentCompanyData.gstNumber || null, formationDate: new Date(parentCompanyData.formationDate), companyType: parentCompanyData.companyType, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, facebookUrl: parentCompanyData.facebookUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null, twitterUrl: parentCompanyData.twitterUrl || null, }, }); 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 }); } } else { // update await tx.hostParent.update({ where: { id: parentRecord.id }, data: { companyName: parentCompanyData.companyName, address1: parentCompanyData.address1, address2: parentCompanyData.address2 || null, cityXid: parentCompanyData.cityXid, stateXid: parentCompanyData.stateXid, countryXid: parentCompanyData.countryXid, pinCode: parentCompanyData.pinCode, logoPath: parentCompanyData.logoPath || null, registrationNumber: parentCompanyData.registrationNumber, panNumber: parentCompanyData.panNumber, gstNumber: parentCompanyData.gstNumber || null, formationDate: new Date(parentCompanyData.formationDate), companyType: parentCompanyData.companyType, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, facebookUrl: parentCompanyData.facebookUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null, twitterUrl: parentCompanyData.twitterUrl || null, }, }); // replace parent docs await tx.hostParenetDocuments.deleteMany({ where: { hostParentXid: parentRecord.id } }); if (parentDocuments?.length) { const parentDocsData = parentDocuments.map((doc) => ({ hostParentXid: parentRecord.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, })); await tx.hostParenetDocuments.createMany({ data: parentDocsData }); } } } else { // If previously had a parent and now isSubsidairy=false -> optionally delete parent and its docs const previousParent = (existingHostCompany as any).hostParent; let prevParentId = null; if (Array.isArray(previousParent) && previousParent.length) prevParentId = previousParent[0].id; else if (previousParent && previousParent.id) prevParentId = previousParent.id; if (prevParentId) { await tx.hostParenetDocuments.deleteMany({ where: { hostParentXid: prevParentId } }); await tx.hostParent.delete({ where: { id: prevParentId } }); } } return updatedHost; }); } async generateHostRefNumber(tx: any) { const lastHost = await tx.hostHeader.findFirst({ orderBy: { id: 'desc', }, select: { id: true, } }); const nextId = lastHost ? lastHost.id + 1 : 1; return `HOSTREFNO-${String(nextId).padStart(6, '0')}`; } 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 } }); } }