Files
MinglarBackendNestJS/src/modules/host/services/host.service.ts

1466 lines
44 KiB
TypeScript
Raw Normal View History

2025-11-10 15:05:01 +05:30
// 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';
2025-11-12 16:03:57 +05:30
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';
2025-11-21 14:53:53 +05:30
import { getPresignedUrl } from '@/common/middlewares/aws/getPreSignedUrl';
import config from '@/config/config';
type HostCompanyDetailsInput = z.infer<typeof hostCompanyDetailsSchema>;
// 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')}`;;
}
2025-11-10 15:05:01 +05:30
function round2(value: number) {
2025-12-02 20:09:42 +05:30
return Math.round(value);
}
const bucket = config.aws.bucketName;
2025-11-10 15:05:01 +05:30
@Injectable()
export class HostService {
constructor(private prisma: PrismaService) { }
2025-11-10 15:05:01 +05:30
async createHost(data: CreateHostDto) {
return this.prisma.user.create({ data });
}
async getAllHosts() {
return this.prisma.user.findMany({ where: { roleXid: 3 } });
}
2025-11-14 14:08:47 +05:30
async getHostIdByUserXid(user_xid: number) {
const host = await this.prisma.hostHeader.findFirst({
where: { userXid: user_xid },
2025-11-14 15:15:13 +05:30
select: { id: true, companyName: true, countryXid: true, stepper: true },
2025-11-14 14:08:47 +05:30
});
return host;
}
2025-11-10 15:05:01 +05:30
async getHostById(id: number) {
2025-11-14 15:04:01 +05:30
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
}
}
}
},
2025-11-14 15:04:01 +05:30
HostBankDetails: true,
2025-11-24 23:19:18 +05:30
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,
}
},
2025-11-24 23:19:18 +05:30
countries: true,
currencies: true,
states: true,
cities: true,
},
});
2025-11-14 15:04:01 +05:30
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;
2025-11-12 16:03:57 +05:30
}
2025-11-21 14:53:53 +05:30
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]
2025-11-21 14:53:53 +05:30
: filePath;
(doc as any).presignedUrl = await getPresignedUrl(bucket, key);
}
}
}
2025-12-02 18:08:39 +05:30
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);
}
2025-11-21 14:53:53 +05:30
2025-12-02 18:08:39 +05:30
if (host?.logoPath) {
const key = host.logoPath.startsWith('http')
? host.logoPath.split('.com/')[1]
: host.logoPath;
host.logoPath = await getPresignedUrl(bucket, key);
}
2025-12-02 18:08:39 +05:30
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);
}
}
}
}
2025-11-10 15:05:01 +05:30
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 } });
}
2025-11-12 16:03:57 +05:30
async getHostByEmail(email: string): Promise<User> {
return this.prisma.user.findUnique({ where: { emailAddress: email } });
}
async verifyHostOtp(email: string, otp: string): Promise<boolean> {
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,
);
2025-11-12 16:03:57 +05:30
if (!matchPassword) {
throw new ApiError(401, 'Invalid credentials');
}
return existingUser;
}
async createMinglarUser(email: string) {
2025-11-12 16:03:57 +05:30
const newUser = await this.prisma.user.create({
data: { emailAddress: email, roleXid: ROLE.HOST },
2025-11-12 16:03:57 +05:30
});
return newUser;
}
async createPassword(user_xid: number, password: string): Promise<boolean> {
// 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.',
);
2025-11-12 16:03:57 +05:30
}
// 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,
},
2025-11-12 16:03:57 +05:30
});
return true;
}
2025-11-12 19:59:54 +05:30
async getBankBranchById(bankBranchXid: number) {
return await this.prisma.bankBranches.findUnique({
where: { id: bankBranchXid },
select: {
id: true,
ifscCode: true,
bankXid: true,
},
});
}
2025-11-21 13:31:41 +05:30
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');
}
2025-11-14 14:08:47 +05:30
2025-11-21 13:31:41 +05:30
await tx.hostHeader.update({
where: { id: data.hostXid },
data: {
stepper: STEPPER.BANK_DETAILS_UPDATED,
currencyXid: data.currencyXid
},
});
});
2025-11-21 13:31:41 +05:30
}
2025-11-14 14:08:47 +05:30
async getAllHostActivity(search?: string, user_xid?: number, paginationOptions?: { page: number; limit: number; skip: number }) {
const hostDetails = await this.prisma.hostHeader.findFirst({
where: { userXid: user_xid, isActive: true }
})
const whereClause = {
isActive: true,
hostXid: hostDetails.id,
};
const [hostAllActivities, totalCount] = await Promise.all([
this.prisma.activities.findMany({
where: whereClause,
include: {
ActivitiesMedia: true,
ActivityAmDetails: {
select: {
accountManager: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
emailAddress: true,
roleXid: true,
},
},
},
},
activityType: true,
},
skip: paginationOptions?.skip || 0,
take: paginationOptions?.limit || 10,
orderBy: { id: 'desc' },
}),
this.prisma.activities.count({ where: whereClause }),
]);
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,
};
}
}
const { paginationService } = require('@/common/utils/pagination/pagination.service');
return paginationService.createPaginatedResponse(
hostAllActivities,
totalCount,
paginationOptions || { page: 1, limit: 10, skip: 0 }
);
2025-11-21 13:31:41 +05:30
}
2025-11-14 14:08:47 +05:30
2025-11-21 13:31:41 +05:30
async acceptMinglarAgreement(user_xid: number) {
const hostDetails = await this.prisma.hostHeader.findFirst({
where: { userXid: user_xid },
select: {
id: true,
userXid: true,
},
});
2025-11-21 13:31:41 +05:30
await this.prisma.hostHeader.update({
where: { id: hostDetails.id },
data: {
stepper: STEPPER.AGREEMENT_ACCEPTED,
isApproved: true,
agreementAccepted: true,
},
});
2025-11-12 19:59:54 +05:30
}
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 },
2025-11-24 19:19:02 +05:30
select: {
pqqQuestionXid: true,
pqqAnswerXid: true,
pqqQuestions: {
select: {
pqqSubCategoryXid: true,
pqqSubCategories: {
select: {
categoryXid: true,
},
},
},
},
2025-11-24 19:19:02 +05:30
},
orderBy: { id: 'desc' },
});
}
2025-11-17 19:05:26 +05:30
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,
},
2025-11-17 19:05:26 +05:30
});
return createdHost;
}
2025-11-14 14:08:47 +05:30
// -------------------------------------------------------
// 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,
2025-11-17 19:05:26 +05:30
logoPath: companyData.logoPath || null,
isSubsidairy: companyData.isSubsidairy,
registrationNumber: companyData.registrationNumber,
panNumber: companyData.panNumber,
2025-11-17 19:05:26 +05:30
gstNumber: companyData.gstNumber || null,
formationDate: companyData.formationDate
? new Date(companyData.formationDate as any)
: null,
companyTypes: companyData.companyTypeXid
? { connect: { id: companyData.companyTypeXid } }
: undefined,
2025-11-17 19:05:26 +05:30
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,
},
});
2025-11-14 14:08:47 +05:30
// documents UPSERT
2025-11-17 19:05:26 +05:30
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,
},
});
}
}
}
2025-11-17 19:05:26 +05:30
// parent logic untouched
if (companyData.isSubsidairy) {
const parentRecords = existingHostCompany.hostParent;
const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords;
2025-11-17 19:05:26 +05:30
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,
},
});
}
}
2025-11-17 19:05:26 +05:30
}
}
} else {
const previousParent = existingHostCompany.hostParent;
let prevParentId = null;
2025-11-14 14:08:47 +05:30
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;
});
}
2025-11-14 14:08:47 +05:30
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
// }
// });
// }
2025-11-24 23:19:18 +05:30
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,
2025-11-24 23:19:18 +05:30
},
});
2025-11-24 23:19:18 +05:30
if (!answers.length) {
return {
overallPercentage: 0,
categoryWise: {},
};
}
2025-11-24 23:19:18 +05:30
// Prepare accumulators
let totalUserPoints = 0;
let totalMaxPoints = 0;
// For category-wise scoring
const categories: Record<
number,
{
categoryId: number;
categoryName: string;
userPoints: number;
maxPoints: number;
}
> = {};
2025-11-24 23:19:18 +05:30
for (const item of answers) {
const question = item.pqqQuestions;
const answer = item.pqqAnswers;
2025-11-24 23:19:18 +05:30
const maxPoints = question.maxPoints;
const userPoints = answer.answerPoints;
2025-11-24 23:19:18 +05:30
totalUserPoints += userPoints;
totalMaxPoints += maxPoints;
2025-11-24 23:19:18 +05:30
// 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;
2025-11-24 23:19:18 +05:30
}
// Overall percent
const overallPercentage =
totalMaxPoints > 0 ? round2((totalUserPoints / totalMaxPoints) * 100) : 0;
2025-11-24 23:19:18 +05:30
// ---------- 🔥 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<string, number> = {};
for (const c of topTwo) {
categoryWise[c.categoryName] =
c.maxPoints > 0 ? round2((c.userPoints / c.maxPoints) * 100) : 0;
}
2025-11-24 23:19:18 +05:30
await this.prisma.activities.update({
where: {
id: activityXid
},
data: {
totalScore: round2(overallPercentage),
sustainabilityScore: round2(categoryWise.Sustainability),
safetyScore: round2(categoryWise.Safety),
}
})
2025-11-24 23:19:18 +05:30
// Return final score object
return {
overallPercentage,
categoryWise,
};
});
2025-11-24 23:19:18 +05:30
}
async createHeader(
2025-11-19 16:55:54 +05:30
activityXid: number,
pqqQuestionXid: number,
pqqAnswerXid: number,
comments?: string | null,
2025-11-19 16:55:54 +05:30
) {
return await this.prisma.activityPQQheader.create({
data: {
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments: comments || null, // Handle null comments
},
2025-11-19 16:55:54 +05:30
});
}
2025-11-19 16:55:54 +05:30
async findHeaderByCompositeKey(
activityXid: number,
pqqQuestionXid: number,
) {
return await this.prisma.activityPQQheader.findFirst({
where: {
activityXid,
pqqQuestionXid,
},
2025-11-19 16:55:54 +05:30
});
}
2025-11-19 16:55:54 +05:30
async updateHeader(headerId: number, pqqAnswerXid: number, comments?: string | null) {
2025-11-19 16:55:54 +05:30
return await this.prisma.activityPQQheader.update({
where: {
id: headerId,
},
2025-11-19 16:55:54 +05:30
data: {
comments: comments || null, // Handle null comments
pqqAnswerXid: pqqAnswerXid,
updatedAt: new Date(),
},
2025-11-19 16:55:54 +05:30
});
}
async addSupportingFile(headerId: number, mimeType: string, fileUrl: string) {
2025-11-19 16:55:54 +05:30
return await this.prisma.activityPQQSupportings.create({
data: {
activityPqqHeaderXid: headerId,
mediaType: mimeType,
mediaFileName: fileUrl,
},
2025-11-19 16:55:54 +05:30
});
}
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,
activityDisplayStatus: true,
activityInternalStatus: true,
amInternalStatus: true,
amDisplayStatus: true
}
})
if (!activity) {
throw new ApiError(404, "Activity not found")
}
if (activity.activityInternalStatus == ACTIVITY_INTERNAL_STATUS.PQ_TO_UPDATE) {
return 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.REVISED
}
})
} else {
return 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;
});
}
2025-11-10 15:05:01 +05:30
}