feat: enhance user and activity models with location relations and add landing page details API
This commit is contained in:
@@ -235,6 +235,9 @@ model Countries {
|
||||
HostHeader HostHeader[]
|
||||
hostParent HostParent[]
|
||||
userAddressDetails UserAddressDetails[]
|
||||
// 🔹 Activity relations
|
||||
checkInActivities Activities[] @relation("CheckInCountry")
|
||||
checkOutActivities Activities[] @relation("CheckOutCountry")
|
||||
|
||||
@@map("countries")
|
||||
@@schema("mst")
|
||||
@@ -272,6 +275,9 @@ model States {
|
||||
HostHeader HostHeader[]
|
||||
hostParent HostParent[]
|
||||
userAddressDetails UserAddressDetails[]
|
||||
// 🔹 Activity relations
|
||||
checkInActivities Activities[] @relation("CheckInState")
|
||||
checkOutActivities Activities[] @relation("CheckOutState")
|
||||
|
||||
@@map("states")
|
||||
@@schema("mst")
|
||||
@@ -281,7 +287,7 @@ model Cities {
|
||||
id Int @id @default(autoincrement())
|
||||
stateXid Int @map("state_xid")
|
||||
states States @relation(fields: [stateXid], references: [id], onDelete: Cascade)
|
||||
cityName String @unique @map("city_name") @db.VarChar(50)
|
||||
cityName String @map("city_name") @db.VarChar(50)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
@@ -290,6 +296,9 @@ model Cities {
|
||||
HostHeader HostHeader[]
|
||||
hostParent HostParent[]
|
||||
userAddressDetails UserAddressDetails[]
|
||||
// 🔹 Activity relations
|
||||
checkInActivities Activities[] @relation("CheckInCity")
|
||||
checkOutActivities Activities[] @relation("CheckOutCity")
|
||||
|
||||
@@map("cities")
|
||||
@@schema("mst")
|
||||
@@ -886,10 +895,22 @@ model Activities {
|
||||
pickUpDropIsChargeable Boolean? @map("pick_up_drop_is_chargeable")
|
||||
inActivityAvailable Boolean? @map("in_activity_available")
|
||||
inActivityIsChargeable Boolean? @map("in_activity_is_chargeable")
|
||||
is_late_checking_allowed Boolean? @map("is_late_checking_allowed")
|
||||
isLateCheckingAllowed Boolean? @map("is_late_checking_allowed")
|
||||
equipmentAvailable Boolean? @map("equipment_available")
|
||||
equipmentIsChargeable Boolean? @map("equipment_is_chargeable")
|
||||
cancellationAvailable Boolean? @map("cancellation_available")
|
||||
checkInStateXid Int? @map("check_in_state_xid")
|
||||
checkInState States? @relation("CheckInState", fields: [checkInStateXid], references: [id], onDelete: Restrict)
|
||||
checkInCityXid Int? @map("check_in_city_xid")
|
||||
checkInCity Cities? @relation("CheckInCity", fields: [checkInCityXid], references: [id], onDelete: Restrict)
|
||||
checkInCountryXid Int? @map("check_in_country_xid")
|
||||
checkInCountry Countries? @relation("CheckInCountry", fields: [checkInCountryXid], references: [id], onDelete: Restrict)
|
||||
checkOutStateXid Int? @map("check_out_state_xid")
|
||||
checkOutState States? @relation("CheckOutState", fields: [checkOutStateXid], references: [id], onDelete: Restrict)
|
||||
checkOutCityXid Int? @map("check_out_city_xid")
|
||||
checkOutCity Cities? @relation("CheckOutCity", fields: [checkOutCityXid], references: [id], onDelete: Restrict)
|
||||
checkOutCountryXid Int? @map("check_out_country_xid")
|
||||
checkOutCountry Countries? @relation("CheckOutCountry", fields: [checkOutCountryXid], references: [id], onDelete: Restrict)
|
||||
// 🔹 Creator / owner
|
||||
userId Int?
|
||||
user User? @relation("UserActivities", fields: [userId], references: [id])
|
||||
|
||||
@@ -90,4 +90,19 @@ setUserLocationss:
|
||||
events:
|
||||
- httpApi:
|
||||
path: /user/set-location-user
|
||||
method: post
|
||||
method: post
|
||||
|
||||
getLandingPageDetails:
|
||||
handler: src/modules/user/handlers/activities/landingPageAllDetails.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/user/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/get-landing-page-details
|
||||
method: get
|
||||
@@ -45,7 +45,7 @@ export const scheduleActivity = z.object({
|
||||
occurrenceDate: z.string().nullable().optional(),
|
||||
maxCapacity: z.number(),
|
||||
})
|
||||
).min(1),
|
||||
).optional().default([]),
|
||||
})
|
||||
),
|
||||
|
||||
|
||||
@@ -107,6 +107,12 @@ export const CreateActivityDto = z.object({
|
||||
checkOutLat: z.number().nullable().optional(),
|
||||
checkOutLong: z.number().nullable().optional(),
|
||||
checkOutAddress: z.string().nullable().optional(),
|
||||
checkInStateName: z.string().nullable().optional(),
|
||||
checkInCityName: z.string().nullable().optional(),
|
||||
checkInCountryName: z.string().nullable().optional(),
|
||||
checkOutStateName: z.string().nullable().optional(),
|
||||
checkOutCityName: z.string().nullable().optional(),
|
||||
checkOutCountryName: z.string().nullable().optional(),
|
||||
|
||||
energyLevelXid: z.number().int().nullable().optional(),
|
||||
durationDays: z.number().int().optional(),
|
||||
|
||||
@@ -371,7 +371,7 @@ export class SchedulingService {
|
||||
activityDurationMins: true,
|
||||
activityTitle: true,
|
||||
activityRefNumber: true,
|
||||
is_late_checking_allowed: true,
|
||||
isLateCheckingAllowed: true,
|
||||
isInstantBooking: true,
|
||||
frequenciesXid: true,
|
||||
frequency: {
|
||||
|
||||
@@ -102,6 +102,94 @@ function computeBasePriceAndTaxes(
|
||||
return { basePrice, taxDetails };
|
||||
}
|
||||
|
||||
const normalize = (v?: string | null) =>
|
||||
v ? v.trim().toLowerCase() : null;
|
||||
|
||||
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;
|
||||
|
||||
const state = await tx.states.findFirst({
|
||||
where: {
|
||||
stateName: { equals: stateName.trim(), mode: 'insensitive' },
|
||||
countryXid,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (state) return state.id;
|
||||
|
||||
const created = await tx.states.create({
|
||||
data: {
|
||||
stateName: stateName.trim(),
|
||||
countryXid,
|
||||
},
|
||||
});
|
||||
|
||||
return created.id;
|
||||
};
|
||||
|
||||
const findOrCreateCity = async (
|
||||
tx: any,
|
||||
cityName?: string | null,
|
||||
stateXid?: number | null,
|
||||
) => {
|
||||
if (!cityName || !stateXid) return null;
|
||||
|
||||
const city = await tx.cities.findFirst({
|
||||
where: {
|
||||
cityName: { equals: cityName.trim(), mode: 'insensitive' },
|
||||
stateXid,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (city) return city.id;
|
||||
|
||||
const created = await tx.cities.create({
|
||||
data: {
|
||||
cityName: cityName.trim(),
|
||||
stateXid,
|
||||
},
|
||||
});
|
||||
|
||||
return created.id;
|
||||
};
|
||||
|
||||
|
||||
const bucket = config.aws.bucketName;
|
||||
|
||||
@Injectable()
|
||||
@@ -2688,6 +2776,47 @@ export class HostService {
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------
|
||||
* 🌍 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,
|
||||
);
|
||||
|
||||
|
||||
/* --------------------------------
|
||||
* 4️⃣ UPDATE ACTIVITY CORE + FLAGS
|
||||
* -------------------------------- */
|
||||
@@ -2735,6 +2864,13 @@ export class HostService {
|
||||
activityDisplayStatus,
|
||||
amInternalStatus,
|
||||
amDisplayStatus,
|
||||
checkInCountryXid: checkInCountryXid ?? undefined,
|
||||
checkInStateXid: checkInStateXid ?? undefined,
|
||||
checkInCityXid: checkInCityXid ?? undefined,
|
||||
|
||||
checkOutCountryXid: checkOutCountryXid ?? undefined,
|
||||
checkOutStateXid: checkOutStateXid ?? undefined,
|
||||
checkOutCityXid: checkOutCityXid ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2969,14 +3105,14 @@ export class HostService {
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(payload.equipments) && payload.equipments.length) {
|
||||
if (Array.isArray(payload.equipments)) {
|
||||
for (const eq of payload.equipments) {
|
||||
const isChargeable = toBool(eq.isEquipmentChargeable);
|
||||
const totalPrice = isChargeable
|
||||
? toNumber(eq.equipmentTotalPrice) ?? 0
|
||||
: 0;
|
||||
|
||||
// On submit enforce > 0, on draft just skip invalid/zero
|
||||
// ❌ Validate only on submit
|
||||
if (!isDraft && isChargeable && totalPrice <= 0) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
@@ -2984,13 +3120,12 @@ export class HostService {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isChargeable || totalPrice <= 0) continue;
|
||||
|
||||
const { basePrice, taxDetails } = computeBasePriceAndTaxes(
|
||||
totalPrice,
|
||||
rootTaxes,
|
||||
);
|
||||
const { basePrice, taxDetails } =
|
||||
isChargeable && totalPrice > 0
|
||||
? computeBasePriceAndTaxes(totalPrice, rootTaxes)
|
||||
: { basePrice: 0, taxDetails: [] };
|
||||
|
||||
// ✅ ALWAYS CREATE EQUIPMENT
|
||||
const equipment = await tx.activityEquipments.create({
|
||||
data: {
|
||||
activityXid,
|
||||
@@ -3001,7 +3136,8 @@ export class HostService {
|
||||
},
|
||||
});
|
||||
|
||||
if (taxDetails.length) {
|
||||
// 💰 Taxes ONLY if chargeable
|
||||
if (isChargeable && taxDetails.length) {
|
||||
await tx.activityEquipmentTaxes.createMany({
|
||||
data: taxDetails.map((t) => ({
|
||||
activityEquipmentXid: equipment.id,
|
||||
@@ -3117,12 +3253,12 @@ export class HostService {
|
||||
if (Array.isArray(payload.pickupDetails)) {
|
||||
for (const detail of payload.pickupDetails) {
|
||||
const isChargeable = pickUpDropIsChargeable;
|
||||
|
||||
|
||||
// 🔒 HARD RULE: NOT chargeable → ALWAYS 0
|
||||
const totalPrice = isChargeable
|
||||
? toNumber(detail.transportTotalPrice) ?? 0
|
||||
: 0;
|
||||
|
||||
|
||||
// ❌ Validate ONLY when chargeable + submit
|
||||
if (!isDraft && isChargeable && totalPrice <= 0) {
|
||||
throw new ApiError(
|
||||
@@ -3130,12 +3266,12 @@ export class HostService {
|
||||
'transportTotalPrice must be > 0 when pickup/drop is chargeable',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const { basePrice, taxDetails } =
|
||||
isChargeable && totalPrice > 0
|
||||
? computeBasePriceAndTaxes(totalPrice, rootTaxes)
|
||||
: { basePrice: 0, taxDetails: [] };
|
||||
|
||||
|
||||
// ✅ ALWAYS CREATE PICKUP DETAIL
|
||||
const pickupDetail = await tx.activityPickUpDetails.create({
|
||||
data: {
|
||||
@@ -3144,13 +3280,13 @@ export class HostService {
|
||||
locationLat: toNumber(detail.locationLat),
|
||||
locationLong: toNumber(detail.locationLong),
|
||||
locationAddress: detail.locationAddress ?? null,
|
||||
|
||||
|
||||
// ✅ Guaranteed consistency
|
||||
transportBasePrice: basePrice,
|
||||
transportTotalPrice: totalPrice,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// 💰 Taxes ONLY when chargeable
|
||||
if (isChargeable && taxDetails.length) {
|
||||
await tx.activityPickUpTransportTaxes.createMany({
|
||||
@@ -3164,7 +3300,7 @@ export class HostService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------------
|
||||
* 1️⃣2️⃣ CLEAN & CREATE NAVIGATION MODES WITH TAXES
|
||||
* -------------------------------- */
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
// Verify token and get user info
|
||||
const userInfo = await verifyUserToken(token);
|
||||
const userId = Number(userInfo.id);
|
||||
|
||||
if (!userId || isNaN(userId)) {
|
||||
throw new ApiError(400, 'Invalid user ID');
|
||||
}
|
||||
|
||||
// Fetch user with their HostHeader stepper info
|
||||
const result = await userService.getLandingPageAllDetails(userId);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Data retrieved successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -48,13 +48,18 @@ export const handler = safeHandler(async (
|
||||
});
|
||||
|
||||
let newUserLocal;
|
||||
let isNewUser = false;
|
||||
|
||||
const referenceNumber = await generateUserRefNumber(tx);
|
||||
|
||||
if (user && !user.userPasscode) {
|
||||
// reuse existing invited user record
|
||||
newUserLocal = user;
|
||||
} else {
|
||||
} else if (user) {
|
||||
// Fully registered user already exists
|
||||
newUserLocal = user;
|
||||
}
|
||||
else {
|
||||
// create new user record within the transaction
|
||||
newUserLocal = await tx.user.create({
|
||||
data: {
|
||||
@@ -68,6 +73,8 @@ export const handler = safeHandler(async (
|
||||
userStatus: USER_STATUS.ACTIVE
|
||||
},
|
||||
});
|
||||
|
||||
isNewUser = true;
|
||||
}
|
||||
|
||||
// Generate OTP (6-digit) and store within the same transaction
|
||||
@@ -93,7 +100,7 @@ export const handler = safeHandler(async (
|
||||
|
||||
const encryptedId = encryptUserId(String(newUserLocal.id));
|
||||
|
||||
return { newUser: newUserLocal, otp, encryptedId };
|
||||
return { newUser: newUserLocal, otp, encryptedId, isNewUser };
|
||||
});
|
||||
|
||||
if (!transactionResult || !transactionResult.otp) {
|
||||
@@ -112,7 +119,9 @@ export const handler = safeHandler(async (
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'OTP sent successfully.',
|
||||
data: {},
|
||||
data: {
|
||||
isNewUser: transactionResult.isNewUser,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient, User, UserAddressDetails } from '@prisma/client';
|
||||
import ApiError from '@/common/utils/helper/ApiError';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { UserPersonalInfoSchema } from '@/common/utils/validation/user/addPersonalInfo.validation';
|
||||
import { create } from 'domain';
|
||||
import { UserPersonalInfoSchema } from '../../../common/utils/validation/user/addPersonalInfo.validation';
|
||||
import { ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_INTERNAL_STATUS } from '../../../common/utils/constants/host.constant';
|
||||
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
||||
|
||||
import config from '@/config/config';
|
||||
// function deg2rad(deg) {
|
||||
// return deg * (Math.PI / 180);
|
||||
// }
|
||||
|
||||
// function getDistanceFromLatLon(userLat1, userLon1, activityLat2, activityLon2) {
|
||||
// const R = 6371; // Earth radius in km
|
||||
|
||||
// const dLat = deg2rad(activityLat2 - userLat1);
|
||||
// const dLon = deg2rad(activityLon2 - userLon1);
|
||||
|
||||
// const a =
|
||||
// Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
// Math.cos(deg2rad(userLat1)) *
|
||||
// Math.cos(deg2rad(activityLat2)) *
|
||||
// Math.sin(dLon / 2) *
|
||||
// Math.sin(dLon / 2);
|
||||
|
||||
// const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
// return R * c;
|
||||
// }
|
||||
|
||||
|
||||
const bucket = config.aws.bucketName;
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private prisma: PrismaClient) { }
|
||||
@@ -150,75 +178,253 @@ export class UserService {
|
||||
longitude?: number,
|
||||
locationName?: string,
|
||||
locationAddress?: string
|
||||
): Promise<UserAddressDetails> {
|
||||
): Promise<UserAddressDetails> {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
|
||||
// 1️⃣ Country: find or create
|
||||
let country = await tx.countries.findUnique({
|
||||
where: { countryName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!country) {
|
||||
country = await tx.countries.create({
|
||||
data: {
|
||||
countryName,
|
||||
countryCode: countryName.slice(0, 3).toUpperCase(),
|
||||
countryFlag: '',
|
||||
},
|
||||
select: { id: true },
|
||||
|
||||
// 1️⃣ Country: find or create
|
||||
let country = await tx.countries.findUnique({
|
||||
where: { countryName },
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
// 2️⃣ State: find or create (GLOBAL UNIQUE)
|
||||
let state = await tx.states.findUnique({
|
||||
where: { stateName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!state) {
|
||||
state = await tx.states.create({
|
||||
data: {
|
||||
stateName,
|
||||
countryXid: country.id,
|
||||
},
|
||||
select: { id: true },
|
||||
|
||||
if (!country) {
|
||||
country = await tx.countries.create({
|
||||
data: {
|
||||
countryName,
|
||||
countryCode: countryName.slice(0, 3).toUpperCase(),
|
||||
countryFlag: '',
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
// 2️⃣ State: find or create (GLOBAL UNIQUE)
|
||||
let state = await tx.states.findUnique({
|
||||
where: { stateName },
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
// 3️⃣ City: find or create (GLOBAL UNIQUE)
|
||||
let city = await tx.cities.findUnique({
|
||||
where: { cityName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!city) {
|
||||
city = await tx.cities.create({
|
||||
data: {
|
||||
cityName,
|
||||
stateXid: state.id,
|
||||
},
|
||||
select: { id: true },
|
||||
|
||||
if (!state) {
|
||||
state = await tx.states.create({
|
||||
data: {
|
||||
stateName,
|
||||
countryXid: country.id,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
// 3️⃣ City: find or create (GLOBAL UNIQUE)
|
||||
let city = await tx.cities.findUnique({
|
||||
where: { cityName },
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
return tx.userAddressDetails.create({
|
||||
data: {
|
||||
user: { connect: { id: userId } },
|
||||
country: { connect: { id: country.id } },
|
||||
states: { connect: { id: state.id } },
|
||||
cities: { connect: { id: city.id } },
|
||||
address1: locationAddress ?? '',
|
||||
pinCode,
|
||||
locationName: locationName ?? null,
|
||||
locationAddress: locationAddress ?? null,
|
||||
locationLat: latitude ?? null,
|
||||
locationLong: longitude ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!city) {
|
||||
city = await tx.cities.create({
|
||||
data: {
|
||||
cityName,
|
||||
stateXid: state.id,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
return tx.userAddressDetails.create({
|
||||
data: {
|
||||
user: { connect: { id: userId } },
|
||||
country: { connect: { id: country.id } },
|
||||
states: { connect: { id: state.id } },
|
||||
cities: { connect: { id: city.id } },
|
||||
address1: locationAddress ?? '',
|
||||
pinCode,
|
||||
locationName: locationName ?? null,
|
||||
locationAddress: locationAddress ?? null,
|
||||
locationLat: latitude ?? null,
|
||||
locationLong: longitude ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getLandingPageAllDetails(userId: number) {
|
||||
const data = await this.prisma.$transaction(async (tx) => {
|
||||
const userInterests = await tx.userInterests.findMany({
|
||||
where: { userXid: userId, isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
interestXid: true,
|
||||
interest: {
|
||||
select: {
|
||||
id: true,
|
||||
interestName: true,
|
||||
interestColor: true,
|
||||
interestImage: true,
|
||||
displayOrder: true
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if (!userInterests.length) {
|
||||
return { userAddress: null, activities: [] };
|
||||
}
|
||||
|
||||
const activitiyTypesOfUserInterests = await tx.activityTypes.findMany({
|
||||
where: { interestXid: { in: userInterests.map(ui => ui.interestXid) }, isActive: true },
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
|
||||
const userAddressDetails = await tx.userAddressDetails.findFirst({
|
||||
where: { userXid: userId },
|
||||
select: {
|
||||
id: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
pinCode: true,
|
||||
locationName: true,
|
||||
stateXid: true,
|
||||
cityXid: true,
|
||||
countryXid: true,
|
||||
locationLat: true,
|
||||
locationLong: true,
|
||||
}
|
||||
})
|
||||
|
||||
if (!activitiyTypesOfUserInterests.length) {
|
||||
return {
|
||||
userAddressDetails,
|
||||
activities: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
const activities = await tx.activities.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
activityTypeXid: {
|
||||
in: activitiyTypesOfUserInterests.map(at => at.id),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
activityDurationMins: true,
|
||||
sustainabilityScore: true,
|
||||
checkInLat: true,
|
||||
checkInLong: true,
|
||||
|
||||
activityType: {
|
||||
select: {
|
||||
interestXid: true, // ✅ VERY IMPORTANT
|
||||
energyLevel: {
|
||||
select: {
|
||||
id: true,
|
||||
energyLevelName: true,
|
||||
energyColor: true,
|
||||
energyIcon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
ActivityVenues: {
|
||||
select: {
|
||||
ActivityPrices: {
|
||||
select: {
|
||||
sellPrice: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
ActivitiesMedia: {
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
mediaFileName: true,
|
||||
mediaType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const formattedActivities = await Promise.all(
|
||||
activities.map(async (activity) => {
|
||||
let cheapestPrice: number | null = null;
|
||||
|
||||
for (const venue of activity.ActivityVenues) {
|
||||
for (const price of venue.ActivityPrices) {
|
||||
if (
|
||||
typeof price.sellPrice === 'number' &&
|
||||
(cheapestPrice === null || price.sellPrice < cheapestPrice)
|
||||
) {
|
||||
cheapestPrice = price.sellPrice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const media = await Promise.all(
|
||||
(activity.ActivitiesMedia ?? []).map(async (m) => {
|
||||
if (!m?.mediaFileName) return null;
|
||||
|
||||
const key = m.mediaFileName.startsWith('http')
|
||||
? new URL(m.mediaFileName).pathname.replace(/^\/+/, '')
|
||||
: m.mediaFileName;
|
||||
|
||||
return {
|
||||
id: m.id,
|
||||
mediaType: m.mediaType,
|
||||
mediaFileName: m.mediaFileName,
|
||||
presignedUrl: await getPresignedUrl(bucket, key),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
interestXid: activity.activityType.interestXid,
|
||||
activityTitle: activity.activityTitle,
|
||||
activityDurationMins: activity.activityDurationMins,
|
||||
sustainabilityScore: activity.sustainabilityScore,
|
||||
energyLevel: activity.activityType?.energyLevel ?? null,
|
||||
cheapestPrice,
|
||||
media: media.filter(Boolean), // ✅ IMPORTANT
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const interestsWithActivities = userInterests.map(ui => {
|
||||
const activitiesForInterest = formattedActivities.filter(
|
||||
act => act.interestXid === ui.interestXid
|
||||
);
|
||||
|
||||
return {
|
||||
interestId: ui.interest.id,
|
||||
interestName: ui.interest.interestName,
|
||||
interestColor: ui.interest.interestColor,
|
||||
interestImage: ui.interest.interestImage,
|
||||
displayOrder: ui.interest.displayOrder,
|
||||
activities: activitiesForInterest.map(({ interestXid, ...rest }) => rest),
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
userAddressDetails,
|
||||
interests: interestsWithActivities,
|
||||
};
|
||||
})
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user