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

3130 lines
94 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/modules/host/services/host.service.ts
import { Injectable } from '@nestjs/common';
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 { PrismaClient } 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 {
ACTIVITY_TRACK_STATUS,
ACTIVITY_TRACK_TYPE,
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';
function sanitizeDocumentName(name?: string) {
if (!name) return null;
return name
.replace(/[^a-zA-Z0-9 _-]/g, '') // remove / .
.substring(0, 100);
}
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')}`;
}
function round2(value: number) {
return Math.round(value);
}
function computeBasePriceAndTaxes(
sellPrice: number,
taxes: Array<{ id: number; taxPer: number }>,
) {
if (!taxes?.length) {
return {
basePrice: round2(sellPrice),
taxDetails: [] as Array<{
taxXid: number;
taxPer: number;
taxAmount: number;
}>,
};
}
const totalTaxPer = taxes.reduce(
(sum, t) => sum + (Number(t.taxPer) || 0),
0,
);
const denominator = 1 + totalTaxPer / 100;
const basePrice =
denominator > 0 ? round2(sellPrice / denominator) : round2(sellPrice);
const taxDetails = taxes.map((t) => ({
taxXid: t.id,
taxPer: t.taxPer,
taxAmount: round2(basePrice * (t.taxPer / 100)),
}));
return { basePrice, taxDetails };
}
const bucket = config.aws.bucketName;
@Injectable()
export class HostService {
constructor(private prisma: PrismaClient) { }
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, stepper: true },
});
const user = await this.prisma.user.findUnique({
where: { id: user_xid },
select: { id: true, emailAddress: true },
});
return { host, user };
}
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<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, isActive: true },
select: {
id: true,
roleXid: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
userPassword: true,
userStatus: true,
},
});
if (!existingUser) {
throw new ApiError(404, 'User not found');
}
if (existingUser.userStatus == USER_STATUS.REJECTED) {
throw new ApiError(
403,
'You are not allowed to login. Please contact minglar admin.',
);
}
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<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.',
);
}
// 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 getBankBranchById(bankBranchXid: number) {
return await this.prisma.bankBranches.findUnique({
where: { id: bankBranchXid },
select: {
id: true,
ifscCode: true,
bankXid: true,
},
});
}
async addPaymentDetails(data: AddPaymentDetailsDTO) {
return await this.prisma.$transaction(async (tx) => {
const existingAccount = await tx.hostBankDetails.findFirst({
where: {
accountNumber: data.accountNumber,
isActive: true,
},
});
if (existingAccount) {
throw new ApiError(
400,
'Host account with this account number already exists.',
);
}
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,
paginationOptions?: { page: number; limit: number; skip: number },
) {
const hostDetails = await this.prisma.hostHeader.findFirst({
where: { userXid: user_xid, isActive: true },
});
const whereClause: any = {
isActive: true,
hostXid: hostDetails.id,
};
if (!hostDetails) {
return {
data: [],
total: 0,
page: paginationOptions?.page || 1,
limit: paginationOptions?.limit || 10,
};
}
// 🔍 SEARCH (fixed)
if (search?.trim()) {
const term = search.trim();
whereClause.OR = [
{ activityRefNumber: { contains: term, mode: 'insensitive' } },
{ activityTitle: { contains: term, mode: 'insensitive' } },
{
activityType: {
activityTypeName: { contains: term, mode: 'insensitive' },
},
},
];
}
const [hostAllActivities, totalCount] = await Promise.all([
this.prisma.activities.findMany({
where: whereClause,
select: {
id: true,
activityRefNumber: true,
activityTitle: true,
totalScore: true,
activityInternalStatus: true,
activityDisplayStatus: true,
amInternalStatus: true,
amDisplayStatus: true,
createdAt: true,
checkInAddress: true,
frequency: {
select: {
id: true,
frequencyName: true,
},
},
ActivityAmDetails: {
select: {
accountManager: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
emailAddress: true,
roleXid: true,
},
},
},
},
activityType: {
select: {
id: true,
activityTypeName: true,
interests: {
select: {
id: true,
interestName: true,
},
},
},
},
},
skip: paginationOptions?.skip || 0,
take: paginationOptions?.limit || 10,
orderBy: { id: 'desc' },
}),
this.prisma.activities.count({ where: whereClause }),
]);
for (const activity of hostAllActivities) {
/** 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 },
);
}
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,
pqqAnswerXid: {
not: null,
},
},
select: {
pqqQuestionXid: true,
pqqAnswerXid: true,
pqqQuestions: {
select: {
pqqSubCategoryXid: true,
pqqSubCategories: {
select: {
categoryXid: true,
},
},
},
},
},
orderBy: { id: 'desc' },
});
}
async getParentDocumentsByHostId(userId: number) {
const host = await this.prisma.hostHeader.findFirst({
where: { userXid: userId },
select: { id: true },
});
if (!host) return [];
const parents = await this.prisma.hostParent.findMany({
where: { hostXid: host.id },
include: { HostParenetDocuments: true },
});
return parents.flatMap((p) => p.HostParenetDocuments);
}
async deleteExistingParentRecords(userId: number) {
const host = await this.prisma.hostHeader.findFirst({
where: { userXid: userId },
select: { id: true },
});
if (!host) return;
const parents = await this.prisma.hostParent.findMany({
where: { hostXid: host.id },
select: { id: true },
});
if (!parents.length) return;
const parentIds = parents.map((p) => p.id);
// 1⃣ Delete documents first
await this.prisma.hostParenetDocuments.deleteMany({
where: { hostParentXid: { in: parentIds } },
});
// 2⃣ Then delete parent records
await this.prisma.hostParent.deleteMany({
where: { id: { in: parentIds } },
});
}
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 },
});
console.log(existingHostCompany, '-: Existing hai');
let existingParentCompany;
if (existingHostCompany) {
existingParentCompany = await tx.hostParent.findFirst({
where: { hostXid: existingHostCompany.id },
select: {
id: true,
logoPath: 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) {
console.log('First time direct final submit.');
const existingByPan = await tx.hostHeader.findFirst({
where: { panNumber: companyData.panNumber },
});
if (existingByPan)
throw new ApiError(
400,
'Company already exists with this pan/bin number',
);
}
console.log('First Time Aaya hai');
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,
referencedBy: companyData.referencedBy || null,
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: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
}));
await tx.hostDocuments.createMany({ data: docsData });
}
// parent create
if (companyData.isSubsidairy && parentCompanyData) {
console.log('Parent ke saath aaya hai first time.');
const createdParent = await tx.hostParent.create({
data: {
host: { connect: { id: createdHost.id } },
companyName: parentCompanyData.companyName || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
// Safely handle city connection - only connect if valid ID exists
cities:
parentCompanyData?.cityXid &&
!isNaN(Number(parentCompanyData.cityXid))
? { connect: { id: Number(parentCompanyData.cityXid) } }
: undefined,
states:
parentCompanyData?.stateXid &&
!isNaN(Number(parentCompanyData.stateXid))
? { connect: { id: Number(parentCompanyData.stateXid) } }
: undefined,
countries:
parentCompanyData?.countryXid &&
!isNaN(Number(parentCompanyData.countryXid))
? { connect: { id: Number(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: sanitizeDocumentName(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,
// Safely handle city connection - only connect if valid ID exists
cities:
companyData.cityXid && !isNaN(Number(companyData.cityXid))
? { connect: { id: Number(companyData.cityXid) } }
: undefined, // Don't change if not provided
// Same for state
states:
companyData.stateXid && !isNaN(Number(companyData.stateXid))
? { connect: { id: Number(companyData.stateXid) } }
: undefined,
// Same for country
countries:
companyData.countryXid && !isNaN(Number(companyData.countryXid))
? { connect: { id: Number(companyData.countryXid) } }
: undefined,
pinCode: companyData.pinCode,
logoPath: companyData.logoPath || existingHostCompany.logoPath,
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,
referencedBy: companyData.referencedBy || null,
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) {
if (!doc.filePath) continue;
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:
sanitizeDocumentName(doc.documentName) ||
existingDoc.documentName,
},
});
} else {
await tx.hostDocuments.create({
data: {
hostXid: updatedHost.id,
documentTypeXid: doc.documentTypeXid,
documentName: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
},
});
}
}
}
// parent logic untouched
if (companyData.isSubsidairy) {
const parentRecords = existingHostCompany.hostParent;
const parentRecord = Array.isArray(parentRecords)
? parentRecords[0]
: parentRecords;
console.log('Yaha aaya update in the apretn me');
if (!parentRecord) {
console.log('Parent record nahi mila to create kar raha hai.');
const createdParent = await tx.hostParent.create({
data: {
host: { connect: { id: updatedHost.id } },
companyName: parentCompanyData.companyName || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
cities:
parentCompanyData?.cityXid &&
!isNaN(Number(parentCompanyData.cityXid))
? { connect: { id: Number(parentCompanyData.cityXid) } }
: undefined,
states:
parentCompanyData?.stateXid &&
!isNaN(Number(parentCompanyData.stateXid))
? { connect: { id: Number(parentCompanyData.stateXid) } }
: undefined,
countries:
parentCompanyData?.countryXid &&
!isNaN(Number(parentCompanyData.countryXid))
? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined,
pinCode: parentCompanyData.pinCode || null,
logoPath:
parentCompanyData?.logoPath ||
existingParentCompany?.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: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
},
});
}
}
} else {
await tx.hostParent.update({
where: { id: parentRecord.id },
data: {
companyName: parentCompanyData.companyName || null,
address1: parentCompanyData.address1 || null,
address2: parentCompanyData.address2 || null,
cities:
parentCompanyData?.cityXid &&
!isNaN(Number(parentCompanyData.cityXid))
? { connect: { id: Number(parentCompanyData.cityXid) } }
: undefined,
states:
parentCompanyData?.stateXid &&
!isNaN(Number(parentCompanyData.stateXid))
? { connect: { id: Number(parentCompanyData.stateXid) } }
: undefined,
countries:
parentCompanyData?.countryXid &&
!isNaN(Number(parentCompanyData.countryXid))
? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined,
pinCode: parentCompanyData.pinCode || null,
logoPath:
parentCompanyData?.logoPath ||
existingParentCompany?.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:
sanitizeDocumentName(doc.documentName) ||
existingParentDoc.documentName,
},
});
} else {
await tx.hostParenetDocuments.create({
data: {
hostParentXid: parentRecord.id,
documentTypeXid: doc.documentTypeXid,
documentName: sanitizeDocumentName(doc.documentName),
filePath: doc.filePath,
},
});
}
}
}
}
} else {
console.log('Last ke else block me aaya hai');
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<string, number> = {};
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, user_xid: number) {
return await this.prisma.$transaction(async (tx) => {
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.$transaction(async (tx) => {
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,
},
});
await tx.activityTrack.create({
data: {
activityXid: activity_xid,
trackType: ACTIVITY_TRACK_TYPE.PQ,
trackStatus: ACTIVITY_TRACK_STATUS.PQ_SUBMITTED,
updatedByXid: user_xid,
updatedByRole: ROLE_NAME.HOST,
updatedOn: new Date(),
},
});
});
} else {
return await this.prisma.$transaction(async (tx) => {
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,
},
});
await tx.activityTrack.create({
data: {
activityXid: activity_xid,
trackType: ACTIVITY_TRACK_TYPE.PQ,
trackStatus: ACTIVITY_TRACK_STATUS.PQ_SUBMITTED,
updatedByXid: user_xid,
updatedByRole: ROLE_NAME.HOST,
updatedOn: new Date(),
},
});
});
}
});
}
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(activity_xid: number) {
return await this.prisma.activityPQQheader.findMany({
where: { isActive: true, activityXid: activity_xid },
select: {
id: true,
activityXid: true,
pqqQuestionXid: true,
pqqAnswerXid: true,
comments: true,
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 getAllDetailsOfActivityAndVenue(activityXid: number) {
const activity = await this.prisma.activities.findFirst({
where: { id: activityXid, isActive: true },
select: {
id: true,
activityTitle: true,
activityDescription: true,
activityDisplayStatus: true,
activityInternalStatus: true,
activityRefNumber: true,
checkInAddress: true,
checkInLat: true,
checkInLong: true,
checkOutAddress: true,
checkOutLat: true,
checkOutLong: true,
pickUpDropAvailable: true,
pickUpDropIsChargeable: true,
activityDurationMins: true,
activityType: {
select: {
id: true,
activityTypeName: true,
}
},
ActivitiesMedia: {
where: {
isActive: true
},
select: {
id: true,
mediaType: true,
mediaFileName: true,
}
},
ActivityVenues: {
where: {
isActive: true
},
select: {
id: true,
venueName: true,
venueCapacity: true,
availableSeats: true,
isMinPeopleReqMandatory: true,
minPeopleRequired: true,
minReqfullfilledBeforeMins: true,
venueDescription: true,
ActivityVenueArtifacts: {
select: {
id: true,
mediaType: true,
mediaFileName: true
}
}
}
},
ActivityPickUpDetails: {
where: {
isActive: true,
activityPickUpTransport: {
isActive: true,
transportMode: {
isActive: true
}
}
},
select: {
id: true,
isPickUp: true,
locationAddress: true,
locationLat: true,
locationLong: true,
transportTotalPrice: true,
transportBasePrice: true,
activityPickUpTransport: {
select: {
id: true,
isTransportModeChargeable: true,
transportMode: {
select: {
transportModeName: true,
transportModeIcon: true
}
}
}
}
}
},
foodAvailable: true,
foodIsChargeable: true,
activityFoodTypes: {
where: { isActive: true },
select: {
id: true,
foodType: {
select: {
id: true,
foodTypeName: true
}
},
}
},
activityCuisines: {
where: {
isActive: true
},
select: {
id: true,
foodCuisine: {
select: {
id: true,
cuisineName: true
}
}
}
},
alcoholAvailable: true,
trainerAvailable: true,
trainerIsChargeable: true,
ActivityTrainers: {
where: {
isActive: true
},
select: {
id: true,
totalAmount: true
}
},
ActivityNavigationModes: {
where: {
isActive: true
},
select: {
id: true,
isInActivityChargeable: true,
navigationModesTotalPrice: true,
navigationMode: {
select: {
id: true,
navigationModeName: true,
navigationModeIcon: true
}
}
}
},
equipmentAvailable: true,
equipmentIsChargeable: true,
ActivityEquipments: {
where: {
isActive: true
},
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentTotalPrice: true,
}
},
ActivityOtherDetails: {
where: {
isActive: true
},
select: {
id: true,
exclusiveNotes: true,
dosNotes: true,
dontsNotes: true,
tipsNotes: true,
termsAndCondition: true,
}
},
energyLevel: {
where: {
isActive: true
},
select: {
id: true,
energyLevelName: true,
energyIcon: true,
energyColor: true
}
},
ActivityEligibility: {
where: {
isActive: true
},
select: {
id: true,
isAgeRestriction: true,
ageRestriction: {
select: {
id: true,
ageRestrictionName: true,
minAge: true,
maxAge: true,
}
},
isWeightRestriction: true,
weightRestrictionName: true,
weightEntered: true,
weightIn: true,
minWeight: true,
maxWeight: true,
isHeightRestriction: true,
heightRestrictionName: true,
heightEntered: true,
heightIn: true,
minHeight: true,
maxHeight: true
}
},
ActivityAllowedEntry: {
where: {
isActive: true
},
select: {
id: true,
allowedEntryType: {
select: {
id: true,
allowedEntryTypeName: true
}
}
}
},
ActivityAmenities: {
where: {
isActive: true
},
select: {
id: true,
amenities: {
select: {
id: true,
amenitiesName: true
}
}
}
},
cancellationAvailable: true,
cancellationAllowedBeforeMins: true
// accountManager: {
// select: {
// id: true,
// firstName: true,
// lastName: true,
// emailAddress: true,
// mobileNumber: true,
// }
// },
// host: {
// select: {
// id: true,
// companyName: true,
// stepper: true,
// adminStatusDisplay: true,
// adminStatusInternal: true,
// }
// }
}
})
if (!activity) {
throw new ApiError(404, 'Activity not found');
}
if (Array.isArray(activity.ActivitiesMedia)) {
for (const media of activity.ActivitiesMedia) {
if (!media?.mediaFileName) continue;
const filePath = media.mediaFileName;
// ✅ Robust S3 key extraction
const key = filePath.startsWith('http')
? new URL(filePath).pathname.replace(/^\/+/, '')
: filePath;
(media as any).presignedUrl = await getPresignedUrl(bucket, key);
}
}
if (Array.isArray(activity.ActivityVenues)) {
for (const venue of activity.ActivityVenues) {
if (!Array.isArray(venue.ActivityVenueArtifacts)) continue;
for (const artifact of venue.ActivityVenueArtifacts) {
if (!artifact?.mediaFileName) continue;
const filePath = artifact.mediaFileName;
// ✅ Robust S3 key extraction
const key = filePath.startsWith('http')
? new URL(filePath).pathname.replace(/^\/+/, '')
: filePath;
(artifact as any).preSignedURL = await getPresignedUrl(bucket, key);
}
}
}
return activity;
}
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;
});
}
async createActivityAndAllQuestionsEntry(
userId: number,
activityTypeXid: number,
frequenciesXid: number,
) {
return await this.prisma.$transaction(async (tx) => {
const host = await tx.hostHeader.findFirst({
where: { userXid: userId, isActive: true },
});
if (!host) throw new ApiError(404, 'Host not found for the user');
const activityType = await tx.activityTypes.findUnique({
where: { id: activityTypeXid },
});
if (!activityType) throw new ApiError(404, 'Activity type not found');
if (frequenciesXid) {
const freq = await tx.frequencies.findUnique({
where: { id: frequenciesXid },
});
if (!freq) throw new ApiError(404, 'Frequency not found');
}
const referenceNumber = await generateActivityRefNumber(tx);
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,
},
});
const questions = await tx.pQQCategories.findMany({
where: { isActive: true },
select: {
id: true,
categoryName: true,
displayOrder: true,
pqqsubCategories: {
where: { isActive: true },
select: {
id: true,
subCategoryName: true,
displayOrder: true,
questions: {
where: { isActive: true },
select: {
id: true,
questionName: true,
maxPoints: true,
displayOrder: true,
},
orderBy: { displayOrder: 'asc' },
},
},
orderBy: { displayOrder: 'asc' },
},
},
orderBy: { displayOrder: 'asc' },
});
// FLATTEN questions
const allQuestions: number[] = [];
for (const cat of questions) {
for (const sub of cat.pqqsubCategories) {
for (const q of sub.questions) {
allQuestions.push(q.id);
}
}
}
await tx.activityPQQheader.createMany({
data: allQuestions.map((id) => ({
activityXid: created.id,
pqqQuestionXid: id,
pqqAnswerXid: null,
})),
});
const pqqHeaderData = await tx.activityPQQheader.findMany({
where: {
activityXid: created.id,
isActive: true,
},
select: {
comments: true,
pqqAnswerXid: true,
pqqQuestions: {
select: {
id: true,
questionName: true,
maxPoints: true,
displayOrder: true,
pqqSubCategories: {
select: {
id: true,
subCategoryName: true,
displayOrder: true,
category: {
select: {
id: true,
categoryName: true,
displayOrder: true,
},
},
},
},
// 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION
PQQAnswers: {
where: { isActive: true },
select: {
id: true,
answerName: true,
answerPoints: true,
displayOrder: true,
},
orderBy: { displayOrder: 'asc' },
},
},
},
ActivityPQQSuggestions: {
where: { isActive: true },
select: {
id: true,
title: true,
comments: true,
activityPqqHeaderXid: true,
},
},
ActivityPQQSupportings: {
where: { isActive: true },
select: {
id: true,
mediaType: true,
mediaFileName: true,
},
},
},
orderBy: { id: 'asc' },
});
// ---------------- GROUPING ------------------
const grouped: any = {};
for (const item of pqqHeaderData) {
const q = item.pqqQuestions;
const sub = q.pqqSubCategories;
const cat = sub.category;
// 1⃣ Category level
if (!grouped[cat.id]) {
grouped[cat.id] = {
id: cat.id,
categoryName: cat.categoryName,
displayOrder: cat.displayOrder,
pqqsubCategories: [],
};
}
const category = grouped[cat.id];
let subCat = category.pqqsubCategories.find(
(s: any) => s.id === sub.id,
);
if (!subCat) {
subCat = {
id: sub.id,
subCategoryName: sub.subCategoryName,
displayOrder: sub.displayOrder,
questions: [],
};
category.pqqsubCategories.push(subCat);
}
subCat.questions.push({
id: q.id,
questionName: q.questionName,
maxPoints: q.maxPoints,
comments: item.comments || null,
displayOrder: q.displayOrder,
allAnswerOptions: q.PQQAnswers || [],
suggestions: item.ActivityPQQSuggestions || [],
supportings: item.ActivityPQQSupportings || [],
});
}
const sortedCategories: any = Object.values(grouped).sort(
(a: any, b: any) => a.displayOrder - b.displayOrder,
);
for (const cat of sortedCategories) {
cat.pqqsubCategories.sort(
(a: any, b: any) => a.displayOrder - b.displayOrder,
);
for (const sub of cat.pqqsubCategories) {
sub.questions.sort(
(a: any, b: any) => a.displayOrder - b.displayOrder,
);
}
}
return {
activity_xid: created.id,
sortedCategories,
};
});
}
/**
* Create a full activity with related records based on payload from the onboarding form.
* This method will create Activities + ActivityOtherDetails + ActivitiesMedia +
* ActivityVenues + ActivityPrices + ActivityFoodTypes + ActivityCuisine +
* ActivityPickUpTransport/Details + ActivityNavigationModes + ActivityEquipments +
* ActivityAmenities + ActivityEligibility
*/
async createOrUpdateActivity(
userId: number,
payload: any,
isDraft: boolean,
) {
/* =====================================================
* HELPERS
* ===================================================== */
const toBool = (v: any) =>
v === true || v === 'true' || v === 1 || v === '1';
const toNumber = (v: any) =>
v === undefined || v === null || v === '' ? undefined : Number(v);
const round2 = (v: number) => Math.round(v);
const computeBasePriceAndTaxes = (
sellPrice: number,
taxes: Array<{ id: number; taxPer: number }>,
) => {
if (!taxes.length) {
return { basePrice: round2(sellPrice), taxDetails: [] };
}
const totalTaxPer = taxes.reduce((s, t) => s + Number(t.taxPer || 0), 0);
const basePrice = round2(sellPrice / (1 + totalTaxPer / 100));
return {
basePrice,
taxDetails: taxes.map((t) => ({
taxXid: t.id,
taxPer: t.taxPer,
taxAmount: round2(basePrice * (t.taxPer / 100)),
})),
};
};
/* =====================================================
* BASIC GUARDS
* ===================================================== */
if (!payload.activityXid) {
throw new ApiError(400, 'activityXid is required');
}
/* =====================================================
* HARD NORMALIZATION (SERVICE-LEVEL)
* ===================================================== */
payload.foodAvailable = toBool(payload.foodAvailable);
payload.alcoholAvailable = toBool(payload.alcoholAvailable);
payload.trainerAvailable = toBool(payload.trainerAvailable);
payload.pickUpDropAvailable = toBool(payload.pickUpDropAvailable);
payload.inActivityAvailable = toBool(payload.inActivityAvailable);
payload.equipmentAvailable = toBool(payload.equipmentAvailable);
payload.cancellationAvailable = toBool(payload.cancellationAvailable);
payload.isInstantBooking = toBool(payload.isInstantBooking);
payload.isCheckOutSame = toBool(payload.isCheckOutSame);
payload.trainerTotalAmount = toNumber(payload.trainerTotalAmount);
if (payload.trainerAvailable) {
if (
typeof payload.trainerTotalAmount !== 'number' ||
Number.isNaN(payload.trainerTotalAmount) ||
payload.trainerTotalAmount <= 0
) {
throw new ApiError(400, 'trainerTotalAmount must be > 0');
}
} else {
delete payload.trainerTotalAmount;
}
if (payload.venues && !Array.isArray(payload.venues)) {
throw new ApiError(400, 'venues must be an array');
}
payload.venues?.forEach((v, idx) => {
v.isMinPeopleReqMandatory = toBool(v.isMinPeopleReqMandatory);
if (!v.venueName) {
throw new ApiError(400, `venues[${idx}] venueName required`);
}
if (v.isMinPeopleReqMandatory && !v.minPeopleRequired) {
throw new ApiError(400, `venues[${idx}] min people requirement missing`);
}
if (!Array.isArray(v.prices) || !v.prices.length) {
throw new ApiError(400, `venues[${idx}] must have at least one price`);
}
});
/* =====================================================
* ROOT TAX
* ===================================================== */
const taxIds = Array.isArray(payload.taxXids)
? payload.taxXids.map(Number)
: [];
const rootTaxes =
taxIds.length > 0
? await this.prisma.taxes.findMany({
where: { id: { in: taxIds }, isActive: true },
select: { id: true, taxPer: true },
})
: [];
if (taxIds.length !== rootTaxes.length) {
throw new ApiError(400, 'Invalid or inactive tax provided');
}
/* =====================================================
* TRANSACTION
* ===================================================== */
return await this.prisma.$transaction(async (tx) => {
/* --------------------------------
* 1⃣ HOST
* -------------------------------- */
const host = await tx.hostHeader.findFirst({
where: { userXid: userId, isActive: true },
});
if (!host) throw new ApiError(404, 'Host not found');
/* --------------------------------
* 2⃣ ACTIVITY
* -------------------------------- */
const existingActivity = await tx.activities.findFirst({
where: {
id: Number(payload.activityXid),
hostXid: host.id,
isActive: true,
},
});
if (!existingActivity) {
throw new ApiError(404, 'Activity not found');
}
/* --------------------------------
* 3⃣ STATUS DECISION
* -------------------------------- */
let activityInternalStatus;
let activityDisplayStatus;
let amInternalStatus;
let amDisplayStatus;
const wasRejected =
existingActivity.activityInternalStatus ===
ACTIVITY_INTERNAL_STATUS.ACTIVITY_REJECTED;
if (wasRejected) {
if (isDraft) {
activityInternalStatus = existingActivity.activityInternalStatus;
activityDisplayStatus = existingActivity.activityDisplayStatus;
amInternalStatus = existingActivity.amInternalStatus;
amDisplayStatus = existingActivity.amDisplayStatus;
} else {
activityInternalStatus = ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED;
activityDisplayStatus = ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW;
amInternalStatus = ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW;
amDisplayStatus = ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW;
}
} else {
if (isDraft) {
activityInternalStatus = ACTIVITY_INTERNAL_STATUS.ACTIVITY_DRAFT;
activityDisplayStatus = ACTIVITY_DISPLAY_STATUS.ACTIVITY_DRAFT;
amInternalStatus = ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_DRAFT;
amDisplayStatus = ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_DRAFT;
} else {
activityInternalStatus = ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED;
activityDisplayStatus = ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW;
amInternalStatus = ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW;
amDisplayStatus = ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW;
}
}
/* --------------------------------
* 4⃣ UPDATE ACTIVITY CORE + FLAGS
* -------------------------------- */
const activity = await tx.activities.update({
where: { id: existingActivity.id },
data: {
activityTypeXid: payload.activityTypeXid ?? undefined,
frequenciesXid: payload.frequenciesXid ?? undefined,
activityTitle: payload.activityTitle ?? undefined,
activityDescription: payload.activityDescription ?? undefined,
checkInLat: payload.checkInLat ?? undefined,
checkInLong: payload.checkInLong ?? undefined,
checkInAddress: payload.checkInAddress ?? undefined,
isCheckOutSame: toBool(payload.isCheckOutSame),
checkOutLat: payload.checkOutLat ?? undefined,
checkOutLong: payload.checkOutLong ?? undefined,
checkOutAddress: payload.checkOutAddress ?? undefined,
energyLevelXid: payload.energyLevelXid ?? undefined,
activityDurationMins: payload.activityDurationMins ?? undefined,
currencyXid: payload.currencyXid ?? undefined,
sustainabilityScore: payload.sustainabilityScore ?? undefined,
safetyScore: payload.safetyScore ?? undefined,
isInstantBooking: payload.isInstantBooking ?? undefined,
foodAvailable: payload.foodAvailable,
foodIsChargeable: toBool(payload.foodIsChargeable),
alcoholAvailable: payload.alcoholAvailable,
trainerAvailable: payload.trainerAvailable,
trainerIsChargeable: toBool(payload.trainerIsChargeable),
pickUpDropAvailable: payload.pickUpDropAvailable,
pickUpDropIsChargeable: toBool(payload.pickUpDropIsChargeable),
inActivityAvailable: payload.inActivityAvailable,
inActivityIsChargeable: toBool(payload.inActivityIsChargeable),
equipmentAvailable: payload.equipmentAvailable,
equipmentIsChargeable: toBool(payload.equipmentIsChargeable),
cancellationAvailable: payload.cancellationAvailable,
activityInternalStatus,
activityDisplayStatus,
amInternalStatus,
amDisplayStatus,
},
});
const activityXid = activity.id;
/* --------------------------------
* 5⃣ CLEAN OLD ACTIVITY MEDIA
* -------------------------------- */
await tx.activitiesMedia.deleteMany({ where: { activityXid } });
/* --------------------------------
* 6⃣ SAVE NEW ACTIVITY MEDIA
* -------------------------------- */
if (Array.isArray(payload.media) && payload.media.length) {
await tx.activitiesMedia.createMany({
data: payload.media.map((m, index) => ({
activityXid,
mediaType: m.mediaType ?? 'unknown',
mediaFileName: m.mediaFileName,
displayOrder: index + 1,
})),
});
}
/* --------------------------------
* 7⃣ CLEAN OLD VENUES & RELATED DATA
* -------------------------------- */
const oldVenueIds = (
await tx.activityVenues.findMany({
where: { activityXid },
select: { id: true },
})
).map((v) => v.id);
if (oldVenueIds.length) {
// Clean venue artifacts (media)
await tx.activityVenueArtifacts.deleteMany({
where: { activityVenueXid: { in: oldVenueIds } },
});
// Clean price taxes and prices
const priceIds = (
await tx.activityPrices.findMany({
where: { activityVenueXid: { in: oldVenueIds } },
select: { id: true },
})
).map((p) => p.id);
if (priceIds.length) {
await tx.activityPriceTaxes.deleteMany({
where: { activityPriceXid: { in: priceIds } },
});
await tx.activityPrices.deleteMany({
where: { id: { in: priceIds } },
});
}
// Clean venues
await tx.activityVenues.deleteMany({
where: { id: { in: oldVenueIds } },
});
}
/* --------------------------------
* 8⃣ CREATE VENUES WITH MEDIA & PRICES
* -------------------------------- */
for (const venue of payload.venues ?? []) {
const venueRow = await tx.activityVenues.create({
data: {
activityXid,
venueName: venue.venueName,
venueCapacity: toNumber(venue.venueCapacity) ?? 0,
availableSeats: toNumber(venue.availableSeats) ?? 0,
isMinPeopleReqMandatory: venue.isMinPeopleReqMandatory,
minPeopleRequired: toNumber(venue.minPeopleRequired) ?? null,
minReqfullfilledBeforeMins:
toNumber(venue.minReqfullfilledBeforeMins) ?? null,
venueDescription: venue.venueDescription ?? null,
},
});
// Create venue media/artifacts
if (Array.isArray(venue.media) && venue.media.length) {
await tx.activityVenueArtifacts.createMany({
data: venue.media.map((m) => ({
activityVenueXid: venueRow.id,
mediaType: m.mediaType ?? 'image',
mediaFileName: m.mediaFileName,
})),
});
}
// Create venue prices with taxes
for (const price of venue.prices ?? []) {
const sellPrice = Number(price.sellPrice);
const { basePrice, taxDetails } = computeBasePriceAndTaxes(
sellPrice,
rootTaxes,
);
const priceRow = await tx.activityPrices.create({
data: {
activityVenueXid: venueRow.id,
noOfSession: price.noOfSession ?? 1,
isPackage: price.isPackage ?? false,
sessionValidity: price.sessionValidity ?? 0,
sessionValidityFrequency: price.sessionValidityFrequency ?? 'Days',
basePrice,
sellPrice,
},
});
if (taxDetails.length) {
await tx.activityPriceTaxes.createMany({
data: taxDetails.map((t) => ({
activityPriceXid: priceRow.id,
taxXid: t.taxXid,
taxPer: t.taxPer,
taxAmount: t.taxAmount,
})),
});
}
}
}
/* --------------------------------
* 9⃣ CLEAN & CREATE EQUIPMENT WITH TAXES
* -------------------------------- */
const oldEquipmentIds = (
await tx.activityEquipments.findMany({
where: { activityXid },
select: { id: true },
})
).map((e) => e.id);
if (oldEquipmentIds.length) {
await tx.activityEquipmentTaxes.deleteMany({
where: { activityEquipmentXid: { in: oldEquipmentIds } },
});
await tx.activityEquipments.deleteMany({
where: { id: { in: oldEquipmentIds } },
});
}
if (Array.isArray(payload.equipments) && payload.equipments.length) {
for (const eq of payload.equipments) {
const totalPrice = toNumber(eq.equipmentTotalPrice) ?? 0;
const { basePrice, taxDetails } = computeBasePriceAndTaxes(
totalPrice,
rootTaxes,
);
const equipment = await tx.activityEquipments.create({
data: {
activityXid,
equipmentName: eq.equipmentName,
isEquipmentChargeable: toBool(eq.isEquipmentChargeable),
equipmentBasePrice: basePrice,
equipmentTotalPrice: totalPrice,
},
});
if (taxDetails.length) {
await tx.activityEquipmentTaxes.createMany({
data: taxDetails.map((t) => ({
activityEquipmentXid: equipment.id,
taxXid: t.taxXid,
taxPer: t.taxPer,
taxAmount: t.taxAmount,
})),
});
}
}
}
/* --------------------------------
* 🔟 CLEAN & CREATE TRAINER WITH TAXES
* -------------------------------- */
const oldTrainerIds = (
await tx.activityTrainers.findMany({
where: { activityXid },
select: { id: true },
})
).map((t) => t.id);
if (oldTrainerIds.length) {
await tx.activityTrainerTaxes.deleteMany({
where: { activityTrainerXid: { in: oldTrainerIds } },
});
await tx.activityTrainers.deleteMany({
where: { id: { in: oldTrainerIds } },
});
}
if (payload.trainerAvailable) {
const { basePrice, taxDetails } = computeBasePriceAndTaxes(
payload.trainerTotalAmount,
rootTaxes,
);
const trainer = await tx.activityTrainers.create({
data: {
activityXid,
baseAmount: basePrice,
totalAmount: payload.trainerTotalAmount,
},
});
if (taxDetails.length) {
await tx.activityTrainerTaxes.createMany({
data: taxDetails.map((t) => ({
activityTrainerXid: trainer.id,
taxXid: t.taxXid,
taxPer: t.taxPer,
taxAmount: t.taxAmount,
})),
});
}
}
/* --------------------------------
* 1⃣1⃣ CLEAN & CREATE PICKUP/DROP TRANSPORTS WITH DETAILS & TAXES
* -------------------------------- */
const oldTransportIds = (
await tx.activityPickUpTransport.findMany({
where: { activityXid },
select: { id: true },
})
).map((t) => t.id);
if (oldTransportIds.length) {
// Get all pickup details for these transports
const oldPickupDetailIds = (
await tx.activityPickUpDetails.findMany({
where: { activityPickUpTransportXid: { in: oldTransportIds } },
select: { id: true },
})
).map((p) => p.id);
if (oldPickupDetailIds.length) {
// Delete taxes first
await tx.activityPickUpTransportTaxes.deleteMany({
where: { activityPickUpDetailsXid: { in: oldPickupDetailIds } },
});
// Delete pickup details
await tx.activityPickUpDetails.deleteMany({
where: { id: { in: oldPickupDetailIds } },
});
}
// Delete transports
await tx.activityPickUpTransport.deleteMany({
where: { id: { in: oldTransportIds } },
});
}
if (
Array.isArray(payload.pickupTransports) &&
payload.pickupTransports.length
) {
for (const transport of payload.pickupTransports) {
// Create transport mode
const transportRow = await tx.activityPickUpTransport.create({
data: {
activityXid,
transportModeXid: transport.transportModeXid,
isTransportModeChargeable: toBool(
transport.isTransportModeChargeable,
),
},
});
// Create pickup details for this transport
if (
Array.isArray(transport.pickupDetails) &&
transport.pickupDetails.length
) {
for (const detail of transport.pickupDetails) {
const totalPrice = toNumber(detail.transportTotalPrice) ?? 0;
const { basePrice, taxDetails } = computeBasePriceAndTaxes(
totalPrice,
rootTaxes,
);
const pickupDetail = await tx.activityPickUpDetails.create({
data: {
activityPickUpTransportXid: transportRow.id,
isPickUp: toBool(detail.isPickUp),
locationLat: toNumber(detail.locationLat),
locationLong: toNumber(detail.locationLong),
locationAddress: detail.locationAddress ?? null,
transportBasePrice: basePrice,
transportTotalPrice: totalPrice,
},
});
if (taxDetails.length) {
await tx.activityPickUpTransportTaxes.createMany({
data: taxDetails.map((t) => ({
activityPickUpDetailsXid: pickupDetail.id,
taxXid: t.taxXid,
taxPer: t.taxPer,
taxAmount: t.taxAmount,
})),
});
}
}
}
}
}
/* --------------------------------
* 1⃣2⃣ CLEAN & CREATE NAVIGATION MODES WITH TAXES
* -------------------------------- */
const oldNavIds = (
await tx.activityNavigationModes.findMany({
where: { activityXid },
select: { id: true },
})
).map((n) => n.id);
if (oldNavIds.length) {
await tx.activityNavigationModesTaxes.deleteMany({
where: { activityNavigationModeXid: { in: oldNavIds } },
});
await tx.activityNavigationModes.deleteMany({
where: { id: { in: oldNavIds } },
});
}
if (
Array.isArray(payload.navigationModes) &&
payload.navigationModes.length
) {
const totalPrice = toNumber(payload.navigationModeTotalPrice) ?? 0;
const { basePrice, taxDetails } = computeBasePriceAndTaxes(
totalPrice,
rootTaxes,
);
for (const modeId of payload.navigationModes) {
const navMode = await tx.activityNavigationModes.create({
data: {
activityXid,
navigationModeXid: modeId,
isInActivityChargeable: toBool(payload.navigationModeIsChargeable),
navigationModesBasePrice: basePrice,
navigationModesTotalPrice: totalPrice,
},
});
if (taxDetails.length) {
await tx.activityNavigationModesTaxes.createMany({
data: taxDetails.map((t) => ({
activityNavigationModeXid: navMode.id,
taxXid: t.taxXid,
taxPer: t.taxPer,
taxAmount: t.taxAmount,
})),
});
}
}
}
/* --------------------------------
* 1⃣3⃣ CLEAN & CREATE AMENITIES
* -------------------------------- */
await tx.activityAmenities.deleteMany({ where: { activityXid } });
if (Array.isArray(payload.amenitiesIds) && payload.amenitiesIds.length) {
await tx.activityAmenities.createMany({
data: payload.amenitiesIds.map((amenityId) => ({
activityXid,
amenitiesXid: amenityId,
})),
});
}
/* --------------------------------
* 1⃣4⃣ CLEAN & CREATE ELIGIBILITY
* -------------------------------- */
await tx.activityEligibility.deleteMany({ where: { activityXid } });
if (payload.eligibility) {
await tx.activityEligibility.create({
data: {
activityXid,
isAgeRestriction: toBool(payload.eligibility.isAgeRestriction),
ageRestrictionXid: toNumber(payload.eligibility.ageRestrictionXid),
isWeightRestriction: toBool(payload.eligibility.isWeightRestriction),
weightRestrictionName: payload.eligibility.weightRestrictionName ?? null,
weightEntered: toNumber(payload.eligibility.weightEntered),
weightIn: payload.eligibility.weightIn ?? null,
minWeight: toNumber(payload.eligibility.minWeight),
maxWeight: toNumber(payload.eligibility.maxWeight),
isHeightRestriction: toBool(payload.eligibility.isHeightRestriction),
heightRestrictionName: payload.eligibility.heightRestrictionName ?? null,
heightEntered: toNumber(payload.eligibility.heightEntered),
heightIn: payload.eligibility.heightIn ?? null,
minHeight: toNumber(payload.eligibility.minHeight),
maxHeight: toNumber(payload.eligibility.maxHeight),
},
});
}
/* --------------------------------
* 1⃣5⃣ CLEAN & CREATE OTHER DETAILS
* -------------------------------- */
await tx.activityOtherDetails.deleteMany({ where: { activityXid } });
if (payload.otherDetails) {
await tx.activityOtherDetails.create({
data: {
activityXid,
exclusiveNotes: payload.otherDetails.exclusiveNotes ?? null,
dosNotes: payload.otherDetails.dosNotes ?? null,
dontsNotes: payload.otherDetails.dontsNotes ?? null,
tipsNotes: payload.otherDetails.tipsNotes ?? null,
termsAndCondition: payload.otherDetails.termsAndCondition ?? null,
},
});
}
/* --------------------------------
* 1⃣6⃣ CLEAN & CREATE FOOD TYPES
* -------------------------------- */
await tx.activityFoodTypes.deleteMany({ where: { activityXid } });
if (Array.isArray(payload.foodTypeIds) && payload.foodTypeIds.length) {
await tx.activityFoodTypes.createMany({
data: payload.foodTypeIds.map((foodTypeId) => ({
activityXid,
foodTypeXid: foodTypeId,
})),
});
}
/* --------------------------------
* 1⃣7⃣ CLEAN & CREATE CUISINES
* -------------------------------- */
await tx.activityCuisine.deleteMany({ where: { activityXid } });
if (Array.isArray(payload.cuisineIds) && payload.cuisineIds.length) {
await tx.activityCuisine.createMany({
data: payload.cuisineIds.map((cuisineId) => ({
activityXid,
foodCuisineXid: cuisineId,
})),
});
}
/* --------------------------------
* 1⃣8⃣ ACTIVITY TRACK
* -------------------------------- */
await tx.activityTrack.create({
data: {
activityXid,
trackType: 'ACTIVITY',
trackStatus: activityInternalStatus,
updatedByXid: userId,
updatedByRole: ROLE_NAME.HOST,
updatedOn: new Date(),
},
});
/* --------------------------------
* 1⃣9⃣ RESPONSE
* -------------------------------- */
return {
activityXid,
activityRefNumber: activity.activityRefNumber,
status: isDraft ? ACTIVITY_INTERNAL_STATUS.ACTIVITY_DRAFT : ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED,
};
});
}
async getAllPQUpdatedResponse(activityXid: number) {
const pqqHeaderData = await this.prisma.activityPQQheader.findMany({
where: {
activityXid: activityXid,
isActive: true,
},
select: {
id: true,
comments: true,
pqqAnswerXid: true,
pqqQuestions: {
select: {
id: true,
questionName: true,
maxPoints: true,
displayOrder: true,
pqqSubCategories: {
select: {
id: true,
subCategoryName: true,
displayOrder: true,
category: {
select: {
id: true,
categoryName: true,
displayOrder: true,
},
},
},
},
// 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION
PQQAnswers: {
where: { isActive: true },
select: {
id: true,
answerName: true,
answerPoints: true,
displayOrder: true,
},
orderBy: { displayOrder: 'asc' },
},
},
},
ActivityPQQSuggestions: {
where: { isActive: true },
select: {
id: true,
title: true,
comments: true,
activityPqqHeaderXid: true,
},
},
ActivityPQQSupportings: {
where: { isActive: true },
select: {
id: true,
mediaType: true,
mediaFileName: true,
},
},
},
orderBy: { id: 'asc' },
});
// ---------- GROUPING START ----------
const grouped: any = {};
for (const item of pqqHeaderData) {
const q = item.pqqQuestions;
const sub = q.pqqSubCategories;
const cat = sub.category;
// 1⃣ Category level
// 1⃣ Category level
if (!grouped[cat.id]) {
grouped[cat.id] = {
id: cat.id,
categoryName: cat.categoryName,
displayOrder: cat.displayOrder,
activityPqqHeaderId: item.id, // ✅ Added to match AM response
pqqsubCategories: [],
};
} else if (!grouped[cat.id].activityPqqHeaderId) {
grouped[cat.id].activityPqqHeaderId = item.id; // Ensures it's set if missing
}
const category = grouped[cat.id];
// 2⃣ Subcategory level
let subCat = category.pqqsubCategories.find((s: any) => s.id === sub.id);
if (!subCat) {
subCat = {
id: sub.id,
subCategoryName: sub.subCategoryName,
displayOrder: sub.displayOrder,
questions: [],
};
category.pqqsubCategories.push(subCat);
}
// 3⃣ Questions level
subCat.questions.push({
id: q.id,
questionName: q.questionName,
maxPoints: q.maxPoints,
pqqAnswerXid: item.pqqAnswerXid,
comments: item.comments || null,
displayOrder: q.displayOrder,
allAnswerOptions: q.PQQAnswers || [], // 🔥 All answers
suggestions: item.ActivityPQQSuggestions,
supportings: item.ActivityPQQSupportings,
});
}
// ---------- SORTING ----------
const sortedCategories: any = Object.values(grouped).sort(
(a: any, b: any) => a.displayOrder - b.displayOrder,
);
for (const cat of sortedCategories) {
cat.pqqsubCategories.sort(
(a: any, b: any) => a.displayOrder - b.displayOrder,
);
for (const sub of cat.pqqsubCategories) {
sub.questions.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
}
}
// ---------- PRESIGNED URL GENERATION ----------
for (const cat of sortedCategories) {
for (const sub of cat.pqqsubCategories) {
for (const q of sub.questions) {
if (q.supportings?.length) {
for (const doc of q.supportings) {
if (doc.mediaFileName) {
const filePath = doc.mediaFileName;
const key = filePath.startsWith('http')
? filePath.split('.com/')[1]
: filePath;
doc.presignedUrl = await getPresignedUrl(bucket, key);
}
}
}
}
}
}
// ---------- RETURN GROUPED STRUCTURE ----------
return sortedCategories;
}
}