added gender name column and interest color and image column added activity seeder data till gamecraft made register and add personal info api for user mobile endpoints lambda and service

This commit is contained in:
2026-01-16 17:50:30 +05:30
parent 6d377296fc
commit f20e4191ee
8 changed files with 351 additions and 33 deletions

View File

@@ -16,8 +16,9 @@ model User {
lastName String? @map("last_name") @db.VarChar(50)
roleXid Int? @map("role_xid")
dateOfBirth DateTime? @map("date_of_birth")
genderName String? @map("gender_name") @db.VarChar(20)
role Roles? @relation(fields: [roleXid], references: [id], onDelete: Restrict)
emailAddress String @unique @map("email_address") @db.VarChar(150)
emailAddress String? @unique @map("email_address") @db.VarChar(150)
isdCode String? @map("isd_code") @db.VarChar(6) // +91, +1, +971 etc.
mobileNumber String? @map("mobile_number") @db.VarChar(15) // international safe limit
userPassword String? @map("user_password") @db.VarChar(255) // hashed passwords
@@ -355,6 +356,8 @@ model BankBranches {
model Interests {
id Int @id @default(autoincrement())
interestName String @unique @map("interest_name") @db.VarChar(50)
interestColor String @map("interest_color") @db.VarChar(20)
interestImage String @map("interest_image") @db.VarChar(500)
displayOrder Int @map("display_order")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")

View File

@@ -163,56 +163,118 @@ async function main() {
// ✅ Interests + Activity Types
const chillandzen = await prisma.interests.upsert({
where: { interestName: 'Chill and Zen' },
where: { interestName: 'Chill & Zen' },
update: {},
create: { interestName: 'Chill and Zen', displayOrder: 1 },
create: { interestName: 'Chill & Zen', displayOrder: 1 },
});
const artsyfeels = await prisma.interests.upsert({
where: { interestName: 'Artsy Feels' },
update: {},
create: { interestName: 'Artsy Feels', displayOrder: 2 },
});
const sweatmode = await prisma.interests.upsert({
where: { interestName: 'Sweat Mode On' },
where: { interestName: 'Sweat Mode' },
update: {},
create: { interestName: 'Sweat Mode On', displayOrder: 2 },
create: { interestName: 'Sweat Mode', displayOrder: 3 },
});
const trackracer = await prisma.interests.upsert({
where: { interestName: 'Track Racer' },
const gamecraft = await prisma.interests.upsert({
where: { interestName: 'Gamecraft' },
update: {},
create: { interestName: 'Track Racer', displayOrder: 3 },
create: { interestName: 'Gamecraft', displayOrder: 4 },
});
const circuitracer = await prisma.interests.upsert({
where: { interestName: 'Circuit Racer' },
const wildandfree = await prisma.interests.upsert({
where: { interestName: 'Wild & Free' },
update: {},
create: { interestName: 'Circuit Racer', displayOrder: 4 },
create: { interestName: 'Wild & Free', displayOrder: 5 },
});
const thermalGliding = await prisma.interests.upsert({
where: { interestName: 'Thermal Gliding' },
const splashlife = await prisma.interests.upsert({
where: { interestName: 'Splash Life' },
update: {},
create: { interestName: 'Thermal Gliding', displayOrder: 5 },
create: { interestName: 'Splash Life', displayOrder: 6 },
});
const partycentral = await prisma.interests.upsert({
where: { interestName: 'Party Central' },
const cultureandheritage = await prisma.interests.upsert({
where: { interestName: 'Culture & Heritage' },
update: {},
create: { interestName: 'Party Central', displayOrder: 6 },
create: { interestName: 'Culture & Heritage', displayOrder: 7 },
});
const aqua = await prisma.interests.upsert({
where: { interestName: 'Aqua' },
const Gastronomé = await prisma.interests.upsert({
where: { interestName: 'Gastronomé' },
update: {},
create: { interestName: 'Aqua', displayOrder: 7 },
create: { interestName: 'Gastronomé', displayOrder: 8 },
});
const foodie = await prisma.interests.upsert({
where: { interestName: 'Foodie' },
const sportsarena = await prisma.interests.upsert({
where: { interestName: 'Sports Arena' },
update: {},
create: { interestName: 'Foodie', displayOrder: 8 },
create: { interestName: 'Sports Arena', displayOrder: 9 },
});
const nightlifeevents = await prisma.interests.upsert({
where: { interestName: 'Nightlife & Events' },
update: {},
create: { interestName: 'Nightlife & Events', displayOrder: 10 },
});
const furfam = await prisma.interests.upsert({
where: { interestName: 'Fur Fam' },
update: {},
create: { interestName: 'Fur Fam', displayOrder: 11 },
});
const dogoodfeelgood = await prisma.interests.upsert({
where: { interestName: 'Do Good, Feel Good' },
update: {},
create: { interestName: 'Do Good, Feel Good', displayOrder: 12 },
});
await prisma.activityTypes.createMany({
data: [
{ interestXid: aqua.id, activityTypeName: 'Scuba-Diving', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Cloudboarding', energyLevelXid: highEnergy.id },
{ interestXid: partycentral.id, activityTypeName: 'Soaring Glider', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Speedway Racer', energyLevelXid: highEnergy.id },
{ interestXid: aqua.id, activityTypeName: 'Aerial Surfing', energyLevelXid: highEnergy.id },
{ interestXid: foodie.id, activityTypeName: 'Wine Tasting', energyLevelXid: lowEnergy.id },
{ interestXid: trackracer.id, activityTypeName: 'Track Racer', energyLevelXid: highEnergy.id },
{ interestXid: thermalGliding.id, activityTypeName: 'Thermal Gliding', energyLevelXid: mediumEnergy.id },
// --------Chill & Zen--------
{ interestXid: chillandzen.id, activityTypeName: 'Yoga', energyLevelXid: lowEnergy.id },
{ interestXid: chillandzen.id, activityTypeName: 'Meditation', energyLevelXid: lowEnergy.id },
{ interestXid: chillandzen.id, activityTypeName: 'Spa Retreat', energyLevelXid: lowEnergy.id },
{ interestXid: chillandzen.id, activityTypeName: 'Bath Experience', energyLevelXid: lowEnergy.id },
{ interestXid: chillandzen.id, activityTypeName: 'Stargazing', energyLevelXid: lowEnergy.id },
{ interestXid: chillandzen.id, activityTypeName: 'Nail Spa/Art', energyLevelXid: lowEnergy.id },
// --------Artsy Feels--------
{ interestXid: artsyfeels.id, activityTypeName: 'Canvas Painting', energyLevelXid: lowEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Textile Painting', energyLevelXid: lowEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Music and Instruments', energyLevelXid: mediumEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Pottery', energyLevelXid: mediumEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Knitting / Crocheting', energyLevelXid: lowEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Lipstick Customisation', energyLevelXid: lowEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Tufting', energyLevelXid: mediumEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Acting', energyLevelXid: mediumEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Art', energyLevelXid: lowEnergy.id },
{ interestXid: artsyfeels.id, activityTypeName: 'Tattoos', energyLevelXid: lowEnergy.id },
// --------Sweat Mode--------
{ interestXid: sweatmode.id, activityTypeName: 'Dance', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Kickboxing', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Gym with Personal Trainer', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Aerobic', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Skating', energyLevelXid: mediumEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Martial Arts', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Trampoline Park', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Wall Climbing', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Rope Course', energyLevelXid: highEnergy.id },
{ interestXid: sweatmode.id, activityTypeName: 'Running', energyLevelXid: highEnergy.id },
//---------Game Craft---------
{ interestXid: gamecraft.id, activityTypeName: 'Billiard / Snooker', energyLevelXid: lowEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Squash', energyLevelXid: highEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Rage Room', energyLevelXid: highEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'E-Sports', energyLevelXid: lowEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Table Tennis', energyLevelXid: mediumEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'VR Games', energyLevelXid: mediumEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Escape Room', energyLevelXid: mediumEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Paintball', energyLevelXid: highEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Bowling', energyLevelXid: mediumEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Shooting Range', energyLevelXid: lowEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Bumper Cars', energyLevelXid: lowEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Ice Skating', energyLevelXid: mediumEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Snow City', energyLevelXid: lowEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Pole Artistry', energyLevelXid: highEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Hula Hoop', energyLevelXid: mediumEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Foosball', energyLevelXid: lowEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Go Karting', energyLevelXid: mediumEnergy.id },
{ interestXid: gamecraft.id, activityTypeName: 'Laser Maze', energyLevelXid: mediumEnergy.id },
//---------Wild & Free---------
],
skipDuplicates: true,
});

View File

@@ -0,0 +1,23 @@
// validations/hostBankDetails.validation.ts
import { z } from "zod";
export const userPersonalInfoSchema = z.object({
firstName: z
.string()
.nonempty("First name is required"),
lastName: z
.string()
.optional(),
genderName: z
.string()
.nonempty("Gender is required"),
dateOfBirth: z
.string()
.nonempty("Date of birth is required"),
});
export type UserPersonalInfoSchema = z.infer<typeof userPersonalInfoSchema>;

View File

@@ -27,7 +27,6 @@ export const handler = safeHandler(async (
if (!email) {
throw new ApiError(400, 'Email is required');
}
console.log(email, " -: Email")
const emailToLowerCase = email.toLowerCase()
@@ -35,7 +34,6 @@ export const handler = safeHandler(async (
where: { emailAddress: emailToLowerCase, isActive: true, userStatus: USER_STATUS.INVITED },
select: { emailAddress: true, id: true, userPassword: true, roleXid: true },
});
console.log(user, "sljdfjdf")
if (!user) {
throw new ApiError(403, 'You are not allowed to register directly. Please contact minglar admin.');

View File

@@ -0,0 +1,13 @@
export class AddPersonalInfoDTO {
firstName: string;
lastName?: string;
genderName: string;
dateOfBirth: string;
constructor(firstName: string, genderName: string, dateOfBirth: string, lastName?: string) {
this.firstName = firstName;
this.lastName = lastName;
this.genderName = genderName;
this.dateOfBirth = dateOfBirth;
}
}

View File

@@ -0,0 +1,122 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import * as bcrypt from 'bcryptjs';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { ROLE } from '../../../../common/utils/constants/common.constant';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { encryptUserId } from '../../../../common/utils/helper/CodeGenerator';
import { OtpGeneratorSixDigit } from '../../../../common/utils/helper/OtpGenerator';
export async function generateUserRefNumber(tx: any) {
const lastrecord = await tx.user.findFirst({
orderBy: {
id: 'desc',
},
select: {
id: true,
},
});
const nextId = lastrecord ? lastrecord.id + 1 : 1;
return `USR-${String(nextId).padStart(6, '0')}`;;
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse request body
let body: { mobileNumber?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { mobileNumber } = body;
if (!mobileNumber) {
throw new ApiError(400, 'Mobile number is required');
}
// Use a single transaction for user creation/lookup and OTP storage
const transactionResult = await prismaClient.$transaction(async (tx) => {
const user = await tx.user.findFirst({
where: { mobileNumber: mobileNumber, isActive: true },
select: { emailAddress: true, id: true, userPasscode: true, mobileNumber: true },
});
if (user && user.userPasscode) {
throw new ApiError(409, 'User is already registered. Please login.');
}
let newUserLocal;
const referenceNumber = await generateUserRefNumber(tx);
if (user && !user.userPasscode) {
// reuse existing invited user record
newUserLocal = user;
} else {
// create new user record within the transaction
newUserLocal = await tx.user.create({
data: {
mobileNumber: mobileNumber,
role: {
connect: {
id: ROLE.USER, // 👈 Role ID
},
},
userRefNumber: referenceNumber
},
});
}
// Generate OTP (6-digit) and store within the same transaction
const otp = OtpGeneratorSixDigit.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes
// delete old active OTPs for this user/purpose
await tx.userOtp.deleteMany({
where: { userXid: Number(newUserLocal.id), otpType: 'Register', isActive: true },
});
await tx.userOtp.create({
data: {
userXid: Number(newUserLocal.id),
otpType: 'Register',
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
const encryptedId = encryptUserId(String(newUserLocal.id));
return { newUser: newUserLocal, otp, encryptedId };
});
if (!transactionResult || !transactionResult.otp) {
throw new ApiError(500, 'Failed to generate OTP');
}
// Send OTP email outside the DB transaction
// await sendOtpEmailForHost(transactionResult.newUser.emailAddress, transactionResult.otp);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP sent successfully.',
data: {},
}),
};
});

View File

@@ -0,0 +1,70 @@
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 { userPersonalInfoSchema } from '../../../../common/utils/validation/user/addPersonalInfo.validation';
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 the shared authForHost function
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: { firstName?: string; lastName?: string; genderName: string; dateOfBirth?: string; };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
// ✅ Validate payload using Zod
const validationResult = userPersonalInfoSchema.safeParse({
...(body as object),
});
if (!validationResult.success) {
const errorMessages = validationResult.error.issues.map(e => e.message).join(', ');
throw new ApiError(400, `Validation failed: ${errorMessages}`);
}
const validatedData = validationResult.data;
await userService.addPersonalInfo({
...validatedData
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Personal Info added successfully',
}),
};
});

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient, User } from '@prisma/client';
import { AddPersonalInfoDTO } from '../dto/user.dto';
import ApiError from '@/common/utils/helper/ApiError';
@Injectable()
export class UserService {
constructor(private prisma: PrismaClient) { }
async getUserById(userId: number) {
return this.prisma.user.findUnique({
where: { id: userId, isActive: true },
});
}
async addPersonalInfo(data: AddPersonalInfoDTO){
return await this.prisma.$transaction(async (tx) => {
const addPersonalInfo = await tx.user.create({
data,
});
if (!addPersonalInfo) {
throw new ApiError(400, 'Failed to add personal info');
}
})
}
}