15 Commits

Author SHA1 Message Date
a31ec97640 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-05 12:49:44 +05:30
0c97412057 sending pincode in getbyid 2026-03-05 11:25:33 +05:30
paritosh18
b4ff39c0d7 removed logs 2026-03-04 18:59:37 +05:30
bb5da7647b sending the address details in the getbyid host api 2026-03-04 18:57:14 +05:30
3f19bb4087 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-04 18:45:44 +05:30
be8b9cef7d gbbb 2026-03-04 18:45:33 +05:30
paritosh18
77cef98091 Implement updateHostProfile endpoint and related service logic; remove navigation modes seeding; add logging for user activities 2026-03-04 17:04:14 +05:30
97f9c2b26e fixed the get in the getbyid 2026-03-04 12:23:29 +05:30
paritosh18
b93cd6b32c Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-03-02 19:07:20 +05:30
paritosh18
51319a69fc Enhance getCityByState handler to support optional search term with validation and update service method to filter cities accordingly. 2026-03-02 19:06:48 +05:30
5ad46309ef getting and storing the string for the navigation modes not the xids 2026-03-02 19:05:08 +05:30
paritosh18
781212277a ghfghf 2026-03-02 13:01:53 +05:30
6b0ee461c5 sending the hostId in the get stepper api 2026-03-02 12:35:51 +05:30
cc2fa3eb6b sending the 5 random users profile image in the getbyid api 2026-02-28 13:36:33 +05:30
fe6bb59cc7 Made add to bucket interested api and sending the count of bucket interested in the landing page api and surprise me api and sending the updated count in the add to bucket api 2026-02-28 13:11:26 +05:30
14 changed files with 930 additions and 115 deletions

View File

