2025-11-10 15:05:01 +05:30
|
|
|
|
// src/modules/host/services/host.service.ts
|
|
|
|
|
|
import { Injectable } from '@nestjs/common';
|
2026-01-08 12:14:12 +05:30
|
|
|
|
import { PrismaClient, User } from '@prisma/client';
|
2026-02-27 19:48:29 +05:30
|
|
|
|
import AWS from 'aws-sdk';
|
2025-11-12 16:03:57 +05:30
|
|
|
|
import * as bcrypt from 'bcryptjs';
|
2026-02-27 19:48:29 +05:30
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
|
import { PDFDocument, StandardFonts } from 'pdf-lib';
|
2025-11-13 15:53:35 +05:30
|
|
|
|
import { z } from 'zod';
|
2026-01-08 12:14:12 +05:30
|
|
|
|
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
2026-02-27 19:48:29 +05:30
|
|
|
|
import { AGREEMENT_TEMPLATE } from '../../../common/utils/constants/agreementTemplate';
|
2026-01-08 12:14:12 +05:30
|
|
|
|
import {
|
|
|
|
|
|
RESTRICTION_NAME,
|
|
|
|
|
|
ROLE,
|
|
|
|
|
|
ROLE_NAME,
|
|
|
|
|
|
USER_STATUS,
|
|
|
|
|
|
} from '../../../common/utils/constants/common.constant';
|
2025-11-27 17:26:33 +05:30
|
|
|
|
import {
|
2025-11-27 20:00:29 +05:30
|
|
|
|
ACTIVITY_AM_DISPLAY_STATUS,
|
|
|
|
|
|
ACTIVITY_AM_INTERNAL_STATUS,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
ACTIVITY_DISPLAY_STATUS,
|
|
|
|
|
|
ACTIVITY_INTERNAL_STATUS,
|
|
|
|
|
|
HOST_STATUS_DISPLAY,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
HOST_STATUS_INTERNAL,
|
|
|
|
|
|
STEPPER,
|
2025-12-08 11:23:58 +05:30
|
|
|
|
} from '../../../common/utils/constants/host.constant';
|
2025-11-27 17:26:33 +05:30
|
|
|
|
import {
|
2025-12-03 12:44:07 +05:30
|
|
|
|
ACTIVITY_TRACK_STATUS,
|
|
|
|
|
|
ACTIVITY_TRACK_TYPE,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
MINGLAR_STATUS_DISPLAY,
|
|
|
|
|
|
MINGLAR_STATUS_INTERNAL,
|
2025-12-08 11:23:58 +05:30
|
|
|
|
} from '../../../common/utils/constants/minglar.constant';
|
2026-01-08 12:14:12 +05:30
|
|
|
|
import ApiError from '../../../common/utils/helper/ApiError';
|
|
|
|
|
|
import { hostCompanyDetailsSchema } from '../../../common/utils/validation/host/hostCompanyDetails.validation';
|
2025-12-08 11:23:58 +05:30
|
|
|
|
import config from '../../../config/config';
|
2025-12-24 17:10:54 +05:30
|
|
|
|
import { CreateActivityInput } from '../dto/createActivity.schema';
|
2026-01-08 12:14:12 +05:30
|
|
|
|
import {
|
|
|
|
|
|
AddPaymentDetailsDTO,
|
|
|
|
|
|
CreateHostDto,
|
|
|
|
|
|
UpdateHostDto,
|
|
|
|
|
|
} from '../dto/host.dto';
|
2025-11-13 15:53:35 +05:30
|
|
|
|
|
2025-12-16 13:16:22 +05:30
|
|
|
|
function sanitizeDocumentName(name?: string) {
|
|
|
|
|
|
if (!name) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return name
|
|
|
|
|
|
.replace(/[^a-zA-Z0-9 _-]/g, '') // remove / .
|
|
|
|
|
|
.substring(0, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-13 15:53:35 +05:30
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-02-27 12:15:48 +05:30
|
|
|
|
export async function generateActivityRefNumber(
|
|
|
|
|
|
tx: any,
|
|
|
|
|
|
hostXid: number,
|
2026-02-27 17:24:39 +05:30
|
|
|
|
activityTypeXid: number,
|
|
|
|
|
|
hostRefNumber: string
|
2026-02-27 12:15:48 +05:30
|
|
|
|
) {
|
|
|
|
|
|
// 1️⃣ Get ActivityType with Interest
|
|
|
|
|
|
const activityType = await tx.activityTypes.findUnique({
|
|
|
|
|
|
where: { id: activityTypeXid },
|
|
|
|
|
|
include: {
|
2026-02-27 19:48:29 +05:30
|
|
|
|
interests: true, // relation is named "interests" in schema
|
2026-02-27 12:15:48 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-27 19:48:29 +05:30
|
|
|
|
if (!activityType || !activityType.interests) {
|
2026-02-27 12:15:48 +05:30
|
|
|
|
throw new Error("Invalid activity type or interest not found");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 23:08:23 +05:30
|
|
|
|
// Use the Interest's ID from the relation
|
2026-02-27 19:48:29 +05:30
|
|
|
|
const interestId = activityType.interests.id;
|
|
|
|
|
|
const interestCode = activityType.interests.interestCode;
|
2026-02-27 12:15:48 +05:30
|
|
|
|
|
|
|
|
|
|
// 2️⃣ Check if this host already has activities under this interest
|
|
|
|
|
|
const existingActivityForInterest = await tx.activities.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
hostXid,
|
|
|
|
|
|
activityType: {
|
2026-02-27 23:08:23 +05:30
|
|
|
|
is: {
|
|
|
|
|
|
interestXid: interestId,
|
|
|
|
|
|
},
|
2026-02-27 12:15:48 +05:30
|
|
|
|
},
|
2025-12-02 13:42:14 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
2026-02-27 12:15:48 +05:30
|
|
|
|
activityRefNumber: true,
|
2025-12-02 13:42:14 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-27 12:15:48 +05:30
|
|
|
|
let interestSequence: number;
|
|
|
|
|
|
|
|
|
|
|
|
if (existingActivityForInterest?.activityRefNumber) {
|
|
|
|
|
|
// Extract existing interest sequence from ref number
|
|
|
|
|
|
const match =
|
|
|
|
|
|
existingActivityForInterest.activityRefNumber.match(
|
|
|
|
|
|
new RegExp(`E-${interestCode}(\\d{3})-`)
|
|
|
|
|
|
);
|
2025-12-02 13:42:14 +05:30
|
|
|
|
|
2026-02-27 12:15:48 +05:30
|
|
|
|
interestSequence = match ? parseInt(match[1], 10) : 1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Count distinct interests already used by this host
|
|
|
|
|
|
const distinctInterests = await tx.activities.findMany({
|
|
|
|
|
|
where: { hostXid },
|
2026-02-27 23:08:23 +05:30
|
|
|
|
select: {
|
2026-02-27 12:15:48 +05:30
|
|
|
|
activityType: {
|
|
|
|
|
|
select: {
|
2026-02-27 23:08:23 +05:30
|
|
|
|
interestXid: true,
|
2026-02-27 12:15:48 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const uniqueInterestIds = new Set(
|
2026-02-27 23:08:23 +05:30
|
|
|
|
distinctInterests
|
|
|
|
|
|
.map((a: any) => a.activityType?.interestXid)
|
|
|
|
|
|
.filter((id: number | null | undefined): id is number => id != null)
|
2026-02-27 12:15:48 +05:30
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
interestSequence = uniqueInterestIds.size + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3️⃣ Count activities for same host + same interest + same activityType
|
|
|
|
|
|
const activityTypeCount = await tx.activities.count({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
hostXid,
|
|
|
|
|
|
activityTypeXid,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const nextActivityTypeSequence = activityTypeCount + 1;
|
|
|
|
|
|
|
2026-02-27 17:24:39 +05:30
|
|
|
|
return `${hostRefNumber}-E-${interestCode}${String(interestSequence).padStart(
|
2026-02-27 12:15:48 +05:30
|
|
|
|
3,
|
|
|
|
|
|
"0"
|
|
|
|
|
|
)}-${String(nextActivityTypeSequence).padStart(2, "0")}`;
|
2025-12-02 13:42:14 +05:30
|
|
|
|
}
|
2026-02-27 17:25:48 +05:30
|
|
|
|
|
2025-11-10 15:05:01 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
function round2(value: number) {
|
2025-12-02 20:09:42 +05:30
|
|
|
|
return Math.round(value);
|
2025-12-02 17:24:01 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-21 17:28:08 +05:30
|
|
|
|
function computeBasePriceAndTaxes(
|
|
|
|
|
|
sellPrice: number,
|
|
|
|
|
|
taxes: Array<{ id: number; taxPer: number }>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (!taxes?.length) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
basePrice: round2(sellPrice),
|
2025-12-22 13:30:55 +05:30
|
|
|
|
taxDetails: [] as Array<{
|
|
|
|
|
|
taxXid: number;
|
|
|
|
|
|
taxPer: number;
|
|
|
|
|
|
taxAmount: number;
|
|
|
|
|
|
}>,
|
2025-12-21 17:28:08 +05:30
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const totalTaxPer = taxes.reduce(
|
|
|
|
|
|
(sum, t) => sum + (Number(t.taxPer) || 0),
|
|
|
|
|
|
0,
|
|
|
|
|
|
);
|
2025-12-21 17:28:08 +05:30
|
|
|
|
const denominator = 1 + totalTaxPer / 100;
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const basePrice =
|
|
|
|
|
|
denominator > 0 ? round2(sellPrice / denominator) : round2(sellPrice);
|
2025-12-21 17:28:08 +05:30
|
|
|
|
|
|
|
|
|
|
const taxDetails = taxes.map((t) => ({
|
|
|
|
|
|
taxXid: t.id,
|
|
|
|
|
|
taxPer: t.taxPer,
|
|
|
|
|
|
taxAmount: round2(basePrice * (t.taxPer / 100)),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
return { basePrice, taxDetails };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 15:49:25 +05:30
|
|
|
|
const normalize = (v?: string | null, maxLength: number = 50) =>
|
|
|
|
|
|
v ? v.trim().toLowerCase().substring(0, maxLength) : null;
|
2026-02-04 15:32:11 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
async function renderAgreementPdf(vars: {
|
2026-02-26 19:45:08 +05:30
|
|
|
|
effectiveDate: string;
|
|
|
|
|
|
companyName: string;
|
|
|
|
|
|
companyType?: string | null;
|
|
|
|
|
|
fullAddress: string;
|
|
|
|
|
|
durationText: string;
|
|
|
|
|
|
expiryDate: string;
|
|
|
|
|
|
commissionText: string;
|
|
|
|
|
|
acceptDate: string;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const pdfDoc = await PDFDocument.create();
|
|
|
|
|
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
|
|
|
|
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
const fontSize = 10;
|
|
|
|
|
|
const titleSize = 14;
|
2026-02-26 19:45:08 +05:30
|
|
|
|
const margin = 50;
|
2026-02-27 17:25:48 +05:30
|
|
|
|
const { width, height } = { width: 595, height: 842 }; // A4
|
|
|
|
|
|
const contentWidth = width - 2 * margin;
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
let page = pdfDoc.addPage([width, height]);
|
|
|
|
|
|
let cursorY = height - margin;
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
const addNewPage = () => {
|
|
|
|
|
|
page = pdfDoc.addPage([width, height]);
|
|
|
|
|
|
cursorY = height - margin;
|
|
|
|
|
|
};
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
const drawLine = (text: string, isBold = false, size = fontSize) => {
|
|
|
|
|
|
const currentFont = isBold ? fontBold : font;
|
|
|
|
|
|
const words = text.split(' ');
|
|
|
|
|
|
let line = '';
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
for (const word of words) {
|
|
|
|
|
|
const testLine = line + word + ' ';
|
|
|
|
|
|
const testLineWidth = currentFont.widthOfTextAtSize(testLine.trim(), size);
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
if (testLineWidth > contentWidth && line !== '') {
|
|
|
|
|
|
if (cursorY < margin + 20) addNewPage();
|
|
|
|
|
|
page.drawText(line.trim(), { x: margin, y: cursorY, size, font: currentFont });
|
|
|
|
|
|
cursorY -= size * 1.5;
|
|
|
|
|
|
line = word + ' ';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
line = testLine;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
if (line !== '') {
|
|
|
|
|
|
if (cursorY < margin + 20) addNewPage();
|
|
|
|
|
|
page.drawText(line.trim(), { x: margin, y: cursorY, size, font: currentFont });
|
|
|
|
|
|
cursorY -= size * 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
let template = AGREEMENT_TEMPLATE;
|
|
|
|
|
|
template = template.replace(/\[EFFECTIVE_DATE\]/g, vars.effectiveDate);
|
|
|
|
|
|
template = template.replace(/\[HOST_LEGAL_NAME\]/g, vars.companyName);
|
|
|
|
|
|
template = template.replace(/\[COMPANY_TYPE\]/g, vars.companyType || 'Entity');
|
|
|
|
|
|
template = template.replace(/\[FULL_ADDRESS\]/g, vars.fullAddress);
|
|
|
|
|
|
template = template.replace(/\[DURATION_TEXT\]/g, vars.durationText);
|
|
|
|
|
|
template = template.replace(/\[EXPIRY_DATE\]/g, vars.expiryDate);
|
|
|
|
|
|
template = template.replace(/\[COMMISSION_TEXT\]/g, vars.commissionText);
|
|
|
|
|
|
template = template.replace(/\[ACCEPT_DATE\]/g, vars.acceptDate);
|
|
|
|
|
|
|
|
|
|
|
|
const lines = template.split('\n');
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
|
if (!trimmed) {
|
|
|
|
|
|
cursorY -= fontSize; // Paragraph spacing
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
// Heuristic for titles/headers
|
|
|
|
|
|
const isTitle = trimmed === trimmed.toUpperCase() && trimmed.length > 5;
|
|
|
|
|
|
const isSubHeader = /^\d+\.?\s/.test(trimmed) || /^[A-Z]\)\s/.test(trimmed);
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
drawLine(trimmed, isTitle || isSubHeader, isTitle ? titleSize : fontSize);
|
2026-02-26 19:45:08 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
const pdfBytes = await pdfDoc.save();
|
|
|
|
|
|
return Buffer.from(pdfBytes);
|
|
|
|
|
|
}
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
function buildDurationText(host: any) {
|
|
|
|
|
|
if (!host.durationNumber || !host.durationFrequency) return 'N/A';
|
|
|
|
|
|
return `${host.durationNumber} ${host.durationFrequency}`;
|
|
|
|
|
|
}
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
function buildExpiryDate(host: any) {
|
|
|
|
|
|
if (!host.agreementStartDate || !host.durationNumber || !host.durationFrequency) return 'N/A';
|
|
|
|
|
|
const start = dayjs(host.agreementStartDate);
|
|
|
|
|
|
const duration = host.durationNumber;
|
|
|
|
|
|
// Handle frequency units for dayjs (months/years)
|
|
|
|
|
|
const unit = host.durationFrequency.toLowerCase().includes('month') ? 'month' : 'year';
|
|
|
|
|
|
return start.add(duration, unit as any).format('DD-MMM-YY');
|
|
|
|
|
|
}
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
function buildCommissionText(host: any) {
|
|
|
|
|
|
if (host.isCommisionBase) {
|
|
|
|
|
|
return `${host.commisionPer || 0}%`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return `INR ${host.amountPerBooking || 0} per booking`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
function buildFullAddress(host: any) {
|
|
|
|
|
|
const parts = [
|
|
|
|
|
|
host.address1,
|
|
|
|
|
|
host.address2,
|
|
|
|
|
|
host.cities?.cityName,
|
|
|
|
|
|
host.states?.stateName,
|
|
|
|
|
|
host.countries?.countryName,
|
|
|
|
|
|
host.pinCode,
|
|
|
|
|
|
].filter(Boolean);
|
|
|
|
|
|
return parts.join(', ');
|
|
|
|
|
|
}
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
// generateAgreementPdfBuffer and generateAgreementDocxBuffer removed
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-27 17:25:48 +05:30
|
|
|
|
// End of agreement rendering logic
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
const findOrCreateCountry = async (
|
|
|
|
|
|
tx: any,
|
|
|
|
|
|
countryName?: string | null,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
if (!countryName) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const name = normalize(countryName);
|
|
|
|
|
|
|
|
|
|
|
|
let country = await tx.countries.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
countryName: { equals: name, mode: 'insensitive' },
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!country) {
|
|
|
|
|
|
country = await tx.countries.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
countryName: countryName.trim(),
|
|
|
|
|
|
countryCode: countryName
|
|
|
|
|
|
.substring(0, 2)
|
|
|
|
|
|
.toUpperCase(), // fallback
|
|
|
|
|
|
countryFlag: '',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return country.id;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const findOrCreateState = async (
|
|
|
|
|
|
tx: any,
|
|
|
|
|
|
stateName?: string | null,
|
|
|
|
|
|
countryXid?: number | null,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
if (!stateName || !countryXid) return null;
|
|
|
|
|
|
|
2026-03-06 15:49:25 +05:30
|
|
|
|
const trimmedStateName = stateName.trim().substring(0, 50);
|
|
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
const state = await tx.states.findFirst({
|
|
|
|
|
|
where: {
|
2026-03-06 15:49:25 +05:30
|
|
|
|
stateName: { equals: trimmedStateName, mode: 'insensitive' },
|
2026-02-04 15:32:11 +05:30
|
|
|
|
countryXid,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (state) return state.id;
|
|
|
|
|
|
|
|
|
|
|
|
const created = await tx.states.create({
|
|
|
|
|
|
data: {
|
2026-03-06 15:49:25 +05:30
|
|
|
|
stateName: trimmedStateName,
|
2026-02-04 15:32:11 +05:30
|
|
|
|
countryXid,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return created.id;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const findOrCreateCity = async (
|
|
|
|
|
|
tx: any,
|
|
|
|
|
|
cityName?: string | null,
|
|
|
|
|
|
stateXid?: number | null,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
if (!cityName || !stateXid) return null;
|
|
|
|
|
|
|
2026-03-06 15:49:25 +05:30
|
|
|
|
const trimmedCityName = cityName.trim().substring(0, 50);
|
|
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
const city = await tx.cities.findFirst({
|
|
|
|
|
|
where: {
|
2026-03-06 15:49:25 +05:30
|
|
|
|
cityName: { equals: trimmedCityName, mode: 'insensitive' },
|
2026-02-04 15:32:11 +05:30
|
|
|
|
stateXid,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (city) return city.id;
|
|
|
|
|
|
|
|
|
|
|
|
const created = await tx.cities.create({
|
|
|
|
|
|
data: {
|
2026-03-06 15:49:25 +05:30
|
|
|
|
cityName: trimmedCityName,
|
2026-02-04 15:32:11 +05:30
|
|
|
|
stateXid,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return created.id;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
const bucket = config.aws.bucketName;
|
2026-02-27 18:43:37 +05:30
|
|
|
|
const s3 = new AWS.S3({
|
|
|
|
|
|
region: config.aws.region,
|
|
|
|
|
|
});
|
2025-12-02 17:24:01 +05:30
|
|
|
|
|
2026-04-07 18:45:07 +05:30
|
|
|
|
function getS3KeyFromStoredPath(path?: string | null) {
|
|
|
|
|
|
if (!path) return null;
|
|
|
|
|
|
return path.startsWith('http') ? path.split('.com/')[1] || null : path;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveIncomingLogoPath(path?: string | null) {
|
|
|
|
|
|
if (typeof path !== 'string') return null;
|
|
|
|
|
|
const trimmed = path.trim();
|
|
|
|
|
|
return trimmed.length ? trimmed : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
type UpdateHostProfileInput = {
|
|
|
|
|
|
firstName?: string;
|
|
|
|
|
|
lastName?: string | null;
|
|
|
|
|
|
isdCode?: string;
|
|
|
|
|
|
mobileNumber?: string;
|
|
|
|
|
|
dateOfBirth?: Date;
|
|
|
|
|
|
address?: {
|
|
|
|
|
|
address1?: string;
|
|
|
|
|
|
address2?: string;
|
|
|
|
|
|
countryXid?: number;
|
|
|
|
|
|
stateXid?: number;
|
|
|
|
|
|
cityXid?: number;
|
|
|
|
|
|
pinCode?: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-10 15:05:01 +05:30
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class HostService {
|
2026-01-09 18:10:38 +05:30
|
|
|
|
constructor(private prisma: PrismaClient) { }
|
2025-11-10 15:05:01 +05:30
|
|
|
|
|
2026-04-07 18:45:07 +05:30
|
|
|
|
private async getValidLogoUrl(
|
|
|
|
|
|
model: 'hostHeader' | 'hostParent',
|
|
|
|
|
|
recordId: number,
|
|
|
|
|
|
logoPath?: string | null,
|
|
|
|
|
|
) {
|
|
|
|
|
|
const key = getS3KeyFromStoredPath(logoPath);
|
|
|
|
|
|
if (!key) return null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await s3
|
|
|
|
|
|
.headObject({
|
|
|
|
|
|
Bucket: bucket,
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
})
|
|
|
|
|
|
.promise();
|
|
|
|
|
|
|
|
|
|
|
|
return await getPresignedUrl(bucket, key);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
const statusCode = error?.statusCode;
|
|
|
|
|
|
const errorCode = error?.code;
|
|
|
|
|
|
const isMissingObject =
|
|
|
|
|
|
statusCode === 404 ||
|
|
|
|
|
|
errorCode === 'NotFound' ||
|
|
|
|
|
|
errorCode === 'NoSuchKey';
|
|
|
|
|
|
|
|
|
|
|
|
if (isMissingObject) {
|
|
|
|
|
|
await (this.prisma as any)[model].update({
|
|
|
|
|
|
where: { id: recordId },
|
|
|
|
|
|
data: { logoPath: null },
|
|
|
|
|
|
});
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 } });
|
|
|
|
|
|
}
|
2026-01-09 18:10:38 +05:30
|
|
|
|
|
|
|
|
|
|
|
2026-01-05 15:46:47 +05:30
|
|
|
|
async getActivityDetailsById(activityXid: number) {
|
|
|
|
|
|
return this.prisma.activities.findFirst({ where: { id: activityXid } });
|
|
|
|
|
|
}
|
2025-11-10 15:05:01 +05:30
|
|
|
|
|
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-12-16 17:34:10 +05:30
|
|
|
|
select: { id: true, stepper: true },
|
2025-11-14 14:08:47 +05:30
|
|
|
|
});
|
2025-12-16 17:34:10 +05:30
|
|
|
|
|
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
2026-03-02 12:35:51 +05:30
|
|
|
|
where: { id: user_xid, isActive: true },
|
|
|
|
|
|
select: { id: true, emailAddress: true, userRefNumber: true },
|
2025-12-22 13:30:55 +05:30
|
|
|
|
});
|
2025-12-16 17:34:10 +05:30
|
|
|
|
return { host, user };
|
2025-11-14 14:08:47 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
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 },
|
2026-04-01 16:09:04 +05:30
|
|
|
|
select: {
|
2026-04-01 19:51:13 +05:30
|
|
|
|
id: true,
|
2026-04-01 16:09:04 +05:30
|
|
|
|
logoPath: true,
|
2026-04-07 12:00:34 +05:30
|
|
|
|
companyName: true,
|
|
|
|
|
|
address1: true,
|
|
|
|
|
|
address2: true,
|
|
|
|
|
|
pinCode: true,
|
|
|
|
|
|
isSubsidairy: true,
|
|
|
|
|
|
registrationNumber: true,
|
|
|
|
|
|
panNumber: true,
|
|
|
|
|
|
gstNumber: true,
|
|
|
|
|
|
formationDate: true,
|
|
|
|
|
|
companyTypeXid: true,
|
|
|
|
|
|
websiteUrl: true,
|
|
|
|
|
|
instagramUrl: true,
|
|
|
|
|
|
facebookUrl: true,
|
|
|
|
|
|
linkedinUrl: true,
|
|
|
|
|
|
twitterUrl: true,
|
|
|
|
|
|
stepper: true,
|
|
|
|
|
|
hostStatusInternal: true,
|
|
|
|
|
|
hostStatusDisplay: true,
|
|
|
|
|
|
adminStatusInternal: true,
|
|
|
|
|
|
adminStatusDisplay: true,
|
|
|
|
|
|
amStatus: true,
|
|
|
|
|
|
agreementAccepted: true,
|
|
|
|
|
|
assignedOn: true,
|
|
|
|
|
|
agreementStartDate: true,
|
|
|
|
|
|
isApproved: true,
|
|
|
|
|
|
durationNumber: true,
|
|
|
|
|
|
durationFrequency: true,
|
|
|
|
|
|
isCommisionBase: true,
|
|
|
|
|
|
commisionPer: true,
|
|
|
|
|
|
amountPerBooking: true,
|
|
|
|
|
|
payoutDurationNum: true,
|
|
|
|
|
|
payoutDurationFrequency: true,
|
|
|
|
|
|
referencedBy: true,
|
2025-11-28 17:20:27 +05:30
|
|
|
|
hostParent: {
|
2026-03-25 12:36:47 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
logoPath: true,
|
|
|
|
|
|
companyName: true,
|
|
|
|
|
|
address1: true,
|
|
|
|
|
|
address2: true,
|
|
|
|
|
|
cities: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
cityName: true
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
states: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
stateName: true
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
countries: {
|
|
|
|
|
|
select: {
|
2026-04-01 16:09:04 +05:30
|
|
|
|
id: true,
|
|
|
|
|
|
countryName: true
|
2026-03-25 12:36:47 +05:30
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
pinCode: true,
|
|
|
|
|
|
registrationNumber: true,
|
|
|
|
|
|
panNumber: true,
|
|
|
|
|
|
gstNumber: true,
|
|
|
|
|
|
formationDate: true,
|
|
|
|
|
|
companyTypes: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
companyTypeName: true
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
websiteUrl: true,
|
|
|
|
|
|
instagramUrl: true,
|
|
|
|
|
|
facebookUrl: true,
|
|
|
|
|
|
linkedinUrl: true,
|
|
|
|
|
|
twitterUrl: true,
|
2025-11-28 17:20:27 +05:30
|
|
|
|
HostParenetDocuments: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
filePath: true,
|
|
|
|
|
|
documentName: true,
|
|
|
|
|
|
documentTypeXid: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
documentType: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-11-28 17:20:27 +05:30
|
|
|
|
},
|
2025-11-14 15:04:01 +05:30
|
|
|
|
HostBankDetails: true,
|
2025-11-24 23:19:18 +05:30
|
|
|
|
HostDocuments: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
documentType: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-02 17:53:19 +05:30
|
|
|
|
accountManager: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
firstName: true,
|
|
|
|
|
|
lastName: true,
|
|
|
|
|
|
emailAddress: true,
|
|
|
|
|
|
mobileNumber: true,
|
|
|
|
|
|
profileImage: true,
|
|
|
|
|
|
userRefNumber: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
2025-12-02 17:53:19 +05:30
|
|
|
|
},
|
2025-11-28 17:20:27 +05:30
|
|
|
|
user: {
|
|
|
|
|
|
select: {
|
2025-11-28 16:02:30 +05:30
|
|
|
|
id: true,
|
|
|
|
|
|
emailAddress: true,
|
2026-03-25 15:16:49 +05:30
|
|
|
|
dateOfBirth: true,
|
2025-11-28 16:02:30 +05:30
|
|
|
|
firstName: true,
|
|
|
|
|
|
lastName: true,
|
|
|
|
|
|
mobileNumber: true,
|
|
|
|
|
|
profileImage: true,
|
2025-11-29 14:17:31 +05:30
|
|
|
|
userStatus: true,
|
|
|
|
|
|
userRefNumber: true,
|
2026-03-04 18:57:14 +05:30
|
|
|
|
userAddressDetails: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
address1: true,
|
|
|
|
|
|
address2: true,
|
|
|
|
|
|
locationAddress: true,
|
|
|
|
|
|
locationLat: true,
|
|
|
|
|
|
locationLong: true,
|
2026-03-05 11:25:33 +05:30
|
|
|
|
pinCode: true,
|
2026-03-04 18:57:14 +05:30
|
|
|
|
cityXid: true,
|
|
|
|
|
|
cities: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
cityName: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
stateXid: true,
|
|
|
|
|
|
states: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
stateName: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
countryXid: true,
|
2026-03-05 13:05:56 +05:30
|
|
|
|
country: {
|
2026-03-04 18:57:14 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
countryName: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
2025-11-28 16:02:30 +05:30
|
|
|
|
},
|
2025-12-01 13:26:06 +05:30
|
|
|
|
companyTypes: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
companyTypeName: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-01 19:56:21 +05:30
|
|
|
|
HostSuggestion: {
|
|
|
|
|
|
where: {
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
isreviewed: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
hostXid: true,
|
|
|
|
|
|
title: true,
|
|
|
|
|
|
comments: true,
|
|
|
|
|
|
isparent: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
2025-12-01 19:56:21 +05:30
|
|
|
|
},
|
2025-11-24 23:19:18 +05:30
|
|
|
|
countries: true,
|
|
|
|
|
|
currencies: true,
|
|
|
|
|
|
states: true,
|
|
|
|
|
|
cities: true,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
2025-11-13 14:59:50 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-14 15:04:01 +05:30
|
|
|
|
if (!host) {
|
2025-11-20 18:37:26 +05:30
|
|
|
|
// 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-13 14:59:50 +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
|
2025-11-27 17:26:33 +05:30
|
|
|
|
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) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const key = host.user.profileImage.startsWith('http')
|
|
|
|
|
|
? host.user.profileImage.split('.com/')[1]
|
2025-11-28 17:47:43 +05:30
|
|
|
|
: 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) {
|
2026-04-07 18:45:07 +05:30
|
|
|
|
const resolvedLogoUrl = await this.getValidLogoUrl(
|
|
|
|
|
|
'hostHeader',
|
|
|
|
|
|
host.id,
|
|
|
|
|
|
host.logoPath,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!resolvedLogoUrl) {
|
|
|
|
|
|
host.logoPath = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
(host as any).logoPresignedUrl = resolvedLogoUrl;
|
2025-11-28 17:20:27 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 18:08:39 +05:30
|
|
|
|
if (host.accountManager?.profileImage) {
|
2025-12-02 17:53:19 +05:30
|
|
|
|
const key = host.accountManager.profileImage.startsWith('http')
|
|
|
|
|
|
? host.accountManager.profileImage.split('.com/')[1]
|
|
|
|
|
|
: host.accountManager.profileImage;
|
|
|
|
|
|
|
|
|
|
|
|
host.accountManager.profileImage = await getPresignedUrl(bucket, key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 17:20:27 +05:30
|
|
|
|
if (host.hostParent?.length) {
|
|
|
|
|
|
const parent = host.hostParent[0]; // since you allow only 1 parent
|
|
|
|
|
|
|
|
|
|
|
|
// Parent company logo
|
|
|
|
|
|
if (parent.logoPath) {
|
2026-04-07 18:45:07 +05:30
|
|
|
|
const resolvedParentLogoUrl = await this.getValidLogoUrl(
|
|
|
|
|
|
'hostParent',
|
|
|
|
|
|
parent.id,
|
|
|
|
|
|
parent.logoPath,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!resolvedParentLogoUrl) {
|
|
|
|
|
|
parent.logoPath = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
(parent as any).logoPresignedUrl = resolvedParentLogoUrl;
|
2025-11-28 17:20:27 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parent documents
|
|
|
|
|
|
if (parent.HostParenetDocuments?.length) {
|
|
|
|
|
|
for (const doc of parent.HostParenetDocuments) {
|
|
|
|
|
|
if (doc.filePath) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const key = doc.filePath.startsWith('http')
|
|
|
|
|
|
? doc.filePath.split('.com/')[1]
|
2025-11-28 17:20:27 +05:30
|
|
|
|
: 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
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
/**
|
|
|
|
|
|
* Update the logged-in Host's personal profile details.
|
|
|
|
|
|
* Email is intentionally NOT editable here.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async updateHostProfileDetails(userId: number, input: UpdateHostProfileInput) {
|
|
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
|
|
|
|
const user = await tx.user.findUnique({
|
|
|
|
|
|
where: { id: userId, isActive: true },
|
|
|
|
|
|
select: { id: true, roleXid: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!user) throw new ApiError(404, 'User not found');
|
|
|
|
|
|
if (user.roleXid !== ROLE.HOST) throw new ApiError(403, 'Access denied.');
|
|
|
|
|
|
|
|
|
|
|
|
// 1) Update `User` (whitelist only)
|
|
|
|
|
|
const userUpdateData: any = {};
|
|
|
|
|
|
if (input.firstName !== undefined) userUpdateData.firstName = input.firstName || null;
|
|
|
|
|
|
if (input.lastName !== undefined) userUpdateData.lastName = input.lastName;
|
|
|
|
|
|
if (input.isdCode !== undefined) userUpdateData.isdCode = input.isdCode || null;
|
|
|
|
|
|
if (input.mobileNumber !== undefined) userUpdateData.mobileNumber = input.mobileNumber || null;
|
|
|
|
|
|
if (input.dateOfBirth !== undefined) userUpdateData.dateOfBirth = input.dateOfBirth;
|
|
|
|
|
|
|
|
|
|
|
|
if (Object.keys(userUpdateData).length > 0) {
|
|
|
|
|
|
await tx.user.update({
|
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
...userUpdateData,
|
|
|
|
|
|
isProfileUpdated: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2) Update/Create `UserAddressDetails` (if any address field sent)
|
|
|
|
|
|
const addressData = input.address || {};
|
|
|
|
|
|
const hasAnyAddressField = Object.values(addressData).some((v) => v !== undefined);
|
|
|
|
|
|
|
|
|
|
|
|
if (hasAnyAddressField) {
|
|
|
|
|
|
const existingAddress = await tx.userAddressDetails.findFirst({
|
|
|
|
|
|
where: { userXid: userId, isActive: true },
|
|
|
|
|
|
select: { id: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const addressUpdateData: any = {};
|
|
|
|
|
|
if (addressData.address1 !== undefined) addressUpdateData.address1 = addressData.address1;
|
|
|
|
|
|
if (addressData.address2 !== undefined) addressUpdateData.address2 = addressData.address2;
|
|
|
|
|
|
if (addressData.countryXid !== undefined) addressUpdateData.countryXid = addressData.countryXid;
|
|
|
|
|
|
if (addressData.stateXid !== undefined) addressUpdateData.stateXid = addressData.stateXid;
|
|
|
|
|
|
if (addressData.cityXid !== undefined) addressUpdateData.cityXid = addressData.cityXid;
|
|
|
|
|
|
if (addressData.pinCode !== undefined) addressUpdateData.pinCode = addressData.pinCode;
|
|
|
|
|
|
|
|
|
|
|
|
if (existingAddress) {
|
|
|
|
|
|
await tx.userAddressDetails.update({
|
|
|
|
|
|
where: { id: existingAddress.id },
|
|
|
|
|
|
data: addressUpdateData,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const required = ['address1', 'countryXid', 'stateXid', 'cityXid', 'pinCode'] as const;
|
|
|
|
|
|
const missing = required.filter((k) => addressData[k] === undefined);
|
|
|
|
|
|
|
|
|
|
|
|
if (missing.length) {
|
|
|
|
|
|
throw new ApiError(400, `Missing required address fields: ${missing.join(', ')}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await tx.userAddressDetails.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
userXid: userId,
|
|
|
|
|
|
...addressUpdateData,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3) Return updated profile snapshot (including read-only email)
|
|
|
|
|
|
const updated = await tx.user.findUnique({
|
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
firstName: true,
|
|
|
|
|
|
lastName: true,
|
|
|
|
|
|
emailAddress: true,
|
|
|
|
|
|
isdCode: true,
|
|
|
|
|
|
mobileNumber: true,
|
|
|
|
|
|
dateOfBirth: true,
|
|
|
|
|
|
profileImage: true,
|
|
|
|
|
|
isProfileUpdated: true,
|
|
|
|
|
|
userAddressDetails: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
take: 1,
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
address1: true,
|
|
|
|
|
|
address2: true,
|
|
|
|
|
|
countryXid: true,
|
|
|
|
|
|
stateXid: true,
|
|
|
|
|
|
cityXid: true,
|
|
|
|
|
|
pinCode: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
user: updated,
|
|
|
|
|
|
address: updated?.userAddressDetails?.[0] ?? null,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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> {
|
2026-02-25 15:56:18 +05:30
|
|
|
|
const trimmedOtp = (otp || '').toString().trim();
|
|
|
|
|
|
|
|
|
|
|
|
const user = await this.prisma.user.findFirst({
|
2026-01-23 17:56:46 +05:30
|
|
|
|
where: { emailAddress: email, isActive: true },
|
2025-11-12 16:03:57 +05:30
|
|
|
|
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.');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 15:56:18 +05:30
|
|
|
|
const isMatch = await bcrypt.compare(trimmedOtp, userOtp.otpCode);
|
2025-11-12 16:03:57 +05:30
|
|
|
|
|
|
|
|
|
|
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({
|
2025-12-17 16:57:18 +05:30
|
|
|
|
where: { emailAddress: emailAddress, isActive: true },
|
2025-12-04 20:01:09 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
roleXid: true,
|
2025-12-17 16:17:55 +05:30
|
|
|
|
firstName: true,
|
|
|
|
|
|
lastName: true,
|
|
|
|
|
|
emailAddress: true,
|
|
|
|
|
|
mobileNumber: true,
|
2025-12-04 20:01:09 +05:30
|
|
|
|
userPassword: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
userStatus: true,
|
|
|
|
|
|
},
|
2025-11-12 16:03:57 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!existingUser) {
|
|
|
|
|
|
throw new ApiError(404, 'User not found');
|
|
|
|
|
|
}
|
2025-12-04 20:01:09 +05:30
|
|
|
|
if (existingUser.userStatus == USER_STATUS.REJECTED) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
403,
|
|
|
|
|
|
'You are not allowed to login. Please contact minglar admin.',
|
|
|
|
|
|
);
|
2025-12-04 20:01:09 +05:30
|
|
|
|
}
|
2025-11-12 16:03:57 +05:30
|
|
|
|
|
|
|
|
|
|
if (existingUser.roleXid !== 4) {
|
|
|
|
|
|
throw new ApiError(403, 'Access denied. Not a host user.');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 17:26:33 +05:30
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-13 14:59:50 +05:30
|
|
|
|
async createMinglarUser(email: string) {
|
2025-11-12 16:03:57 +05:30
|
|
|
|
const newUser = await this.prisma.user.create({
|
2025-11-13 17:48:09 +05:30
|
|
|
|
data: { emailAddress: email, roleXid: ROLE.HOST },
|
2025-11-12 16:03:57 +05:30
|
|
|
|
});
|
|
|
|
|
|
return newUser;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 19:12:28 +05:30
|
|
|
|
async createPassword(user_xid: number, password: string): Promise<Partial<User>> {
|
2025-11-12 16:03:57 +05:30
|
|
|
|
// Find user by id
|
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
2026-04-04 19:12:28 +05:30
|
|
|
|
where: { id: user_xid, isActive: true },
|
2025-11-12 16:03:57 +05:30
|
|
|
|
select: { id: true, emailAddress: true, userPassword: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
throw new ApiError(404, 'User not found');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if password already exists
|
|
|
|
|
|
if (user.userPassword) {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
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 },
|
2025-11-27 17:26:33 +05:30
|
|
|
|
data: {
|
|
|
|
|
|
userPassword: hashedPassword,
|
|
|
|
|
|
isEmailVerfied: true,
|
|
|
|
|
|
userStatus: USER_STATUS.ACTIVE,
|
|
|
|
|
|
},
|
2025-11-12 16:03:57 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-04 19:12:28 +05:30
|
|
|
|
return user;
|
2025-11-12 16:03:57 +05:30
|
|
|
|
}
|
2025-11-12 19:59:54 +05:30
|
|
|
|
|
2025-12-02 17:27:36 +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) => {
|
2025-12-06 12:06:04 +05:30
|
|
|
|
const existingAccount = await tx.hostBankDetails.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
accountNumber: data.accountNumber,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (existingAccount) {
|
|
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
'Host account with this account number already exists.',
|
2025-12-06 12:06:04 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-11-21 13:31:41 +05:30
|
|
|
|
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: {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
stepper: STEPPER.BANK_DETAILS_UPDATED,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
currencyXid: data.currencyXid,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-11-21 13:31:41 +05:30
|
|
|
|
}
|
2025-11-14 14:08:47 +05:30
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
async getAllHostActivity(
|
|
|
|
|
|
search?: string,
|
|
|
|
|
|
user_xid?: number,
|
|
|
|
|
|
paginationOptions?: { page: number; limit: number; skip: number },
|
|
|
|
|
|
) {
|
2025-12-02 17:53:19 +05:30
|
|
|
|
const hostDetails = await this.prisma.hostHeader.findFirst({
|
2025-12-22 13:30:55 +05:30
|
|
|
|
where: { userXid: user_xid, isActive: true },
|
|
|
|
|
|
});
|
2025-12-02 17:53:19 +05:30
|
|
|
|
|
2025-12-03 13:18:28 +05:30
|
|
|
|
const whereClause: any = {
|
2025-12-02 20:10:10 +05:30
|
|
|
|
isActive: true,
|
|
|
|
|
|
hostXid: hostDetails.id,
|
|
|
|
|
|
};
|
2025-12-03 13:18:28 +05:30
|
|
|
|
if (!hostDetails) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
data: [],
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
page: paginationOptions?.page || 1,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
limit: paginationOptions?.limit || 10,
|
2025-12-03 13:18:28 +05:30
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔍 SEARCH (fixed)
|
|
|
|
|
|
if (search?.trim()) {
|
|
|
|
|
|
const term = search.trim();
|
|
|
|
|
|
|
|
|
|
|
|
whereClause.OR = [
|
|
|
|
|
|
{ activityRefNumber: { contains: term, mode: 'insensitive' } },
|
|
|
|
|
|
{ activityTitle: { contains: term, mode: 'insensitive' } },
|
|
|
|
|
|
{
|
|
|
|
|
|
activityType: {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
activityTypeName: { contains: term, mode: 'insensitive' },
|
|
|
|
|
|
},
|
2025-12-03 13:18:28 +05:30
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
2025-12-02 20:10:10 +05:30
|
|
|
|
|
|
|
|
|
|
const [hostAllActivities, totalCount] = await Promise.all([
|
|
|
|
|
|
this.prisma.activities.findMany({
|
|
|
|
|
|
where: whereClause,
|
2025-12-03 13:18:28 +05:30
|
|
|
|
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,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
frequencyName: true,
|
|
|
|
|
|
},
|
2025-12-03 13:18:28 +05:30
|
|
|
|
},
|
2025-12-02 20:10:10 +05:30
|
|
|
|
ActivityAmDetails: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
accountManager: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
firstName: true,
|
|
|
|
|
|
lastName: true,
|
|
|
|
|
|
profileImage: true,
|
|
|
|
|
|
emailAddress: true,
|
|
|
|
|
|
roleXid: true,
|
|
|
|
|
|
},
|
2025-12-02 17:53:19 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-03 13:18:28 +05:30
|
|
|
|
activityType: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityTypeName: true,
|
|
|
|
|
|
interests: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
interestName: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-02 16:19:33 +05:30
|
|
|
|
energyLevel: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
energyLevelName: true,
|
|
|
|
|
|
energyIcon: true,
|
2026-01-05 14:47:24 +05:30
|
|
|
|
energyColor: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
2025-12-03 13:18:28 +05:30
|
|
|
|
},
|
2025-12-02 17:53:19 +05:30
|
|
|
|
},
|
2025-12-02 20:10:10 +05:30
|
|
|
|
skip: paginationOptions?.skip || 0,
|
|
|
|
|
|
take: paginationOptions?.limit || 10,
|
|
|
|
|
|
orderBy: { id: 'desc' },
|
|
|
|
|
|
}),
|
|
|
|
|
|
this.prisma.activities.count({ where: whereClause }),
|
|
|
|
|
|
]);
|
2025-12-02 17:53:19 +05:30
|
|
|
|
|
|
|
|
|
|
for (const activity of hostAllActivities) {
|
|
|
|
|
|
/** 2️⃣ Process AM Profile Image */
|
|
|
|
|
|
const am = activity.ActivityAmDetails?.[0]?.accountManager;
|
|
|
|
|
|
|
|
|
|
|
|
if (am?.profileImage) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const key = am.profileImage.startsWith('http')
|
|
|
|
|
|
? am.profileImage.split('.com/')[1]
|
2025-12-02 17:53:19 +05:30
|
|
|
|
: am.profileImage;
|
|
|
|
|
|
|
|
|
|
|
|
const presignedUrl = await getPresignedUrl(bucket, key);
|
|
|
|
|
|
|
|
|
|
|
|
activity.ActivityAmDetails[0].accountManager = {
|
|
|
|
|
|
...am,
|
|
|
|
|
|
profileImage: presignedUrl,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const {
|
|
|
|
|
|
paginationService,
|
|
|
|
|
|
} = require('@/common/utils/pagination/pagination.service');
|
2025-12-02 20:10:10 +05:30
|
|
|
|
return paginationService.createPaginatedResponse(
|
|
|
|
|
|
hostAllActivities,
|
|
|
|
|
|
totalCount,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
paginationOptions || { page: 1, limit: 10, skip: 0 },
|
2025-12-02 20:10:10 +05:30
|
|
|
|
);
|
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) {
|
2026-02-26 19:45:08 +05:30
|
|
|
|
const host = await this.prisma.hostHeader.findFirst({
|
|
|
|
|
|
where: { userXid: user_xid, isActive: true },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
cities: true,
|
|
|
|
|
|
states: true,
|
|
|
|
|
|
countries: true,
|
|
|
|
|
|
companyTypes: true,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
|
|
|
|
|
if (!host) {
|
|
|
|
|
|
throw new ApiError(404, 'Host not found for this user');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const effectiveDate = host.agreementStartDate
|
|
|
|
|
|
? dayjs(host.agreementStartDate).format('DD-MMM-YY')
|
|
|
|
|
|
: dayjs().format('DD-MMM-YY');
|
|
|
|
|
|
|
|
|
|
|
|
const durationText = buildDurationText(host);
|
|
|
|
|
|
const expiryDate = buildExpiryDate(host);
|
|
|
|
|
|
const commissionText = buildCommissionText(host);
|
|
|
|
|
|
const fullAddress = buildFullAddress(host);
|
|
|
|
|
|
const acceptDate = dayjs().format('DD-MMM-YYYY');
|
|
|
|
|
|
|
|
|
|
|
|
const agreementVars = {
|
|
|
|
|
|
effectiveDate,
|
|
|
|
|
|
companyName: host.companyName,
|
|
|
|
|
|
companyType: host.companyTypes?.companyTypeName,
|
|
|
|
|
|
fullAddress,
|
|
|
|
|
|
durationText,
|
|
|
|
|
|
expiryDate,
|
|
|
|
|
|
commissionText,
|
|
|
|
|
|
acceptDate,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
let pdfUrl: string | null = null;
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
try {
|
|
|
|
|
|
const pdfBuffer = await renderAgreementPdf(agreementVars);
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
const existingCount = await this.prisma.hostAgreement.count({
|
|
|
|
|
|
where: { hostXid: host.id, isActive: true },
|
|
|
|
|
|
});
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
const nextVersionNumber = `AG${existingCount + 1}`;
|
|
|
|
|
|
const baseKey = `Documents/Host/${host.id}/agreements/${nextVersionNumber}`;
|
|
|
|
|
|
|
|
|
|
|
|
const pdfKey = `${baseKey}.pdf`;
|
|
|
|
|
|
|
|
|
|
|
|
await s3
|
|
|
|
|
|
.upload({
|
|
|
|
|
|
Bucket: config.aws.bucketName,
|
|
|
|
|
|
Key: pdfKey,
|
|
|
|
|
|
Body: pdfBuffer,
|
|
|
|
|
|
ContentType: 'application/pdf',
|
|
|
|
|
|
ACL: 'private',
|
|
|
|
|
|
})
|
|
|
|
|
|
.promise();
|
|
|
|
|
|
|
|
|
|
|
|
pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error generating or uploading PDF:', error);
|
|
|
|
|
|
// Continue without PDF - will return dynamic fields instead
|
|
|
|
|
|
}
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
try {
|
|
|
|
|
|
const existingCount = await this.prisma.hostAgreement.count({
|
2026-02-26 19:45:08 +05:30
|
|
|
|
where: { hostXid: host.id, isActive: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
const nextVersionNumber = `AG${existingCount + 1}`;
|
2026-02-26 19:45:08 +05:30
|
|
|
|
|
2026-03-04 17:04:14 +05:30
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
|
// Optional: mark previous agreements inactive
|
|
|
|
|
|
await tx.hostAgreement.updateMany({
|
|
|
|
|
|
where: { hostXid: host.id, isActive: true },
|
|
|
|
|
|
data: { isActive: false },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await tx.hostAgreement.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
hostXid: host.id,
|
|
|
|
|
|
filePath: pdfUrl,
|
|
|
|
|
|
versionNumber: nextVersionNumber,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await tx.hostHeader.update({
|
|
|
|
|
|
where: { id: host.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
stepper: STEPPER.AGREEMENT_ACCEPTED,
|
|
|
|
|
|
isApproved: true,
|
|
|
|
|
|
agreementAccepted: true,
|
|
|
|
|
|
agreementStartDate: host.agreementStartDate || new Date(),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-02-26 19:45:08 +05:30
|
|
|
|
});
|
2026-03-04 17:04:14 +05:30
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error creating host agreement record:', error);
|
|
|
|
|
|
// Continue without creating agreement record - will return dynamic fields instead
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Return dynamic fields and PDF URL
|
|
|
|
|
|
return {
|
|
|
|
|
|
filePath: pdfUrl,
|
|
|
|
|
|
dynamicFields: agreementVars,
|
|
|
|
|
|
};
|
2025-11-12 19:59:54 +05:30
|
|
|
|
}
|
2025-11-13 15:53:35 +05:30
|
|
|
|
|
2026-03-02 13:01:53 +05:30
|
|
|
|
/**
|
|
|
|
|
|
* Get the latest (active) agreement for a specific host by hostXid.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getLatestHostAgreement(hostXid: number) {
|
|
|
|
|
|
if (!hostXid || Number.isNaN(hostXid)) {
|
|
|
|
|
|
throw new ApiError(400, 'Valid hostXid is required');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 13:05:56 +05:30
|
|
|
|
const hostHeader = await this.prisma.hostHeader.findFirst({
|
|
|
|
|
|
where: { id: hostXid, isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
isCommisionBase: true,
|
|
|
|
|
|
commisionPer: true,
|
|
|
|
|
|
durationNumber: true,
|
|
|
|
|
|
durationFrequency: true,
|
|
|
|
|
|
amountPerBooking: true,
|
|
|
|
|
|
agreementStartDate: true,
|
|
|
|
|
|
payoutDurationNum: true,
|
|
|
|
|
|
payoutDurationFrequency: true,
|
|
|
|
|
|
registrationNumber: true,
|
|
|
|
|
|
companyName: true,
|
|
|
|
|
|
companyTypes: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
companyTypeName: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-02 13:01:53 +05:30
|
|
|
|
const agreement = await this.prisma.hostAgreement.findFirst({
|
|
|
|
|
|
where: { hostXid, isActive: true },
|
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
hostXid: true,
|
|
|
|
|
|
filePath: true,
|
|
|
|
|
|
versionNumber: true,
|
|
|
|
|
|
createdAt: true,
|
|
|
|
|
|
updatedAt: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-05 13:05:56 +05:30
|
|
|
|
// ❌ If both missing
|
|
|
|
|
|
if (!agreement && !hostHeader) {
|
2026-03-02 13:01:53 +05:30
|
|
|
|
throw new ApiError(404, 'No active agreement found for this host');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 13:05:56 +05:30
|
|
|
|
let presignedUrl = "";
|
2026-03-02 13:01:53 +05:30
|
|
|
|
|
2026-03-05 13:05:56 +05:30
|
|
|
|
if (agreement?.filePath) {
|
|
|
|
|
|
const key = agreement.filePath.startsWith('http')
|
|
|
|
|
|
? agreement.filePath.split('.com/')[1]
|
|
|
|
|
|
: agreement.filePath;
|
2026-03-02 13:01:53 +05:30
|
|
|
|
|
2026-03-05 13:05:56 +05:30
|
|
|
|
const bucket = config.aws.bucketName;
|
|
|
|
|
|
presignedUrl = await getPresignedUrl(bucket, key);
|
|
|
|
|
|
}
|
2026-03-02 13:01:53 +05:30
|
|
|
|
|
|
|
|
|
|
return {
|
2026-03-05 13:05:56 +05:30
|
|
|
|
hostHeader: hostHeader || null,
|
|
|
|
|
|
agreement: agreement
|
|
|
|
|
|
? {
|
|
|
|
|
|
...agreement,
|
|
|
|
|
|
presignedUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
: null
|
2026-03-02 13:01:53 +05:30
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 19:30:16 +05:30
|
|
|
|
async getPQQQuestionDetail(question_xid: number, activity_xid: number) {
|
2025-12-02 17:24:01 +05:30
|
|
|
|
const detailsOfQuestion = await this.prisma.activityPQQheader.findFirst({
|
2025-11-27 17:26:33 +05:30
|
|
|
|
where: {
|
|
|
|
|
|
activityXid: activity_xid,
|
|
|
|
|
|
pqqQuestionXid: question_xid,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
2025-11-22 19:30:16 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
pqqQuestionXid: true,
|
|
|
|
|
|
pqqAnswerXid: true,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
ActivityPQQSupportings: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityPqqHeaderXid: true,
|
|
|
|
|
|
mediaFileName: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
mediaType: true,
|
|
|
|
|
|
},
|
2025-12-02 17:24:01 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityPQQSuggestions: {
|
|
|
|
|
|
where: { isActive: true, isReviewed: false },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
title: true,
|
|
|
|
|
|
comments: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
2025-12-02 17:24:01 +05:30
|
|
|
|
},
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-02 17:24:01 +05:30
|
|
|
|
|
|
|
|
|
|
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;
|
2025-11-22 19:30:16 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 19:47:04 +05:30
|
|
|
|
async getLatestQuestionDetailsPQQ(activity_xid: number) {
|
|
|
|
|
|
return await this.prisma.activityPQQheader.findFirst({
|
2025-12-03 19:21:21 +05:30
|
|
|
|
where: {
|
|
|
|
|
|
activityXid: activity_xid,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
pqqAnswerXid: {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
not: null,
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
},
|
2025-11-24 19:19:02 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
pqqQuestionXid: true,
|
|
|
|
|
|
pqqAnswerXid: true,
|
|
|
|
|
|
pqqQuestions: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
pqqSubCategoryXid: true,
|
|
|
|
|
|
pqqSubCategories: {
|
|
|
|
|
|
select: {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
categoryXid: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-11-24 19:19:02 +05:30
|
|
|
|
},
|
2025-11-27 17:26:33 +05:30
|
|
|
|
orderBy: { id: 'desc' },
|
|
|
|
|
|
});
|
2025-11-22 19:47:04 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-16 17:34:10 +05:30
|
|
|
|
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 },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
return parents.flatMap((p) => p.HostParenetDocuments);
|
2025-12-16 17:34:10 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const parentIds = parents.map((p) => p.id);
|
2025-12-16 17:34:10 +05:30
|
|
|
|
|
|
|
|
|
|
// 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 } },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 19:05:26 +05:30
|
|
|
|
async addOrUpdateCompanyDetails(
|
2025-12-02 17:24:01 +05:30
|
|
|
|
user_xid: number,
|
|
|
|
|
|
companyData: HostCompanyDetailsInput,
|
|
|
|
|
|
documents: HostDocumentInput[],
|
|
|
|
|
|
parentCompanyData?: any | null,
|
|
|
|
|
|
parentDocuments?: HostDocumentInput[],
|
|
|
|
|
|
isDraft: boolean = false,
|
2026-04-01 15:10:10 +05:30
|
|
|
|
options?: {
|
|
|
|
|
|
deleteCompanyLogo?: boolean;
|
|
|
|
|
|
deleteParentCompanyLogo?: boolean;
|
|
|
|
|
|
},
|
2025-12-02 17:24:01 +05:30
|
|
|
|
) {
|
|
|
|
|
|
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 },
|
|
|
|
|
|
});
|
2025-12-22 13:30:55 +05:30
|
|
|
|
console.log(existingHostCompany, '-: Existing hai');
|
2025-11-25 12:02:15 +05:30
|
|
|
|
|
2025-12-04 20:18:22 +05:30
|
|
|
|
let existingParentCompany;
|
|
|
|
|
|
|
|
|
|
|
|
if (existingHostCompany) {
|
|
|
|
|
|
existingParentCompany = await tx.hostParent.findFirst({
|
|
|
|
|
|
where: { hostXid: existingHostCompany.id },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
logoPath: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-04 20:18:22 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
let hostStatusInternal;
|
|
|
|
|
|
let hostStatusDisplay;
|
|
|
|
|
|
let minglarStatusInternal;
|
|
|
|
|
|
let minglarStatusDisplay;
|
2025-12-01 20:25:23 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
if (existingHostCompany) {
|
|
|
|
|
|
hostStatusInternal = existingHostCompany.hostStatusInternal;
|
|
|
|
|
|
hostStatusDisplay = existingHostCompany.hostStatusDisplay;
|
|
|
|
|
|
minglarStatusInternal = existingHostCompany.adminStatusInternal;
|
|
|
|
|
|
minglarStatusDisplay = existingHostCompany.adminStatusDisplay;
|
|
|
|
|
|
}
|
2025-12-01 20:25:23 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
// CASE 1: Host was asked to update AND is submitting final
|
|
|
|
|
|
if (
|
|
|
|
|
|
existingHostCompany &&
|
2025-12-22 13:30:55 +05:30
|
|
|
|
existingHostCompany.hostStatusInternal ===
|
2026-01-09 18:10:38 +05:30
|
|
|
|
HOST_STATUS_INTERNAL.HOST_TO_UPDATE &&
|
2025-12-02 17:24:01 +05:30
|
|
|
|
!isDraft
|
|
|
|
|
|
) {
|
|
|
|
|
|
hostStatusInternal = HOST_STATUS_INTERNAL.HOST_SUBMITTED;
|
|
|
|
|
|
hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW;
|
|
|
|
|
|
|
|
|
|
|
|
minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW;
|
2026-03-23 19:31:19 +05:30
|
|
|
|
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.RE_SUBMITTED;
|
2025-12-02 17:24:01 +05:30
|
|
|
|
}
|
2026-02-24 12:30:18 +05:30
|
|
|
|
// CASE 2: Admin has rejected but host can resubmit
|
|
|
|
|
|
else if (
|
|
|
|
|
|
existingHostCompany &&
|
|
|
|
|
|
existingHostCompany.hostStatusInternal ===
|
|
|
|
|
|
HOST_STATUS_INTERNAL.REJECTED &&
|
|
|
|
|
|
!isDraft
|
|
|
|
|
|
) {
|
|
|
|
|
|
hostStatusInternal = HOST_STATUS_INTERNAL.HOST_SUBMITTED;
|
|
|
|
|
|
hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW;
|
|
|
|
|
|
|
|
|
|
|
|
minglarStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW;
|
|
|
|
|
|
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.RE_SUBMITTED;
|
|
|
|
|
|
}
|
2025-12-02 17:24:01 +05:30
|
|
|
|
// CASE 2: Host was asked to update BUT saving draft
|
|
|
|
|
|
else if (
|
|
|
|
|
|
existingHostCompany &&
|
2025-12-22 13:30:55 +05:30
|
|
|
|
existingHostCompany.hostStatusInternal ===
|
2026-01-09 18:10:38 +05:30
|
|
|
|
HOST_STATUS_INTERNAL.HOST_TO_UPDATE &&
|
2025-12-02 17:24:01 +05:30
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-11-28 12:07:45 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
const stepper = isDraft ? STEPPER.NOT_SUBMITTED : STEPPER.UNDER_REVIEW;
|
2025-11-25 12:02:15 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
// -------------------------------------------------------
|
|
|
|
|
|
// CREATE FLOW
|
|
|
|
|
|
// -------------------------------------------------------
|
|
|
|
|
|
if (!existingHostCompany) {
|
|
|
|
|
|
if (!isDraft) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
console.log('First time direct final submit.');
|
2025-12-02 17:24:01 +05:30
|
|
|
|
const existingByPan = await tx.hostHeader.findFirst({
|
|
|
|
|
|
where: { panNumber: companyData.panNumber },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (existingByPan)
|
2025-12-22 13:30:55 +05:30
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
'Company already exists with this pan/bin number',
|
|
|
|
|
|
);
|
2025-12-02 17:24:01 +05:30
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
console.log('First Time Aaya hai');
|
2025-12-02 17:24:01 +05:30
|
|
|
|
|
|
|
|
|
|
const createdHost = await tx.hostHeader.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
user: { connect: { id: user_xid } },
|
|
|
|
|
|
companyName: companyData.companyName,
|
|
|
|
|
|
address1: companyData.address1,
|
|
|
|
|
|
address2: companyData.address2,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
cities: companyData.cityXid
|
|
|
|
|
|
? { connect: { id: companyData.cityXid } }
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
states: companyData.stateXid
|
|
|
|
|
|
? { connect: { id: companyData.stateXid } }
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
countries: companyData.countryXid
|
|
|
|
|
|
? { connect: { id: companyData.countryXid } }
|
|
|
|
|
|
: undefined,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
pinCode: companyData.pinCode,
|
2025-12-16 13:16:22 +05:30
|
|
|
|
logoPath: companyData.logoPath || null,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
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,
|
2025-12-03 14:56:15 +05:30
|
|
|
|
referencedBy: companyData.referencedBy || null,
|
2025-12-02 17:24:01 +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,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// host documents
|
|
|
|
|
|
if (documents?.length) {
|
|
|
|
|
|
const docsData = documents.map((doc) => ({
|
|
|
|
|
|
hostXid: createdHost.id,
|
|
|
|
|
|
documentTypeXid: doc.documentTypeXid,
|
2025-12-16 13:16:22 +05:30
|
|
|
|
documentName: sanitizeDocumentName(doc.documentName),
|
2025-12-02 17:24:01 +05:30
|
|
|
|
filePath: doc.filePath,
|
|
|
|
|
|
}));
|
|
|
|
|
|
await tx.hostDocuments.createMany({ data: docsData });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// parent create
|
|
|
|
|
|
if (companyData.isSubsidairy && parentCompanyData) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
console.log('Parent ke saath aaya hai first time.');
|
2025-12-02 17:24:01 +05:30
|
|
|
|
const createdParent = await tx.hostParent.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
host: { connect: { id: createdHost.id } },
|
2025-12-16 17:34:10 +05:30
|
|
|
|
companyName: parentCompanyData.companyName || null,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
address1: parentCompanyData.address1 || null,
|
|
|
|
|
|
address2: parentCompanyData.address2 || null,
|
2025-12-16 13:16:22 +05:30
|
|
|
|
// Safely handle city connection - only connect if valid ID exists
|
2025-12-22 13:30:55 +05:30
|
|
|
|
cities:
|
|
|
|
|
|
parentCompanyData?.cityXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.cityXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.cityXid) } }
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
|
|
|
|
states:
|
|
|
|
|
|
parentCompanyData?.stateXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.stateXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.stateXid) } }
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
|
|
|
|
countries:
|
|
|
|
|
|
parentCompanyData?.countryXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.countryXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.countryXid) } }
|
|
|
|
|
|
: undefined,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
pinCode: parentCompanyData.pinCode || null,
|
2025-12-16 13:16:22 +05:30
|
|
|
|
logoPath: parentCompanyData.logoPath || null,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
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,
|
2025-12-16 13:16:22 +05:30
|
|
|
|
documentName: sanitizeDocumentName(doc.documentName),
|
2025-12-02 17:24:01 +05:30
|
|
|
|
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
|
|
|
|
});
|
2025-12-02 17:24:01 +05:30
|
|
|
|
|
|
|
|
|
|
return createdHost;
|
2025-11-13 15:53:35 +05:30
|
|
|
|
}
|
2025-11-14 14:08:47 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
// -------------------------------------------------------
|
|
|
|
|
|
// UPDATE FLOW
|
|
|
|
|
|
// -------------------------------------------------------
|
|
|
|
|
|
const updatedHost = await tx.hostHeader.update({
|
|
|
|
|
|
where: { id: existingHostCompany.id },
|
2025-11-13 15:53:35 +05:30
|
|
|
|
data: {
|
|
|
|
|
|
companyName: companyData.companyName,
|
|
|
|
|
|
address1: companyData.address1,
|
|
|
|
|
|
address2: companyData.address2,
|
2025-12-16 13:16:22 +05:30
|
|
|
|
// Safely handle city connection - only connect if valid ID exists
|
2025-12-22 13:30:55 +05:30
|
|
|
|
cities:
|
|
|
|
|
|
companyData.cityXid && !isNaN(Number(companyData.cityXid))
|
|
|
|
|
|
? { connect: { id: Number(companyData.cityXid) } }
|
|
|
|
|
|
: undefined, // Don't change if not provided
|
2025-12-16 13:16:22 +05:30
|
|
|
|
|
|
|
|
|
|
// Same for state
|
2025-12-22 13:30:55 +05:30
|
|
|
|
states:
|
|
|
|
|
|
companyData.stateXid && !isNaN(Number(companyData.stateXid))
|
|
|
|
|
|
? { connect: { id: Number(companyData.stateXid) } }
|
|
|
|
|
|
: undefined,
|
2025-12-16 13:16:22 +05:30
|
|
|
|
|
|
|
|
|
|
// Same for country
|
2025-12-22 13:30:55 +05:30
|
|
|
|
countries:
|
|
|
|
|
|
companyData.countryXid && !isNaN(Number(companyData.countryXid))
|
|
|
|
|
|
? { connect: { id: Number(companyData.countryXid) } }
|
|
|
|
|
|
: undefined,
|
2025-11-13 15:53:35 +05:30
|
|
|
|
pinCode: companyData.pinCode,
|
2026-04-01 15:10:10 +05:30
|
|
|
|
logoPath: options?.deleteCompanyLogo
|
2026-04-07 18:45:07 +05:30
|
|
|
|
? null
|
|
|
|
|
|
: resolveIncomingLogoPath(companyData.logoPath) ??
|
|
|
|
|
|
existingHostCompany.logoPath ??
|
|
|
|
|
|
null,
|
2025-11-13 15:53:35 +05:30
|
|
|
|
isSubsidairy: companyData.isSubsidairy,
|
|
|
|
|
|
registrationNumber: companyData.registrationNumber,
|
|
|
|
|
|
panNumber: companyData.panNumber,
|
2025-11-17 19:05:26 +05:30
|
|
|
|
gstNumber: companyData.gstNumber || null,
|
2025-12-02 13:40:41 +05:30
|
|
|
|
formationDate: companyData.formationDate
|
|
|
|
|
|
? new Date(companyData.formationDate as any)
|
|
|
|
|
|
: null,
|
2025-12-01 13:26:06 +05:30
|
|
|
|
companyTypes: companyData.companyTypeXid
|
|
|
|
|
|
? { connect: { id: companyData.companyTypeXid } }
|
|
|
|
|
|
: undefined,
|
2025-12-03 14:56:15 +05:30
|
|
|
|
referencedBy: companyData.referencedBy || null,
|
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,
|
2025-12-02 13:40:41 +05:30
|
|
|
|
stepper,
|
|
|
|
|
|
hostStatusInternal,
|
|
|
|
|
|
hostStatusDisplay,
|
2025-11-25 12:02:15 +05:30
|
|
|
|
adminStatusInternal: minglarStatusInternal,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
adminStatusDisplay: minglarStatusDisplay,
|
2025-11-13 15:53:35 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-11-14 14:08:47 +05:30
|
|
|
|
|
2026-01-08 20:17:01 +05:30
|
|
|
|
// // 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,
|
|
|
|
|
|
// },
|
|
|
|
|
|
// });
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
// documents handling (FINAL FIX)
|
2026-01-09 18:10:38 +05:30
|
|
|
|
if (documents?.length) {
|
|
|
|
|
|
for (const doc of documents) {
|
|
|
|
|
|
if (!doc.filePath) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// 🔹 CUSTOM DOCUMENTS → ALWAYS CREATE
|
|
|
|
|
|
if (doc.documentTypeXid === 9) {
|
|
|
|
|
|
await tx.hostDocuments.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
hostXid: updatedHost.id,
|
|
|
|
|
|
documentTypeXid: doc.documentTypeXid,
|
|
|
|
|
|
documentName: sanitizeDocumentName(doc.documentName),
|
|
|
|
|
|
filePath: doc.filePath,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-01-08 20:17:01 +05:30
|
|
|
|
|
2026-01-09 18:10:38 +05:30
|
|
|
|
// 🔹 NORMAL DOCUMENTS → UPSERT (ONE PER TYPE)
|
|
|
|
|
|
const existingDoc = await tx.hostDocuments.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
hostXid: updatedHost.id,
|
|
|
|
|
|
documentTypeXid: doc.documentTypeXid,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-08 20:17:01 +05:30
|
|
|
|
|
2026-01-09 18:10:38 +05:30
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-28 17:20:27 +05:30
|
|
|
|
|
2025-11-17 19:05:26 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
// parent logic untouched
|
|
|
|
|
|
if (companyData.isSubsidairy) {
|
|
|
|
|
|
const parentRecords = existingHostCompany.hostParent;
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const parentRecord = Array.isArray(parentRecords)
|
|
|
|
|
|
? parentRecords[0]
|
|
|
|
|
|
: parentRecords;
|
|
|
|
|
|
console.log('Yaha aaya update in the apretn me');
|
2025-12-02 17:24:01 +05:30
|
|
|
|
if (!parentRecord) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
console.log('Parent record nahi mila to create kar raha hai.');
|
2025-12-02 17:24:01 +05:30
|
|
|
|
const createdParent = await tx.hostParent.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
host: { connect: { id: updatedHost.id } },
|
2025-12-16 17:34:10 +05:30
|
|
|
|
companyName: parentCompanyData.companyName || null,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
address1: parentCompanyData.address1 || null,
|
|
|
|
|
|
address2: parentCompanyData.address2 || null,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
cities:
|
|
|
|
|
|
parentCompanyData?.cityXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.cityXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.cityXid) } }
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
|
|
|
|
states:
|
|
|
|
|
|
parentCompanyData?.stateXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.stateXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.stateXid) } }
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
|
|
|
|
countries:
|
|
|
|
|
|
parentCompanyData?.countryXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.countryXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.countryXid) } }
|
|
|
|
|
|
: undefined,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
pinCode: parentCompanyData.pinCode || null,
|
2026-04-01 15:10:10 +05:30
|
|
|
|
logoPath: options?.deleteParentCompanyLogo
|
2026-04-07 18:45:07 +05:30
|
|
|
|
? null
|
|
|
|
|
|
: resolveIncomingLogoPath(parentCompanyData?.logoPath) ??
|
|
|
|
|
|
existingParentCompany?.logoPath ??
|
2026-04-01 16:09:04 +05:30
|
|
|
|
null,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
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({
|
2025-11-28 17:20:27 +05:30
|
|
|
|
data: {
|
2025-12-02 17:24:01 +05:30
|
|
|
|
hostParentXid: createdParent.id,
|
|
|
|
|
|
documentTypeXid: doc.documentTypeXid,
|
2025-12-16 13:16:22 +05:30
|
|
|
|
documentName: sanitizeDocumentName(doc.documentName),
|
2025-11-28 17:20:27 +05:30
|
|
|
|
filePath: doc.filePath,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-02 17:24:01 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await tx.hostParent.update({
|
|
|
|
|
|
where: { id: parentRecord.id },
|
|
|
|
|
|
data: {
|
2025-12-16 17:34:10 +05:30
|
|
|
|
companyName: parentCompanyData.companyName || null,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
address1: parentCompanyData.address1 || null,
|
|
|
|
|
|
address2: parentCompanyData.address2 || null,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
cities:
|
|
|
|
|
|
parentCompanyData?.cityXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.cityXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.cityXid) } }
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
|
|
|
|
states:
|
|
|
|
|
|
parentCompanyData?.stateXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.stateXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.stateXid) } }
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
|
|
|
|
countries:
|
|
|
|
|
|
parentCompanyData?.countryXid &&
|
2026-01-09 18:10:38 +05:30
|
|
|
|
!isNaN(Number(parentCompanyData.countryXid))
|
2025-12-22 13:30:55 +05:30
|
|
|
|
? { connect: { id: Number(parentCompanyData.countryXid) } }
|
|
|
|
|
|
: undefined,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
pinCode: parentCompanyData.pinCode || null,
|
2026-04-01 15:10:10 +05:30
|
|
|
|
logoPath: options?.deleteParentCompanyLogo
|
2026-04-07 18:45:07 +05:30
|
|
|
|
? null
|
|
|
|
|
|
: resolveIncomingLogoPath(parentCompanyData?.logoPath) ??
|
|
|
|
|
|
existingParentCompany?.logoPath ??
|
2026-04-01 16:09:04 +05:30
|
|
|
|
null,
|
2025-12-02 17:24:01 +05:30
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-29 17:07:48 +05:30
|
|
|
|
// 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,
|
|
|
|
|
|
// },
|
|
|
|
|
|
// });
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
2025-12-02 17:24:01 +05:30
|
|
|
|
if (parentDocuments?.length) {
|
2025-12-29 17:07:48 +05:30
|
|
|
|
const parentDocsData = parentDocuments
|
|
|
|
|
|
.filter((doc) => doc.filePath)
|
|
|
|
|
|
.map((doc) => ({
|
|
|
|
|
|
hostParentXid: parentRecord.id,
|
|
|
|
|
|
documentTypeXid: doc.documentTypeXid,
|
|
|
|
|
|
documentName: sanitizeDocumentName(doc.documentName),
|
|
|
|
|
|
filePath: doc.filePath,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
if (parentDocsData.length) {
|
|
|
|
|
|
await tx.hostParenetDocuments.createMany({
|
|
|
|
|
|
data: parentDocsData,
|
|
|
|
|
|
});
|
2025-11-28 17:20:27 +05:30
|
|
|
|
}
|
2025-11-17 19:05:26 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-30 14:22:18 +05:30
|
|
|
|
} else {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
console.log('Last ke else block me aaya hai');
|
2025-12-02 17:24:01 +05:30
|
|
|
|
const previousParent = existingHostCompany.hostParent;
|
|
|
|
|
|
let prevParentId = null;
|
2025-11-14 14:08:47 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
if (Array.isArray(previousParent) && previousParent.length) {
|
|
|
|
|
|
prevParentId = previousParent[0].id;
|
2025-12-22 13:30:55 +05:30
|
|
|
|
} else if (
|
|
|
|
|
|
previousParent &&
|
|
|
|
|
|
typeof previousParent === 'object' &&
|
|
|
|
|
|
'id' in previousParent
|
|
|
|
|
|
) {
|
2025-12-02 17:24:01 +05:30
|
|
|
|
prevParentId = previousParent.id;
|
|
|
|
|
|
}
|
2025-11-25 12:02:15 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
if (prevParentId) {
|
|
|
|
|
|
await tx.hostParenetDocuments.deleteMany({
|
|
|
|
|
|
where: { hostParentXid: prevParentId },
|
|
|
|
|
|
});
|
|
|
|
|
|
await tx.hostParent.delete({ where: { id: prevParentId } });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-02 13:40:41 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
// ⭐ FIX — USE updatedHost instead of re-querying hostHeader
|
|
|
|
|
|
await tx.hostTrack.create({
|
2025-11-25 12:02:15 +05:30
|
|
|
|
data: {
|
2025-12-02 17:24:01 +05:30
|
|
|
|
hostXid: updatedHost.id,
|
|
|
|
|
|
updatedByRole: ROLE_NAME.HOST,
|
|
|
|
|
|
updatedByXid: user_xid,
|
|
|
|
|
|
trackStatus: updatedHost.hostStatusInternal,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-01 17:57:08 +05:30
|
|
|
|
|
2025-12-02 17:24:01 +05:30
|
|
|
|
// 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-22 11:59:48 +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,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
userRefNumber: true,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
2025-11-22 11:59:48 +05:30
|
|
|
|
},
|
|
|
|
|
|
accountManager: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
emailAddress: true,
|
|
|
|
|
|
firstName: true,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-11-22 11:59:48 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!hostDetails) {
|
|
|
|
|
|
return { hostSuggestionDetails: [], hostDetails: null };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const hostSuggestionDetails = await this.prisma.hostSuggestion.findMany({
|
2025-11-27 17:26:33 +05:30
|
|
|
|
where: { hostXid: hostDetails.id, isActive: true, isreviewed: false },
|
2025-11-22 11:59:48 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (hostSuggestionDetails) {
|
|
|
|
|
|
await this.prisma.hostSuggestion.updateMany({
|
|
|
|
|
|
where: { hostXid: hostDetails.id, isActive: true, isreviewed: false },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
isreviewed: true,
|
|
|
|
|
|
reviewedByXid: hostDetails.id,
|
|
|
|
|
|
reviewOn: new Date(),
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-11-22 11:59:48 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { hostSuggestionDetails, hostDetails };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
// 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) {
|
2025-11-27 19:16:46 +05:30
|
|
|
|
return await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
|
// 1. Get all headers for this activity (user's answers)
|
|
|
|
|
|
const answers = await this.prisma.activityPQQheader.findMany({
|
2025-12-02 17:24:01 +05:30
|
|
|
|
where: { activityXid, isActive: true },
|
2025-11-27 19:16:46 +05:30
|
|
|
|
include: {
|
|
|
|
|
|
pqqQuestions: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
pqqSubCategories: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
category: true,
|
|
|
|
|
|
},
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-11-27 19:16:46 +05:30
|
|
|
|
pqqAnswers: true,
|
2025-11-24 23:19:18 +05:30
|
|
|
|
},
|
2025-11-27 19:16:46 +05:30
|
|
|
|
});
|
2025-11-24 23:19:18 +05:30
|
|
|
|
|
2025-11-27 19:16:46 +05:30
|
|
|
|
if (!answers.length) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
overallPercentage: 0,
|
|
|
|
|
|
categoryWise: {},
|
|
|
|
|
|
};
|
2025-11-27 17:26:33 +05:30
|
|
|
|
}
|
2025-11-24 23:19:18 +05:30
|
|
|
|
|
2025-11-27 19:16:46 +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
|
|
|
|
|
2025-11-27 19:16:46 +05:30
|
|
|
|
for (const item of answers) {
|
|
|
|
|
|
const question = item.pqqQuestions;
|
2026-02-26 11:05:56 +05:30
|
|
|
|
const answer = item.pqqAnswers; // may be null if no answer selected
|
2025-11-24 23:19:18 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
// skip if question missing
|
|
|
|
|
|
if (!question) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const maxPoints = Number(question.maxPoints || 0);
|
|
|
|
|
|
const userPoints = Number(answer?.answerPoints || 0);
|
2025-11-24 23:19:18 +05:30
|
|
|
|
|
2025-11-27 19:16:46 +05:30
|
|
|
|
totalUserPoints += userPoints;
|
|
|
|
|
|
totalMaxPoints += maxPoints;
|
2025-11-24 23:19:18 +05:30
|
|
|
|
|
2026-02-26 11:05:56 +05:30
|
|
|
|
// Category (guard nested relations)
|
|
|
|
|
|
const category = question.pqqSubCategories?.category;
|
|
|
|
|
|
if (!category) continue;
|
|
|
|
|
|
|
2025-11-27 19:16:46 +05:30
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 19:16:46 +05:30
|
|
|
|
// Overall percent
|
|
|
|
|
|
const overallPercentage =
|
2025-12-22 13:30:55 +05:30
|
|
|
|
totalMaxPoints > 0
|
|
|
|
|
|
? round2((totalUserPoints / totalMaxPoints) * 100)
|
|
|
|
|
|
: 0;
|
2025-11-24 23:19:18 +05:30
|
|
|
|
|
2025-11-27 19:16:46 +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] =
|
2025-12-02 17:24:01 +05:30
|
|
|
|
c.maxPoints > 0 ? round2((c.userPoints / c.maxPoints) * 100) : 0;
|
2025-11-27 19:16:46 +05:30
|
|
|
|
}
|
2025-11-24 23:19:18 +05:30
|
|
|
|
|
2025-11-27 18:55:25 +05:30
|
|
|
|
await this.prisma.activities.update({
|
|
|
|
|
|
where: {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
id: activityXid,
|
2025-11-27 18:55:25 +05:30
|
|
|
|
},
|
|
|
|
|
|
data: {
|
2025-12-02 17:24:01 +05:30
|
|
|
|
totalScore: round2(overallPercentage),
|
|
|
|
|
|
sustainabilityScore: round2(categoryWise.Sustainability),
|
|
|
|
|
|
safetyScore: round2(categoryWise.Safety),
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-11-24 23:19:18 +05:30
|
|
|
|
|
2025-11-27 19:16:46 +05:30
|
|
|
|
// Return final score object
|
|
|
|
|
|
return {
|
|
|
|
|
|
overallPercentage,
|
|
|
|
|
|
categoryWise,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2025-11-24 23:19:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
async createHeader(
|
2025-11-19 16:55:54 +05:30
|
|
|
|
activityXid: number,
|
|
|
|
|
|
pqqQuestionXid: number,
|
|
|
|
|
|
pqqAnswerXid: number,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
comments?: string | null,
|
2025-11-19 16:55:54 +05:30
|
|
|
|
) {
|
2025-11-22 20:05:43 +05:30
|
|
|
|
return await this.prisma.activityPQQheader.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
pqqQuestionXid,
|
|
|
|
|
|
pqqAnswerXid,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
comments: comments || null, // Handle null comments
|
|
|
|
|
|
},
|
2025-11-19 16:55:54 +05:30
|
|
|
|
});
|
2025-11-22 20:05:43 +05:30
|
|
|
|
}
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
async findHeaderByCompositeKey(activityXid: number, pqqQuestionXid: number) {
|
2025-11-22 20:05:43 +05:30
|
|
|
|
return await this.prisma.activityPQQheader.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
pqqQuestionXid,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
2025-11-19 16:55:54 +05:30
|
|
|
|
});
|
2025-11-22 20:05:43 +05:30
|
|
|
|
}
|
2025-11-19 16:55:54 +05:30
|
|
|
|
|
2025-12-22 13:30:55 +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({
|
2025-11-22 20:05:43 +05:30
|
|
|
|
where: {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
id: headerId,
|
2025-11-22 20:05:43 +05:30
|
|
|
|
},
|
2025-11-19 16:55:54 +05:30
|
|
|
|
data: {
|
2025-11-22 20:05:43 +05:30
|
|
|
|
comments: comments || null, // Handle null comments
|
2025-12-02 17:24:01 +05:30
|
|
|
|
pqqAnswerXid: pqqAnswerXid,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
|
},
|
2025-11-19 16:55:54 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 17:26:33 +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,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
mediaFileName: fileUrl,
|
|
|
|
|
|
},
|
2025-11-19 16:55:54 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
async getSupportingFilesByHeaderId(headerId: number) {
|
|
|
|
|
|
return await this.prisma.activityPQQSupportings.findMany({
|
|
|
|
|
|
where: {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
activityPqqHeaderXid: headerId,
|
2025-11-22 20:05:43 +05:30
|
|
|
|
},
|
|
|
|
|
|
orderBy: {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
id: 'asc', // Maintain consistent order
|
|
|
|
|
|
},
|
2025-11-22 20:05:43 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 12:44:07 +05:30
|
|
|
|
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,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
amDisplayStatus: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-03 12:44:07 +05:30
|
|
|
|
|
|
|
|
|
|
if (!activity) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
throw new ApiError(404, 'Activity not found');
|
2025-12-02 17:24:01 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
if (
|
|
|
|
|
|
activity.activityInternalStatus == ACTIVITY_INTERNAL_STATUS.PQ_TO_UPDATE
|
|
|
|
|
|
) {
|
2025-12-03 12:44:07 +05:30
|
|
|
|
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,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.REVISED,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-02 17:24:01 +05:30
|
|
|
|
|
2025-12-03 12:44:07 +05:30
|
|
|
|
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,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
updatedOn: new Date(),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-12-03 12:44:07 +05:30
|
|
|
|
} 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,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.NEW,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-03 12:44:07 +05:30
|
|
|
|
|
|
|
|
|
|
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,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
updatedOn: new Date(),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-12-03 12:44:07 +05:30
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
});
|
2025-12-02 17:24:01 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 20:05:43 +05:30
|
|
|
|
async updateSupportingFile(
|
|
|
|
|
|
supportingFileId: number,
|
|
|
|
|
|
mimeType: string,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
fileUrl: string,
|
2025-11-22 20:05:43 +05:30
|
|
|
|
) {
|
|
|
|
|
|
return await this.prisma.activityPQQSupportings.update({
|
|
|
|
|
|
where: {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
id: supportingFileId,
|
2025-11-22 20:05:43 +05:30
|
|
|
|
},
|
|
|
|
|
|
data: {
|
|
|
|
|
|
mediaType: mimeType,
|
|
|
|
|
|
mediaFileName: fileUrl,
|
2025-11-27 17:26:33 +05:30
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
|
},
|
2025-11-22 20:05:43 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async deleteSupportingFile(supportingFileId: number) {
|
|
|
|
|
|
return await this.prisma.activityPQQSupportings.delete({
|
|
|
|
|
|
where: {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
id: supportingFileId,
|
|
|
|
|
|
},
|
2025-11-22 20:05:43 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
async markPQQSuggestionReviewed(
|
|
|
|
|
|
user_xid: number,
|
|
|
|
|
|
activityPqqHeaderXid: number,
|
|
|
|
|
|
activityPQQSuggestionId: number,
|
|
|
|
|
|
) {
|
2025-11-27 19:35:24 +05:30
|
|
|
|
return await this.prisma.activityPQQSuggestions.update({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
id: activityPQQSuggestionId,
|
|
|
|
|
|
activityPqqHeaderXid: activityPqqHeaderXid,
|
|
|
|
|
|
isActive: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
isReviewed: false,
|
2025-11-27 19:35:24 +05:30
|
|
|
|
},
|
|
|
|
|
|
data: {
|
|
|
|
|
|
isReviewed: true,
|
|
|
|
|
|
reviewedByXid: user_xid,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
reviewedOn: new Date(),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-11-27 19:35:24 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 13:43:30 +05:30
|
|
|
|
async getAllPQQQuesAndSubmittedAns(activity_xid: number) {
|
2025-11-27 17:26:33 +05:30
|
|
|
|
return await this.prisma.activityPQQheader.findMany({
|
2025-12-03 13:43:30 +05:30
|
|
|
|
where: { isActive: true, activityXid: activity_xid },
|
2025-12-03 13:56:39 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityXid: true,
|
|
|
|
|
|
pqqQuestionXid: true,
|
|
|
|
|
|
pqqAnswerXid: true,
|
|
|
|
|
|
comments: true,
|
2026-02-17 16:27:40 +05:30
|
|
|
|
activity: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityType: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityTypeName: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-11-27 19:16:46 +05:30
|
|
|
|
pqqQuestions: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
questionName: true,
|
|
|
|
|
|
maxPoints: true,
|
|
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
pqqSubCategories: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
subCategoryName: true,
|
|
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
category: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
categoryName: true,
|
|
|
|
|
|
displayOrder: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-11-27 19:16:46 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityPQQSuggestions: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
title: true,
|
|
|
|
|
|
comments: true,
|
|
|
|
|
|
isReviewed: true,
|
|
|
|
|
|
reviewedBy: true,
|
|
|
|
|
|
reviewedOn: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
2025-11-27 19:16:46 +05:30
|
|
|
|
},
|
|
|
|
|
|
pqqAnswers: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
answerName: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
answerPoints: true,
|
|
|
|
|
|
},
|
2025-11-27 19:16:46 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityPQQSupportings: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
mediaFileName: true,
|
|
|
|
|
|
mediaType: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
2025-11-27 19:16:46 +05:30
|
|
|
|
},
|
2025-11-27 17:26:33 +05:30
|
|
|
|
},
|
2025-11-22 20:05:43 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 19:25:07 +05:30
|
|
|
|
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' },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 11:34:55 +05:30
|
|
|
|
/************* ✨ Windsurf Command ⭐ *************/
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get all details of an activity and its venue.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {number} activityXid - The id of the activity to fetch.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns {Promise<object>} - The activity details with its venue.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @example
|
|
|
|
|
|
* const activity = await getAllDetailsOfActivityAndVenue(1);
|
|
|
|
|
|
* console.log(activity);
|
|
|
|
|
|
*/
|
|
|
|
|
|
/******* 88cdc2a8-b07f-4da8-972a-4e00f5399a65 *******/
|
2025-12-23 17:51:32 +05:30
|
|
|
|
async getAllDetailsOfActivityAndVenue(activityXid: number) {
|
2025-12-24 11:50:13 +05:30
|
|
|
|
const activity = await this.prisma.activities.findFirst({
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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,
|
2026-01-09 13:34:19 +05:30
|
|
|
|
inActivityAvailable: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
activityDurationMins: true,
|
2026-01-05 13:28:25 +05:30
|
|
|
|
totalScore: true,
|
2026-01-10 15:24:50 +05:30
|
|
|
|
isCheckOutSame: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
activityType: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityTypeName: true,
|
2025-12-29 17:07:48 +05:30
|
|
|
|
interests: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
interestName: true,
|
2025-12-30 14:22:18 +05:30
|
|
|
|
},
|
2025-12-29 17:07:48 +05:30
|
|
|
|
},
|
|
|
|
|
|
energyLevel: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
energyLevelName: true,
|
|
|
|
|
|
energyIcon: true,
|
|
|
|
|
|
energyColor: true,
|
2025-12-30 14:22:18 +05:30
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
2025-12-24 11:50:13 +05:30
|
|
|
|
ActivitiesMedia: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-24 11:50:13 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
mediaType: true,
|
|
|
|
|
|
mediaFileName: true,
|
2026-02-28 11:30:22 +05:30
|
|
|
|
isCoverImage: true
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
2025-12-24 11:50:13 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityVenues: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-24 11:50:13 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
venueName: true,
|
2025-12-30 12:15:16 +05:30
|
|
|
|
venueLabel: true,
|
2025-12-24 11:50:13 +05:30
|
|
|
|
venueCapacity: true,
|
|
|
|
|
|
availableSeats: true,
|
|
|
|
|
|
isMinPeopleReqMandatory: true,
|
|
|
|
|
|
minPeopleRequired: true,
|
|
|
|
|
|
minReqfullfilledBeforeMins: true,
|
|
|
|
|
|
venueDescription: true,
|
|
|
|
|
|
ActivityVenueArtifacts: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
mediaType: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
mediaFileName: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-29 17:07:48 +05:30
|
|
|
|
ActivityPrices: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
noOfSession: true,
|
|
|
|
|
|
isPackage: true,
|
|
|
|
|
|
sessionValidity: true,
|
|
|
|
|
|
sessionValidityFrequency: true,
|
2025-12-30 14:22:18 +05:30
|
|
|
|
sellPrice: true,
|
2025-12-30 16:59:40 +05:30
|
|
|
|
ActivityPriceTaxes: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
taxXid: true,
|
|
|
|
|
|
taxPer: true,
|
|
|
|
|
|
taxes: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
countryXid: true,
|
|
|
|
|
|
country: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
countryName: true,
|
|
|
|
|
|
countryCode: true,
|
2025-12-31 13:03:23 +05:30
|
|
|
|
countryFlag: true,
|
|
|
|
|
|
},
|
2025-12-30 16:59:40 +05:30
|
|
|
|
},
|
|
|
|
|
|
taxName: true,
|
|
|
|
|
|
taxPer: true,
|
2025-12-31 13:03:23 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-30 14:22:18 +05:30
|
|
|
|
},
|
2026-01-05 13:28:25 +05:30
|
|
|
|
orderBy: { createdAt: 'asc' },
|
2025-12-30 14:22:18 +05:30
|
|
|
|
},
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
2025-12-24 11:50:13 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
ActivityPickUpDetails: {
|
|
|
|
|
|
where: {
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
isPickUp: true,
|
|
|
|
|
|
locationAddress: true,
|
|
|
|
|
|
locationLat: true,
|
|
|
|
|
|
locationLong: true,
|
|
|
|
|
|
transportTotalPrice: true,
|
|
|
|
|
|
transportBasePrice: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
2025-12-30 13:39:36 +05:30
|
|
|
|
activityPickUpTransports: {
|
|
|
|
|
|
where: {
|
2025-12-30 14:22:18 +05:30
|
|
|
|
isActive: true,
|
2025-12-30 13:39:36 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
transportModeXid: true,
|
|
|
|
|
|
transportMode: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
transportModeName: true,
|
2025-12-30 14:22:18 +05:30
|
|
|
|
transportModeIcon: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-30 13:39:36 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
foodAvailable: true,
|
|
|
|
|
|
foodIsChargeable: true,
|
|
|
|
|
|
activityFoodTypes: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
foodType: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
foodTypeName: true,
|
|
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
2025-12-30 16:59:40 +05:30
|
|
|
|
ActivityFoodCost: {
|
|
|
|
|
|
where: {
|
2025-12-31 13:03:23 +05:30
|
|
|
|
isActive: true,
|
2025-12-30 16:59:40 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2025-12-31 13:03:23 +05:30
|
|
|
|
totalAmount: true,
|
|
|
|
|
|
},
|
2025-12-30 16:59:40 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
activityCuisines: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
foodCuisine: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
cuisineName: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
alcoholAvailable: true,
|
|
|
|
|
|
trainerAvailable: true,
|
|
|
|
|
|
trainerIsChargeable: true,
|
|
|
|
|
|
ActivityTrainers: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
totalAmount: true,
|
|
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityNavigationModes: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2026-03-02 19:05:08 +05:30
|
|
|
|
navigationModeName: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
isInActivityChargeable: true,
|
|
|
|
|
|
navigationModesTotalPrice: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
equipmentAvailable: true,
|
|
|
|
|
|
equipmentIsChargeable: true,
|
|
|
|
|
|
ActivityEquipments: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
equipmentName: true,
|
|
|
|
|
|
isEquipmentChargeable: true,
|
|
|
|
|
|
equipmentTotalPrice: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityOtherDetails: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
exclusiveNotes: true,
|
|
|
|
|
|
dosNotes: true,
|
|
|
|
|
|
dontsNotes: true,
|
|
|
|
|
|
tipsNotes: true,
|
|
|
|
|
|
termsAndCondition: true,
|
2026-02-28 11:30:22 +05:30
|
|
|
|
Cancellations: true,
|
|
|
|
|
|
SafetyInstruction: true
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityEligibility: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
isAgeRestriction: true,
|
2025-12-24 17:10:54 +05:30
|
|
|
|
// ageRestriction: {
|
|
|
|
|
|
// select: {
|
|
|
|
|
|
// id: true,
|
|
|
|
|
|
// ageRestrictionName: true,
|
|
|
|
|
|
// minAge: true,
|
|
|
|
|
|
// maxAge: true,
|
|
|
|
|
|
// },
|
|
|
|
|
|
// },
|
|
|
|
|
|
ageRestrictionName: true,
|
|
|
|
|
|
ageEntered: true,
|
|
|
|
|
|
minAge: true,
|
|
|
|
|
|
maxAge: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
isWeightRestriction: true,
|
|
|
|
|
|
weightRestrictionName: true,
|
|
|
|
|
|
weightEntered: true,
|
|
|
|
|
|
weightIn: true,
|
|
|
|
|
|
minWeight: true,
|
|
|
|
|
|
maxWeight: true,
|
|
|
|
|
|
isHeightRestriction: true,
|
|
|
|
|
|
heightRestrictionName: true,
|
|
|
|
|
|
heightEntered: true,
|
|
|
|
|
|
heightIn: true,
|
|
|
|
|
|
minHeight: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
maxHeight: true,
|
|
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityAllowedEntry: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
allowedEntryType: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
allowedEntryTypeName: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
2026-01-07 12:11:09 +05:30
|
|
|
|
frequency: {
|
|
|
|
|
|
where: {
|
|
|
|
|
|
isActive: true
|
|
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
frequencyName: true
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
ActivityAmenities: {
|
|
|
|
|
|
where: {
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isActive: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
amenities: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
amenitiesName: true,
|
2026-02-13 11:34:55 +05:30
|
|
|
|
amenitiesIcon: true
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-23 17:51:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
cancellationAvailable: true,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
cancellationAllowedBeforeMins: true,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
// accountManager: {
|
|
|
|
|
|
// select: {
|
|
|
|
|
|
// id: true,
|
|
|
|
|
|
// firstName: true,
|
|
|
|
|
|
// lastName: true,
|
|
|
|
|
|
// emailAddress: true,
|
|
|
|
|
|
// mobileNumber: true,
|
|
|
|
|
|
// }
|
|
|
|
|
|
// },
|
2026-01-02 16:19:33 +05:30
|
|
|
|
host: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
companyName: true,
|
|
|
|
|
|
stepper: true,
|
|
|
|
|
|
adminStatusDisplay: true,
|
|
|
|
|
|
adminStatusInternal: true,
|
|
|
|
|
|
user: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
userRefNumber: true,
|
|
|
|
|
|
firstName: true,
|
2026-01-05 14:47:24 +05:30
|
|
|
|
lastName: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-24 15:11:37 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-24 11:50:13 +05:30
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 11:34:55 +05:30
|
|
|
|
if (Array.isArray(activity.ActivityAmenities)) {
|
|
|
|
|
|
for (const item of activity.ActivityAmenities) {
|
|
|
|
|
|
|
|
|
|
|
|
const filePath = item?.amenities?.amenitiesIcon;
|
|
|
|
|
|
if (!filePath) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Robust S3 key extraction
|
|
|
|
|
|
const key = filePath.startsWith('http')
|
|
|
|
|
|
? new URL(filePath).pathname.replace(/^\/+/, '')
|
|
|
|
|
|
: filePath;
|
|
|
|
|
|
|
|
|
|
|
|
(item.amenities as any).presignedUrl = await getPresignedUrl(bucket, key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-24 11:50:13 +05:30
|
|
|
|
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;
|
2025-12-23 17:51:32 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 12:15:48 +05:30
|
|
|
|
// 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');
|
2025-12-02 13:42:14 +05:30
|
|
|
|
|
2026-02-27 12:15:48 +05:30
|
|
|
|
// // 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');
|
|
|
|
|
|
// }
|
2025-12-02 13:42:14 +05:30
|
|
|
|
|
2026-02-27 12:15:48 +05:30
|
|
|
|
// // Generate reference number
|
|
|
|
|
|
// const referenceNumber = await generateActivityRefNumber(tx);
|
2025-11-22 20:48:45 +05:30
|
|
|
|
|
2026-02-27 12:15:48 +05:30
|
|
|
|
// // 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,
|
|
|
|
|
|
// },
|
|
|
|
|
|
// });
|
2025-12-02 13:42:14 +05:30
|
|
|
|
|
2026-02-27 12:15:48 +05:30
|
|
|
|
// return created;
|
|
|
|
|
|
// });
|
|
|
|
|
|
// }
|
2025-12-02 13:42:14 +05:30
|
|
|
|
|
2025-12-03 19:21:21 +05:30
|
|
|
|
async createActivityAndAllQuestionsEntry(
|
|
|
|
|
|
userId: number,
|
|
|
|
|
|
activityTypeXid: number,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
frequenciesXid: number,
|
2025-12-03 19:21:21 +05:30
|
|
|
|
) {
|
|
|
|
|
|
return await this.prisma.$transaction(async (tx) => {
|
2026-02-27 17:24:39 +05:30
|
|
|
|
|
|
|
|
|
|
const hostUserDetail = await tx.user.findFirst({
|
2026-02-27 19:03:28 +05:30
|
|
|
|
where: { id: userId, isActive: true },
|
2026-02-27 17:24:39 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
userRefNumber: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-27 19:03:28 +05:30
|
|
|
|
if (!hostUserDetail) {
|
2026-02-27 17:24:39 +05:30
|
|
|
|
throw new ApiError(404, 'User not found');
|
|
|
|
|
|
}
|
2025-12-03 19:21:21 +05:30
|
|
|
|
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({
|
2026-02-27 19:03:28 +05:30
|
|
|
|
where: { id: activityTypeXid, isActive: true },
|
2026-02-27 17:24:39 +05:30
|
|
|
|
include: {
|
2026-02-27 19:03:28 +05:30
|
|
|
|
interests: true, // ✅ correct
|
|
|
|
|
|
energyLevel: true, // ✅ this is correct already
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
});
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 17:24:39 +05:30
|
|
|
|
const referenceNumber = await generateActivityRefNumber(tx, host.id, activityTypeXid, hostUserDetail.userRefNumber);
|
2025-12-03 19:21:21 +05:30
|
|
|
|
|
|
|
|
|
|
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({
|
2025-12-04 08:33:21 +05:30
|
|
|
|
where: {
|
|
|
|
|
|
activityXid: created.id,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
select: {
|
2025-12-04 08:33:21 +05:30
|
|
|
|
comments: true,
|
|
|
|
|
|
pqqAnswerXid: true,
|
2025-12-03 19:21:21 +05:30
|
|
|
|
pqqQuestions: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
questionName: true,
|
|
|
|
|
|
maxPoints: true,
|
|
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
pqqSubCategories: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
subCategoryName: true,
|
|
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
category: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
categoryName: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
},
|
2025-12-04 08:33:21 +05:30
|
|
|
|
|
|
|
|
|
|
// 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION
|
|
|
|
|
|
PQQAnswers: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
answerName: true,
|
|
|
|
|
|
answerPoints: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
displayOrder: true,
|
2025-12-04 08:33:21 +05:30
|
|
|
|
},
|
2025-12-22 13:30:55 +05:30
|
|
|
|
orderBy: { displayOrder: 'asc' },
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityPQQSuggestions: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
title: true,
|
|
|
|
|
|
comments: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
activityPqqHeaderXid: true,
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityPQQSupportings: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
mediaType: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
mediaFileName: true,
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-22 13:30:55 +05:30
|
|
|
|
orderBy: { id: 'asc' },
|
2025-12-03 19:21:21 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- GROUPING ------------------
|
2025-12-04 08:33:21 +05:30
|
|
|
|
const grouped: any = {};
|
2025-12-03 19:21:21 +05:30
|
|
|
|
|
|
|
|
|
|
for (const item of pqqHeaderData) {
|
|
|
|
|
|
const q = item.pqqQuestions;
|
|
|
|
|
|
const sub = q.pqqSubCategories;
|
|
|
|
|
|
const cat = sub.category;
|
|
|
|
|
|
|
2025-12-04 08:33:21 +05:30
|
|
|
|
// 1️⃣ Category level
|
2025-12-03 19:21:21 +05:30
|
|
|
|
if (!grouped[cat.id]) {
|
|
|
|
|
|
grouped[cat.id] = {
|
|
|
|
|
|
id: cat.id,
|
|
|
|
|
|
categoryName: cat.categoryName,
|
|
|
|
|
|
displayOrder: cat.displayOrder,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
pqqsubCategories: [],
|
2025-12-03 19:21:21 +05:30
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const category = grouped[cat.id];
|
|
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
let subCat = category.pqqsubCategories.find(
|
|
|
|
|
|
(s: any) => s.id === sub.id,
|
|
|
|
|
|
);
|
2025-12-03 19:21:21 +05:30
|
|
|
|
if (!subCat) {
|
|
|
|
|
|
subCat = {
|
|
|
|
|
|
id: sub.id,
|
|
|
|
|
|
subCategoryName: sub.subCategoryName,
|
|
|
|
|
|
displayOrder: sub.displayOrder,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
questions: [],
|
2025-12-03 19:21:21 +05:30
|
|
|
|
};
|
|
|
|
|
|
category.pqqsubCategories.push(subCat);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
subCat.questions.push({
|
|
|
|
|
|
id: q.id,
|
|
|
|
|
|
questionName: q.questionName,
|
|
|
|
|
|
maxPoints: q.maxPoints,
|
2025-12-04 08:33:21 +05:30
|
|
|
|
comments: item.comments || null,
|
2025-12-03 19:21:21 +05:30
|
|
|
|
displayOrder: q.displayOrder,
|
2025-12-04 08:33:21 +05:30
|
|
|
|
allAnswerOptions: q.PQQAnswers || [],
|
2025-12-03 19:21:21 +05:30
|
|
|
|
suggestions: item.ActivityPQQSuggestions || [],
|
|
|
|
|
|
supportings: item.ActivityPQQSupportings || [],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const sortedCategories: any = Object.values(grouped).sort(
|
|
|
|
|
|
(a: any, b: any) => a.displayOrder - b.displayOrder,
|
|
|
|
|
|
);
|
2025-12-03 19:21:21 +05:30
|
|
|
|
|
|
|
|
|
|
for (const cat of sortedCategories) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
cat.pqqsubCategories.sort(
|
|
|
|
|
|
(a: any, b: any) => a.displayOrder - b.displayOrder,
|
|
|
|
|
|
);
|
2025-12-04 08:33:21 +05:30
|
|
|
|
|
2025-12-03 19:21:21 +05:30
|
|
|
|
for (const sub of cat.pqqsubCategories) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
sub.questions.sort(
|
|
|
|
|
|
(a: any, b: any) => a.displayOrder - b.displayOrder,
|
|
|
|
|
|
);
|
2025-12-03 19:21:21 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 18:40:41 +05:30
|
|
|
|
return {
|
|
|
|
|
|
activity_xid: created.id,
|
2026-02-17 16:27:40 +05:30
|
|
|
|
activityType: activityType,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
sortedCategories,
|
2025-12-04 18:40:41 +05:30
|
|
|
|
};
|
2025-12-03 19:21:21 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 19:37:32 +05:30
|
|
|
|
/**
|
|
|
|
|
|
* 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 +
|
2025-12-22 13:30:55 +05:30
|
|
|
|
* ActivityAmenities + ActivityEligibility
|
2025-12-18 19:37:32 +05:30
|
|
|
|
*/
|
2025-12-24 19:04:51 +05:30
|
|
|
|
async createOrUpdateActivity(
|
|
|
|
|
|
userId: number,
|
|
|
|
|
|
payload: CreateActivityInput,
|
|
|
|
|
|
isDraft: boolean,
|
|
|
|
|
|
) {
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* =====================================================
|
|
|
|
|
|
* HELPERS
|
|
|
|
|
|
* ===================================================== */
|
|
|
|
|
|
const toBool = (v: any) =>
|
|
|
|
|
|
v === true || v === 'true' || v === 1 || v === '1';
|
|
|
|
|
|
|
2026-01-05 13:28:25 +05:30
|
|
|
|
const toBoolOrNull = (v: any): boolean | null => {
|
|
|
|
|
|
if (v === null || v === undefined || v === '') return null;
|
|
|
|
|
|
return v === true || v === 'true' || v === 1 || v === '1';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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: [] };
|
|
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
const totalTaxPer = taxes.reduce((s, t) => s + Number(t.taxPer || 0), 0);
|
|
|
|
|
|
const basePrice = round2(sellPrice / (1 + totalTaxPer / 100));
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
return {
|
|
|
|
|
|
basePrice,
|
|
|
|
|
|
taxDetails: taxes.map((t) => ({
|
|
|
|
|
|
taxXid: t.id,
|
|
|
|
|
|
taxPer: t.taxPer,
|
|
|
|
|
|
taxAmount: round2(basePrice * (t.taxPer / 100)),
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
2025-12-21 17:28:08 +05:30
|
|
|
|
};
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2025-12-24 17:07:11 +05:30
|
|
|
|
/* =====================================================
|
|
|
|
|
|
* DURATION CONVERSION
|
|
|
|
|
|
* ===================================================== */
|
|
|
|
|
|
const durationDays = Number(payload.durationDays ?? 0);
|
|
|
|
|
|
const durationHours = Number(payload.durationHours ?? 0);
|
|
|
|
|
|
const durationMins = Number(payload.durationMins ?? 0);
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* =====================================================
|
|
|
|
|
|
* BASIC GUARDS
|
|
|
|
|
|
* ===================================================== */
|
|
|
|
|
|
if (!payload.activityXid) {
|
|
|
|
|
|
throw new ApiError(400, 'activityXid is required');
|
2025-12-21 17:28:08 +05:30
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2026-01-09 18:10:38 +05:30
|
|
|
|
payload.venues = Array.isArray(payload.venues)
|
|
|
|
|
|
? payload.venues
|
|
|
|
|
|
.filter(v => v && typeof v === 'object')
|
|
|
|
|
|
.map(v => ({
|
|
|
|
|
|
...v,
|
|
|
|
|
|
venueName: v.venueName ?? null,
|
|
|
|
|
|
venueLabel: v.venueLabel ?? null,
|
|
|
|
|
|
prices: Array.isArray(v.prices) ? v.prices : [],
|
|
|
|
|
|
media: Array.isArray(v.media) ? v.media : [],
|
|
|
|
|
|
}))
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* =====================================================
|
|
|
|
|
|
* HARD NORMALIZATION (SERVICE-LEVEL)
|
|
|
|
|
|
* ===================================================== */
|
2026-01-05 13:28:25 +05:30
|
|
|
|
payload.foodAvailable = toBoolOrNull(payload.foodAvailable);
|
|
|
|
|
|
payload.alcoholAvailable = toBoolOrNull(payload.alcoholAvailable);
|
|
|
|
|
|
payload.trainerAvailable = toBoolOrNull(payload.trainerAvailable);
|
|
|
|
|
|
payload.pickUpDropAvailable = toBoolOrNull(payload.pickUpDropAvailable);
|
|
|
|
|
|
payload.inActivityAvailable = toBoolOrNull(payload.inActivityAvailable);
|
|
|
|
|
|
payload.equipmentAvailable = toBoolOrNull(payload.equipmentAvailable);
|
|
|
|
|
|
payload.cancellationAvailable = toBoolOrNull(payload.cancellationAvailable);
|
2025-12-23 17:51:32 +05:30
|
|
|
|
payload.isInstantBooking = toBool(payload.isInstantBooking);
|
|
|
|
|
|
payload.isCheckOutSame = toBool(payload.isCheckOutSame);
|
2026-01-05 13:28:25 +05:30
|
|
|
|
payload.alcoholAvailable = toBoolOrNull(payload.alcoholAvailable);
|
2025-12-23 17:51:32 +05:30
|
|
|
|
|
|
|
|
|
|
payload.trainerTotalAmount = toNumber(payload.trainerTotalAmount);
|
2025-12-31 13:01:08 +05:30
|
|
|
|
payload.cancellationAllowedBeforeMins = toNumber(
|
2025-12-31 13:03:23 +05:30
|
|
|
|
payload.cancellationAllowedBeforeMins,
|
|
|
|
|
|
);
|
2025-12-31 13:01:08 +05:30
|
|
|
|
|
2025-12-31 13:03:23 +05:30
|
|
|
|
/* =====================================================
|
|
|
|
|
|
* CANCELLATION VALIDATION (NO CONVERSION)
|
|
|
|
|
|
* ===================================================== */
|
|
|
|
|
|
if (payload.cancellationAvailable) {
|
2026-01-08 12:14:12 +05:30
|
|
|
|
if (!isDraft) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
typeof payload.cancellationAllowedBeforeMins !== 'number' ||
|
|
|
|
|
|
Number.isNaN(payload.cancellationAllowedBeforeMins) ||
|
|
|
|
|
|
payload.cancellationAllowedBeforeMins <= 0
|
|
|
|
|
|
) {
|
|
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
'cancellationAllowedBeforeMins must be a positive number (in minutes)',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-12-31 13:03:23 +05:30
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
delete payload.cancellationAllowedBeforeMins;
|
2025-12-31 13:01:08 +05:30
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2026-01-08 23:01:09 +05:30
|
|
|
|
const trainerIsChargeable = toBool(payload.trainerIsChargeable);
|
|
|
|
|
|
const pickUpDropIsChargeable = toBool(payload.pickUpDropIsChargeable);
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.trainerAvailable && trainerIsChargeable) {
|
2026-01-08 12:14:12 +05:30
|
|
|
|
if (!isDraft) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
typeof payload.trainerTotalAmount !== 'number' ||
|
|
|
|
|
|
Number.isNaN(payload.trainerTotalAmount) ||
|
|
|
|
|
|
payload.trainerTotalAmount <= 0
|
|
|
|
|
|
) {
|
|
|
|
|
|
throw new ApiError(400, 'trainerTotalAmount must be > 0');
|
|
|
|
|
|
}
|
2025-12-23 17:51:32 +05:30
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-01-08 23:01:09 +05:30
|
|
|
|
// If trainer cost is not chargeable, default the amount to 0
|
|
|
|
|
|
payload.trainerTotalAmount = 0;
|
2025-12-21 17:28:08 +05:30
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (payload.venues && !Array.isArray(payload.venues)) {
|
|
|
|
|
|
throw new ApiError(400, 'venues must be an array');
|
2025-12-21 17:28:08 +05:30
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
payload.venues?.forEach((v, idx) => {
|
|
|
|
|
|
v.isMinPeopleReqMandatory = toBool(v.isMinPeopleReqMandatory);
|
2025-12-21 17:28:08 +05:30
|
|
|
|
|
2026-01-08 12:14:12 +05:30
|
|
|
|
if (!isDraft) {
|
|
|
|
|
|
if (!v.venueName) {
|
|
|
|
|
|
throw new ApiError(400, `venues[${idx}] venueName required`);
|
|
|
|
|
|
}
|
2025-12-23 17:51:32 +05:30
|
|
|
|
|
2026-01-08 12:14:12 +05:30
|
|
|
|
if (v.isMinPeopleReqMandatory && !v.minPeopleRequired) {
|
|
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
`venues[${idx}] min people requirement missing`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2026-01-08 12:14:12 +05:30
|
|
|
|
if (!Array.isArray(v.prices) || !v.prices.length) {
|
|
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
`venues[${idx}] must have at least one price`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-12-23 17:51:32 +05:30
|
|
|
|
}
|
2025-12-21 17:28:08 +05:30
|
|
|
|
});
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* =====================================================
|
|
|
|
|
|
* ROOT TAX
|
|
|
|
|
|
* ===================================================== */
|
|
|
|
|
|
const taxIds = Array.isArray(payload.taxXids)
|
|
|
|
|
|
? payload.taxXids.map(Number)
|
|
|
|
|
|
: [];
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
const rootTaxes =
|
|
|
|
|
|
taxIds.length > 0
|
|
|
|
|
|
? await this.prisma.taxes.findMany({
|
2026-01-09 18:10:38 +05:30
|
|
|
|
where: { id: { in: taxIds }, isActive: true },
|
|
|
|
|
|
select: { id: true, taxPer: true },
|
|
|
|
|
|
})
|
2025-12-23 17:51:32 +05:30
|
|
|
|
: [];
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (taxIds.length !== rootTaxes.length) {
|
|
|
|
|
|
throw new ApiError(400, 'Invalid or inactive tax provided');
|
2025-12-22 13:30:55 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 17:10:54 +05:30
|
|
|
|
const eligibility = payload.eligibility;
|
|
|
|
|
|
|
|
|
|
|
|
if (eligibility?.isAgeRestriction) {
|
|
|
|
|
|
if (eligibility.ageRestrictionName == RESTRICTION_NAME.ABOVE) {
|
|
|
|
|
|
eligibility.minAge = toNumber(eligibility.ageEntered);
|
|
|
|
|
|
eligibility.maxAge = 150;
|
|
|
|
|
|
} else if (eligibility.ageRestrictionName == RESTRICTION_NAME.BELOW) {
|
|
|
|
|
|
eligibility.maxAge = toNumber(eligibility.ageEntered);
|
|
|
|
|
|
eligibility.minAge = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (eligibility?.isWeightRestriction) {
|
|
|
|
|
|
if (eligibility.weightRestrictionName == RESTRICTION_NAME.ABOVE) {
|
|
|
|
|
|
eligibility.minWeight = toNumber(eligibility.weightEntered);
|
|
|
|
|
|
eligibility.maxWeight = 400;
|
|
|
|
|
|
} else if (eligibility.weightRestrictionName == RESTRICTION_NAME.BELOW) {
|
|
|
|
|
|
eligibility.maxWeight = toNumber(eligibility.weightEntered);
|
|
|
|
|
|
eligibility.minWeight = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (eligibility?.isHeightRestriction) {
|
|
|
|
|
|
if (eligibility.heightRestrictionName == RESTRICTION_NAME.ABOVE) {
|
|
|
|
|
|
eligibility.minHeight = toNumber(eligibility.heightEntered);
|
|
|
|
|
|
eligibility.maxHeight = 250;
|
|
|
|
|
|
} else if (eligibility.heightRestrictionName == RESTRICTION_NAME.BELOW) {
|
|
|
|
|
|
eligibility.maxHeight = toNumber(eligibility.heightEntered);
|
|
|
|
|
|
eligibility.minHeight = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* =====================================================
|
|
|
|
|
|
* TRANSACTION
|
|
|
|
|
|
* ===================================================== */
|
|
|
|
|
|
return await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣ HOST
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
const host = await tx.hostHeader.findFirst({
|
|
|
|
|
|
where: { userXid: userId, isActive: true },
|
2025-12-22 13:30:55 +05:30
|
|
|
|
});
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (!host) throw new ApiError(404, 'Host not found');
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 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');
|
2025-12-18 19:37:32 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 14:42:09 +05:30
|
|
|
|
const normalizedActivityTitle =
|
|
|
|
|
|
typeof payload.activityTitle === 'string'
|
|
|
|
|
|
? payload.activityTitle.trim()
|
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
|
|
if (normalizedActivityTitle) {
|
|
|
|
|
|
payload.activityTitle = normalizedActivityTitle;
|
|
|
|
|
|
|
|
|
|
|
|
const duplicateActivity = await tx.activities.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
id: { not: existingActivity.id },
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
activityTitle: {
|
|
|
|
|
|
equals: normalizedActivityTitle,
|
|
|
|
|
|
mode: 'insensitive',
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
select: { id: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (duplicateActivity) {
|
|
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
'Same activity name already exists. Please choose a different name.',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 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;
|
2026-01-07 19:34:41 +05:30
|
|
|
|
amDisplayStatus = ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_REVISED;
|
2025-12-23 17:51:32 +05:30
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (isDraft) {
|
|
|
|
|
|
activityInternalStatus = ACTIVITY_INTERNAL_STATUS.ACTIVITY_DRAFT;
|
|
|
|
|
|
activityDisplayStatus = ACTIVITY_DISPLAY_STATUS.ACTIVITY_DRAFT;
|
2026-01-07 19:34:41 +05:30
|
|
|
|
amInternalStatus = existingActivity.amInternalStatus;
|
|
|
|
|
|
amDisplayStatus = existingActivity.amDisplayStatus;
|
2025-12-23 17:51:32 +05:30
|
|
|
|
} else {
|
|
|
|
|
|
activityInternalStatus = ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED;
|
|
|
|
|
|
activityDisplayStatus = ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW;
|
2026-01-07 19:34:41 +05:30
|
|
|
|
amInternalStatus = ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_SUBMITED;
|
2025-12-23 17:51:32 +05:30
|
|
|
|
amDisplayStatus = ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-21 17:28:08 +05:30
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 🌍 RESOLVE CHECK-IN LOCATION
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
const checkInCountryXid = await findOrCreateCountry(
|
|
|
|
|
|
tx,
|
|
|
|
|
|
payload.checkInCountryName,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const checkInStateXid = await findOrCreateState(
|
|
|
|
|
|
tx,
|
|
|
|
|
|
payload.checkInStateName,
|
|
|
|
|
|
checkInCountryXid,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const checkInCityXid = await findOrCreateCity(
|
|
|
|
|
|
tx,
|
|
|
|
|
|
payload.checkInCityName,
|
|
|
|
|
|
checkInStateXid,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 🌍 RESOLVE CHECK-OUT LOCATION
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
const checkOutCountryXid = await findOrCreateCountry(
|
|
|
|
|
|
tx,
|
|
|
|
|
|
payload.checkOutCountryName,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const checkOutStateXid = await findOrCreateState(
|
|
|
|
|
|
tx,
|
|
|
|
|
|
payload.checkOutStateName,
|
|
|
|
|
|
checkOutCountryXid,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const checkOutCityXid = await findOrCreateCity(
|
|
|
|
|
|
tx,
|
|
|
|
|
|
payload.checkOutCityName,
|
|
|
|
|
|
checkOutStateXid,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 4️⃣ UPDATE ACTIVITY CORE + FLAGS
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
const activity = await tx.activities.update({
|
|
|
|
|
|
where: { id: existingActivity.id },
|
2025-12-21 17:28:08 +05:30
|
|
|
|
data: {
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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,
|
|
|
|
|
|
|
2025-12-29 19:03:11 +05:30
|
|
|
|
// energyLevelXid: payload.energyLevelXid ?? undefined,
|
2026-01-06 14:17:39 +05:30
|
|
|
|
activityDurationMins: durationMins ?? undefined,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
|
|
|
|
|
|
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,
|
2025-12-31 13:03:23 +05:30
|
|
|
|
cancellationAllowedBeforeMins: payload.cancellationAvailable
|
|
|
|
|
|
? payload.cancellationAllowedBeforeMins
|
|
|
|
|
|
: null,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
|
|
|
|
|
|
activityInternalStatus,
|
|
|
|
|
|
activityDisplayStatus,
|
|
|
|
|
|
amInternalStatus,
|
|
|
|
|
|
amDisplayStatus,
|
2026-02-04 15:32:11 +05:30
|
|
|
|
checkInCountryXid: checkInCountryXid ?? undefined,
|
|
|
|
|
|
checkInStateXid: checkInStateXid ?? undefined,
|
|
|
|
|
|
checkInCityXid: checkInCityXid ?? undefined,
|
|
|
|
|
|
|
|
|
|
|
|
checkOutCountryXid: checkOutCountryXid ?? undefined,
|
|
|
|
|
|
checkOutStateXid: checkOutStateXid ?? undefined,
|
|
|
|
|
|
checkOutCityXid: checkOutCityXid ?? undefined,
|
2025-12-18 19:37:32 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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',
|
2025-12-22 13:30:55 +05:30
|
|
|
|
mediaFileName: m.mediaFileName,
|
2026-02-25 13:34:03 +05:30
|
|
|
|
isCoverImage: m.isCoverImage ?? false,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
displayOrder: index + 1,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 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 } },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-21 17:28:08 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
2026-01-08 15:18:06 +05:30
|
|
|
|
* 8️⃣ CREATE VENUES WITH MEDIA & PRICES (DRAFT SAFE)
|
2025-12-23 17:51:32 +05:30
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
for (const venue of payload.venues ?? []) {
|
|
|
|
|
|
const venueRow = await tx.activityVenues.create({
|
2025-12-21 17:28:08 +05:30
|
|
|
|
data: {
|
2025-12-23 17:51:32 +05:30
|
|
|
|
activityXid,
|
2026-01-09 18:10:38 +05:30
|
|
|
|
venueName: venue.venueName ?? null,
|
|
|
|
|
|
venueLabel: venue.venueLabel ?? null,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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,
|
2025-12-21 17:28:08 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
// 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,
|
2025-12-21 17:28:08 +05:30
|
|
|
|
})),
|
|
|
|
|
|
});
|
2025-12-18 19:37:32 +05:30
|
|
|
|
}
|
2025-12-23 17:51:32 +05:30
|
|
|
|
|
|
|
|
|
|
// Create venue prices with taxes
|
|
|
|
|
|
for (const price of venue.prices ?? []) {
|
|
|
|
|
|
const sellPrice = Number(price.sellPrice);
|
2026-01-08 15:18:06 +05:30
|
|
|
|
|
|
|
|
|
|
// On submit enforce > 0, on draft just skip invalid
|
|
|
|
|
|
if (!isDraft) {
|
|
|
|
|
|
if (!sellPrice || sellPrice <= 0) {
|
|
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
'sellPrice must be > 0 for submitted activities',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!sellPrice || sellPrice <= 0) continue;
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
sessionValidityFrequency:
|
|
|
|
|
|
price.sessionValidityFrequency ?? 'Days',
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
}
|
2025-12-24 19:04:51 +05:30
|
|
|
|
/* 8.1️⃣ CLEAN & CREATE FOOD COST (if chargeable) */
|
|
|
|
|
|
const oldFoodCostIds = (
|
|
|
|
|
|
await tx.activityFoodCost.findMany({
|
|
|
|
|
|
where: { activityXid },
|
|
|
|
|
|
select: { id: true },
|
|
|
|
|
|
})
|
|
|
|
|
|
).map((f) => f.id);
|
|
|
|
|
|
|
|
|
|
|
|
if (oldFoodCostIds.length) {
|
|
|
|
|
|
await tx.activityFoodTaxes.deleteMany({
|
|
|
|
|
|
where: { activityFoodCostXid: { in: oldFoodCostIds } },
|
|
|
|
|
|
});
|
|
|
|
|
|
await tx.activityFoodCost.deleteMany({
|
|
|
|
|
|
where: { id: { in: oldFoodCostIds } },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.foodAvailable && payload.foodIsChargeable) {
|
|
|
|
|
|
const foodTotalAmount = toNumber(payload.foodTotalAmount) ?? 0;
|
2026-01-08 12:14:12 +05:30
|
|
|
|
|
|
|
|
|
|
if (!isDraft && foodTotalAmount <= 0) {
|
2025-12-24 19:04:51 +05:30
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
'foodTotalAmount must be > 0 when foodIsChargeable',
|
|
|
|
|
|
);
|
2026-01-08 12:14:12 +05:30
|
|
|
|
}
|
2025-12-24 19:04:51 +05:30
|
|
|
|
|
2026-01-08 12:14:12 +05:30
|
|
|
|
if (foodTotalAmount > 0) {
|
|
|
|
|
|
const { basePrice, taxDetails } = computeBasePriceAndTaxes(
|
|
|
|
|
|
foodTotalAmount,
|
|
|
|
|
|
rootTaxes,
|
|
|
|
|
|
);
|
2025-12-24 19:04:51 +05:30
|
|
|
|
|
2026-01-08 12:14:12 +05:30
|
|
|
|
const foodCost = await tx.activityFoodCost.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
baseAmount: basePrice,
|
|
|
|
|
|
totalAmount: foodTotalAmount,
|
|
|
|
|
|
},
|
2025-12-24 19:04:51 +05:30
|
|
|
|
});
|
2026-01-08 12:14:12 +05:30
|
|
|
|
|
|
|
|
|
|
if (taxDetails.length) {
|
|
|
|
|
|
await tx.activityFoodTaxes.createMany({
|
|
|
|
|
|
data: taxDetails.map((t) => ({
|
|
|
|
|
|
activityFoodCostXid: foodCost.id,
|
|
|
|
|
|
taxXid: t.taxXid,
|
|
|
|
|
|
taxPer: t.taxPer,
|
|
|
|
|
|
taxAmount: t.taxAmount,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-24 19:04:51 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🍲 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,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🍛 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,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 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 } },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
if (Array.isArray(payload.equipments)) {
|
2025-12-23 17:51:32 +05:30
|
|
|
|
for (const eq of payload.equipments) {
|
2026-01-08 15:18:06 +05:30
|
|
|
|
const isChargeable = toBool(eq.isEquipmentChargeable);
|
|
|
|
|
|
const totalPrice = isChargeable
|
|
|
|
|
|
? toNumber(eq.equipmentTotalPrice) ?? 0
|
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
// ❌ Validate only on submit
|
2026-01-08 15:18:06 +05:30
|
|
|
|
if (!isDraft && isChargeable && totalPrice <= 0) {
|
|
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
'equipmentTotalPrice must be > 0 when equipment is chargeable',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
const { basePrice, taxDetails } =
|
|
|
|
|
|
isChargeable && totalPrice > 0
|
|
|
|
|
|
? computeBasePriceAndTaxes(totalPrice, rootTaxes)
|
|
|
|
|
|
: { basePrice: 0, taxDetails: [] };
|
2025-12-23 17:51:32 +05:30
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
// ✅ ALWAYS CREATE EQUIPMENT
|
2025-12-23 17:51:32 +05:30
|
|
|
|
const equipment = await tx.activityEquipments.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
equipmentName: eq.equipmentName,
|
2026-01-08 15:18:06 +05:30
|
|
|
|
isEquipmentChargeable: isChargeable,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
equipmentBasePrice: basePrice,
|
|
|
|
|
|
equipmentTotalPrice: totalPrice,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
// 💰 Taxes ONLY if chargeable
|
|
|
|
|
|
if (isChargeable && taxDetails.length) {
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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 } },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (payload.trainerAvailable) {
|
2026-01-08 23:01:09 +05:30
|
|
|
|
const isChargeable = trainerIsChargeable;
|
|
|
|
|
|
const totalAmount = isChargeable
|
|
|
|
|
|
? payload.trainerTotalAmount
|
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
|
|
const { basePrice, taxDetails } = isChargeable && totalAmount > 0
|
|
|
|
|
|
? computeBasePriceAndTaxes(totalAmount, rootTaxes)
|
|
|
|
|
|
: { basePrice: 0, taxDetails: [] };
|
2025-12-21 17:28:08 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
const trainer = await tx.activityTrainers.create({
|
2025-12-22 13:30:55 +05:30
|
|
|
|
data: {
|
|
|
|
|
|
activityXid,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
baseAmount: basePrice,
|
2026-01-08 23:01:09 +05:30
|
|
|
|
totalAmount,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-08 23:01:09 +05:30
|
|
|
|
if (isChargeable && taxDetails.length) {
|
2025-12-23 17:51:32 +05:30
|
|
|
|
await tx.activityTrainerTaxes.createMany({
|
2025-12-22 13:30:55 +05:30
|
|
|
|
data: taxDetails.map((t) => ({
|
2025-12-23 17:51:32 +05:30
|
|
|
|
activityTrainerXid: trainer.id,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
taxXid: t.taxXid,
|
|
|
|
|
|
taxPer: t.taxPer,
|
|
|
|
|
|
taxAmount: t.taxAmount,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
2026-01-08 20:17:01 +05:30
|
|
|
|
* 1️⃣1️⃣ CLEAN & CREATE PICKUP/DROP TRANSPORTS (INDEPENDENT ARRAY)
|
2025-12-23 17:51:32 +05:30
|
|
|
|
* -------------------------------- */
|
2025-12-30 14:22:18 +05:30
|
|
|
|
/* --------------------------------
|
2026-01-08 20:17:01 +05:30
|
|
|
|
* 1️⃣1️⃣ CLEAN OLD PICKUP/DROP TRANSPORT DATA (INDEPENDENT FROM NAVIGATION MODES)
|
2025-12-30 14:22:18 +05:30
|
|
|
|
* -------------------------------- */
|
2026-01-08 20:17:01 +05:30
|
|
|
|
// Clean up old pickup transport modes (independent array)
|
|
|
|
|
|
await tx.activityPickUpTransport.deleteMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
activityXid: Number(activityXid),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Clean up old pickup details and their taxes (independent from transport modes)
|
2025-12-30 14:22:18 +05:30
|
|
|
|
const oldPickupDetailIds = (
|
|
|
|
|
|
await tx.activityPickUpDetails.findMany({
|
|
|
|
|
|
where: { activitiesXid: activityXid },
|
2025-12-23 17:51:32 +05:30
|
|
|
|
select: { id: true },
|
|
|
|
|
|
})
|
2025-12-30 14:22:18 +05:30
|
|
|
|
).map((p) => p.id);
|
2025-12-23 17:51:32 +05:30
|
|
|
|
|
2025-12-30 14:22:18 +05:30
|
|
|
|
if (oldPickupDetailIds.length) {
|
|
|
|
|
|
await tx.activityPickUpTransportTaxes.deleteMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
activityPickUpDetailsXid: { in: oldPickupDetailIds },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-30 14:22:18 +05:30
|
|
|
|
await tx.activityPickUpDetails.deleteMany({
|
|
|
|
|
|
where: { id: { in: oldPickupDetailIds } },
|
2025-12-23 17:51:32 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2026-01-08 20:17:01 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣1️⃣ CREATE PICKUP TRANSPORTS (INDEPENDENT ARRAY - JUST TRANSPORT MODES)
|
|
|
|
|
|
* -------------------------------- */
|
2025-12-30 14:22:18 +05:30
|
|
|
|
if (Array.isArray(payload.pickupTransports)) {
|
2025-12-23 17:51:32 +05:30
|
|
|
|
for (const transport of payload.pickupTransports) {
|
2026-01-08 20:17:01 +05:30
|
|
|
|
// ✅ CREATE TRANSPORT MODE INDEPENDENTLY (NO RELATION TO PICKUP DETAILS)
|
|
|
|
|
|
await tx.activityPickUpTransport.create({
|
2026-01-05 13:28:25 +05:30
|
|
|
|
data: {
|
|
|
|
|
|
activityXid: activityXid,
|
|
|
|
|
|
transportModeXid: transport.transportModeXid,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-08 20:17:01 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-21 17:28:08 +05:30
|
|
|
|
|
2026-01-08 20:17:01 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣1️⃣ CREATE PICKUP DETAILS (INDEPENDENT ARRAY - SEPARATE FROM TRANSPORT MODES)
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
if (Array.isArray(payload.pickupDetails)) {
|
|
|
|
|
|
for (const detail of payload.pickupDetails) {
|
2026-01-08 23:01:09 +05:30
|
|
|
|
const isChargeable = pickUpDropIsChargeable;
|
2026-02-04 15:32:11 +05:30
|
|
|
|
|
2026-01-09 19:11:46 +05:30
|
|
|
|
// 🔒 HARD RULE: NOT chargeable → ALWAYS 0
|
2026-01-08 23:01:09 +05:30
|
|
|
|
const totalPrice = isChargeable
|
|
|
|
|
|
? toNumber(detail.transportTotalPrice) ?? 0
|
|
|
|
|
|
: 0;
|
2026-02-04 15:32:11 +05:30
|
|
|
|
|
2026-01-09 19:11:46 +05:30
|
|
|
|
// ❌ Validate ONLY when chargeable + submit
|
2026-01-08 23:01:09 +05:30
|
|
|
|
if (!isDraft && isChargeable && totalPrice <= 0) {
|
2026-01-08 20:17:01 +05:30
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
2026-02-05 16:07:43 +05:30
|
|
|
|
'Pick-up and drop-off price is required.',
|
2026-01-08 20:17:01 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-02-04 15:32:11 +05:30
|
|
|
|
|
2026-01-08 23:01:09 +05:30
|
|
|
|
const { basePrice, taxDetails } =
|
|
|
|
|
|
isChargeable && totalPrice > 0
|
|
|
|
|
|
? computeBasePriceAndTaxes(totalPrice, rootTaxes)
|
|
|
|
|
|
: { basePrice: 0, taxDetails: [] };
|
2026-02-04 15:32:11 +05:30
|
|
|
|
|
2026-01-09 19:11:46 +05:30
|
|
|
|
// ✅ ALWAYS CREATE PICKUP DETAIL
|
2026-01-08 20:17:01 +05:30
|
|
|
|
const pickupDetail = await tx.activityPickUpDetails.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
activitiesXid: activityXid,
|
|
|
|
|
|
isPickUp: toBool(detail.isPickUp),
|
|
|
|
|
|
locationLat: toNumber(detail.locationLat),
|
|
|
|
|
|
locationLong: toNumber(detail.locationLong),
|
|
|
|
|
|
locationAddress: detail.locationAddress ?? null,
|
2026-02-04 15:32:11 +05:30
|
|
|
|
|
2026-01-09 19:11:46 +05:30
|
|
|
|
// ✅ Guaranteed consistency
|
2026-01-08 20:17:01 +05:30
|
|
|
|
transportBasePrice: basePrice,
|
|
|
|
|
|
transportTotalPrice: totalPrice,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-02-04 15:32:11 +05:30
|
|
|
|
|
2026-01-09 19:11:46 +05:30
|
|
|
|
// 💰 Taxes ONLY when chargeable
|
2026-01-08 23:01:09 +05:30
|
|
|
|
if (isChargeable && taxDetails.length) {
|
2026-01-08 20:17:01 +05:30
|
|
|
|
await tx.activityPickUpTransportTaxes.createMany({
|
|
|
|
|
|
data: taxDetails.map((t) => ({
|
|
|
|
|
|
activityPickUpDetailsXid: pickupDetail.id,
|
|
|
|
|
|
taxXid: t.taxXid,
|
|
|
|
|
|
taxPer: t.taxPer,
|
|
|
|
|
|
taxAmount: t.taxAmount,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
2025-12-23 17:51:32 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
}
|
2026-02-04 15:32:11 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣2️⃣ CLEAN & CREATE NAVIGATION MODES WITH TAXES
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
const oldNavIds = (
|
|
|
|
|
|
await tx.activityNavigationModes.findMany({
|
|
|
|
|
|
where: { activityXid },
|
2025-12-22 13:30:55 +05:30
|
|
|
|
select: { id: true },
|
|
|
|
|
|
})
|
2025-12-23 17:51:32 +05:30
|
|
|
|
).map((n) => n.id);
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (oldNavIds.length) {
|
|
|
|
|
|
await tx.activityNavigationModesTaxes.deleteMany({
|
|
|
|
|
|
where: { activityNavigationModeXid: { in: oldNavIds } },
|
2025-12-22 13:30:55 +05:30
|
|
|
|
});
|
2025-12-31 14:42:30 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
await tx.activityNavigationModes.deleteMany({
|
|
|
|
|
|
where: { id: { in: oldNavIds } },
|
2025-12-22 13:30:55 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 14:42:30 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣2️⃣ CREATE NAVIGATION MODES (PER MODE)
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
if (Array.isArray(payload.navigationModes)) {
|
|
|
|
|
|
for (const mode of payload.navigationModes) {
|
|
|
|
|
|
const isChargeable = toBool(mode.isChargeable);
|
|
|
|
|
|
const totalPrice = isChargeable
|
|
|
|
|
|
? (toNumber(mode.totalPrice) ?? 0)
|
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
2026-01-08 12:14:12 +05:30
|
|
|
|
if (!isDraft && isChargeable && totalPrice <= 0) {
|
2025-12-31 14:42:30 +05:30
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
'totalPrice must be > 0 when navigation mode is chargeable',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let basePrice = 0;
|
|
|
|
|
|
let taxDetails: Array<{
|
|
|
|
|
|
taxXid: number;
|
|
|
|
|
|
taxPer: number;
|
|
|
|
|
|
taxAmount: number;
|
|
|
|
|
|
}> = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (isChargeable) {
|
|
|
|
|
|
const result = computeBasePriceAndTaxes(totalPrice, rootTaxes);
|
|
|
|
|
|
|
|
|
|
|
|
basePrice = result.basePrice;
|
|
|
|
|
|
taxDetails = result.taxDetails;
|
|
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-31 14:42:30 +05:30
|
|
|
|
/* 1️⃣ CREATE NAVIGATION MODE ROW */
|
2025-12-23 17:51:32 +05:30
|
|
|
|
const navMode = await tx.activityNavigationModes.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
activityXid,
|
2026-03-02 19:05:08 +05:30
|
|
|
|
navigationModeName: mode.navigationModeName,
|
2025-12-31 14:42:30 +05:30
|
|
|
|
isInActivityChargeable: isChargeable,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
navigationModesBasePrice: basePrice,
|
|
|
|
|
|
navigationModesTotalPrice: totalPrice,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-31 14:42:30 +05:30
|
|
|
|
/* 2️⃣ CREATE TAXES (ONLY IF CHARGEABLE) */
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (taxDetails.length) {
|
|
|
|
|
|
await tx.activityNavigationModesTaxes.createMany({
|
|
|
|
|
|
data: taxDetails.map((t) => ({
|
|
|
|
|
|
activityNavigationModeXid: navMode.id,
|
|
|
|
|
|
taxXid: t.taxXid,
|
|
|
|
|
|
taxPer: t.taxPer,
|
|
|
|
|
|
taxAmount: t.taxAmount,
|
|
|
|
|
|
})),
|
2025-12-22 13:30:55 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-18 19:37:32 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣3️⃣ CLEAN & CREATE AMENITIES
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
await tx.activityAmenities.deleteMany({ where: { activityXid } });
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
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 } });
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (payload.eligibility) {
|
|
|
|
|
|
await tx.activityEligibility.create({
|
2025-12-22 13:30:55 +05:30
|
|
|
|
data: {
|
|
|
|
|
|
activityXid,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
isAgeRestriction: toBool(payload.eligibility.isAgeRestriction),
|
2025-12-24 19:04:51 +05:30
|
|
|
|
ageRestrictionName: payload.eligibility.ageRestrictionName,
|
|
|
|
|
|
ageEntered: payload.eligibility.ageEntered,
|
|
|
|
|
|
ageIn: payload.eligibility.ageIn,
|
|
|
|
|
|
minAge: payload.eligibility.minAge,
|
|
|
|
|
|
maxAge: payload.eligibility.maxAge,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isWeightRestriction: toBool(
|
|
|
|
|
|
payload.eligibility.isWeightRestriction,
|
|
|
|
|
|
),
|
|
|
|
|
|
weightRestrictionName:
|
|
|
|
|
|
payload.eligibility.weightRestrictionName ?? null,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
weightEntered: toNumber(payload.eligibility.weightEntered),
|
|
|
|
|
|
weightIn: payload.eligibility.weightIn ?? null,
|
|
|
|
|
|
minWeight: toNumber(payload.eligibility.minWeight),
|
|
|
|
|
|
maxWeight: toNumber(payload.eligibility.maxWeight),
|
2025-12-24 15:11:37 +05:30
|
|
|
|
isHeightRestriction: toBool(
|
|
|
|
|
|
payload.eligibility.isHeightRestriction,
|
|
|
|
|
|
),
|
|
|
|
|
|
heightRestrictionName:
|
|
|
|
|
|
payload.eligibility.heightRestrictionName ?? null,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
heightEntered: toNumber(payload.eligibility.heightEntered),
|
|
|
|
|
|
heightIn: payload.eligibility.heightIn ?? null,
|
|
|
|
|
|
minHeight: toNumber(payload.eligibility.minHeight),
|
|
|
|
|
|
maxHeight: toNumber(payload.eligibility.maxHeight),
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣5️⃣ CLEAN & CREATE OTHER DETAILS
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
await tx.activityOtherDetails.deleteMany({ where: { activityXid } });
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (payload.otherDetails) {
|
|
|
|
|
|
await tx.activityOtherDetails.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
exclusiveNotes: payload.otherDetails.exclusiveNotes ?? null,
|
2026-02-25 14:41:58 +05:30
|
|
|
|
SafetyInstruction: (() => {
|
2026-02-27 17:25:48 +05:30
|
|
|
|
const s = payload.otherDetails.safetyInstruction ?? null;
|
2026-02-25 14:41:58 +05:30
|
|
|
|
if (s === undefined || s === null) return null;
|
|
|
|
|
|
if (typeof s !== 'string') {
|
|
|
|
|
|
throw new ApiError(400, 'safetyInstruction must be a string');
|
|
|
|
|
|
}
|
|
|
|
|
|
return s;
|
|
|
|
|
|
})(),
|
|
|
|
|
|
Cancellations: (() => {
|
|
|
|
|
|
const c = payload.otherDetails.cancellations ?? null;
|
|
|
|
|
|
if (c === undefined || c === null) return null;
|
|
|
|
|
|
if (typeof c !== 'string') {
|
|
|
|
|
|
throw new ApiError(400, 'cancellations must be a string');
|
|
|
|
|
|
}
|
|
|
|
|
|
return c;
|
|
|
|
|
|
})(),
|
2025-12-23 17:51:32 +05:30
|
|
|
|
dosNotes: payload.otherDetails.dosNotes ?? null,
|
|
|
|
|
|
dontsNotes: payload.otherDetails.dontsNotes ?? null,
|
|
|
|
|
|
tipsNotes: payload.otherDetails.tipsNotes ?? null,
|
|
|
|
|
|
termsAndCondition: payload.otherDetails.termsAndCondition ?? null,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣6️⃣ CLEAN & CREATE FOOD TYPES
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
await tx.activityFoodTypes.deleteMany({ where: { activityXid } });
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (Array.isArray(payload.foodTypeIds) && payload.foodTypeIds.length) {
|
|
|
|
|
|
await tx.activityFoodTypes.createMany({
|
|
|
|
|
|
data: payload.foodTypeIds.map((foodTypeId) => ({
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
foodTypeXid: foodTypeId,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣7️⃣ CLEAN & CREATE CUISINES
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
await tx.activityCuisine.deleteMany({ where: { activityXid } });
|
2025-12-22 13:30:55 +05:30
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
if (Array.isArray(payload.cuisineIds) && payload.cuisineIds.length) {
|
|
|
|
|
|
await tx.activityCuisine.createMany({
|
|
|
|
|
|
data: payload.cuisineIds.map((cuisineId) => ({
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
foodCuisineXid: cuisineId,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 15:11:37 +05:30
|
|
|
|
const allowedEntryIds = Array.isArray(payload.allowedEntryTypes)
|
|
|
|
|
|
? payload.allowedEntryTypes.map(Number)
|
|
|
|
|
|
: [];
|
|
|
|
|
|
if (allowedEntryIds.length) {
|
|
|
|
|
|
const validEntryTypes = await this.prisma.allowedEntryTypes.findMany({
|
|
|
|
|
|
where: { id: { in: allowedEntryIds }, isActive: true },
|
|
|
|
|
|
select: { id: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (validEntryTypes.length !== allowedEntryIds.length)
|
|
|
|
|
|
throw new ApiError(
|
|
|
|
|
|
400,
|
|
|
|
|
|
'Invalid or inactive allowed entry type(s) provided',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* CLEAN & CREATE ALLOWED ENTRY
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
await tx.activityAllowedEntry.deleteMany({ where: { activityXid } });
|
|
|
|
|
|
if (allowedEntryIds.length) {
|
|
|
|
|
|
await tx.activityAllowedEntry.createMany({
|
|
|
|
|
|
data: allowedEntryIds.map((entryId) => ({
|
|
|
|
|
|
activityXid,
|
|
|
|
|
|
allowedEntryTypeXid: entryId,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 14:47:24 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* ✅ MARK ACTIVITY SUGGESTIONS AS REVIEWED
|
|
|
|
|
|
* (REJECTED → ENHANCE → SUBMIT FLOW)
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
if (wasRejected && !isDraft) {
|
|
|
|
|
|
await tx.activitySuggestions.updateMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
activityXid: activityXid,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
isReviewed: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
data: {
|
|
|
|
|
|
isReviewed: true,
|
|
|
|
|
|
reviewedByXid: userId,
|
|
|
|
|
|
reviewedOn: new Date(),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣8️⃣ ACTIVITY TRACK
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
await tx.activityTrack.create({
|
2025-12-22 13:30:55 +05:30
|
|
|
|
data: {
|
|
|
|
|
|
activityXid,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
trackType: 'ACTIVITY',
|
|
|
|
|
|
trackStatus: activityInternalStatus,
|
|
|
|
|
|
updatedByXid: userId,
|
|
|
|
|
|
updatedByRole: ROLE_NAME.HOST,
|
|
|
|
|
|
updatedOn: new Date(),
|
2025-12-22 13:30:55 +05:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-23 17:51:32 +05:30
|
|
|
|
/* --------------------------------
|
|
|
|
|
|
* 1️⃣9️⃣ RESPONSE
|
|
|
|
|
|
* -------------------------------- */
|
|
|
|
|
|
return {
|
2025-12-21 17:28:08 +05:30
|
|
|
|
activityXid,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
activityRefNumber: activity.activityRefNumber,
|
2025-12-24 15:11:37 +05:30
|
|
|
|
status: isDraft
|
|
|
|
|
|
? ACTIVITY_INTERNAL_STATUS.ACTIVITY_DRAFT
|
|
|
|
|
|
: ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED,
|
2025-12-23 17:51:32 +05:30
|
|
|
|
};
|
2025-12-18 19:37:32 +05:30
|
|
|
|
});
|
2025-12-23 17:51:32 +05:30
|
|
|
|
}
|
2025-12-24 15:11:37 +05:30
|
|
|
|
|
2025-12-03 19:21:21 +05:30
|
|
|
|
async getAllPQUpdatedResponse(activityXid: number) {
|
|
|
|
|
|
const pqqHeaderData = await this.prisma.activityPQQheader.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
activityXid: activityXid,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
select: {
|
2025-12-08 11:23:58 +05:30
|
|
|
|
id: true,
|
2025-12-03 19:43:22 +05:30
|
|
|
|
comments: true,
|
|
|
|
|
|
pqqAnswerXid: true,
|
2025-12-03 19:21:21 +05:30
|
|
|
|
pqqQuestions: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
questionName: true,
|
|
|
|
|
|
maxPoints: true,
|
|
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
pqqSubCategories: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
subCategoryName: true,
|
|
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
category: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
categoryName: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
displayOrder: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-03 19:43:22 +05:30
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION
|
|
|
|
|
|
PQQAnswers: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
answerName: true,
|
|
|
|
|
|
answerPoints: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
displayOrder: true,
|
2025-12-03 19:43:22 +05:30
|
|
|
|
},
|
2025-12-22 13:30:55 +05:30
|
|
|
|
orderBy: { displayOrder: 'asc' },
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityPQQSuggestions: {
|
2026-01-09 18:10:38 +05:30
|
|
|
|
where: { isActive: true, isReviewed: false },
|
2025-12-03 19:21:21 +05:30
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
title: true,
|
|
|
|
|
|
comments: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
activityPqqHeaderXid: true,
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
},
|
|
|
|
|
|
ActivityPQQSupportings: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
mediaType: true,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
mediaFileName: true,
|
|
|
|
|
|
},
|
2025-12-03 19:21:21 +05:30
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-12-22 13:30:55 +05:30
|
|
|
|
orderBy: { id: 'asc' },
|
2025-12-03 19:21:21 +05:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- GROUPING START ----------
|
|
|
|
|
|
const grouped: any = {};
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of pqqHeaderData) {
|
|
|
|
|
|
const q = item.pqqQuestions;
|
|
|
|
|
|
const sub = q.pqqSubCategories;
|
|
|
|
|
|
const cat = sub.category;
|
|
|
|
|
|
|
2025-12-08 11:23:58 +05:30
|
|
|
|
// 1️⃣ Category level
|
2025-12-03 19:21:21 +05:30
|
|
|
|
// 1️⃣ Category level
|
|
|
|
|
|
if (!grouped[cat.id]) {
|
|
|
|
|
|
grouped[cat.id] = {
|
|
|
|
|
|
id: cat.id,
|
|
|
|
|
|
categoryName: cat.categoryName,
|
|
|
|
|
|
displayOrder: cat.displayOrder,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
activityPqqHeaderId: item.id, // ✅ Added to match AM response
|
|
|
|
|
|
pqqsubCategories: [],
|
2025-12-03 19:21:21 +05:30
|
|
|
|
};
|
2025-12-08 11:23:58 +05:30
|
|
|
|
} else if (!grouped[cat.id].activityPqqHeaderId) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
grouped[cat.id].activityPqqHeaderId = item.id; // Ensures it's set if missing
|
2025-12-03 19:21:21 +05:30
|
|
|
|
}
|
2025-12-08 11:23:58 +05:30
|
|
|
|
|
2025-12-03 19:21:21 +05:30
|
|
|
|
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,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
questions: [],
|
2025-12-03 19:21:21 +05:30
|
|
|
|
};
|
|
|
|
|
|
category.pqqsubCategories.push(subCat);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3️⃣ Questions level
|
|
|
|
|
|
subCat.questions.push({
|
|
|
|
|
|
id: q.id,
|
|
|
|
|
|
questionName: q.questionName,
|
|
|
|
|
|
maxPoints: q.maxPoints,
|
2025-12-03 19:43:22 +05:30
|
|
|
|
pqqAnswerXid: item.pqqAnswerXid,
|
|
|
|
|
|
comments: item.comments || null,
|
2025-12-03 19:21:21 +05:30
|
|
|
|
displayOrder: q.displayOrder,
|
2025-12-22 13:30:55 +05:30
|
|
|
|
allAnswerOptions: q.PQQAnswers || [], // 🔥 All answers
|
2025-12-03 19:21:21 +05:30
|
|
|
|
suggestions: item.ActivityPQQSuggestions,
|
2025-12-03 19:43:22 +05:30
|
|
|
|
supportings: item.ActivityPQQSupportings,
|
2025-12-03 19:21:21 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- SORTING ----------
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const sortedCategories: any = Object.values(grouped).sort(
|
|
|
|
|
|
(a: any, b: any) => a.displayOrder - b.displayOrder,
|
|
|
|
|
|
);
|
2025-12-03 19:21:21 +05:30
|
|
|
|
|
|
|
|
|
|
for (const cat of sortedCategories) {
|
2025-12-22 13:30:55 +05:30
|
|
|
|
cat.pqqsubCategories.sort(
|
|
|
|
|
|
(a: any, b: any) => a.displayOrder - b.displayOrder,
|
|
|
|
|
|
);
|
2025-12-03 19:21:21 +05:30
|
|
|
|
|
|
|
|
|
|
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;
|
2025-12-22 13:30:55 +05:30
|
|
|
|
const key = filePath.startsWith('http')
|
|
|
|
|
|
? filePath.split('.com/')[1]
|
2025-12-03 19:21:21 +05:30
|
|
|
|
: filePath;
|
|
|
|
|
|
|
|
|
|
|
|
doc.presignedUrl = await getPresignedUrl(bucket, key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- RETURN GROUPED STRUCTURE ----------
|
|
|
|
|
|
return sortedCategories;
|
|
|
|
|
|
}
|
2025-11-10 15:05:01 +05:30
|
|
|
|
}
|