Files
MinglarBackendNestJS/src/modules/minglaradmin/services/minglar.service.ts

1497 lines
40 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

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

import {
ROLE,
ROLE_NAME,
USER_STATUS,
} from '@/common/utils/constants/common.constant';
import {
ACTIVITY_AM_DISPLAY_STATUS,
ACTIVITY_AM_INTERNAL_STATUS,
ACTIVITY_DISPLAY_STATUS,
ACTIVITY_INTERNAL_STATUS,
HOST_STATUS_DISPLAY,
HOST_STATUS_INTERNAL,
STEPPER,
} from '@/common/utils/constants/host.constant';
import {
MINGLAR_INVITATION_STATUS,
MINGLAR_STATUS_DISPLAY,
MINGLAR_STATUS_INTERNAL,
} from '@/common/utils/constants/minglar.constant';
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
import { PrismaService } from '../../../common/database/prisma.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { CreateMinglarDto, UpdateMinglarDto } from '../dto/minglar.dto';
import { sendAMEmailForHostAssign } from './AMEmail.service';
import { getPresignedUrl } from '@/common/middlewares/aws/getPreSignedUrl';
import config from '@/config/config';
import { PaginationOptions } from '@/common/utils/pagination/pagination.types';
@Injectable()
export class MinglarService {
constructor(private prisma: PrismaService) { }
async createPassword(user_xid: number, password: string): Promise<boolean> {
// Find user by id
const user = await this.prisma.user.findUnique({
where: { id: user_xid, isActive: true, userStatus: USER_STATUS.INVITED },
select: { id: true, emailAddress: true, userPassword: true },
});
const invitationDetails = await this.prisma.inviteDetails.findMany({
where: {
userXid: user.id,
isActive: true,
isMinglarInvitation: true,
},
});
if (invitationDetails.length > 0) {
await this.prisma.inviteDetails.update({
where: { id: invitationDetails[0].id },
data: {
invitation_status: MINGLAR_INVITATION_STATUS.ACCEPTED,
accepted_on: new Date(),
is_accepted: true,
},
});
}
if (!user) {
throw new ApiError(404, 'User not found');
}
// Check if password already exists
if (user.userPassword) {
throw new ApiError(
400,
'Password already exists. Use update password instead.',
);
}
// Hash the password
const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10);
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Update user with hashed password
await this.prisma.user.update({
where: { id: user.id },
data: {
userPassword: hashedPassword,
userStatus: USER_STATUS.ACTIVE,
isEmailVerfied: true,
},
});
return true;
}
async generateHostRefNumber(tx: any, role_xid: number) {
const lastrecord = await tx.user.findFirst({
orderBy: {
id: 'desc',
},
select: {
id: true,
},
});
let referenceId = '';
const nextId = lastrecord ? lastrecord.id + 1 : 1;
if (role_xid === ROLE.ACCOUNT_MANAGER) {
referenceId = `AM-${String(nextId).padStart(6, '0')}`;
} else if (role_xid === ROLE.CO_ADMIN) {
referenceId = `CA-${String(nextId).padStart(6, '0')}`;
}
return referenceId;
}
async createHost(data: CreateMinglarDto) {
return this.prisma.user.create({ data });
}
async getAllHosts() {
return this.prisma.user.findMany({ where: { roleXid: ROLE.HOST } });
}
async updateHost(id: number, data: UpdateMinglarDto) {
return this.prisma.user.update({
where: { id },
data,
});
}
async deleteHost(id: number) {
return this.prisma.user.delete({ where: { id } });
}
async getHostByEmail(email: string): Promise<User> {
return this.prisma.user.findUnique({ where: { emailAddress: email } });
}
async getUserDetails(id: number) {
return await this.prisma.user.findUnique({
where: { id: id },
});
}
async verifyHostOtp(email: string, otp: string): Promise<boolean> {
const user = await this.prisma.user.findUnique({
where: { emailAddress: email },
select: {
id: true,
emailAddress: true,
UserOtp: {
where: { isActive: true, isVerified: false },
orderBy: { createdAt: 'desc' },
take: 1,
},
},
});
if (!user) {
throw new ApiError(404, 'User not found.');
}
const userOtp = user.UserOtp[0];
if (!userOtp) {
throw new ApiError(400, 'No OTP found.');
}
if (new Date() > userOtp.expiresOn) {
throw new ApiError(400, 'OTP has expired.');
}
const isMatch = await bcrypt.compare(otp, userOtp.otpCode);
if (!isMatch) {
throw new ApiError(400, 'Invalid OTP.');
}
await this.prisma.userOtp.update({
where: { id: userOtp.id },
data: {
isVerified: true,
verifiedOn: new Date(),
isActive: false,
},
});
return true;
}
async loginForMinglar(emailAddress: string, userPassword: string) {
const existingUser = await this.prisma.user.findUnique({
where: {
emailAddress: emailAddress,
isActive: true,
userStatus: USER_STATUS.ACTIVE,
},
});
if (!existingUser) {
throw new ApiError(404, 'User not found');
}
if (
existingUser.roleXid !== ROLE.MINGLAR_ADMIN &&
existingUser.roleXid !== ROLE.CO_ADMIN &&
existingUser.roleXid !== ROLE.ACCOUNT_MANAGER
) {
throw new ApiError(403, 'Access denied.');
}
const matchPassword = await bcrypt.compare(
userPassword,
existingUser.userPassword,
);
if (!matchPassword) {
throw new ApiError(401, 'Invalid credentials');
}
return existingUser;
}
async checkUserExists(emailAddress: string) {
return await this.prisma.user.findUnique({
where: { emailAddress: emailAddress, isActive: true },
});
}
async createUserForInvite(emailAddress: string, roleXid: number) {
return await this.prisma.user.create({
data: {
emailAddress: emailAddress,
roleXid: roleXid,
userStatus: USER_STATUS.INVITED,
},
});
}
async getAllHostActivityForMinglar(search?: string, hostXid?: number) {
const hostActivities = await this.prisma.activities.findMany({
where: {
isActive: true,
...(hostXid ? { hostXid } : {}), // Add only if provided
},
include: {
ActivitiesMedia: {
select: {
id: true,
mediaFileName: true,
mediaType: true,
displayOrder: true,
},
},
ActivityAmDetails: {
select: {
accountManager: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
emailAddress: true,
roleXid: true,
},
},
},
},
activityType: true,
},
});
const bucket = config.aws.bucketName;
// Process each activity
for (const activity of hostActivities) {
/** --------------------------
* 1⃣ Process Activity Media
* -------------------------- */
if (activity.ActivitiesMedia?.length) {
for (const media of activity.ActivitiesMedia) {
if (!media.mediaFileName) continue;
// Extract S3 key if URL or keep raw key
const key = media.mediaFileName.startsWith("http")
? media.mediaFileName.split(".com/")[1]
: media.mediaFileName;
media.mediaFileName = await getPresignedUrl(bucket, key);
}
}
/** --------------------------
* 2⃣ Process AM Profile Image
* -------------------------- */
const am = activity.ActivityAmDetails?.[0]?.accountManager;
if (am?.profileImage) {
const key = am.profileImage.startsWith("http")
? am.profileImage.split(".com/")[1]
: am.profileImage;
am.profileImage = await getPresignedUrl(bucket, key);
}
}
return hostActivities;
}
async createUserRevenue(
userXid: number,
isFixedSalary: boolean,
perValue: number,
) {
return await this.prisma.userRevenue.create({
data: {
userXid: userXid,
is_fixed_salary: isFixedSalary,
per_value: perValue || 0,
isActive: true,
},
});
}
async createInviteDetails(
userXid: number,
invitedBy: number,
invitationStatus: string,
) {
return await this.prisma.inviteDetails.create({
data: {
userXid: userXid,
is_invited: true,
invited_by: invitedBy,
invited_on: new Date(),
is_accepted: false,
invitation_status: invitationStatus,
isActive: true,
isMinglarInvitation: true,
},
});
}
/**
* Invite teammate flow: checks existing user, creates user, revenue and invite details
* All operations are performed inside a single DB transaction to avoid races.
*/
async inviteTeammate(
emailAddress: string,
roleXid: number,
isFixedSalary: boolean,
perValue: number,
invitedBy: number,
) {
return await this.prisma.$transaction(async (tx) => {
// Check existing user
const existingUser = await tx.user.findFirst({
where: { emailAddress: emailAddress, isActive: true },
});
if (existingUser) {
throw new ApiError(400, 'User already exists.');
}
const referenceNumber = await this.generateHostRefNumber(tx, roleXid);
// Create user with INVITED status
const user = await tx.user.create({
data: {
emailAddress: emailAddress,
roleXid: roleXid,
userStatus: USER_STATUS.INVITED,
userRefNumber: referenceNumber
},
});
// Create revenue record
await tx.userRevenue.create({
data: {
userXid: user.id,
is_fixed_salary: isFixedSalary,
per_value: perValue || 0,
isActive: true,
},
});
// Create invite details
await tx.inviteDetails.create({
data: {
userXid: user.id,
is_invited: true,
invited_by: invitedBy,
invited_on: new Date(),
is_accepted: false,
invitation_status: MINGLAR_INVITATION_STATUS.INVITED,
isActive: true,
isMinglarInvitation: true,
},
});
return user;
});
}
async updateProfile(
userId: number,
userData: {
firstName?: string;
lastName?: string;
mobileNumber?: string;
dateOfBirth?: string;
profileImage?: string;
},
addressData: {
address1?: string;
address2?: string;
stateXid?: number;
countryXid?: number;
cityXid?: number;
pinCode?: string;
},
documents: Array<{ fileName: string; filePath: string, documentTypeName?: string }>,
) {
try {
return await this.prisma.$transaction(async (tx) => {
console.log('Starting transaction for user:', userId);
// 1. Update User table (optimized)
const userUpdateData: any = {};
const userFields = [
'firstName',
'lastName',
'mobileNumber',
'dateOfBirth',
'profileImage',
];
userFields.forEach((field) => {
if (userData[field as keyof typeof userData] !== undefined) {
if (field === 'dateOfBirth' && userData.dateOfBirth) {
userUpdateData[field] = new Date(userData.dateOfBirth);
} else {
userUpdateData[field] = userData[field as keyof typeof userData];
}
}
});
if (Object.keys(userUpdateData).length > 0) {
console.log('Updating user data:', userUpdateData);
await tx.user.update({
where: { id: userId },
data: userUpdateData,
});
}
// 2. Update or create UserAddressDetails
if (Object.keys(addressData).length > 0) {
console.log('Processing address data:', addressData);
const existingAddress = await tx.userAddressDetails.findFirst({
where: { userXid: userId, isActive: true },
select: { id: true }, // Only select needed field
});
const addressUpdateData: any = {};
const addressFields = [
'address1',
'address2',
'stateXid',
'countryXid',
'cityXid',
'pinCode',
];
addressFields.forEach((field) => {
if (addressData[field as keyof typeof addressData] !== undefined) {
addressUpdateData[field] =
addressData[field as keyof typeof addressData];
}
});
if (existingAddress) {
await tx.userAddressDetails.update({
where: { id: existingAddress.id },
data: addressUpdateData,
});
} else {
// Validate required fields
const requiredFields = [
'address1',
'stateXid',
'countryXid',
'cityXid',
'pinCode',
];
const missingFields = requiredFields.filter(
(field) => !addressData[field as keyof typeof addressData],
);
if (missingFields.length > 0) {
throw new ApiError(
400,
`Missing required address fields: ${missingFields.join(', ')}`,
);
}
await tx.userAddressDetails.create({
data: {
userXid: userId,
...addressUpdateData,
},
});
}
}
// 3. Handle documents more efficiently
if (documents && documents.length > 0) {
console.log('Processing documents:', documents.length);
// Use deleteMany and createMany for better performance
await tx.userDocuments.deleteMany({
where: { userXid: userId, isActive: true },
});
if (documents.length > 0) {
await tx.userDocuments.createMany({
data: documents.map((doc) => ({
userXid: userId,
documentTypeName: doc.documentTypeName,
fileName: doc.filePath,
isActive: true,
})),
});
}
}
// 4. Fetch updated user data efficiently
const updatedUser = await tx.user.findUnique({
where: { id: userId },
select: {
id: true,
firstName: true,
lastName: true,
mobileNumber: true,
dateOfBirth: true,
profileImage: true,
userAddressDetails: {
where: { isActive: true },
take: 1,
select: {
id: true,
address1: true,
address2: true,
stateXid: true,
countryXid: true,
cityXid: true,
pinCode: true,
},
},
userDocuments: {
where: { isActive: true },
select: {
id: true,
fileName: true,
documentTypeName: true,
},
},
},
});
if (!updatedUser) {
throw new ApiError(404, 'User not found after update');
}
// 5. Calculate profile completion percentage
let percentage = 0;
// Profile Image: 15%
if (updatedUser.profileImage) percentage += 15;
// Name and Phone Number: 15%
if (
updatedUser.firstName &&
updatedUser.lastName &&
updatedUser.mobileNumber
) {
percentage += 15;
}
// Location Info: 25%
if (updatedUser.userAddressDetails.length > 0) {
const address = updatedUser.userAddressDetails[0];
if (
address.address1 &&
address.stateXid &&
address.countryXid &&
address.cityXid &&
address.pinCode
) {
percentage += 25;
}
}
// Documents: 45%
if (updatedUser.userDocuments.length >= 2) {
percentage += 45;
} else if (updatedUser.userDocuments.length === 1) {
percentage += 22.5;
}
const profilePercentage = Math.min(percentage, 100);
// Update profile completion status
if (profilePercentage > 80) {
await tx.user.update({
where: { id: userId },
data: { isProfileUpdated: true },
});
}
console.log('Transaction completed successfully');
return {
user: {
id: updatedUser.id,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
mobileNumber: updatedUser.mobileNumber,
dateOfBirth: updatedUser.dateOfBirth,
profileImage: updatedUser.profileImage,
},
address: updatedUser.userAddressDetails[0] || null,
documents: updatedUser.userDocuments,
profileCompletionPercentage: profilePercentage,
};
});
} catch (error) {
console.error('Error in updateProfile transaction:', error);
throw error;
}
}
async getAllInvitationDetails() {
return await this.prisma.inviteDetails.findMany({
where: {
isMinglarInvitation: true,
isActive: true,
},
include: {
user: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
roleXid: true,
userRefNumber: true,
role: {
select: {
id: true,
roleName: true,
},
},
},
},
},
});
}
// Update your MinglarService method
async getAllHostApplications(
userId: number,
userRoleXid: number,
search?: string,
userStatus?: string,
paginationOptions?: PaginationOptions,
) {
const filters: any = {
isActive: true,
user: {
roleXid: {
notIn: [ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER],
},
},
};
/** -----------------------------------
* SEARCH FILTER (ID, EMAIL, NAME)
* ----------------------------------- */
if (search?.trim()) {
const term = search.trim();
if (/^\d+$/.test(term)) {
// Search by Host ID
filters.id = Number(term);
} else {
// Search by email or name
filters.user = {
...filters.user,
OR: [
{ emailAddress: { contains: term, mode: 'insensitive' } },
{ firstName: { contains: term, mode: 'insensitive' } },
{ lastName: { contains: term, mode: 'insensitive' } },
],
};
}
}
/** -----------------------------------
* USER STATUS FILTER (NEW)
* ----------------------------------- */
if (
userStatus &&
userStatus.trim().toLowerCase() ===
MINGLAR_STATUS_DISPLAY.NEW.toLowerCase()
) {
filters.adminStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW;
}
/** -----------------------------------
* ROLE-BASED FILTER:
* CO_ADMIN & ACCOUNT_MANAGER only see assigned hosts
* ----------------------------------- */
if (userRoleXid === ROLE.CO_ADMIN || userRoleXid === ROLE.ACCOUNT_MANAGER) {
filters.accountManagerXid = userId;
}
/** -----------------------------------
* COUNT TOTAL RECORDS
* ----------------------------------- */
const totalCount = await this.prisma.hostHeader.count({
where: filters,
});
/** -----------------------------------
* MAIN QUERY WITH PAGINATION
* ----------------------------------- */
const results = await this.prisma.hostHeader.findMany({
where: filters,
select: {
id: true,
hostStatusInternal: true,
hostStatusDisplay: true,
adminStatusDisplay: true,
adminStatusInternal: true,
createdAt: true,
companyName: true,
assignedOn: true,
cities: { select: { id: true, cityName: true } },
states: { select: { id: true, stateName: true } },
countries: { select: { id: true, countryName: true } },
user: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
userRefNumber: true,
},
},
accountManager: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
roleXid: true,
},
},
},
orderBy: { createdAt: 'desc' },
skip: paginationOptions?.skip || 0,
take: paginationOptions?.limit || 10,
});
/** -----------------------------------
* TRANSFORM RESPONSE
* ----------------------------------- */
const transformedData = results.map((h) => ({
hostId: h.id,
host: h.user,
hostStatusDisplay: h.hostStatusDisplay,
hostStatusInternal: h.hostStatusInternal,
adminStatusDisplay: h.adminStatusDisplay,
adminStatusInternal: h.adminStatusInternal,
submittedOn: h.createdAt,
accountManager: h.accountManager || null,
companyName: h.companyName || null,
city: h.cities || null,
state: h.states || null,
country: h.countries || null,
assignedOn: h.assignedOn || null,
}));
return {
data: transformedData,
totalCount,
};
}
async getAllOnboardingHostApplications() {
const onBoardingHostApp = await this.prisma.hostHeader.findMany({
where: {
isActive: true,
hostStatusInternal: { notIn: [HOST_STATUS_INTERNAL.DRAFT] },
},
select: {
id: true,
companyName: true,
adminStatusDisplay: true,
assignedOn: true,
accountManagerXid: true,
createdAt: true,
user: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
userRefNumber: true,
mobileNumber: true,
},
},
accountManager: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
},
},
},
});
const bucket = config.aws.bucketName;
/** ---------------------------------
* Add presigned URL for AM profile
* --------------------------------- */
for (const host of onBoardingHostApp) {
const am = host.accountManager;
if (am?.profileImage) {
const key = am.profileImage.startsWith("http")
? am.profileImage.split(".com/")[1]
: am.profileImage;
am.profileImage = await getPresignedUrl(bucket, key);
}
}
return onBoardingHostApp;
}
async getAllOnboardingHostApplications_New() {
const onBoardingHostApp = await this.prisma.hostHeader.findMany({
where: {
isActive: true,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW,
},
select: {
id: true,
companyName: true,
adminStatusDisplay: true,
assignedOn: true,
accountManagerXid: true,
createdAt: true,
cities: {
select: {
id: true,
cityName: true,
},
},
countries: {
select: {
id: true,
countryName: true,
},
},
states: {
select: {
id: true,
stateName: true,
},
},
user: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
userRefNumber: true,
},
},
accountManager: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
},
},
},
});
const bucket = config.aws.bucketName;
/** ---------------------------------
* Add presigned URL for AM profile
* --------------------------------- */
for (const host of onBoardingHostApp) {
const am = host.accountManager;
if (am?.profileImage) {
const key = am.profileImage.startsWith("http")
? am.profileImage.split(".com/")[1]
: am.profileImage;
am.profileImage = await getPresignedUrl(bucket, key);
}
}
return onBoardingHostApp;
}
async getAllCoadminAndAM() {
// 1. Fetch all required users (Admin, Co-Admin, AM)
const users = await this.prisma.user.findMany({
where: {
roleXid: {
in: [
ROLE.MINGLAR_ADMIN, // Admin
ROLE.CO_ADMIN, // Co-Admin
ROLE.ACCOUNT_MANAGER, // AM
],
},
isActive: true,
userStatus: USER_STATUS.ACTIVE,
},
include: {
role: {
select: {
id: true,
roleName: true,
},
},
},
});
if (!users.length) return [];
const userIds = users.map((u) => u.id);
// 2. Count assigned hosts for ANY user (Admin / Co-Admin / AM)
const groupedHosts = await this.prisma.hostHeader.groupBy({
by: ['accountManagerXid'],
where: {
accountManagerXid: { in: userIds }, // assigned user
isActive: true,
},
_count: {
id: true,
},
});
// 3. Build quick lookup map: userId -> hostCount
const hostCountMap: Record<number, number> = {};
groupedHosts.forEach((g) => {
const uid = Number(g.accountManagerXid);
hostCountMap[uid] = g._count.id;
});
// 4. Attach host counts to each user
return users.map((user) => ({
...user,
assignedHostCount: hostCountMap[user.id] ?? 0,
}));
}
async getAllInvitedCoadminAndAM() {
return await this.prisma.user.findMany({
where: {
roleXid: {
in: [
ROLE.MINGLAR_ADMIN, // Admin
ROLE.CO_ADMIN, // Co-Admin
ROLE.ACCOUNT_MANAGER, // AM
],
},
isActive: true,
userStatus: {
not: USER_STATUS.DE_ACTIVATED, // Exclude DE_ACTIVATED status
},
},
include: {
role: {
select: {
id: true,
roleName: true,
},
},
},
});
}
async assignAMToHost(
userId: number,
hostXid: number,
accountManagerXid: number,
) {
const hostDetails = await this.prisma.hostHeader.findFirst({
where: { id: hostXid },
});
if (!hostDetails) {
throw new ApiError(404, 'Host not found');
}
if (hostDetails.accountManagerXid !== null) {
throw new ApiError(400, 'AM already assigned to this host');
}
if (
hostDetails.adminStatusInternal !==
MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED &&
hostDetails.adminStatusDisplay !== MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED
) {
throw new ApiError(400, 'Invalid host status');
}
await this.prisma.hostHeader.update({
where: { id: hostXid },
data: {
accountManagerXid: accountManagerXid,
assignedOn: new Date(),
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW,
},
});
return true;
}
/**
* Notify Account Manager by email after assignment.
* Encapsulates lookup + email send so handlers can call a single method.
*/
async notifyAMOfAssignment(accountManagerXid: number): Promise<boolean> {
if (!accountManagerXid) return false;
const amUser = await this.prisma.user.findUnique({
where: { id: accountManagerXid, isActive: true },
select: { emailAddress: true },
});
if (!amUser || !amUser.emailAddress) {
console.warn(
`AM notification skipped: user not found or missing email for id=${accountManagerXid}`,
);
return false;
}
try {
await sendAMEmailForHostAssign(amUser.emailAddress);
return true;
} catch (err) {
console.error('Error sending AM assignment email', err);
return false;
}
}
async addHostSuggestion(
hostXid: number,
title: string,
comments: string,
reviewedByXid: number,
) {
// Check if host exists
const hostHeader = await this.prisma.hostHeader.findUnique({
where: { id: hostXid },
select: { id: true },
});
console.log(hostHeader);
if (!hostHeader) {
throw new ApiError(404, 'Host not found');
}
// Create suggestion in host_suggestion table
await this.prisma.hostSuggestion.create({
data: {
hostXid: hostXid,
title: title,
comments: comments,
isparent: false,
isreviewed: false,
reviewedByXid: reviewedByXid,
reviewOn: null,
isActive: true,
},
});
return true;
}
async addPqqSuggestion(
title: string,
comments: string,
activity_pqq_header_xid: number,
reviewedByXid: number,
) {
// Check if host exists
const ActivityHeader = await this.prisma.activityPQQheader.findUnique({
where: { id: activity_pqq_header_xid, isActive: true },
select: { id: true },
});
if (!ActivityHeader) {
throw new ApiError(404, 'Host not found');
}
await this.prisma.activityPQQSuggestions.create({
data: {
title: title,
comments: comments,
isReviewed: false,
reviewedOn: new Date(),
isActive: true,
activityPqqHeaderXid: activity_pqq_header_xid,
reviewedByXid: reviewedByXid,
},
});
return true;
}
async getHostSuggestions(userId: number) {
const hostDetail = await this.prisma.hostHeader.findFirst({
where: { userXid: userId, isActive: true },
});
const suggestions = await this.prisma.hostSuggestion.findMany({
where: { hostXid: hostDetail.id, isreviewed: false, isActive: true },
select: {
id: true,
title: true,
comments: true,
isparent: true,
isreviewed: true,
reviewOn: true,
},
orderBy: {
id: 'asc',
},
});
return suggestions;
}
async editAgreementDetails(
host_xid: number,
agreementStartDate: string,
duration: number,
isCommisionBase: boolean,
commisionPer: number,
amountPerBooking: number,
durationFrequency: string,
payoutDurationNum: number,
payoutDurationFrequency: string,
) {
return await this.prisma.hostHeader.update({
where: { id: host_xid },
data: {
durationNumber: Number(duration),
durationFrequency: durationFrequency,
agreementStartDate: new Date(agreementStartDate),
isCommisionBase: isCommisionBase,
commisionPer: commisionPer ? Number(commisionPer) : null, // Convert to number if exists
amountPerBooking: amountPerBooking ? Number(amountPerBooking) : null, // Convert to number if exists
payoutDurationNum: Number(payoutDurationNum), // Convert to number
payoutDurationFrequency: payoutDurationFrequency,
},
});
}
async acceptHostApplication(host_xid: number, user_xid: number) {
return await this.prisma.$transaction(async (tx) => {
await this.prisma.hostHeader.update({
where: {
id: host_xid,
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.TO_REVIEW,
},
data: {
hostStatusInternal: HOST_STATUS_INTERNAL.APPROVED,
hostStatusDisplay: HOST_STATUS_DISPLAY.APPROVED,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_APPROVED,
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.APPROVED,
stepper: STEPPER.COMPANY_DETAILS_APPROVED,
},
});
await this.prisma.hostTrack.create({
data: {
hostXid: host_xid,
updatedByRole: ROLE_NAME.ACCOUNT_MANAGER,
updatedByXid: user_xid,
trackStatus: MINGLAR_STATUS_INTERNAL.AM_APPROVED,
},
});
});
}
async acceptHostApplicationMinglarAdmin(host_xid: number, user_xid: number) {
return await this.prisma.$transaction(async (tx) => {
await tx.hostHeader.update({
where: {
id: host_xid,
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW,
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW,
},
data: {
isApproved: true,
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED,
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED,
},
});
await this.prisma.hostTrack.create({
data: {
hostXid: host_xid,
updatedByRole: ROLE_NAME.MINGLAR_ADMIN,
updatedByXid: user_xid,
trackStatus: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED,
},
});
});
}
async rejectHostApplication(host_xid: number, user_xid: number) {
await this.prisma.$transaction(async (tx) => {
const hostDetails = await tx.hostHeader.findFirst({
where: { id: host_xid },
select: { id: true, userXid: true },
});
if (!hostDetails) {
throw new Error('Host not found');
}
await tx.hostHeader.update({
where: {
id: host_xid,
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
},
data: {
hostStatusInternal: HOST_STATUS_INTERNAL.REJECTED,
hostStatusDisplay: HOST_STATUS_DISPLAY.REJECTED,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED,
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.REJECTED,
},
});
await this.prisma.hostTrack.create({
data: {
hostXid: hostDetails.id,
updatedByRole: ROLE_NAME.MINGLAR_ADMIN,
updatedByXid: user_xid,
trackStatus: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED,
},
});
await tx.user.update({
where: { id: hostDetails.userXid },
data: {
userStatus: USER_STATUS.REJECTED,
},
});
});
}
async rejectHostApplicationAM(host_xid: number, user_xid: number) {
return await this.prisma.$transaction(async (tx) => {
const hostDetails = await this.prisma.hostHeader.findFirst({
where: { id: host_xid },
select: { id: true, userXid: true },
});
if (!hostDetails) {
throw new Error('Host not found');
}
await this.prisma.hostHeader.update({
where: {
id: host_xid,
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
},
data: {
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_TO_UPDATE,
hostStatusDisplay: HOST_STATUS_DISPLAY.ENHANCING,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_REJECTED,
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.ENHANCING,
},
});
await this.prisma.hostTrack.create({
data: {
hostXid: hostDetails.id,
updatedByRole: ROLE_NAME.ACCOUNT_MANAGER,
updatedByXid: user_xid,
trackStatus: MINGLAR_STATUS_INTERNAL.AM_REJECTED,
},
});
});
}
async getAMdetailById(id: number) {
return this.prisma.user.findUnique({
where: { id: id, isActive: true, userStatus: USER_STATUS.ACTIVE },
include: {
userAddressDetails: {
select: {
id: true,
userXid: true,
address1: true,
address2: true,
locationAddress: true,
locationLat: true,
locationLong: true,
locationName: true,
}
},
userDocuments: {
select: {
id: true,
fileName: true,
}
},
userRevenues: {
select: {
id: true,
is_fixed_salary: true,
per_value: true
}
},
},
});
}
async getBasicUserDetails(user_xid) {
return await this.prisma.user.findFirst({
where: {
id: user_xid
},
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
userStatus: true,
isProfileUpdated: true,
roleXid: true,
role: true,
}
})
}
async rejectPQQbyAM(activityId: number) {
return await this.prisma.activities.update({
where: {
id: activityId,
isActive: true
},
data: {
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQQ_TO_UPDATE,
activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.ENHANCING,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQQ_REJECTED,
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.ENHANCING
}
})
}
async getHostDetailsById(host_xid) {
const host = await this.prisma.hostHeader.findFirst({
where: { id: host_xid },
include: {
hostParent: {
include: {
HostParenetDocuments: {
select: {
id: true,
filePath: true,
documentName: true,
documentTypeXid: true,
documentType: true
}
}
}
},
HostBankDetails: true,
HostDocuments: {
include: {
documentType: true,
},
},
user: {
select: {
id: true,
emailAddress: true,
firstName: true,
lastName: true,
mobileNumber: true,
profileImage: true,
userStatus: true,
userRefNumber: true,
}
},
HostSuggestion: true,
HostTrack: true,
countries: true,
currencies: true,
states: true,
cities: true,
},
});
const bucket = config.aws.bucketName;
if (host.HostDocuments?.length) {
for (const doc of host.HostDocuments) {
if (doc.filePath) {
const filePath = doc.filePath;
// If full URL is saved, extract only key
const key = filePath.startsWith('http')
? filePath.split('.com/')[1]
: filePath;
(doc as any).presignedUrl = await getPresignedUrl(bucket, key);
}
}
}
if (host.logoPath) {
const key = host.logoPath.startsWith('http')
? host.logoPath.split('.com/')[1]
: host.logoPath;
host.logoPath = await getPresignedUrl(bucket, key);
}
if (host.user.profileImage) {
const key = host.user.profileImage.startsWith("http")
? host.user.profileImage.split(".com/")[1]
: host.user.profileImage;
host.user.profileImage = await getPresignedUrl(bucket, key);
}
if (host.hostParent?.length) {
const parent = host.hostParent[0]; // since you allow only 1 parent
// Parent company logo
if (parent.logoPath) {
const key = parent.logoPath.startsWith("http")
? parent.logoPath.split(".com/")[1]
: parent.logoPath;
parent.logoPath = await getPresignedUrl(bucket, key);
}
// Parent documents
if (parent.HostParenetDocuments?.length) {
for (const doc of parent.HostParenetDocuments) {
if (doc.filePath) {
const key = doc.filePath.startsWith("http")
? doc.filePath.split(".com/")[1]
: doc.filePath;
(doc as any).presignedUrl = await getPresignedUrl(bucket, key);
}
}
}
}
return host;
}
}