@@ -554,20 +554,6 @@ model Frequencies {
@@schema("mst")
}
model NavigationModes {
id Int @id @default(autoincrement())
navigationModeName String @unique @map("navigation_mode_name") @db.VarChar(30)
navigationModeIcon String @map("navigation_mode_icon") @db.VarChar(500)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityNavigationModes ActivityNavigationModes[]
@@map("navigation_modes")
@@schema("mst")
}
model TransportModes {
id Int @id @default(autoincrement())
transportModeName String @unique @map("transport_mode_name") @db.VarChar(60)
@@ -1456,8 +1442,7 @@ model ActivityNavigationModes {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
navigationModeXid Int @map("navigation_mode_xid")
navigationMode NavigationModes @relation(fields: [navigationModeXid], references: [id], onDelete: Restrict)
navigationModeName String @map("navigation_mode_name") @db.VarChar(30)
isInActivityChargeable Boolean @default(false) @map("is_in_activity_chargeable")
navigationModesBasePrice Int @map("navigation_modes_base_price")
navigationModesTotalPrice Int @map("navigation_modes_total_price")
@@ -1637,8 +1622,8 @@ model Cancellations {
scheduleHeaderXid Int @map("schedule_header_xid")
scheduleHeader ScheduleHeader @relation(fields: [scheduleHeaderXid], references: [id], onDelete: Cascade)
occurenceDate DateTime? @map("occurence_date")
startTime String? @map("start_time") @db.VarChar(30)
endTime String? @map("end_time") @db.VarChar(30)
startTime String? @map("start_time") @db.VarChar(30)
endTime String? @map("end_time") @db.VarChar(30)
cancellationReason String? @map("cancellation_reason")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")

View File

@@ -268,9 +268,9 @@ async function main() {
create: { interestName: 'Nightlife & Events', displayOrder: 10, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/NightlifeandEvents.png', interestCode: 'NE' },
});
const furfam = await prisma.interests.upsert({
where: { interestName: 'Fur Fam' },
where: { interestName: 'Pet space' },
update: {},
create: { interestName: 'Fur Fam', displayOrder: 11, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/petspace.jpg', interestCode: 'PS' },
create: { interestName: 'Pet space', displayOrder: 11, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/petspace.jpg', interestCode: 'PS' },
});
const dogoodfeelgood = await prisma.interests.upsert({
where: { interestName: 'Do Good, Feel Good' },
@@ -693,16 +693,6 @@ async function main() {
skipDuplicates: true,
});
// ✅ Navigation Modes
await prisma.navigationModes.createMany({
data: [
{ navigationModeName: 'Elephant Ride', navigationModeIcon: '🚗' },
{ navigationModeName: 'Horse Ride', navigationModeIcon: '🏍️' },
{ navigationModeName: 'Camel Ride', navigationModeIcon: '🚶' },
],
skipDuplicates: true,
});
// ✅ Transport Modes
await prisma.transportModes.createMany({
data: [

View File

@@ -258,6 +258,22 @@ acceptAggrement:
path: /host/Host_Admin/onboarding/accept-agreement
method: patch
getLatestAgreement:
handler: src/modules/host/handlers/Host_Admin/onboarding/getLatestAgreement.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Host_Admin/onboarding/getLatestAgreement.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/get-latest-agreement
method: get
getStepperInfo:
handler: src/modules/host/handlers/getStepper.handler
memorySize: 384
@@ -276,6 +292,22 @@ getStepperInfo:
path: /stepper
method: get
updateHostProfile:
handler: src/modules/host/handlers/updateHostProfile.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/updateHostProfile.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/profile
method: patch
# Functions with S3/AWS SDK dependencies
submitCompanyDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler

View File

@@ -346,4 +346,19 @@ getNearbyActivities:
events:
- httpApi:
path: /user/activities/get-nearby-activities
method: get
method: get
addActivityToBucketInterested:
handler: src/modules/user/handlers/activities/addToBucketInterested.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/handlers/activities/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/add-to-bucket-interested
method: post

View File

@@ -54,7 +54,7 @@ export const EquipmentDto = z.object({
/* ================= NAVIGATION MODE ================= */
export const NavigationModeDto = z.object({
navigationModeXid: z.number().int(),
navigationModeName: z.string().optional(),
isChargeable: z.boolean().optional(),
totalPrice: z.number().int().optional().default(0),
});

View File

@@ -1,6 +1,6 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
@@ -25,9 +25,8 @@ export const handler = safeHandler(async (
// Verify token and get user info
const userInfo = await verifyHostToken(token);
// Add suggestion using service
await hostService.acceptMinglarAgreement(userInfo.id);
// Accept agreement and get dynamic fields and PDF URL
const result = await hostService.acceptMinglarAgreement(userInfo.id);
return {
statusCode: 200,
@@ -38,7 +37,10 @@ export const handler = safeHandler(async (
body: JSON.stringify({
success: true,
message: 'Application accepted successfully',
data: null,
data: {
filePath: result.filePath,
dynamicFields: result.dynamicFields,
},
}),
};
});

View File

@@ -0,0 +1,54 @@
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const hostService = new HostService(prismaClient);
/**
* Get latest active agreement for a specific host by hostXid.
* Accessible for Minglar Admin / Host Admin using admin-host token.
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Validate admin/host admin token
await verifyMinglarAdminHostToken(token);
const hostXidParam =
event.queryStringParameters?.hostXid ?? event.queryStringParameters?.host_xid;
const hostXid = Number(hostXidParam);
if (!hostXidParam) {
throw new ApiError(400, 'hostXid is required');
}
if (Number.isNaN(hostXid)) {
throw new ApiError(400, 'Invalid hostXid format');
}
const agreement = await hostService.getLatestHostAgreement(hostXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Latest host agreement retrieved successfully',
data: agreement,
}),
};
});

View File

@@ -39,6 +39,7 @@ export const handler = safeHandler(async (
data: {
stepper: host?.host?.stepper || null,
emailAddress: host.user?.emailAddress || null,
hostId: host.user?.userRefNumber || null,
},
}),
};

View File

@@ -0,0 +1,244 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import dayjs from 'dayjs';
import { z } from 'zod';
import { prismaClient } from '../../../common/database/prisma.lambda.service';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
import { ROLE } from '../../../common/utils/constants/common.constant';
const updateHostProfileSchema = z
.strictObject({
// Personal
fullName: z.string().min(1).optional(),
firstName: z.string().min(1).optional(),
lastName: z.string().min(1).optional(),
isdCode: z.string().min(1).max(6).optional(),
mobileNumber: z.string().min(5).max(15).optional(),
dateOfBirth: z.string().min(1).optional(),
// Address
address1: z.string().min(1).optional(),
address2: z.string().min(1).optional(),
countryXid: z.number().int().positive().optional(),
stateXid: z.number().int().positive().optional(),
cityXid: z.number().int().positive().optional(),
pinCode: z.string().min(1).optional(),
// explicitly forbidden
emailAddress: z.any().optional(),
})
.strip();
function parseDob(dateOfBirth: string): Date {
const parsed = dayjs(dateOfBirth, ['YYYY-MM-DD', 'MM/DD/YYYY', 'DD/MM/YYYY'], true);
if (!parsed.isValid()) {
throw new ApiError(400, 'Invalid dateOfBirth. Use YYYY-MM-DD (recommended) or MM/DD/YYYY.');
}
return parsed.toDate();
}
function splitFullName(fullName: string): { firstName: string; lastName: string | null } {
const parts = fullName.trim().split(/\s+/).filter(Boolean);
const firstName = parts[0] || '';
const lastName = parts.length > 1 ? parts.slice(1).join(' ') : null;
return { firstName, lastName };
}
function getAuthToken(event: APIGatewayProxyEvent): string {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
return token;
}
function parseJsonBody(event: APIGatewayProxyEvent): any {
try {
return event.body ? JSON.parse(event.body) : {};
} catch {
throw new ApiError(400, 'Invalid JSON in request body');
}
}
function validateBody(body: any) {
const parsed = updateHostProfileSchema.safeParse(body);
if (!parsed.success) {
throw new ApiError(400, parsed.error.issues.map((i) => i.message).join(', '));
}
if (parsed.data.emailAddress !== undefined) {
throw new ApiError(400, 'Email address cannot be updated.');
}
return parsed.data;
}
function normalizeNameFields(data: any): { firstName?: string; lastName?: string | null } {
if (data.fullName && !data.firstName && !data.lastName) {
const split = splitFullName(data.fullName);
return { firstName: split.firstName, lastName: split.lastName };
}
return { firstName: data.firstName, lastName: data.lastName };
}
function buildAddressInput(data: any) {
return {
address1: data.address1,
address2: data.address2,
countryXid: data.countryXid,
stateXid: data.stateXid,
cityXid: data.cityXid,
pinCode: data.pinCode,
};
}
function hasAnyDefined(obj: Record<string, unknown>) {
return Object.values(obj).some((v) => v !== undefined);
}
async function ensureHostUser(tx: any, userId: number) {
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.');
}
async function updateUserIfNeeded(tx: any, userId: number, input: { firstName?: string; lastName?: string | null; isdCode?: string; mobileNumber?: string; dateOfBirth?: string }) {
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 ? parseDob(input.dateOfBirth) : null;
}
if (!hasAnyDefined(userUpdateData)) return;
await tx.user.update({
where: { id: userId },
data: {
...userUpdateData,
isProfileUpdated: true,
},
});
}
async function upsertAddressIfNeeded(tx: any, userId: number, addressData: Record<string, any>) {
if (!hasAnyDefined(addressData)) return;
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,
});
return;
}
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,
},
});
}
async function getProfileSnapshot(tx: any, userId: number) {
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,
};
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = getAuthToken(event);
const userInfo = await verifyHostToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user id');
}
const body = parseJsonBody(event);
const data = validateBody(body);
const name = normalizeNameFields(data);
const address = buildAddressInput(data);
const result = await prismaClient.$transaction(async (tx) => {
await ensureHostUser(tx, userId);
await updateUserIfNeeded(tx, userId, {
firstName: name.firstName,
lastName: name.lastName,
isdCode: data.isdCode,
mobileNumber: data.mobileNumber,
dateOfBirth: data.dateOfBirth,
});
await upsertAddressIfNeeded(tx, userId, address);
return getProfileSnapshot(tx, userId);
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Profile updated successfully',
data: result,
}),
};
});

View File

@@ -391,6 +391,22 @@ const s3 = new AWS.S3({
region: config.aws.region,
});
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;
};
};
@Injectable()
export class HostService {
constructor(private prisma: PrismaClient) { }
@@ -415,8 +431,8 @@ export class HostService {
});
const user = await this.prisma.user.findUnique({
where: { id: user_xid },
select: { id: true, emailAddress: true },
where: { id: user_xid, isActive: true },
select: { id: true, emailAddress: true, userRefNumber: true },
});
return { host, user };
}
@@ -465,6 +481,39 @@ export class HostService {
profileImage: true,
userStatus: true,
userRefNumber: true,
userAddressDetails: {
where: { isActive: true },
select: {
id: true,
address1: true,
address2: true,
locationAddress: true,
locationLat: true,
locationLong: true,
pinCode: true,
cityXid: true,
cities: {
select: {
id: true,
cityName: true,
}
},
stateXid: true,
states: {
select: {
id: true,
stateName: true,
}
},
countryXid: true,
country:{
select: {
id: true,
countryName: true
}
}
}
}
},
},
companyTypes: {
@@ -577,6 +626,114 @@ export class HostService {
return this.prisma.user.delete({ where: { id } });
}
/**
* 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,
};
});
}
async getHostByEmail(email: string): Promise<User> {
return this.prisma.user.findUnique({ where: { emailAddress: email } });
}
@@ -919,55 +1076,120 @@ export class HostService {
acceptDate,
};
const pdfBuffer = await renderAgreementPdf(agreementVars);
let pdfUrl: string | null = null;
const existingCount = await this.prisma.hostAgreement.count({
where: { hostXid: host.id, isActive: true },
});
try {
const pdfBuffer = await renderAgreementPdf(agreementVars);
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();
const pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`;
await this.prisma.$transaction(async (tx) => {
// Optional: mark previous agreements inactive
await tx.hostAgreement.updateMany({
const existingCount = await this.prisma.hostAgreement.count({
where: { hostXid: host.id, isActive: true },
data: { isActive: false },
});
await tx.hostAgreement.create({
data: {
hostXid: host.id,
filePath: pdfUrl,
versionNumber: nextVersionNumber,
isActive: true,
},
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
}
try {
const existingCount = await this.prisma.hostAgreement.count({
where: { hostXid: host.id, isActive: true },
});
await tx.hostHeader.update({
where: { id: host.id },
data: {
stepper: STEPPER.AGREEMENT_ACCEPTED,
isApproved: true,
agreementAccepted: true,
agreementStartDate: host.agreementStartDate || new Date(),
},
const nextVersionNumber = `AG${existingCount + 1}`;
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(),
},
});
});
} 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,
};
}
/**
* 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');
}
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,
},
});
if (!agreement) {
throw new ApiError(404, 'No active agreement found for this host');
}
const filePath = agreement.filePath;
// If full URL is saved, extract only S3 key part
const key = filePath.startsWith('http')
? filePath.split('.com/')[1]
: filePath;
const bucket = config.aws.bucketName;
const presignedUrl = await getPresignedUrl(bucket, key);
return {
...agreement,
presignedUrl,
};
}
async getPQQQuestionDetail(question_xid: number, activity_xid: number) {
@@ -2374,15 +2596,9 @@ export class HostService {
},
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesTotalPrice: true,
navigationMode: {
select: {
id: true,
navigationModeName: true,
navigationModeIcon: true,
},
},
},
},
equipmentAvailable: true,
@@ -3706,7 +3922,7 @@ export class HostService {
const navMode = await tx.activityNavigationModes.create({
data: {
activityXid,
navigationModeXid: mode.navigationModeXid,
navigationModeName: mode.navigationModeName,
isInActivityChargeable: isChargeable,
navigationModesBasePrice: basePrice,
navigationModesTotalPrice: totalPrice,

View File

@@ -27,15 +27,21 @@ export const handler = safeHandler(async (
// 2) Authenticate user
await verifyMinglarAdminHostToken(token);
// 3) Get bankXid from query params
// 3) Get stateXid and optional search term from query params
const stateXid = Number(event.queryStringParameters?.stateXid);
const search = event.queryStringParameters?.search?.trim();
if (!stateXid || isNaN(stateXid)) {
throw new ApiError(400, "Valid stateXid is required in query params.");
}
// 4) Fetch branches for the bank
const branches = await prePopulateService.getCityByStateId(stateXid);
// If search is provided, enforce minimum 3 characters
if (search && search.length < 3) {
throw new ApiError(400, "Search term must be at least 3 characters long.");
}
// 4) Fetch cities for the state (optionally filtered by search)
const branches = await prePopulateService.getCityByStateId(stateXid, search);
return {
statusCode: 200,

View File

@@ -39,12 +39,20 @@ export class PrePopulateService {
}
async getCityByStateId(stateXid: number) {
async getCityByStateId(stateXid: number, search?: string) {
return await this.prisma.cities.findMany({
where: {
stateXid,
isActive: true,
deletedAt: null
deletedAt: null,
...(search && search.length >= 3
? {
cityName: {
contains: search,
mode: 'insensitive',
},
}
: {}),
},
select: {
id: true,
@@ -153,7 +161,6 @@ export class PrePopulateService {
foodType,
cuisineDetails,
vehicleType,
navigationMode,
taxDetails,
energyLevel,
aminitiesDetails,
@@ -171,9 +178,6 @@ export class PrePopulateService {
this.prisma.transportModes.findMany({
where: { isActive: true },
}),
this.prisma.navigationModes.findMany({
where: { isActive: true },
}),
this.prisma.taxes.findMany({
where: { isActive: true },
}),
@@ -215,7 +219,6 @@ export class PrePopulateService {
foodType,
cuisineDetails,
vehicleType,
navigationMode,
taxDetails,
energyLevel,
aminitiesDetails,

View File

@@ -0,0 +1,72 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { UserService } from '../../services/user.service';
const userService = new UserService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Extract token from headers
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Authenticate user using verifyUserToken
const userInfo = await verifyUserToken(token);
const userId = userInfo.id;
if (Number.isNaN(userId)) {
throw new ApiError(400, 'User id must be a number');
}
const user = await userService.getUserById(userId);
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse request body
let body: { activityXid: number; isBucket: boolean; bucketTypeName: string; };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityXid, isBucket, bucketTypeName } = body;
// Validate required fields
if (
typeof activityXid !== 'number' ||
typeof isBucket !== 'boolean' ||
!bucketTypeName
) {
throw new ApiError(400, 'Required fields missing or invalid');
}
// Set the passcode
const counts = await userService.addToBucketInterested(userId, isBucket, bucketTypeName, activityXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: `Activity added to ${isBucket ? 'bucket' : 'interested'} successfully`,
data: {
bucketCount: counts.bucketCount,
interestedCount: counts.interestedCount,
}
}),
};
});

View File

@@ -709,6 +709,29 @@ export class UserService {
};
}
const userBucketInterested = await tx.userBucketInterested.findMany({
where: {
userXid: userId,
isActive: true,
},
select: {
activityXid: true,
isBucket: true,
},
});
const userBucketActivityIds = userBucketInterested
.filter(u => u.isBucket)
.map(u => u.activityXid);
const userInterestedActivityIds = userBucketInterested
.filter(u => !u.isBucket)
.map(u => u.activityXid);
const allUserExcludedActivityIds = userBucketInterested.map(
u => u.activityXid,
);
const userConnectionDetails = await tx.connectDetails.findMany({
where: { userXid: userId, isActive: true },
select: {
@@ -762,6 +785,11 @@ export class UserService {
activityTypeXid: {
in: activitiyTypesOfUserInterests.map((at) => at.id),
},
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
},
},
skip,
take: limit,
@@ -842,7 +870,12 @@ export class UserService {
// IF user wants the standard 4-step ranking applied TO the most hyped items:
const mostHypedActivitiesRaw = await tx.activities.findMany({
where: {
id: { in: mostHypedActivityIds },
id: {
in: mostHypedActivityIds,
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -966,6 +999,11 @@ export class UserService {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
},
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
};
@@ -984,6 +1022,11 @@ export class UserService {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
},
};
if (effectiveCountryXid) {
@@ -1010,6 +1053,11 @@ export class UserService {
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
},
},
});
@@ -1034,6 +1082,11 @@ export class UserService {
amInternalStatus:
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
},
},
select: {
id: true,
@@ -1076,6 +1129,11 @@ export class UserService {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
},
};
if (effectiveCountryXid) {
@@ -1152,6 +1210,8 @@ export class UserService {
loggedInNetworkCount: 0,
citiesInNetworkCount: 0,
rating: 0,
interestedCount: userInterestedActivityIds.length,
bucketCount: userBucketActivityIds.length,
pagination: {
page,
limit,
@@ -1237,6 +1297,32 @@ export class UserService {
const skip = (page - 1) * limit;
const userBucketInterested = await tx.userBucketInterested.findMany({
where: {
userXid: userId,
isActive: true,
},
select: {
activityXid: true,
isBucket: true,
},
});
const bucketActivityIds = userBucketInterested
.filter(u => u.isBucket)
.map(u => u.activityXid);
const interestedActivityIds = userBucketInterested
.filter(u => !u.isBucket)
.map(u => u.activityXid);
const excludedActivityIds = userBucketInterested.map(
u => u.activityXid,
);
const safeExcludedIds =
excludedActivityIds.length > 0 ? excludedActivityIds : [-1];
/* =====================================================
CONNECTION INTEREST MAP
===================================================== */
@@ -1270,7 +1356,6 @@ export class UserService {
where: {
userXid: { in: connectionUserIds },
isActive: true,
isBucket: true,
},
_count: { activityXid: true },
});
@@ -1302,6 +1387,9 @@ export class UserService {
const otherInterestActivities = await tx.activities.findMany({
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: { notIn: safeExcludedIds },
...excludeUserInterestCondition,
},
skip,
@@ -1388,7 +1476,16 @@ export class UserService {
).length;
const hypedActivities = await tx.activities.findMany({
where: { id: { in: mostHypedGrouped.map((h) => h.activityXid) } },
where: {
id: {
in: mostHypedGrouped.map((h) => h.activityXid),
notIn: safeExcludedIds,
},
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
isActive: true,
},
select: {
id: true,
activityTitle: true,
@@ -1427,7 +1524,10 @@ export class UserService {
5⃣ NEW ARRIVALS
===================================================== */
const newArrivalsWhere = {
id: { notIn: safeExcludedIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
...excludeUserInterestCondition,
};
@@ -1457,6 +1557,9 @@ export class UserService {
===================================================== */
const otherStatesWhere: any = {
isActive: true,
id: { notIn: safeExcludedIds },
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
...excludeUserInterestCondition,
};
if (effectiveCountryXid)
@@ -1466,6 +1569,9 @@ export class UserService {
const overseasWhere: any = {
isActive: true,
id: { notIn: safeExcludedIds },
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
...excludeUserInterestCondition,
};
if (effectiveCountryXid)
@@ -1513,6 +1619,8 @@ export class UserService {
return {
pagination: { page, limit },
interests: interestsWithActivities,
interestedCount: interestedActivityIds.length,
bucketCount: bucketActivityIds.length,
mostHypedActivities: {
page,
@@ -1743,14 +1851,7 @@ export class UserService {
where: { isActive: true },
select: {
id: true,
navigationModeXid: true,
navigationMode: {
select: {
id: true,
navigationModeName: true,
navigationModeIcon: true,
},
},
navigationModeName: true,
isInActivityChargeable: true,
navigationModesTotalPrice: true,
},
@@ -1951,13 +2052,6 @@ export class UserService {
const connectionUserIds = connectionUsers.map((u) => u.userXid);
const interestedCount = await tx.userBucketInterested.count({
where: {
activityXid,
isBucket: false,
isActive: true,
},
});
const connectionInterestedCount = connectionUserIds.length
? await tx.userBucketInterested.count({
@@ -1979,6 +2073,50 @@ export class UserService {
(v) => v.venueCapacity ?? 0,
).reduce((sum, capacity) => sum + capacity, 0);
const interestedCount = await tx.userBucketInterested.count({
where: {
activityXid,
isBucket: false,
isActive: true,
},
});
const interestedUsers = await tx.userBucketInterested.findMany({
where: {
activityXid,
isBucket: false,
isActive: true,
user: {
isActive: true,
},
},
select: {
user: {
select: {
profileImage: true,
},
},
},
});
const shuffledUsers = interestedUsers.sort(() => 0.5 - Math.random());
const randomFive = shuffledUsers.slice(0, 5);
const interestedUserImages: string[] = [];
for (const item of randomFive) {
const profileImage = item.user.profileImage;
if (profileImage) {
const key = profileImage.startsWith('http')
? new URL(profileImage).pathname.replace(/^\/+/, '')
: profileImage;
const presignedUrl = await getPresignedUrl(bucket, key);
interestedUserImages.push(presignedUrl);
}
}
return {
activity,
interestedCount,
@@ -1987,6 +2125,7 @@ export class UserService {
totalCapacity,
rating: 0, // ⭐ Placeholder, implement rating logic as needed
distance: 0,
interestedUserImages
};
});
}
@@ -2522,6 +2661,7 @@ export class UserService {
},
});
if (!networkUsers.length) {
return {
interests: [],
@@ -3500,4 +3640,59 @@ export class UserService {
});
}
async addToBucketInterested(
userXid: number,
isBucket: boolean,
bucketTypeName: string,
activityXid: number
) {
const activityExists = await this.prisma.activities.findFirst({
where: { id: activityXid, isActive: true },
});
if (!activityExists) {
throw new ApiError(404, 'Activity not found');
}
const existing = await this.prisma.userBucketInterested.findFirst({
where: { userXid, activityXid },
});
if (existing) {
throw new ApiError(400, 'Activity already added');
}
await this.prisma.userBucketInterested.create({
data: {
userXid,
activityXid,
isBucket,
bucketTypeName,
},
});
// ✅ Get updated counts
const [bucketCount, interestedCount] = await Promise.all([
this.prisma.userBucketInterested.count({
where: {
userXid,
isBucket: true,
isActive: true,
},
}),
this.prisma.userBucketInterested.count({
where: {
userXid,
isBucket: false,
isActive: true,
},
}),
]);
return {
bucketCount,
interestedCount,
};
}
}