Compare commits
16 Commits
mayankSpri
...
paritosh-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d18a77ab5 | ||
|
|
e6ba52520d | ||
|
|
e164e1c25a | ||
|
|
ce9a9b6211 | ||
|
|
4e04781a06 | ||
|
|
e77d9e50c9 | ||
|
|
be9780f9ec | ||
|
|
3c56c45b01 | ||
|
|
d1c4ad76ba | ||
|
|
75025b62d9 | ||
|
|
04ae88b239 | ||
|
|
b18b6bc468 | ||
|
|
6f1504e93f | ||
|
|
1a6411acdc | ||
|
|
841539b8cc | ||
|
|
5fcff67916 |
12
serverless.operator.yml
Normal file
12
serverless.operator.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
service: minglar-operator
|
||||||
|
|
||||||
|
useDotenv: ${file(./serverless/common.yml):useDotenv}
|
||||||
|
params: ${file(./serverless/common.yml):params}
|
||||||
|
provider: ${file(./serverless/common.yml):provider}
|
||||||
|
build: ${file(./serverless/common.yml):build}
|
||||||
|
package: ${file(./serverless/common.yml):package}
|
||||||
|
plugins: ${file(./serverless/common.yml):plugins}
|
||||||
|
custom: ${file(./serverless/common.yml):custom}
|
||||||
|
|
||||||
|
functions:
|
||||||
|
- ${file(./serverless/functions/operator.yml)}
|
||||||
@@ -155,6 +155,7 @@ package:
|
|||||||
# Import function definitions from separate files organized by module
|
# Import function definitions from separate files organized by module
|
||||||
functions:
|
functions:
|
||||||
- ${file(./serverless/functions/host.yml)}
|
- ${file(./serverless/functions/host.yml)}
|
||||||
|
- ${file(./serverless/functions/operator.yml)}
|
||||||
- ${file(./serverless/functions/minglaradmin.yml)}
|
- ${file(./serverless/functions/minglaradmin.yml)}
|
||||||
- ${file(./serverless/functions/prepopulate.yml)}
|
- ${file(./serverless/functions/prepopulate.yml)}
|
||||||
- ${file(./serverless/functions/user.yml)}
|
- ${file(./serverless/functions/user.yml)}
|
||||||
|
|||||||
@@ -388,6 +388,22 @@ getHostMemberRoles:
|
|||||||
path: /settings/member-roles
|
path: /settings/member-roles
|
||||||
method: get
|
method: get
|
||||||
|
|
||||||
|
getMemberPermissions:
|
||||||
|
handler: src/modules/host/handlers/settings/getMemberPermissions.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/settings/**'
|
||||||
|
- '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: /settings/member-permissions/{memberUserXid}
|
||||||
|
method: get
|
||||||
|
|
||||||
# Functions with S3/AWS SDK dependencies
|
# Functions with S3/AWS SDK dependencies
|
||||||
submitCompanyDetails:
|
submitCompanyDetails:
|
||||||
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
|
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
|
||||||
@@ -511,7 +527,6 @@ resendOTPmail:
|
|||||||
path: /resend-otp
|
path: /resend-otp
|
||||||
method: post
|
method: post
|
||||||
|
|
||||||
|
|
||||||
mediaUploadTos3:
|
mediaUploadTos3:
|
||||||
handler: src/modules/host/handlers/mediaUploadToS3.handler
|
handler: src/modules/host/handlers/mediaUploadToS3.handler
|
||||||
memorySize: 512
|
memorySize: 512
|
||||||
@@ -527,7 +542,6 @@ mediaUploadTos3:
|
|||||||
path: /media/upload/activity/{activityXid}
|
path: /media/upload/activity/{activityXid}
|
||||||
method: post
|
method: post
|
||||||
|
|
||||||
|
|
||||||
venueMediaUploadTos3:
|
venueMediaUploadTos3:
|
||||||
handler: src/modules/host/handlers/mediaUploadForVenueToS3.handler
|
handler: src/modules/host/handlers/mediaUploadForVenueToS3.handler
|
||||||
memorySize: 512
|
memorySize: 512
|
||||||
@@ -543,7 +557,6 @@ venueMediaUploadTos3:
|
|||||||
path: /media/upload/venue/activity/{activityXid}
|
path: /media/upload/venue/activity/{activityXid}
|
||||||
method: post
|
method: post
|
||||||
|
|
||||||
|
|
||||||
mediaDeleteFroms3:
|
mediaDeleteFroms3:
|
||||||
handler: src/modules/host/handlers/mediaDeleteFromS3.handler
|
handler: src/modules/host/handlers/mediaDeleteFromS3.handler
|
||||||
memorySize: 512
|
memorySize: 512
|
||||||
|
|||||||
198
serverless/functions/operator.yml
Normal file
198
serverless/functions/operator.yml
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Operator Module Functions
|
||||||
|
|
||||||
|
operatorSignUp:
|
||||||
|
handler: src/modules/host/handlers/operator/signUp.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||||
|
- 'src/modules/host/services/token.service.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /signup
|
||||||
|
method: post
|
||||||
|
|
||||||
|
operatorVerifyOtp:
|
||||||
|
handler: src/modules/host/handlers/operator/verifyOtp.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||||
|
- 'src/modules/host/services/token.service.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /verify-otp
|
||||||
|
method: post
|
||||||
|
|
||||||
|
operatorCreatePassword:
|
||||||
|
handler: src/modules/host/handlers/operator/createPassword.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||||
|
- 'src/modules/host/services/token.service.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /create-password
|
||||||
|
method: post
|
||||||
|
|
||||||
|
operatorLogin:
|
||||||
|
handler: src/modules/host/handlers/operator/login.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||||
|
- 'src/modules/host/services/token.service.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /login
|
||||||
|
method: post
|
||||||
|
|
||||||
|
operatorVerifyPassword:
|
||||||
|
handler: src/modules/host/handlers/operator/verifyPassword.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorAuth.service.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /verify-password
|
||||||
|
method: post
|
||||||
|
|
||||||
|
operatorGetActivitiesByDate:
|
||||||
|
handler: src/modules/host/handlers/operator/getActivitiesByDate.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||||
|
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /activities-by-date
|
||||||
|
method: get
|
||||||
|
|
||||||
|
operatorGetReservationByCheckInCode:
|
||||||
|
handler: src/modules/host/handlers/operator/getReservationByCheckInCode.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||||
|
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /reservation-by-checkin-code
|
||||||
|
method: get
|
||||||
|
|
||||||
|
operatorSendOtpCheckIn:
|
||||||
|
handler: src/modules/host/handlers/operator/sendOtpCheckIn.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||||
|
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /send-otp-checkin
|
||||||
|
method: post
|
||||||
|
|
||||||
|
operatorSendOtpCheckout:
|
||||||
|
handler: src/modules/host/handlers/operator/sendOtpCheckout.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||||
|
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /send-otp-checkout
|
||||||
|
method: post
|
||||||
|
|
||||||
|
operatorVerifyOtpCheckIn:
|
||||||
|
handler: src/modules/host/handlers/operator/verifyOtpCheckIn.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||||
|
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /verify-otp-checkin
|
||||||
|
method: post
|
||||||
|
|
||||||
|
operatorVerifyOtpCheckout:
|
||||||
|
handler: src/modules/host/handlers/operator/verifyOtpCheckout.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/operator/**'
|
||||||
|
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||||
|
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||||
|
- 'src/common/**'
|
||||||
|
- ${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: /verify-otp-checkout
|
||||||
|
method: post
|
||||||
@@ -485,7 +485,7 @@ saveItineraryActivitySelections:
|
|||||||
|
|
||||||
getAllUserSavedItineraries:
|
getAllUserSavedItineraries:
|
||||||
handler: src/modules/user/handlers/itinerary/getAllUserSavedItineraries.handler
|
handler: src/modules/user/handlers/itinerary/getAllUserSavedItineraries.handler
|
||||||
memorySize: 512
|
memorySize: 1024
|
||||||
package:
|
package:
|
||||||
patterns:
|
patterns:
|
||||||
- 'src/modules/user/**'
|
- 'src/modules/user/**'
|
||||||
@@ -513,6 +513,21 @@ cancelUserItinerary:
|
|||||||
path: /itinerary/cancel-itinerary
|
path: /itinerary/cancel-itinerary
|
||||||
method: post
|
method: post
|
||||||
|
|
||||||
|
afterBookingFromCalendar:
|
||||||
|
handler: src/modules/user/handlers/itinerary/afterBookingFromCalendar.handler
|
||||||
|
memorySize: 512
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/user/**'
|
||||||
|
- ${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: /itinerary/after-booking-from-calendar
|
||||||
|
method: post
|
||||||
|
|
||||||
createRazorpayOrder:
|
createRazorpayOrder:
|
||||||
handler: src/modules/user/handlers/payment/createOrder.handler
|
handler: src/modules/user/handlers/payment/createOrder.handler
|
||||||
memorySize: 512
|
memorySize: 512
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import httpStatus from 'http-status';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
import ApiError from '../../utils/helper/ApiError';
|
|
||||||
import config from '../../../config/config';
|
|
||||||
import { ROLE } from '@/common/utils/constants/common.constant';
|
import { ROLE } from '@/common/utils/constants/common.constant';
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
import httpStatus from 'http-status';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import config from '../../../config/config';
|
||||||
import { prisma } from '../../database/prisma.client';
|
import { prisma } from '../../database/prisma.client';
|
||||||
|
import ApiError from '../../utils/helper/ApiError';
|
||||||
|
|
||||||
interface DecodedToken {
|
interface DecodedToken {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -29,6 +29,68 @@ declare module 'express-serve-static-core' {
|
|||||||
* Core authentication function - verifies JWT and validates Host user
|
* Core authentication function - verifies JWT and validates Host user
|
||||||
* Can be used by both Express middleware and Lambda handlers
|
* Can be used by both Express middleware and Lambda handlers
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Verifies JWT and validates Operator user (role_xid = 5)
|
||||||
|
*/
|
||||||
|
export async function verifyOperatorToken(token: string): Promise<{ id: number; role?: string }> {
|
||||||
|
if (!token) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
|
||||||
|
|
||||||
|
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Fetch user from Prisma (Operator user only)
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
include: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestToken = await prisma.token.findFirst({
|
||||||
|
where: {
|
||||||
|
userXid: userId
|
||||||
|
},
|
||||||
|
orderBy: { id: 'desc' }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (latestToken?.isBlackListed == true) {
|
||||||
|
throw new ApiError(401, "This session is expired. Please login.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check if user is active
|
||||||
|
if (user.isActive === false) {
|
||||||
|
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Check Operator role (role_xid = 5)
|
||||||
|
if (user.roleXid !== ROLE.OPERATOR) {
|
||||||
|
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: user.id, role: user.role?.roleName };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function verifyHostToken(token: string): Promise<{ id: number; role?: string }> {
|
export async function verifyHostToken(token: string): Promise<{ id: number; role?: string }> {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||||
|
|||||||
112
src/common/middlewares/jwt/authForOperator.ts
Normal file
112
src/common/middlewares/jwt/authForOperator.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import httpStatus from 'http-status';
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
import { prisma } from '../../database/prisma.client';
|
||||||
|
import ApiError from '../../utils/helper/ApiError';
|
||||||
|
import config from '../../../config/config';
|
||||||
|
import { ROLE } from '../../utils/constants/common.constant';
|
||||||
|
|
||||||
|
interface DecodedToken {
|
||||||
|
id?: number;
|
||||||
|
sub?: string | number;
|
||||||
|
role?: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserPayload {
|
||||||
|
id: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'express-serve-static-core' {
|
||||||
|
interface Request {
|
||||||
|
user?: UserPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyOperatorToken(
|
||||||
|
token: string,
|
||||||
|
): Promise<{ id: number; role?: string }> {
|
||||||
|
if (!token) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
|
||||||
|
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
include: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestToken = await prisma.token.findFirst({
|
||||||
|
where: { userXid: userId },
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (latestToken?.isBlackListed === true) {
|
||||||
|
throw new ApiError(401, 'This session is expired. Please login.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isActive === false) {
|
||||||
|
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.roleXid !== ROLE.OPERATOR) {
|
||||||
|
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: user.id, role: user.role?.roleName };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
throw new ApiError(
|
||||||
|
httpStatus.UNAUTHORIZED,
|
||||||
|
'Your session has expired. Please log in again.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyCallback = async (
|
||||||
|
req: Request,
|
||||||
|
resolve: (value?: unknown) => void,
|
||||||
|
reject: (reason?: Error) => void,
|
||||||
|
) => {
|
||||||
|
const token = req.header('x-auth-token') || req.cookies?.accessToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userInfo = await verifyOperatorToken(token);
|
||||||
|
req.user = { id: userInfo.id.toString(), role: userInfo.role };
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
return reject(error as Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const authForOperator =
|
||||||
|
() =>
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
verifyCallback(req, resolve, reject);
|
||||||
|
})
|
||||||
|
.then(() => next())
|
||||||
|
.catch((err) => next(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authForOperator;
|
||||||
@@ -12,7 +12,7 @@ export interface OtpResult {
|
|||||||
export async function resendOtpHelper(
|
export async function resendOtpHelper(
|
||||||
prisma: any,
|
prisma: any,
|
||||||
userId: number,
|
userId: number,
|
||||||
emailPurpose: "Register" | "Login" | "ForgotPassword",
|
emailPurpose: string,
|
||||||
otpLength: 4 | 6 = 4,
|
otpLength: 4 | 6 = 4,
|
||||||
expiryMinutes: number = 5
|
expiryMinutes: number = 5
|
||||||
): Promise<OtpResult> {
|
): Promise<OtpResult> {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function generateOtpHelper(
|
|||||||
prisma: any, // ⭐ Inject prisma
|
prisma: any, // ⭐ Inject prisma
|
||||||
userId: number,
|
userId: number,
|
||||||
email: string,
|
email: string,
|
||||||
emailPurpose: "Register" | "Login" | "ForgotPassword",
|
emailPurpose: string,
|
||||||
otpLength: 4 | 6 = 4,
|
otpLength: 4 | 6 = 4,
|
||||||
expiryMinutes: number = 5
|
expiryMinutes: number = 5
|
||||||
): Promise<OtpResult> {
|
): Promise<OtpResult> {
|
||||||
|
|||||||
113
src/modules/host/dto/operator.activity.dto.ts
Normal file
113
src/modules/host/dto/operator.activity.dto.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
export class GetActivitiesByDateRequestDTO {
|
||||||
|
activityDate?: string; // ISO date format: YYYY-MM-DD (optional, defaults to today)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetReservationByCheckInCodeRequestDTO {
|
||||||
|
checkInCode!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorReservationVerificationOtpRequestDTO {
|
||||||
|
checkInCode!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorReservationVerifyOtpRequestDTO {
|
||||||
|
checkInCode!: string;
|
||||||
|
otp!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DateBreakdownDTO {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActivitySummaryDTO {
|
||||||
|
activityName: string;
|
||||||
|
activityImage: string | null;
|
||||||
|
activityImagePreSignedUrl: string | null;
|
||||||
|
count: number;
|
||||||
|
dateBreakdown: DateBreakdownDTO[]; // Booking count for each scheduled date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetActivitiesByDateResponseDTO {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
date: string;
|
||||||
|
activities: ActivitySummaryDTO[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorReservationPersonalDetailsDTO {
|
||||||
|
fullName: string;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
role: string | null;
|
||||||
|
mobileNumber: string | null;
|
||||||
|
profileImage: string | null;
|
||||||
|
profileImagePreSignedUrl: string | null;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorReservationBookingInformationDTO {
|
||||||
|
activityName: string | null;
|
||||||
|
slot: string | null;
|
||||||
|
startTime: string | null;
|
||||||
|
endTime: string | null;
|
||||||
|
track: string | null;
|
||||||
|
trackLabel: string | null;
|
||||||
|
date: string | null;
|
||||||
|
dateLabel: string | null;
|
||||||
|
bookedOn: string | null;
|
||||||
|
bookedOnLabel: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorReservationBookingIncludedDTO {
|
||||||
|
food: string;
|
||||||
|
selectedFoodTypes: string[];
|
||||||
|
equipment: string;
|
||||||
|
selectedEquipments: string[];
|
||||||
|
trainerOrGuide: string;
|
||||||
|
pickupLocation: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorReservationByCheckInCodeDTO {
|
||||||
|
itineraryHeaderXid: number;
|
||||||
|
itineraryActivityXid: number;
|
||||||
|
bookingId: string | null;
|
||||||
|
checkInCode: string | null;
|
||||||
|
reservationStatus: string | null;
|
||||||
|
personalDetails: OperatorReservationPersonalDetailsDTO;
|
||||||
|
bookingInformation: OperatorReservationBookingInformationDTO;
|
||||||
|
bookingIncluded: OperatorReservationBookingIncludedDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetReservationByCheckInCodeResponseDTO {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: OperatorReservationByCheckInCodeDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorReservationVerificationOtpResponseDTO {
|
||||||
|
itineraryActivityXid: number;
|
||||||
|
itineraryMemberXid: number;
|
||||||
|
activityName: string | null;
|
||||||
|
checkInCode: string;
|
||||||
|
verificationType: 'check-in' | 'checkout';
|
||||||
|
requestedChannel: 'email' | 'mobile';
|
||||||
|
deliveryChannel: 'email';
|
||||||
|
destination: string;
|
||||||
|
reservationStatus: string | null;
|
||||||
|
expiresInMinutes: number;
|
||||||
|
deliveryNote: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorReservationVerifyOtpResponseDTO {
|
||||||
|
itineraryActivityXid: number;
|
||||||
|
itineraryMemberXid: number;
|
||||||
|
activityName: string | null;
|
||||||
|
checkInCode: string;
|
||||||
|
verificationType: 'check-in' | 'checkout';
|
||||||
|
verified: boolean;
|
||||||
|
reservationStatus: string;
|
||||||
|
}
|
||||||
58
src/modules/host/handlers/operator/createPassword.ts
Normal file
58
src/modules/host/handlers/operator/createPassword.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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 { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForOperator';
|
||||||
|
import { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||||
|
|
||||||
|
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = await verifyOperatorToken(token);
|
||||||
|
|
||||||
|
let body: { password?: string; confirmPassword?: string };
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : {};
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { password, confirmPassword } = body;
|
||||||
|
|
||||||
|
if (!password || !confirmPassword) {
|
||||||
|
throw new ApiError(400, 'Password and confirm password are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
throw new ApiError(400, 'Password and confirm password do not match');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
throw new ApiError(400, 'Password must be at least 8 characters long');
|
||||||
|
}
|
||||||
|
|
||||||
|
await operatorAuthService.createOperatorPassword(userInfo.id, password);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Password created successfully',
|
||||||
|
data: null,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
59
src/modules/host/handlers/operator/getActivitiesByDate.ts
Normal file
59
src/modules/host/handlers/operator/getActivitiesByDate.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||||
|
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||||
|
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||||
|
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||||
|
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||||
|
import { GetActivitiesByDateRequestDTO } from '../../dto/operator.activity.dto';
|
||||||
|
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||||
|
|
||||||
|
const operatorActivityService = new OperatorActivityService(prismaClient);
|
||||||
|
|
||||||
|
export const handler = safeHandler(async (
|
||||||
|
event: APIGatewayProxyEvent,
|
||||||
|
context?: Context,
|
||||||
|
): Promise<APIGatewayProxyResult> => {
|
||||||
|
try {
|
||||||
|
// 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 operator info
|
||||||
|
const operatorInfo = await verifyOperatorToken(token);
|
||||||
|
const operatorId = Number(operatorInfo.id);
|
||||||
|
|
||||||
|
if (!operatorId || isNaN(operatorId)) {
|
||||||
|
throw new ApiError(400, 'Invalid operator ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get activityDate from query parameters
|
||||||
|
const { activityDate } = event.queryStringParameters || {};
|
||||||
|
|
||||||
|
const requestDTO: GetActivitiesByDateRequestDTO = {
|
||||||
|
activityDate: activityDate?.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch activities by date and operator
|
||||||
|
const result = await operatorActivityService.getActivitiesByDate(
|
||||||
|
operatorId,
|
||||||
|
requestDTO.activityDate,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Activities fetched successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Error will be handled by safeHandler
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
APIGatewayProxyEvent,
|
||||||
|
APIGatewayProxyResult,
|
||||||
|
Context,
|
||||||
|
} from 'aws-lambda';
|
||||||
|
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||||
|
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||||
|
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||||
|
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||||
|
import { GetReservationByCheckInCodeRequestDTO } from '../../dto/operator.activity.dto';
|
||||||
|
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||||
|
|
||||||
|
const operatorActivityService = new OperatorActivityService(prismaClient);
|
||||||
|
|
||||||
|
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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatorInfo = await verifyOperatorToken(token);
|
||||||
|
const operatorId = Number(operatorInfo.id);
|
||||||
|
|
||||||
|
if (!operatorId || Number.isNaN(operatorId)) {
|
||||||
|
throw new ApiError(400, 'Invalid operator ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestDTO: GetReservationByCheckInCodeRequestDTO = {
|
||||||
|
checkInCode:
|
||||||
|
event.queryStringParameters?.checkInCode?.trim() ||
|
||||||
|
event.queryStringParameters?.offlineCode?.trim() ||
|
||||||
|
'',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!requestDTO.checkInCode) {
|
||||||
|
throw new ApiError(400, 'checkInCode is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await operatorActivityService.getReservationByCheckInCode(
|
||||||
|
operatorId,
|
||||||
|
requestDTO.checkInCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Reservation details fetched successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
55
src/modules/host/handlers/operator/login.ts
Normal file
55
src/modules/host/handlers/operator/login.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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 { GetHostLoginResponseDTO } from '../../dto/host.dto';
|
||||||
|
import { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||||
|
import { TokenService } from '../../services/token.service';
|
||||||
|
|
||||||
|
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||||
|
const tokenService = new TokenService(prismaClient);
|
||||||
|
|
||||||
|
export const handler = safeHandler(async (
|
||||||
|
event: APIGatewayProxyEvent,
|
||||||
|
context?: Context,
|
||||||
|
): Promise<APIGatewayProxyResult> => {
|
||||||
|
let body: { emailAddress?: string; userPassword?: string };
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : {};
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { emailAddress, userPassword } = body;
|
||||||
|
|
||||||
|
if (!emailAddress || !userPassword) {
|
||||||
|
throw new ApiError(400, 'Email and password are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const operator = await operatorAuthService.loginForOperator(
|
||||||
|
emailAddress.trim().toLowerCase(),
|
||||||
|
userPassword,
|
||||||
|
);
|
||||||
|
|
||||||
|
const generatedToken = await tokenService.generateAuthToken(operator.id);
|
||||||
|
|
||||||
|
const response = new GetHostLoginResponseDTO(
|
||||||
|
operator,
|
||||||
|
generatedToken.access.token,
|
||||||
|
generatedToken.refresh.token,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Login successful',
|
||||||
|
data: response,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
70
src/modules/host/handlers/operator/sendOtpCheckIn.ts
Normal file
70
src/modules/host/handlers/operator/sendOtpCheckIn.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
APIGatewayProxyEvent,
|
||||||
|
APIGatewayProxyResult,
|
||||||
|
Context,
|
||||||
|
} from 'aws-lambda';
|
||||||
|
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||||
|
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||||
|
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||||
|
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||||
|
import { OperatorReservationVerificationOtpRequestDTO } from '../../dto/operator.activity.dto';
|
||||||
|
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||||
|
|
||||||
|
const operatorActivityService = new OperatorActivityService(prismaClient);
|
||||||
|
|
||||||
|
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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatorInfo = await verifyOperatorToken(token);
|
||||||
|
const operatorId = Number(operatorInfo.id);
|
||||||
|
|
||||||
|
if (!operatorId || Number.isNaN(operatorId)) {
|
||||||
|
throw new ApiError(400, 'Invalid operator ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: OperatorReservationVerificationOtpRequestDTO;
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : { checkInCode: '' };
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestDTO: OperatorReservationVerificationOtpRequestDTO = {
|
||||||
|
checkInCode: body?.checkInCode?.trim() || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!requestDTO.checkInCode) {
|
||||||
|
throw new ApiError(400, 'checkInCode is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await operatorActivityService.sendOtpCheckIn(
|
||||||
|
operatorId,
|
||||||
|
requestDTO.checkInCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Check-in OTP sent successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
70
src/modules/host/handlers/operator/sendOtpCheckout.ts
Normal file
70
src/modules/host/handlers/operator/sendOtpCheckout.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
APIGatewayProxyEvent,
|
||||||
|
APIGatewayProxyResult,
|
||||||
|
Context,
|
||||||
|
} from 'aws-lambda';
|
||||||
|
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||||
|
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||||
|
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||||
|
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||||
|
import { OperatorReservationVerificationOtpRequestDTO } from '../../dto/operator.activity.dto';
|
||||||
|
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||||
|
|
||||||
|
const operatorActivityService = new OperatorActivityService(prismaClient);
|
||||||
|
|
||||||
|
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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatorInfo = await verifyOperatorToken(token);
|
||||||
|
const operatorId = Number(operatorInfo.id);
|
||||||
|
|
||||||
|
if (!operatorId || Number.isNaN(operatorId)) {
|
||||||
|
throw new ApiError(400, 'Invalid operator ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: OperatorReservationVerificationOtpRequestDTO;
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : { checkInCode: '' };
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestDTO: OperatorReservationVerificationOtpRequestDTO = {
|
||||||
|
checkInCode: body?.checkInCode?.trim() || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!requestDTO.checkInCode) {
|
||||||
|
throw new ApiError(400, 'checkInCode is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await operatorActivityService.sendOtpCheckout(
|
||||||
|
operatorId,
|
||||||
|
requestDTO.checkInCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Checkout OTP sent successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
53
src/modules/host/handlers/operator/signUp.ts
Normal file
53
src/modules/host/handlers/operator/signUp.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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 { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||||
|
|
||||||
|
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||||
|
|
||||||
|
export const handler = safeHandler(async (
|
||||||
|
event: APIGatewayProxyEvent,
|
||||||
|
context?: Context,
|
||||||
|
): Promise<APIGatewayProxyResult> => {
|
||||||
|
let body: {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
emailAddress?: string;
|
||||||
|
isdCode?: string;
|
||||||
|
mobileNumber?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : {};
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { firstName, lastName, emailAddress, isdCode, mobileNumber } = body;
|
||||||
|
|
||||||
|
if (!emailAddress && !mobileNumber) {
|
||||||
|
throw new ApiError(400, 'Email address or mobile number is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signupResponse = await operatorAuthService.signUpOperator({
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
emailAddress,
|
||||||
|
isdCode,
|
||||||
|
mobileNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'OTP sent successfully',
|
||||||
|
data: signupResponse,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
51
src/modules/host/handlers/operator/verifyOtp.ts
Normal file
51
src/modules/host/handlers/operator/verifyOtp.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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 { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||||
|
import { TokenService } from '../../services/token.service';
|
||||||
|
|
||||||
|
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||||
|
const tokenService = new TokenService(prismaClient);
|
||||||
|
|
||||||
|
export const handler = safeHandler(async (
|
||||||
|
event: APIGatewayProxyEvent,
|
||||||
|
context?: Context,
|
||||||
|
): Promise<APIGatewayProxyResult> => {
|
||||||
|
let body: { emailAddress?: string; mobileNumber?: string; otp?: string };
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : {};
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { emailAddress, mobileNumber, otp } = body;
|
||||||
|
|
||||||
|
if ((!emailAddress && !mobileNumber) || !otp) {
|
||||||
|
throw new ApiError(400, 'Email address or mobile number and OTP are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const operator = await operatorAuthService.verifyOperatorOtp({
|
||||||
|
emailAddress,
|
||||||
|
mobileNumber,
|
||||||
|
otp,
|
||||||
|
});
|
||||||
|
|
||||||
|
const generatedToken = await tokenService.generateAuthToken(operator.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'OTP verified successfully',
|
||||||
|
accessToken: generatedToken.access.token,
|
||||||
|
refreshToken: generatedToken.refresh.token,
|
||||||
|
data: null,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
72
src/modules/host/handlers/operator/verifyOtpCheckIn.ts
Normal file
72
src/modules/host/handlers/operator/verifyOtpCheckIn.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
APIGatewayProxyEvent,
|
||||||
|
APIGatewayProxyResult,
|
||||||
|
Context,
|
||||||
|
} from 'aws-lambda';
|
||||||
|
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||||
|
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||||
|
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||||
|
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||||
|
import { OperatorReservationVerifyOtpRequestDTO } from '../../dto/operator.activity.dto';
|
||||||
|
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||||
|
|
||||||
|
const operatorActivityService = new OperatorActivityService(prismaClient);
|
||||||
|
|
||||||
|
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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatorInfo = await verifyOperatorToken(token);
|
||||||
|
const operatorId = Number(operatorInfo.id);
|
||||||
|
|
||||||
|
if (!operatorId || Number.isNaN(operatorId)) {
|
||||||
|
throw new ApiError(400, 'Invalid operator ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: OperatorReservationVerifyOtpRequestDTO;
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : { checkInCode: '', otp: '' };
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestDTO: OperatorReservationVerifyOtpRequestDTO = {
|
||||||
|
checkInCode: body?.checkInCode?.trim() || '',
|
||||||
|
otp: body?.otp?.trim() || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!requestDTO.checkInCode || !requestDTO.otp) {
|
||||||
|
throw new ApiError(400, 'checkInCode and otp are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await operatorActivityService.verifyOtpCheckIn(
|
||||||
|
operatorId,
|
||||||
|
requestDTO.checkInCode,
|
||||||
|
requestDTO.otp,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Check-in OTP verified successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
72
src/modules/host/handlers/operator/verifyOtpCheckout.ts
Normal file
72
src/modules/host/handlers/operator/verifyOtpCheckout.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
APIGatewayProxyEvent,
|
||||||
|
APIGatewayProxyResult,
|
||||||
|
Context,
|
||||||
|
} from 'aws-lambda';
|
||||||
|
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||||
|
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||||
|
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||||
|
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||||
|
import { OperatorReservationVerifyOtpRequestDTO } from '../../dto/operator.activity.dto';
|
||||||
|
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||||
|
|
||||||
|
const operatorActivityService = new OperatorActivityService(prismaClient);
|
||||||
|
|
||||||
|
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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatorInfo = await verifyOperatorToken(token);
|
||||||
|
const operatorId = Number(operatorInfo.id);
|
||||||
|
|
||||||
|
if (!operatorId || Number.isNaN(operatorId)) {
|
||||||
|
throw new ApiError(400, 'Invalid operator ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: OperatorReservationVerifyOtpRequestDTO;
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : { checkInCode: '', otp: '' };
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestDTO: OperatorReservationVerifyOtpRequestDTO = {
|
||||||
|
checkInCode: body?.checkInCode?.trim() || '',
|
||||||
|
otp: body?.otp?.trim() || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!requestDTO.checkInCode || !requestDTO.otp) {
|
||||||
|
throw new ApiError(400, 'checkInCode and otp are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await operatorActivityService.verifyOtpCheckout(
|
||||||
|
operatorId,
|
||||||
|
requestDTO.checkInCode,
|
||||||
|
requestDTO.otp,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Checkout OTP verified successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
44
src/modules/host/handlers/operator/verifyPassword.ts
Normal file
44
src/modules/host/handlers/operator/verifyPassword.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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 { OperatorAuthService } from '../../services/operatorAuth.service';
|
||||||
|
|
||||||
|
const operatorAuthService = new OperatorAuthService(prismaClient);
|
||||||
|
|
||||||
|
export const handler = safeHandler(async (
|
||||||
|
event: APIGatewayProxyEvent,
|
||||||
|
context?: Context,
|
||||||
|
): Promise<APIGatewayProxyResult> => {
|
||||||
|
let body: { emailAddress?: string; userPassword?: string };
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : {};
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { emailAddress, userPassword } = body;
|
||||||
|
|
||||||
|
if (!emailAddress || !userPassword) {
|
||||||
|
throw new ApiError(400, 'Email and password are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = await operatorAuthService.verifyPasswordForOperator(
|
||||||
|
emailAddress.trim().toLowerCase(),
|
||||||
|
userPassword,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: isPasswordValid ? 'Password is valid' : 'Password is invalid',
|
||||||
|
data: { isValid: isPasswordValid },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
58
src/modules/host/handlers/settings/getMemberPermissions.ts
Normal file
58
src/modules/host/handlers/settings/getMemberPermissions.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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 { HostRolePermissionService } from '../../services/hostRolePermission.service';
|
||||||
|
|
||||||
|
const hostRolePermissionService = new HostRolePermissionService(prismaClient);
|
||||||
|
|
||||||
|
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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = await verifyHostToken(token);
|
||||||
|
|
||||||
|
const memberUserXid = event.pathParameters?.memberUserXid;
|
||||||
|
|
||||||
|
if (!memberUserXid) {
|
||||||
|
throw new ApiError(400, 'memberUserXid is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberUserId = parseInt(memberUserXid, 10);
|
||||||
|
if (isNaN(memberUserId)) {
|
||||||
|
throw new ApiError(400, 'Invalid memberUserXid. Must be a number.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await hostRolePermissionService.getMemberPermissions({
|
||||||
|
hostUserXid: userInfo.id,
|
||||||
|
memberUserXid: memberUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Member permissions retrieved successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { ROLE } from '../../../common/utils/constants/common.constant';
|
||||||
|
|
||||||
import ApiError from '../../../common/utils/helper/ApiError';
|
import ApiError from '../../../common/utils/helper/ApiError';
|
||||||
|
|
||||||
@@ -126,4 +127,128 @@ export class HostRolePermissionService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMemberPermissions(input: {
|
||||||
|
hostUserXid: number;
|
||||||
|
memberUserXid: number;
|
||||||
|
}) {
|
||||||
|
return this.prisma.$transaction(async (tx) => {
|
||||||
|
// Find the host
|
||||||
|
const host = await tx.hostHeader.findFirst({
|
||||||
|
where: {
|
||||||
|
userXid: input.hostUserXid,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
companyName: true,
|
||||||
|
userXid: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
throw new ApiError(404, 'Host company not found for the logged-in user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the host member
|
||||||
|
const hostMember = await tx.hostMembers.findFirst({
|
||||||
|
where: {
|
||||||
|
hostXid: host.id,
|
||||||
|
userXid: input.memberUserXid,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
memberStatus: 'accepted',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userXid: true,
|
||||||
|
roleXid: true,
|
||||||
|
role: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
roleName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hostRolePermissionMasterXid: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hostMember) {
|
||||||
|
throw new ApiError(404, 'Host member not found or not active.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if role is operator or co-admin
|
||||||
|
if (hostMember.roleXid !== ROLE.CO_ADMIN && hostMember.roleXid !== ROLE.OPERATOR) {
|
||||||
|
throw new ApiError(400, 'Member is not an operator or co-admin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hostMember.hostRolePermissionMasterXid) {
|
||||||
|
// Return empty permissions if no role permissions assigned
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
member: {
|
||||||
|
userXid: hostMember.userXid,
|
||||||
|
role: hostMember.role,
|
||||||
|
},
|
||||||
|
permissions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the role permissions
|
||||||
|
const rolePermissions = await tx.hostRolePermissionMasters.findFirst({
|
||||||
|
where: {
|
||||||
|
id: hostMember.hostRolePermissionMasterXid,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
permissionMasterXids: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rolePermissions) {
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
member: {
|
||||||
|
userXid: hostMember.userXid,
|
||||||
|
role: hostMember.role,
|
||||||
|
},
|
||||||
|
permissions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the actual permissions
|
||||||
|
const permissionIds = rolePermissions.permissionMasterXids as number[];
|
||||||
|
const permissions = await tx.hostPermissionMasters.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: permissionIds },
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
permissionKey: true,
|
||||||
|
permissionGroup: true,
|
||||||
|
permissionSection: true,
|
||||||
|
permissionAction: true,
|
||||||
|
displayLabel: true,
|
||||||
|
displayOrder: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
displayOrder: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
member: {
|
||||||
|
userXid: hostMember.userXid,
|
||||||
|
role: hostMember.role,
|
||||||
|
},
|
||||||
|
permissions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
992
src/modules/host/services/operatorActivity.service.ts
Normal file
992
src/modules/host/services/operatorActivity.service.ts
Normal file
@@ -0,0 +1,992 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
|
import { brevoService } from '../../../common/email/brevoApi';
|
||||||
|
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
||||||
|
import ApiError from '../../../common/utils/helper/ApiError';
|
||||||
|
import { generateOtpHelper } from '../../../common/utils/helper/sendOtp';
|
||||||
|
import config from '../../../config/config';
|
||||||
|
import {
|
||||||
|
ActivitySummaryDTO,
|
||||||
|
OperatorReservationByCheckInCodeDTO,
|
||||||
|
OperatorReservationVerificationOtpResponseDTO,
|
||||||
|
OperatorReservationVerifyOtpResponseDTO,
|
||||||
|
} from '../dto/operator.activity.dto';
|
||||||
|
|
||||||
|
const formatDateOnly = (date: Date): string => date.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const formatReadableDateTime = (date: Date): string =>
|
||||||
|
new Intl.DateTimeFormat('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
}).format(date);
|
||||||
|
|
||||||
|
const formatReadableDate = (date: Date): string =>
|
||||||
|
new Intl.DateTimeFormat('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
}).format(date);
|
||||||
|
|
||||||
|
const getDateLabel = (date: Date): string => {
|
||||||
|
const today = new Date();
|
||||||
|
return formatDateOnly(today) === formatDateOnly(date)
|
||||||
|
? 'Today'
|
||||||
|
: formatReadableDate(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFullName = (
|
||||||
|
firstName?: string | null,
|
||||||
|
lastName?: string | null,
|
||||||
|
): string => `${firstName ?? ''} ${lastName ?? ''}`.trim();
|
||||||
|
|
||||||
|
type ReservationVerificationPurpose = 'CheckInVerify' | 'CheckOutVerify';
|
||||||
|
type ReservationVerificationRequestChannel = 'email' | 'mobile';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OperatorActivityService {
|
||||||
|
constructor(private prisma: PrismaClient) {}
|
||||||
|
|
||||||
|
private async getAcceptedHostXidsForOperator(
|
||||||
|
operatorId: number,
|
||||||
|
): Promise<number[]> {
|
||||||
|
const hostMembers = await this.prisma.hostMembers.findMany({
|
||||||
|
where: {
|
||||||
|
userXid: operatorId,
|
||||||
|
isActive: true,
|
||||||
|
memberStatus: 'accepted',
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
hostXid: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return hostMembers.map((member) => member.hostXid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private maskEmailAddress(emailAddress: string): string {
|
||||||
|
const [localPart, domainPart = ''] = emailAddress.split('@');
|
||||||
|
|
||||||
|
if (!localPart) {
|
||||||
|
return emailAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localPart.length <= 2) {
|
||||||
|
return `${localPart[0] ?? '*'}*@${domainPart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${localPart[0]}${'*'.repeat(
|
||||||
|
Math.max(localPart.length - 2, 1),
|
||||||
|
)}${localPart[localPart.length - 1]}@${domainPart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendReservationVerificationOtpEmail(input: {
|
||||||
|
emailAddress: string;
|
||||||
|
otp: string;
|
||||||
|
checkInCode: string;
|
||||||
|
verificationType: 'check-in' | 'checkout';
|
||||||
|
firstName?: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
const subject =
|
||||||
|
input.verificationType === 'check-in'
|
||||||
|
? 'Your Minglar check-in verification code'
|
||||||
|
: 'Your Minglar checkout verification code';
|
||||||
|
|
||||||
|
const greetingName = input.firstName?.trim() || 'there';
|
||||||
|
const htmlContent = `
|
||||||
|
<p>Hi ${greetingName},</p>
|
||||||
|
<p>Your ${input.verificationType} verification code for Minglar is:</p>
|
||||||
|
<p><strong>${input.otp}</strong></p>
|
||||||
|
<p>Check-in code: <strong>${input.checkInCode}</strong></p>
|
||||||
|
<p>This code is valid for the next 5 minutes.</p>
|
||||||
|
<p>If you did not request this code, you can ignore this email.</p>
|
||||||
|
<p>Warm regards,<br />Team Minglar</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await brevoService.sendEmail({
|
||||||
|
recipients: [{ email: input.emailAddress }],
|
||||||
|
subject,
|
||||||
|
htmlContent,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reservation OTP email send failed:', error);
|
||||||
|
throw new ApiError(500, 'Failed to send OTP to the registered email address.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findReservationForOperatorByCheckInCode(
|
||||||
|
operatorId: number,
|
||||||
|
checkInCode: string,
|
||||||
|
) {
|
||||||
|
const hostXids = await this.getAcceptedHostXidsForOperator(operatorId);
|
||||||
|
|
||||||
|
if (hostXids.length === 0) {
|
||||||
|
throw new ApiError(404, 'Reservation not found for this check-in code');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reservation = await this.prisma.itineraryDetails.findFirst({
|
||||||
|
where: {
|
||||||
|
offlineCode: checkInCode,
|
||||||
|
itineraryKind: 'ACTIVITY',
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
itineraryActivity: {
|
||||||
|
itineraryType: 'ACTIVITY',
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
activity: {
|
||||||
|
hostXid: {
|
||||||
|
in: hostXids,
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
itineraryMemberXid: true,
|
||||||
|
offlineCode: true,
|
||||||
|
activityStatus: true,
|
||||||
|
itineraryMember: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
member: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
emailAddress: true,
|
||||||
|
mobileNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itineraryActivity: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
bookingStatus: true,
|
||||||
|
activity: {
|
||||||
|
select: {
|
||||||
|
activityTitle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!reservation) {
|
||||||
|
throw new ApiError(404, 'Reservation not found for this check-in code');
|
||||||
|
}
|
||||||
|
|
||||||
|
return reservation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyReservationOtp(
|
||||||
|
operatorId: number,
|
||||||
|
checkInCode: string,
|
||||||
|
otp: string,
|
||||||
|
input: {
|
||||||
|
otpType: ReservationVerificationPurpose;
|
||||||
|
verificationType: 'check-in' | 'checkout';
|
||||||
|
nextStatus: 'checked_in' | 'checked_out';
|
||||||
|
},
|
||||||
|
): Promise<OperatorReservationVerifyOtpResponseDTO> {
|
||||||
|
const normalizedCheckInCode = checkInCode.trim();
|
||||||
|
const trimmedOtp = otp.trim();
|
||||||
|
|
||||||
|
if (!normalizedCheckInCode) {
|
||||||
|
throw new ApiError(400, 'checkInCode is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trimmedOtp) {
|
||||||
|
throw new ApiError(400, 'otp is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reservation = await this.findReservationForOperatorByCheckInCode(
|
||||||
|
operatorId,
|
||||||
|
normalizedCheckInCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
const member = reservation.itineraryMember?.member;
|
||||||
|
if (!member?.id) {
|
||||||
|
throw new ApiError(404, 'User not found for this reservation');
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStatus = (
|
||||||
|
reservation.activityStatus ??
|
||||||
|
reservation.itineraryActivity.bookingStatus ??
|
||||||
|
''
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
if (input.verificationType === 'check-in') {
|
||||||
|
if (currentStatus === 'checked_in') {
|
||||||
|
throw new ApiError(400, 'This user is already checked in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStatus === 'checked_out') {
|
||||||
|
throw new ApiError(400, 'This user has already checked out.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.verificationType === 'checkout') {
|
||||||
|
if (currentStatus === 'checked_out') {
|
||||||
|
throw new ApiError(400, 'This user is already checked out.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStatus !== 'checked_in') {
|
||||||
|
throw new ApiError(
|
||||||
|
400,
|
||||||
|
'Check-in verification must be completed before checkout.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestOtp = await this.prisma.userOtp.findFirst({
|
||||||
|
where: {
|
||||||
|
userXid: member.id,
|
||||||
|
otpType: input.otpType,
|
||||||
|
isActive: true,
|
||||||
|
isVerified: false,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!latestOtp) {
|
||||||
|
throw new ApiError(400, 'No OTP found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date() > latestOtp.expiresOn) {
|
||||||
|
throw new ApiError(400, 'OTP has expired.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(trimmedOtp, latestOtp.otpCode);
|
||||||
|
|
||||||
|
if (!isMatch) {
|
||||||
|
throw new ApiError(400, 'Invalid OTP.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.userOtp.update({
|
||||||
|
where: {
|
||||||
|
id: latestOtp.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isVerified: true,
|
||||||
|
verifiedOn: new Date(),
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.itineraryDetails.update({
|
||||||
|
where: {
|
||||||
|
id: reservation.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
activityStatus: input.nextStatus,
|
||||||
|
updatedOn: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
itineraryActivityXid: reservation.itineraryActivity.id,
|
||||||
|
itineraryMemberXid: reservation.itineraryMemberXid,
|
||||||
|
activityName: reservation.itineraryActivity.activity?.activityTitle ?? null,
|
||||||
|
checkInCode: normalizedCheckInCode,
|
||||||
|
verificationType: input.verificationType,
|
||||||
|
verified: true,
|
||||||
|
reservationStatus: input.nextStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestReservationVerificationOtp(
|
||||||
|
operatorId: number,
|
||||||
|
checkInCode: string,
|
||||||
|
input: {
|
||||||
|
otpType: ReservationVerificationPurpose;
|
||||||
|
verificationType: 'check-in' | 'checkout';
|
||||||
|
requestedChannel: ReservationVerificationRequestChannel;
|
||||||
|
},
|
||||||
|
): Promise<OperatorReservationVerificationOtpResponseDTO> {
|
||||||
|
const normalizedCheckInCode = checkInCode.trim();
|
||||||
|
|
||||||
|
if (!normalizedCheckInCode) {
|
||||||
|
throw new ApiError(400, 'checkInCode is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reservation = await this.findReservationForOperatorByCheckInCode(
|
||||||
|
operatorId,
|
||||||
|
normalizedCheckInCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
const member = reservation.itineraryMember?.member;
|
||||||
|
if (!member?.id) {
|
||||||
|
throw new ApiError(404, 'User not found for this reservation');
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailAddress = member.emailAddress?.trim().toLowerCase() || null;
|
||||||
|
const mobileNumber = member.mobileNumber?.trim() || null;
|
||||||
|
|
||||||
|
if (!emailAddress && !mobileNumber) {
|
||||||
|
throw new ApiError(
|
||||||
|
400,
|
||||||
|
'No registered mobile number or email address found for this user.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emailAddress) {
|
||||||
|
throw new ApiError(
|
||||||
|
501,
|
||||||
|
'SMS OTP delivery is not configured yet for reservation verification. Please add a registered email address for this user.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveryNote =
|
||||||
|
input.requestedChannel === 'mobile'
|
||||||
|
? 'SMS OTP delivery is not configured yet, so the OTP was sent to the registered email address instead.'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const otpResult = await generateOtpHelper(
|
||||||
|
this.prisma,
|
||||||
|
member.id,
|
||||||
|
emailAddress,
|
||||||
|
input.otpType,
|
||||||
|
6,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.sendReservationVerificationOtpEmail({
|
||||||
|
emailAddress,
|
||||||
|
otp: otpResult.otp,
|
||||||
|
checkInCode: normalizedCheckInCode,
|
||||||
|
verificationType: input.verificationType,
|
||||||
|
firstName: member.firstName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
itineraryActivityXid: reservation.itineraryActivity.id,
|
||||||
|
itineraryMemberXid: reservation.itineraryMemberXid,
|
||||||
|
activityName: reservation.itineraryActivity.activity?.activityTitle ?? null,
|
||||||
|
checkInCode: normalizedCheckInCode,
|
||||||
|
verificationType: input.verificationType,
|
||||||
|
requestedChannel: input.requestedChannel,
|
||||||
|
deliveryChannel: 'email',
|
||||||
|
destination: this.maskEmailAddress(emailAddress),
|
||||||
|
reservationStatus:
|
||||||
|
reservation.activityStatus ?? reservation.itineraryActivity.bookingStatus,
|
||||||
|
expiresInMinutes: 5,
|
||||||
|
deliveryNote,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async attachPresignedUrl(
|
||||||
|
fileName?: string | null,
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!fileName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await getPresignedUrl(config.aws.bucketName, fileName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to generate presigned URL for ${fileName}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActivitiesByDate(
|
||||||
|
operatorId: number,
|
||||||
|
activityDate?: string,
|
||||||
|
): Promise<{
|
||||||
|
date: string;
|
||||||
|
activities: ActivitySummaryDTO[];
|
||||||
|
totalCount: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Get operator's assigned hosts
|
||||||
|
const hostMembers = await this.prisma.hostMembers.findMany({
|
||||||
|
where: {
|
||||||
|
userXid: operatorId,
|
||||||
|
isActive: true,
|
||||||
|
memberStatus: 'accepted', // Only accepted memberships
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
hostXid: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hostMembers.length === 0) {
|
||||||
|
return {
|
||||||
|
date: new Date().toISOString().split('T')[0],
|
||||||
|
activities: [],
|
||||||
|
totalCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostXids = hostMembers.map((m) => m.hostXid);
|
||||||
|
|
||||||
|
// Use today's date if not provided
|
||||||
|
const queryDate = activityDate ? new Date(activityDate) : new Date();
|
||||||
|
|
||||||
|
// Validate date format
|
||||||
|
if (isNaN(queryDate.getTime())) {
|
||||||
|
throw new ApiError(400, 'Invalid date format. Use YYYY-MM-DD format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set time to start of day (UTC)
|
||||||
|
queryDate.setUTCHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Set end of day
|
||||||
|
const endOfDay = new Date(queryDate);
|
||||||
|
endOfDay.setUTCHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
// Get all schedule occurrences from today onwards (not just query date)
|
||||||
|
// This includes future dates to show all scheduled dates
|
||||||
|
const allScheduleOccurrences =
|
||||||
|
await this.prisma.scheduleOccurences.findMany({
|
||||||
|
where: {
|
||||||
|
occurenceDate: {
|
||||||
|
gte: queryDate,
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
scheduleHeader: {
|
||||||
|
activity: {
|
||||||
|
hostXid: {
|
||||||
|
in: hostXids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
scheduleHeader: {
|
||||||
|
include: {
|
||||||
|
activity: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityTitle: true,
|
||||||
|
isActive: true,
|
||||||
|
ActivitiesMedia: {
|
||||||
|
where: {
|
||||||
|
isCoverImage: true,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
mediaFileName: true,
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
occurenceDate: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allScheduleOccurrences.length === 0) {
|
||||||
|
return {
|
||||||
|
date: queryDate.toISOString().split('T')[0],
|
||||||
|
activities: [],
|
||||||
|
totalCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group activities by activity ID with all scheduled dates
|
||||||
|
const activityMap = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
activityTitle: string;
|
||||||
|
coverImage: string | null;
|
||||||
|
coverImageUrl: string | null;
|
||||||
|
activityId: number;
|
||||||
|
scheduledDates: Date[];
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
// Process all schedule occurrences to build activity list with all scheduled dates
|
||||||
|
for (const occurrence of allScheduleOccurrences) {
|
||||||
|
const activity = occurrence.scheduleHeader.activity;
|
||||||
|
|
||||||
|
if (!activityMap.has(activity.id)) {
|
||||||
|
let coverImage: string | null = null;
|
||||||
|
let coverImageUrl: string | null = null;
|
||||||
|
|
||||||
|
if (activity.ActivitiesMedia.length > 0) {
|
||||||
|
coverImage = activity.ActivitiesMedia[0].mediaFileName;
|
||||||
|
try {
|
||||||
|
// Generate presigned URL for the image
|
||||||
|
coverImageUrl = await getPresignedUrl(
|
||||||
|
config.aws.bucketName,
|
||||||
|
coverImage,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to generate presigned URL for ${coverImage}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
coverImageUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activityMap.set(activity.id, {
|
||||||
|
activityTitle: activity.activityTitle || 'Unknown Activity',
|
||||||
|
coverImage,
|
||||||
|
coverImageUrl,
|
||||||
|
activityId: activity.id,
|
||||||
|
scheduledDates: [new Date(occurrence.occurenceDate)],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Add to scheduled dates if not already present
|
||||||
|
const existingActivity = activityMap.get(activity.id)!;
|
||||||
|
const occurrenceDateStr = new Date(occurrence.occurenceDate)
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[0];
|
||||||
|
const dateExists = existingActivity.scheduledDates.some(
|
||||||
|
(d) => d.toISOString().split('T')[0] === occurrenceDateStr,
|
||||||
|
);
|
||||||
|
if (!dateExists) {
|
||||||
|
existingActivity.scheduledDates.push(
|
||||||
|
new Date(occurrence.occurenceDate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all unique scheduled dates for all activities
|
||||||
|
const allScheduledDates = new Set<string>();
|
||||||
|
activityMap.forEach((activity) => {
|
||||||
|
activity.scheduledDates.forEach((date) => {
|
||||||
|
allScheduledDates.add(date.toISOString().split('T')[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all activity IDs
|
||||||
|
const activityIds = Array.from(activityMap.keys());
|
||||||
|
|
||||||
|
// Query all bookings for these activities from query date onwards
|
||||||
|
const allBookings = await this.prisma.itineraryActivities.findMany({
|
||||||
|
where: {
|
||||||
|
activityXid: {
|
||||||
|
in: activityIds,
|
||||||
|
},
|
||||||
|
occurenceDate: {
|
||||||
|
gte: queryDate,
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
activityXid: true,
|
||||||
|
itineraryHeaderXid: true,
|
||||||
|
occurenceDate: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count bookings by activity and date
|
||||||
|
const bookingsByActivityAndDate = new Map<
|
||||||
|
number,
|
||||||
|
Map<string, Set<number>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
allBookings.forEach((booking) => {
|
||||||
|
if (booking.activityXid) {
|
||||||
|
if (!bookingsByActivityAndDate.has(booking.activityXid)) {
|
||||||
|
bookingsByActivityAndDate.set(booking.activityXid, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateStr = new Date(booking.occurenceDate)
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[0];
|
||||||
|
const dateMap = bookingsByActivityAndDate.get(booking.activityXid)!;
|
||||||
|
|
||||||
|
if (!dateMap.has(dateStr)) {
|
||||||
|
dateMap.set(dateStr, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
dateMap.get(dateStr)!.add(booking.itineraryHeaderXid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert map to array format with date breakdown
|
||||||
|
const activities: ActivitySummaryDTO[] = Array.from(
|
||||||
|
activityMap.values(),
|
||||||
|
).map((activity) => {
|
||||||
|
const activityBookings = bookingsByActivityAndDate.get(
|
||||||
|
activity.activityId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get bookings for each scheduled date
|
||||||
|
const dateBreakdown = Array.from(allScheduledDates)
|
||||||
|
.sort()
|
||||||
|
.map((dateStr) => {
|
||||||
|
const count = activityBookings?.get(dateStr)?.size || 0;
|
||||||
|
return {
|
||||||
|
date: dateStr,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count for the query date only
|
||||||
|
const queryDateStr = queryDate.toISOString().split('T')[0];
|
||||||
|
const countForQueryDate =
|
||||||
|
activityBookings?.get(queryDateStr)?.size || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activityName: activity.activityTitle,
|
||||||
|
activityImage: activity.coverImage,
|
||||||
|
activityImagePreSignedUrl: activity.coverImageUrl,
|
||||||
|
count: countForQueryDate,
|
||||||
|
dateBreakdown,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Total count is bookings for the requested date only
|
||||||
|
const queryDateStr = queryDate.toISOString().split('T')[0];
|
||||||
|
const totalCount = allBookings.filter(
|
||||||
|
(b) =>
|
||||||
|
new Date(b.occurenceDate).toISOString().split('T')[0] ===
|
||||||
|
queryDateStr,
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: queryDateStr,
|
||||||
|
activities,
|
||||||
|
totalCount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new ApiError(
|
||||||
|
500,
|
||||||
|
error instanceof Error ? error.message : 'Error fetching activities',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReservationByCheckInCode(
|
||||||
|
operatorId: number,
|
||||||
|
checkInCode: string,
|
||||||
|
): Promise<OperatorReservationByCheckInCodeDTO> {
|
||||||
|
try {
|
||||||
|
const normalizedCheckInCode = checkInCode.trim();
|
||||||
|
if (!normalizedCheckInCode) {
|
||||||
|
throw new ApiError(400, 'checkInCode is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostMembers = await this.prisma.hostMembers.findMany({
|
||||||
|
where: {
|
||||||
|
userXid: operatorId,
|
||||||
|
isActive: true,
|
||||||
|
memberStatus: 'accepted',
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
hostXid: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hostXids = hostMembers.map((member) => member.hostXid);
|
||||||
|
if (hostXids.length === 0) {
|
||||||
|
throw new ApiError(404, 'Reservation not found for this check-in code');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reservation = await this.prisma.itineraryDetails.findFirst({
|
||||||
|
where: {
|
||||||
|
offlineCode: normalizedCheckInCode,
|
||||||
|
itineraryKind: 'ACTIVITY',
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
itineraryActivity: {
|
||||||
|
itineraryType: 'ACTIVITY',
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
activity: {
|
||||||
|
hostXid: {
|
||||||
|
in: hostXids,
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
itineraryMemberXid: true,
|
||||||
|
offlineCode: true,
|
||||||
|
activityStatus: true,
|
||||||
|
createdAt: true,
|
||||||
|
paidOn: true,
|
||||||
|
itineraryMember: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
memberRole: true,
|
||||||
|
member: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
mobileNumber: true,
|
||||||
|
profileImage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itineraryActivity: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
occurenceDate: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
bookingStatus: true,
|
||||||
|
venue: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
venueName: true,
|
||||||
|
venueLabel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itineraryHeader: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
itineraryNo: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activity: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityTitle: true,
|
||||||
|
checkInAddress: true,
|
||||||
|
ActivityPickUpDetails: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
isPickUp: true,
|
||||||
|
locationAddress: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itineraryActivitySelections: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
itineraryMemberXid: true,
|
||||||
|
isFoodOpted: true,
|
||||||
|
isTrainerOpted: true,
|
||||||
|
selectedFoodTypes: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityFoodType: {
|
||||||
|
select: {
|
||||||
|
foodType: {
|
||||||
|
select: {
|
||||||
|
foodTypeName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedEquipments: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityEquipment: {
|
||||||
|
select: {
|
||||||
|
equipmentName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!reservation) {
|
||||||
|
throw new ApiError(404, 'Reservation not found for this check-in code');
|
||||||
|
}
|
||||||
|
|
||||||
|
const activitySelection =
|
||||||
|
reservation.itineraryActivity.itineraryActivitySelections.find(
|
||||||
|
(selection) =>
|
||||||
|
selection.itineraryMemberXid === reservation.itineraryMemberXid,
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
const selectedFoodTypes = (activitySelection?.selectedFoodTypes ?? [])
|
||||||
|
.map(
|
||||||
|
(selectedFoodType) =>
|
||||||
|
selectedFoodType.activityFoodType.foodType.foodTypeName,
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
const selectedEquipments = (activitySelection?.selectedEquipments ?? [])
|
||||||
|
.map(
|
||||||
|
(selectedEquipment) =>
|
||||||
|
selectedEquipment.activityEquipment.equipmentName,
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const pickupLocation =
|
||||||
|
reservation.itineraryActivity.activity.ActivityPickUpDetails.find(
|
||||||
|
(detail) => detail.isPickUp && detail.locationAddress,
|
||||||
|
)?.locationAddress ??
|
||||||
|
reservation.itineraryActivity.activity.ActivityPickUpDetails.find(
|
||||||
|
(detail) => detail.locationAddress,
|
||||||
|
)?.locationAddress ??
|
||||||
|
reservation.itineraryActivity.activity.checkInAddress ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
const member = reservation.itineraryMember.member;
|
||||||
|
const fullName =
|
||||||
|
buildFullName(member.firstName, member.lastName) || 'Guest';
|
||||||
|
const profileImagePreSignedUrl = await this.attachPresignedUrl(
|
||||||
|
member.profileImage,
|
||||||
|
);
|
||||||
|
const occurenceDate = new Date(
|
||||||
|
reservation.itineraryActivity.occurenceDate,
|
||||||
|
);
|
||||||
|
const bookedOnDate =
|
||||||
|
reservation.paidOn ??
|
||||||
|
reservation.createdAt ??
|
||||||
|
reservation.itineraryActivity.itineraryHeader.createdAt;
|
||||||
|
|
||||||
|
return {
|
||||||
|
itineraryHeaderXid: reservation.itineraryActivity.itineraryHeader.id,
|
||||||
|
itineraryActivityXid: reservation.itineraryActivity.id,
|
||||||
|
bookingId: reservation.itineraryActivity.itineraryHeader.itineraryNo,
|
||||||
|
checkInCode: reservation.offlineCode,
|
||||||
|
reservationStatus:
|
||||||
|
reservation.activityStatus ??
|
||||||
|
reservation.itineraryActivity.bookingStatus,
|
||||||
|
personalDetails: {
|
||||||
|
fullName,
|
||||||
|
firstName: member.firstName,
|
||||||
|
lastName: member.lastName,
|
||||||
|
role: reservation.itineraryMember.memberRole,
|
||||||
|
mobileNumber: member.mobileNumber,
|
||||||
|
profileImage: member.profileImage,
|
||||||
|
profileImagePreSignedUrl,
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
bookingInformation: {
|
||||||
|
activityName: reservation.itineraryActivity.activity.activityTitle,
|
||||||
|
slot:
|
||||||
|
reservation.itineraryActivity.startTime &&
|
||||||
|
reservation.itineraryActivity.endTime
|
||||||
|
? `${reservation.itineraryActivity.startTime} - ${reservation.itineraryActivity.endTime}`
|
||||||
|
: null,
|
||||||
|
startTime: reservation.itineraryActivity.startTime,
|
||||||
|
endTime: reservation.itineraryActivity.endTime,
|
||||||
|
track: reservation.itineraryActivity.venue?.venueName ?? null,
|
||||||
|
trackLabel: reservation.itineraryActivity.venue?.venueLabel ?? null,
|
||||||
|
date: formatDateOnly(occurenceDate),
|
||||||
|
dateLabel: getDateLabel(occurenceDate),
|
||||||
|
bookedOn: bookedOnDate ? bookedOnDate.toISOString() : null,
|
||||||
|
bookedOnLabel: bookedOnDate
|
||||||
|
? formatReadableDateTime(bookedOnDate)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
bookingIncluded: {
|
||||||
|
food:
|
||||||
|
selectedFoodTypes.length > 0 ? selectedFoodTypes.join(', ') : 'No',
|
||||||
|
selectedFoodTypes,
|
||||||
|
equipment: selectedEquipments.length > 0 ? 'Yes' : 'No',
|
||||||
|
selectedEquipments,
|
||||||
|
trainerOrGuide: activitySelection?.isTrainerOpted ? 'Yes' : 'No',
|
||||||
|
pickupLocation,
|
||||||
|
},
|
||||||
|
// description1: null,
|
||||||
|
// description2: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError(
|
||||||
|
500,
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Error fetching reservation details',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendOtpCheckIn(
|
||||||
|
operatorId: number,
|
||||||
|
checkInCode: string,
|
||||||
|
): Promise<OperatorReservationVerificationOtpResponseDTO> {
|
||||||
|
return this.requestReservationVerificationOtp(operatorId, checkInCode, {
|
||||||
|
otpType: 'CheckInVerify',
|
||||||
|
verificationType: 'check-in',
|
||||||
|
requestedChannel: 'email',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendOtpCheckout(
|
||||||
|
operatorId: number,
|
||||||
|
checkInCode: string,
|
||||||
|
): Promise<OperatorReservationVerificationOtpResponseDTO> {
|
||||||
|
return this.requestReservationVerificationOtp(operatorId, checkInCode, {
|
||||||
|
otpType: 'CheckOutVerify',
|
||||||
|
verificationType: 'checkout',
|
||||||
|
requestedChannel: 'mobile',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOtpCheckIn(
|
||||||
|
operatorId: number,
|
||||||
|
checkInCode: string,
|
||||||
|
otp: string,
|
||||||
|
): Promise<OperatorReservationVerifyOtpResponseDTO> {
|
||||||
|
return this.verifyReservationOtp(operatorId, checkInCode, otp, {
|
||||||
|
otpType: 'CheckInVerify',
|
||||||
|
verificationType: 'check-in',
|
||||||
|
nextStatus: 'checked_in',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOtpCheckout(
|
||||||
|
operatorId: number,
|
||||||
|
checkInCode: string,
|
||||||
|
otp: string,
|
||||||
|
): Promise<OperatorReservationVerifyOtpResponseDTO> {
|
||||||
|
return this.verifyReservationOtp(operatorId, checkInCode, otp, {
|
||||||
|
otpType: 'CheckOutVerify',
|
||||||
|
verificationType: 'checkout',
|
||||||
|
nextStatus: 'checked_out',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
286
src/modules/host/services/operatorAuth.service.ts
Normal file
286
src/modules/host/services/operatorAuth.service.ts
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
|
import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant';
|
||||||
|
import ApiError from '../../../common/utils/helper/ApiError';
|
||||||
|
import { OtpGeneratorSixDigit } from '../../../common/utils/helper/OtpGenerator';
|
||||||
|
|
||||||
|
type OperatorSignupInput = {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
emailAddress?: string;
|
||||||
|
isdCode?: string;
|
||||||
|
mobileNumber?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OperatorAuthService {
|
||||||
|
constructor(private prisma: PrismaClient) {}
|
||||||
|
|
||||||
|
async findInvitedOperator(input: {
|
||||||
|
emailAddress?: string;
|
||||||
|
mobileNumber?: string;
|
||||||
|
}) {
|
||||||
|
const emailAddress = input.emailAddress?.trim().toLowerCase();
|
||||||
|
const mobileNumber = input.mobileNumber?.trim();
|
||||||
|
|
||||||
|
if (!emailAddress && !mobileNumber) {
|
||||||
|
throw new ApiError(400, 'Email address or mobile number is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const invitedOperator = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
roleXid: ROLE.OPERATOR,
|
||||||
|
isActive: true,
|
||||||
|
OR: [
|
||||||
|
...(emailAddress ? [{ emailAddress }] : []),
|
||||||
|
...(mobileNumber ? [{ mobileNumber }] : []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
emailAddress: true,
|
||||||
|
isdCode: true,
|
||||||
|
mobileNumber: true,
|
||||||
|
roleXid: true,
|
||||||
|
userPassword: true,
|
||||||
|
isEmailVerfied: true,
|
||||||
|
userStatus: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!invitedOperator) {
|
||||||
|
throw new ApiError(
|
||||||
|
403,
|
||||||
|
'Operator record not found. Please contact your host.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return invitedOperator;
|
||||||
|
}
|
||||||
|
|
||||||
|
async signUpOperator(input: OperatorSignupInput) {
|
||||||
|
const emailAddress = input.emailAddress?.trim().toLowerCase();
|
||||||
|
const mobileNumber = input.mobileNumber?.trim();
|
||||||
|
const invitedOperator = await this.findInvitedOperator({
|
||||||
|
emailAddress,
|
||||||
|
mobileNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invitedOperator.userPassword) {
|
||||||
|
return {
|
||||||
|
userId: invitedOperator.id,
|
||||||
|
emailAddress: emailAddress || invitedOperator.emailAddress,
|
||||||
|
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
|
||||||
|
otp: null,
|
||||||
|
expiresOn: null,
|
||||||
|
isNewOperator: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const otp = OtpGeneratorSixDigit.generateOtp();
|
||||||
|
const hashedOtp = await bcrypt.hash(otp, 10);
|
||||||
|
const expiry = new Date(Date.now() + 5 * 60 * 1000);
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.user.update({
|
||||||
|
where: { id: invitedOperator.id },
|
||||||
|
data: {
|
||||||
|
firstName: input.firstName?.trim() || invitedOperator.firstName || null,
|
||||||
|
lastName: input.lastName?.trim() || invitedOperator.lastName || null,
|
||||||
|
emailAddress: emailAddress || invitedOperator.emailAddress,
|
||||||
|
isdCode: input.isdCode?.trim() || invitedOperator.isdCode || null,
|
||||||
|
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
|
||||||
|
userStatus: USER_STATUS.INVITED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.userOtp.updateMany({
|
||||||
|
where: {
|
||||||
|
userXid: invitedOperator.id,
|
||||||
|
otpType: 'Register',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isActive: false,
|
||||||
|
isVerified: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.userOtp.create({
|
||||||
|
data: {
|
||||||
|
userXid: invitedOperator.id,
|
||||||
|
otpType: 'Register',
|
||||||
|
otpCode: hashedOtp,
|
||||||
|
expiresOn: expiry,
|
||||||
|
isVerified: false,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isNewOperator: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOperatorOtp(input: { emailAddress?: string; mobileNumber?: string; otp: string }) {
|
||||||
|
const emailAddress = input.emailAddress?.trim().toLowerCase();
|
||||||
|
const mobileNumber = input.mobileNumber?.trim();
|
||||||
|
const otp = input.otp.trim();
|
||||||
|
|
||||||
|
const invitedOperator = await this.findInvitedOperator({
|
||||||
|
emailAddress,
|
||||||
|
mobileNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestOtp = await this.prisma.userOtp.findFirst({
|
||||||
|
where: {
|
||||||
|
userXid: invitedOperator.id,
|
||||||
|
otpType: 'Register',
|
||||||
|
isActive: true,
|
||||||
|
isVerified: false,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!latestOtp) {
|
||||||
|
throw new ApiError(400, 'No OTP found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date() > latestOtp.expiresOn) {
|
||||||
|
throw new ApiError(400, 'OTP has expired.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(otp, latestOtp.otpCode);
|
||||||
|
|
||||||
|
if (!isMatch) {
|
||||||
|
throw new ApiError(400, 'Invalid OTP.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.userOtp.update({
|
||||||
|
where: { id: latestOtp.id },
|
||||||
|
data: {
|
||||||
|
isVerified: true,
|
||||||
|
verifiedOn: new Date(),
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prisma.user.update({
|
||||||
|
where: { id: invitedOperator.id },
|
||||||
|
data: {
|
||||||
|
isEmailVerfied: emailAddress ? true : invitedOperator.isEmailVerfied,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return invitedOperator;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOperatorPassword(userId: number, password: string) {
|
||||||
|
const user = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
roleXid: ROLE.OPERATOR,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
emailAddress: true,
|
||||||
|
userPassword: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(404, 'Operator not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.userPassword) {
|
||||||
|
throw new ApiError(400, 'Password already created. Please login.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10);
|
||||||
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||||
|
|
||||||
|
await this.prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
userPassword: hashedPassword,
|
||||||
|
userStatus: USER_STATUS.ACTIVE,
|
||||||
|
isEmailVerfied: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
emailAddress: user.emailAddress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginForOperator(emailAddress: string, userPassword: string) {
|
||||||
|
const existingOperator = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
emailAddress,
|
||||||
|
roleXid: ROLE.OPERATOR,
|
||||||
|
isActive: true,
|
||||||
|
userStatus: USER_STATUS.ACTIVE,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
emailAddress: true,
|
||||||
|
mobileNumber: true,
|
||||||
|
roleXid: true,
|
||||||
|
isActive: true,
|
||||||
|
userStatus: true,
|
||||||
|
userPassword: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingOperator) {
|
||||||
|
throw new ApiError(404, 'Operator not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordMatched = await bcrypt.compare(
|
||||||
|
userPassword,
|
||||||
|
existingOperator.userPassword || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isPasswordMatched) {
|
||||||
|
throw new ApiError(401, 'Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete existingOperator.userPassword;
|
||||||
|
|
||||||
|
return existingOperator;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyPasswordForOperator(emailAddress: string, userPassword: string) {
|
||||||
|
const existingOperator = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
emailAddress,
|
||||||
|
roleXid: ROLE.OPERATOR,
|
||||||
|
isActive: true,
|
||||||
|
userStatus: USER_STATUS.ACTIVE,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userPassword: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingOperator) {
|
||||||
|
throw new ApiError(404, 'Operator not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordMatched = await bcrypt.compare(
|
||||||
|
userPassword,
|
||||||
|
existingOperator.userPassword || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
return isPasswordMatched;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/modules/minglaradmin/handlers/addPQQSuggestion.ts
Normal file
91
src/modules/minglaradmin/handlers/addPQQSuggestion.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||||
|
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||||
|
import { PrismaService } from '../../../common/database/prisma.service';
|
||||||
|
import { MinglarService } from '../services/minglar.service';
|
||||||
|
import ApiError from '../../../common/utils/helper/ApiError';
|
||||||
|
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
|
||||||
|
import { HOST_SUGGESTION_TITLES } from '../../../common/utils/constants/minglar.constant';
|
||||||
|
|
||||||
|
const prismaService = new PrismaService();
|
||||||
|
const minglarService = new MinglarService(prismaService);
|
||||||
|
|
||||||
|
interface AddSuggestionBody {
|
||||||
|
hostXid: number;
|
||||||
|
title: string;
|
||||||
|
comments: string;
|
||||||
|
activity_pqq_header_xid:number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add suggestion handler for host applications
|
||||||
|
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
|
||||||
|
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
|
||||||
|
*/
|
||||||
|
export const handler = safeHandler(async (
|
||||||
|
event: APIGatewayProxyEvent,
|
||||||
|
context?: Context
|
||||||
|
): Promise<APIGatewayProxyResult> => {
|
||||||
|
// Verify authentication token
|
||||||
|
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||||
|
if (!token) {
|
||||||
|
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token and get user info
|
||||||
|
const userInfo = await verifyMinglarAdminToken(token);
|
||||||
|
|
||||||
|
// Get user details
|
||||||
|
const user = await prismaService.user.findUnique({
|
||||||
|
where: { id: userInfo.id },
|
||||||
|
select: { id: true, roleXid: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(404, 'User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
let body: AddSuggestionBody;
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : {};
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, comments , activity_pqq_header_xid} = body;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
throw new ApiError(400, 'Title is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!comments) {
|
||||||
|
throw new ApiError(400, 'Comments are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!activity_pqq_header_xid){
|
||||||
|
throw new ApiError(400 , "Activity Pqq HeaderXid Required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate title is one of the allowed types
|
||||||
|
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
|
||||||
|
if (!allowedTitles.includes(title)) {
|
||||||
|
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add suggestion using service
|
||||||
|
await minglarService.addPqqSuggestion(title, comments, activity_pqq_header_xid);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Suggestion added successfully',
|
||||||
|
data: null,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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 { ItineraryService } from '../../services/itinerary.service';
|
||||||
|
|
||||||
|
const itineraryService = new ItineraryService(prismaClient);
|
||||||
|
|
||||||
|
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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = await verifyUserToken(token);
|
||||||
|
const userId = Number(userInfo.id);
|
||||||
|
|
||||||
|
if (!userId || isNaN(userId)) {
|
||||||
|
throw new ApiError(400, 'Invalid user ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: {
|
||||||
|
itineraryHeaderXid?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = event.body ? JSON.parse(event.body) : {};
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiError(400, 'Invalid JSON in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { itineraryHeaderXid } = body;
|
||||||
|
|
||||||
|
if (!itineraryHeaderXid) {
|
||||||
|
throw new ApiError(400, 'itineraryHeaderXid is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await itineraryService.getActivityDetailsAfterBooking(
|
||||||
|
userId,
|
||||||
|
itineraryHeaderXid,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Activity details retrieved successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -60,7 +60,6 @@ export const handler = safeHandler(async (
|
|||||||
const itineraryHeaderXidRaw =
|
const itineraryHeaderXidRaw =
|
||||||
event.queryStringParameters?.itineraryHeaderXid ?? null;
|
event.queryStringParameters?.itineraryHeaderXid ?? null;
|
||||||
const startDateRaw = event.queryStringParameters?.startDate ?? null;
|
const startDateRaw = event.queryStringParameters?.startDate ?? null;
|
||||||
const endDateRaw = event.queryStringParameters?.endDate ?? null;
|
|
||||||
|
|
||||||
let itineraryHeaderXid: number | undefined;
|
let itineraryHeaderXid: number | undefined;
|
||||||
if (
|
if (
|
||||||
@@ -79,38 +78,17 @@ export const handler = safeHandler(async (
|
|||||||
startDateRaw !== null &&
|
startDateRaw !== null &&
|
||||||
startDateRaw !== undefined &&
|
startDateRaw !== undefined &&
|
||||||
startDateRaw.trim() !== '';
|
startDateRaw.trim() !== '';
|
||||||
const hasEndDate =
|
|
||||||
endDateRaw !== null &&
|
|
||||||
endDateRaw !== undefined &&
|
|
||||||
endDateRaw.trim() !== '';
|
|
||||||
|
|
||||||
if (hasStartDate !== hasEndDate) {
|
|
||||||
throw new ApiError(
|
|
||||||
400,
|
|
||||||
'startDate and endDate must be provided together',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let startDate: Date | undefined;
|
let startDate: Date | undefined;
|
||||||
let endDate: Date | undefined;
|
|
||||||
|
|
||||||
if (hasStartDate && hasEndDate) {
|
if (hasStartDate) {
|
||||||
startDate = parseQueryDate(startDateRaw, 'startDate');
|
startDate = parseQueryDate(startDateRaw, 'startDate');
|
||||||
endDate = parseQueryDate(endDateRaw, 'endDate');
|
|
||||||
|
|
||||||
if (startDate > endDate) {
|
|
||||||
throw new ApiError(
|
|
||||||
400,
|
|
||||||
'startDate must be earlier than or equal to endDate',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await itineraryService.getAllUserSavedItineraries(
|
const result = await itineraryService.getAllUserSavedItineraries(
|
||||||
userId,
|
userId,
|
||||||
itineraryHeaderXid,
|
itineraryHeaderXid,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ export const handler = safeHandler(async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(payload.page) || payload.page <= 0) {
|
||||||
|
throw new ApiError(400, 'page must be a positive integer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(payload.limit) || payload.limit <= 0) {
|
||||||
|
throw new ApiError(400, 'limit must be a positive integer.');
|
||||||
|
}
|
||||||
|
|
||||||
const result = await itineraryService.getMatchingBucketInterestedActivities(
|
const result = await itineraryService.getMatchingBucketInterestedActivities(
|
||||||
userId,
|
userId,
|
||||||
payload,
|
payload,
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Prisma, PrismaClient } from '@prisma/client';
|
import { Prisma, PrismaClient } from '@prisma/client';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
||||||
import ApiError from '../../../common/utils/helper/ApiError';
|
|
||||||
import {
|
import {
|
||||||
ACTIVITY_AM_INTERNAL_STATUS,
|
ACTIVITY_AM_INTERNAL_STATUS,
|
||||||
ACTIVITY_INTERNAL_STATUS,
|
ACTIVITY_INTERNAL_STATUS,
|
||||||
} from '../../../common/utils/constants/host.constant';
|
} from '../../../common/utils/constants/host.constant';
|
||||||
|
import ApiError from '../../../common/utils/helper/ApiError';
|
||||||
|
|
||||||
import config from '@/config/config';
|
import config from '@/config/config';
|
||||||
|
|
||||||
@@ -269,6 +269,28 @@ const formatDateKey = (date: Date) => {
|
|||||||
const addMinutes = (date: Date, minutes: number) =>
|
const addMinutes = (date: Date, minutes: number) =>
|
||||||
new Date(date.getTime() + minutes * 60 * 1000);
|
new Date(date.getTime() + minutes * 60 * 1000);
|
||||||
|
|
||||||
|
const formatTicketTime = (value?: string | null) => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim().toUpperCase().replace(/\s+(AM|PM)$/i, '$1');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTicketTimeRange = (
|
||||||
|
startTime?: string | null,
|
||||||
|
endTime?: string | null,
|
||||||
|
) => {
|
||||||
|
const formattedStartTime = formatTicketTime(startTime);
|
||||||
|
const formattedEndTime = formatTicketTime(endTime);
|
||||||
|
|
||||||
|
if (formattedStartTime && formattedEndTime) {
|
||||||
|
return `${formattedStartTime} - ${formattedEndTime}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedStartTime ?? formattedEndTime ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
const getDateRange = (fromDate: Date, toDate: Date) => {
|
const getDateRange = (fromDate: Date, toDate: Date) => {
|
||||||
const dates: Date[] = [];
|
const dates: Date[] = [];
|
||||||
const cursor = startOfDay(fromDate);
|
const cursor = startOfDay(fromDate);
|
||||||
@@ -1779,19 +1801,15 @@ export class ItineraryService {
|
|||||||
userXid: number,
|
userXid: number,
|
||||||
itineraryHeaderXid?: number,
|
itineraryHeaderXid?: number,
|
||||||
startDate?: Date,
|
startDate?: Date,
|
||||||
endDate?: Date,
|
|
||||||
) {
|
) {
|
||||||
const itineraries = await this.prisma.itineraryHeader.findMany({
|
const itineraries = await this.prisma.itineraryHeader.findMany({
|
||||||
where: {
|
where: {
|
||||||
...(itineraryHeaderXid ? { id: itineraryHeaderXid } : {}),
|
...(itineraryHeaderXid ? { id: itineraryHeaderXid } : {}),
|
||||||
...(startDate && endDate
|
...(startDate
|
||||||
? {
|
? {
|
||||||
fromDate: {
|
fromDate: {
|
||||||
gte: startDate,
|
gte: startDate,
|
||||||
},
|
},
|
||||||
toDate: {
|
|
||||||
lte: endDate,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -4466,8 +4484,8 @@ export class ItineraryService {
|
|||||||
.filter((item): item is number => typeof item === 'number');
|
.filter((item): item is number => typeof item === 'number');
|
||||||
|
|
||||||
const totalCount = activities.length;
|
const totalCount = activities.length;
|
||||||
const sanitizedLimit = Math.min(Math.max(payload.limit, 1), 20);
|
const sanitizedLimit = Math.min(Math.max(Math.floor(payload.limit || 20), 1), 20);
|
||||||
const sanitizedPage = Math.max(payload.page, 1);
|
const sanitizedPage = Math.max(Math.floor(payload.page), 1);
|
||||||
const totalPages = totalCount ? Math.ceil(totalCount / sanitizedLimit) : 0;
|
const totalPages = totalCount ? Math.ceil(totalCount / sanitizedLimit) : 0;
|
||||||
const startIndex = (sanitizedPage - 1) * sanitizedLimit;
|
const startIndex = (sanitizedPage - 1) * sanitizedLimit;
|
||||||
const paginatedActivities = activities.slice(
|
const paginatedActivities = activities.slice(
|
||||||
@@ -4511,4 +4529,517 @@ export class ItineraryService {
|
|||||||
activities: paginatedActivities,
|
activities: paginatedActivities,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getActivityDetailsAfterBooking(
|
||||||
|
userXid: number,
|
||||||
|
itineraryHeaderXid: number,
|
||||||
|
) {
|
||||||
|
const itineraryHeader = await this.prisma.itineraryHeader.findFirst({
|
||||||
|
where: {
|
||||||
|
id: itineraryHeaderXid,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
ownerXid: userXid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ItineraryMembers: {
|
||||||
|
some: {
|
||||||
|
memberXid: userXid,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
itineraryNo: true,
|
||||||
|
title: true,
|
||||||
|
fromDate: true,
|
||||||
|
fromTime: true,
|
||||||
|
toDate: true,
|
||||||
|
toTime: true,
|
||||||
|
itineraryStatus: true,
|
||||||
|
ownerXid: true,
|
||||||
|
owner: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
emailAddress: true,
|
||||||
|
mobileNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ItineraryMembers: {
|
||||||
|
where: {
|
||||||
|
memberXid: userXid,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
memberXid: true,
|
||||||
|
memberRole: true,
|
||||||
|
memberStatus: true,
|
||||||
|
member: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
emailAddress: true,
|
||||||
|
mobileNumber: true,
|
||||||
|
profileImage: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ItineraryActivities: {
|
||||||
|
where: {
|
||||||
|
activityXid: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
occurenceDate: 'asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 'asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayOrder: 'asc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
displayOrder: true,
|
||||||
|
occurenceDate: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
paxCount: true,
|
||||||
|
totalAmount: true,
|
||||||
|
bookingStatus: true,
|
||||||
|
venue: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
venueName: true,
|
||||||
|
venueLabel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ItineraryDetails: {
|
||||||
|
where: {
|
||||||
|
itineraryKind: 'ACTIVITY',
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
itineraryMember: {
|
||||||
|
is: {
|
||||||
|
memberXid: userXid,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
itineraryMemberXid: true,
|
||||||
|
offlineCode: true,
|
||||||
|
description1: true,
|
||||||
|
description2: true,
|
||||||
|
activityStatus: true,
|
||||||
|
itineraryStatus: true,
|
||||||
|
isPaid: true,
|
||||||
|
paidOn: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itineraryActivitySelections: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
itineraryMember: {
|
||||||
|
is: {
|
||||||
|
memberXid: userXid,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
itineraryMemberXid: true,
|
||||||
|
isFoodOpted: true,
|
||||||
|
isTrainerOpted: true,
|
||||||
|
isInActivityNavigationOpted: true,
|
||||||
|
activityNavigationMode: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
navigationModeName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedFoodTypes: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityFoodTypeXid: true,
|
||||||
|
activityFoodType: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
foodTypeXid: true,
|
||||||
|
foodType: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
foodTypeName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedEquipments: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityEquipmentXid: true,
|
||||||
|
activityEquipment: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
equipmentName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activity: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityTitle: true,
|
||||||
|
activityDescription: true,
|
||||||
|
checkInAddress: true,
|
||||||
|
checkInLat: true,
|
||||||
|
checkInLong: true,
|
||||||
|
activityDurationMins: true,
|
||||||
|
foodAvailable: true,
|
||||||
|
trainerAvailable: true,
|
||||||
|
equipmentAvailable: true,
|
||||||
|
pickUpDropAvailable: true,
|
||||||
|
inActivityAvailable: true,
|
||||||
|
ActivitiesMedia: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
displayOrder: 'asc',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
mediaType: true,
|
||||||
|
mediaFileName: true,
|
||||||
|
isCoverImage: true,
|
||||||
|
displayOrder: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ActivityPickUpDetails: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
isPickUp: true,
|
||||||
|
locationLat: true,
|
||||||
|
locationLong: true,
|
||||||
|
locationAddress: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ActivityOtherDetails: {
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
exclusiveNotes: true,
|
||||||
|
SafetyInstruction: true,
|
||||||
|
Cancellations: true,
|
||||||
|
dosNotes: true,
|
||||||
|
dontsNotes: true,
|
||||||
|
tipsNotes: true,
|
||||||
|
termsAndCondition: true,
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!itineraryHeader) {
|
||||||
|
throw new ApiError(404, 'Itinerary not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const itineraryMember = itineraryHeader.ItineraryMembers[0] ?? null;
|
||||||
|
|
||||||
|
if (itineraryHeader.ItineraryActivities.length === 0) {
|
||||||
|
throw new ApiError(404, 'No activities found for this itinerary');
|
||||||
|
}
|
||||||
|
|
||||||
|
const activities = await Promise.all(
|
||||||
|
itineraryHeader.ItineraryActivities.map(async (itineraryActivity) => {
|
||||||
|
const itineraryDetail = itineraryActivity.ItineraryDetails[0] ?? null;
|
||||||
|
const activitySelection =
|
||||||
|
itineraryActivity.itineraryActivitySelections[0] ?? null;
|
||||||
|
const selectedFoodTypes = (
|
||||||
|
activitySelection?.selectedFoodTypes ?? []
|
||||||
|
).map(
|
||||||
|
(selectedFoodType) =>
|
||||||
|
selectedFoodType.activityFoodType.foodType.foodTypeName,
|
||||||
|
);
|
||||||
|
const selectedEquipments = (
|
||||||
|
activitySelection?.selectedEquipments ?? []
|
||||||
|
).map(
|
||||||
|
(selectedEquipment) =>
|
||||||
|
selectedEquipment.activityEquipment.equipmentName,
|
||||||
|
);
|
||||||
|
const selectedNavigationMode =
|
||||||
|
activitySelection?.activityNavigationMode?.navigationModeName ?? null;
|
||||||
|
const mediaWithUrls = await Promise.all(
|
||||||
|
(itineraryActivity.activity?.ActivitiesMedia ?? []).map(
|
||||||
|
async (media) => ({
|
||||||
|
...media,
|
||||||
|
mediaUrl: await attachPresignedUrl(media.mediaFileName),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const coverImage =
|
||||||
|
mediaWithUrls.find((media) => media.isCoverImage) ??
|
||||||
|
mediaWithUrls[0] ??
|
||||||
|
null;
|
||||||
|
const pickUpDetail =
|
||||||
|
itineraryActivity.activity?.ActivityPickUpDetails.find(
|
||||||
|
(detail) => detail.isPickUp && detail.locationAddress,
|
||||||
|
) ??
|
||||||
|
itineraryActivity.activity?.ActivityPickUpDetails.find(
|
||||||
|
(detail) => detail.locationAddress,
|
||||||
|
) ??
|
||||||
|
null;
|
||||||
|
const dropDetail =
|
||||||
|
itineraryActivity.activity?.ActivityPickUpDetails.find(
|
||||||
|
(detail) => !detail.isPickUp && detail.locationAddress,
|
||||||
|
) ?? null;
|
||||||
|
const bookingDate = formatDateKey(itineraryActivity.occurenceDate);
|
||||||
|
const ticketTime = formatTicketTimeRange(
|
||||||
|
itineraryActivity.startTime,
|
||||||
|
itineraryActivity.endTime,
|
||||||
|
);
|
||||||
|
const venueName =
|
||||||
|
itineraryActivity.venue?.venueLabel ??
|
||||||
|
itineraryActivity.venue?.venueName ??
|
||||||
|
null;
|
||||||
|
const foodIncluded = selectedFoodTypes.length > 0 ? 'Yes' : 'No';
|
||||||
|
const equipmentIncluded = selectedEquipments.length > 0 ? 'Yes' : 'No';
|
||||||
|
const pickAndDropIncluded = itineraryActivity.activity?.pickUpDropAvailable
|
||||||
|
? 'Yes'
|
||||||
|
: 'No';
|
||||||
|
const inActivityNavigation =
|
||||||
|
selectedNavigationMode ??
|
||||||
|
(activitySelection?.isInActivityNavigationOpted ? 'Yes' : 'No');
|
||||||
|
|
||||||
|
return {
|
||||||
|
itineraryActivityId: itineraryActivity.id,
|
||||||
|
bookingId: itineraryHeader.itineraryNo,
|
||||||
|
activityId: itineraryActivity.activity?.id ?? null,
|
||||||
|
activityTitle: itineraryActivity.activity?.activityTitle ?? null,
|
||||||
|
activityDescription:
|
||||||
|
itineraryActivity.activity?.activityDescription ?? null,
|
||||||
|
occurenceDate: itineraryActivity.occurenceDate,
|
||||||
|
startTime: itineraryActivity.startTime,
|
||||||
|
endTime: itineraryActivity.endTime,
|
||||||
|
duration: itineraryActivity.activity?.activityDurationMins ?? null,
|
||||||
|
checkInCode: itineraryDetail?.offlineCode ?? null,
|
||||||
|
qrCodeValue: itineraryDetail?.offlineCode ?? null,
|
||||||
|
checkInAddress: itineraryActivity.activity?.checkInAddress ?? null,
|
||||||
|
checkInLat: itineraryActivity.activity?.checkInLat ?? null,
|
||||||
|
checkInLong: itineraryActivity.activity?.checkInLong ?? null,
|
||||||
|
bookingStatus:
|
||||||
|
itineraryDetail?.activityStatus ??
|
||||||
|
itineraryActivity.bookingStatus ??
|
||||||
|
null,
|
||||||
|
description1: itineraryDetail?.description1 ?? null,
|
||||||
|
description2: itineraryDetail?.description2 ?? null,
|
||||||
|
itineraryStatus: itineraryDetail?.itineraryStatus ?? null,
|
||||||
|
isPaid: itineraryDetail?.isPaid ?? false,
|
||||||
|
paidOn: itineraryDetail?.paidOn ?? null,
|
||||||
|
paxCount: itineraryActivity.paxCount ?? null,
|
||||||
|
totalAmount: itineraryActivity.totalAmount ?? null,
|
||||||
|
venue: {
|
||||||
|
id: itineraryActivity.venue?.id ?? null,
|
||||||
|
venueName: itineraryActivity.venue?.venueName ?? null,
|
||||||
|
venueLabel: itineraryActivity.venue?.venueLabel ?? null,
|
||||||
|
displayName: venueName,
|
||||||
|
},
|
||||||
|
images: mediaWithUrls.map((media) => ({
|
||||||
|
id: media.id,
|
||||||
|
type: media.mediaType,
|
||||||
|
fileName: media.mediaFileName,
|
||||||
|
url: media.mediaUrl,
|
||||||
|
isCover: media.isCoverImage,
|
||||||
|
order: media.displayOrder,
|
||||||
|
})),
|
||||||
|
coverImage: coverImage
|
||||||
|
? {
|
||||||
|
id: coverImage.id,
|
||||||
|
fileName: coverImage.mediaFileName,
|
||||||
|
url: coverImage.mediaUrl,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
bookingInformation: {
|
||||||
|
activityName: itineraryActivity.activity?.activityTitle ?? null,
|
||||||
|
date: bookingDate,
|
||||||
|
venue: venueName,
|
||||||
|
venueName: itineraryActivity.venue?.venueName ?? null,
|
||||||
|
venueLabel: itineraryActivity.venue?.venueLabel ?? null,
|
||||||
|
time: ticketTime,
|
||||||
|
startTime: formatTicketTime(itineraryActivity.startTime),
|
||||||
|
endTime: formatTicketTime(itineraryActivity.endTime),
|
||||||
|
},
|
||||||
|
bookingIncluded: {
|
||||||
|
food: foodIncluded,
|
||||||
|
selectedFoodTypes,
|
||||||
|
equipment: equipmentIncluded,
|
||||||
|
selectedEquipments,
|
||||||
|
pickAndDrop: pickAndDropIncluded,
|
||||||
|
pickupLocation:
|
||||||
|
pickUpDetail?.locationAddress ??
|
||||||
|
itineraryActivity.activity?.checkInAddress ??
|
||||||
|
null,
|
||||||
|
dropLocation: dropDetail?.locationAddress ?? null,
|
||||||
|
inActivityNavigation: inActivityNavigation ?? 'No',
|
||||||
|
trainerOrGuide: activitySelection?.isTrainerOpted ? 'Yes' : 'No',
|
||||||
|
},
|
||||||
|
ticketCard: {
|
||||||
|
checkInCode: itineraryDetail?.offlineCode ?? null,
|
||||||
|
activityTitle: itineraryActivity.activity?.activityTitle ?? null,
|
||||||
|
date: bookingDate,
|
||||||
|
venue: venueName,
|
||||||
|
time: ticketTime,
|
||||||
|
food: foodIncluded,
|
||||||
|
equipments: equipmentIncluded,
|
||||||
|
pickAndDrop: pickAndDropIncluded,
|
||||||
|
inActivityNavigation: inActivityNavigation ?? 'No',
|
||||||
|
},
|
||||||
|
userSelections: {
|
||||||
|
isFoodOpted: activitySelection?.isFoodOpted ?? false,
|
||||||
|
selectedFoodTypes,
|
||||||
|
isTrainerOpted: activitySelection?.isTrainerOpted ?? false,
|
||||||
|
isInActivityNavigationOpted:
|
||||||
|
activitySelection?.isInActivityNavigationOpted ?? false,
|
||||||
|
selectedNavigationMode: selectedNavigationMode,
|
||||||
|
selectedEquipments,
|
||||||
|
},
|
||||||
|
pickupDetails: {
|
||||||
|
pickUpDropAvailable:
|
||||||
|
itineraryActivity.activity?.pickUpDropAvailable ?? false,
|
||||||
|
pickUpLocation: pickUpDetail
|
||||||
|
? {
|
||||||
|
id: pickUpDetail.id,
|
||||||
|
locationAddress: pickUpDetail.locationAddress,
|
||||||
|
locationLat: pickUpDetail.locationLat,
|
||||||
|
locationLong: pickUpDetail.locationLong,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
dropLocation: dropDetail
|
||||||
|
? {
|
||||||
|
id: dropDetail.id,
|
||||||
|
locationAddress: dropDetail.locationAddress,
|
||||||
|
locationLat: dropDetail.locationLat,
|
||||||
|
locationLong: dropDetail.locationLong,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
activityInfo: {
|
||||||
|
exclusiveNotes:
|
||||||
|
itineraryActivity.activity?.ActivityOtherDetails[0]
|
||||||
|
?.exclusiveNotes || null,
|
||||||
|
safetyInstructions:
|
||||||
|
itineraryActivity.activity?.ActivityOtherDetails[0]
|
||||||
|
?.SafetyInstruction || null,
|
||||||
|
cancellationPolicy:
|
||||||
|
itineraryActivity.activity?.ActivityOtherDetails[0]
|
||||||
|
?.Cancellations || null,
|
||||||
|
dosAndDonts: {
|
||||||
|
dos:
|
||||||
|
itineraryActivity.activity?.ActivityOtherDetails[0]?.dosNotes ||
|
||||||
|
null,
|
||||||
|
donts:
|
||||||
|
itineraryActivity.activity?.ActivityOtherDetails[0]
|
||||||
|
?.dontsNotes || null,
|
||||||
|
tips:
|
||||||
|
itineraryActivity.activity?.ActivityOtherDetails[0]?.tipsNotes ||
|
||||||
|
null,
|
||||||
|
},
|
||||||
|
termsAndConditions:
|
||||||
|
itineraryActivity.activity?.ActivityOtherDetails[0]
|
||||||
|
?.termsAndCondition || null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return itinerary object with activities inside
|
||||||
|
return {
|
||||||
|
itinerary: {
|
||||||
|
id: itineraryHeader.id,
|
||||||
|
itineraryNo: itineraryHeader.itineraryNo,
|
||||||
|
title: itineraryHeader.title,
|
||||||
|
status: itineraryHeader.itineraryStatus,
|
||||||
|
fromDate: itineraryHeader.fromDate,
|
||||||
|
fromTime: itineraryHeader.fromTime,
|
||||||
|
toDate: itineraryHeader.toDate,
|
||||||
|
toTime: itineraryHeader.toTime,
|
||||||
|
owner: {
|
||||||
|
id: itineraryHeader.owner?.id,
|
||||||
|
firstName: itineraryHeader.owner?.firstName,
|
||||||
|
lastName: itineraryHeader.owner?.lastName,
|
||||||
|
email: itineraryHeader.owner?.emailAddress,
|
||||||
|
phone: itineraryHeader.owner?.mobileNumber,
|
||||||
|
},
|
||||||
|
viewer: itineraryMember
|
||||||
|
? {
|
||||||
|
itineraryMemberXid: itineraryMember.id,
|
||||||
|
memberXid: itineraryMember.memberXid,
|
||||||
|
memberRole: itineraryMember.memberRole,
|
||||||
|
memberStatus: itineraryMember.memberStatus,
|
||||||
|
firstName: itineraryMember.member.firstName,
|
||||||
|
lastName: itineraryMember.member.lastName,
|
||||||
|
email: itineraryMember.member.emailAddress,
|
||||||
|
phone: itineraryMember.member.mobileNumber,
|
||||||
|
profileImage: itineraryMember.member.profileImage,
|
||||||
|
profileImagePresignedUrl: await attachPresignedUrl(
|
||||||
|
itineraryMember.member.profileImage,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
totalActivities: activities.length,
|
||||||
|
ticketCards: activities.map((activity) => activity.ticketCard),
|
||||||
|
activities,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user