creat activity handler

This commit is contained in:
paritosh18
2025-12-21 17:28:08 +05:30
parent 2e4f318684
commit 5a223f126f
8 changed files with 769 additions and 382 deletions

View File

@@ -913,7 +913,6 @@ model Activities {
ActivityEligibility ActivityEligibility[]
ActivitySuggestions ActivitySuggestions[]
ActivityAmDetails ActivityAmDetails[]
ActivityPrices ActivityPrices[]
ActivityVenueArtifacts ActivityVenueArtifacts[]
ActivityPQQheader ActivityPQQheader[]
ActivityAllowedEntry ActivityAllowedEntry[]
@@ -922,7 +921,6 @@ model Activities {
ActivityNavigationModes ActivityNavigationModes[]
ActivityPickUpDetails ActivityPickUpDetails[]
ActivityAmenities ActivityAmenities[]
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
ScheduleHeader ScheduleHeader[]
ItineraryActivities ItineraryActivities[]
activityTracks ActivityTrack[]
@@ -1004,6 +1002,7 @@ model ActivityVenues {
deletedAt DateTime? @map("deleted_at")
ScheduleHeader ScheduleHeader[]
ItineraryActivities ItineraryActivities[]
ActivityPrices ActivityPrices[] // <-- Added opposite relation
@@map("activity_venues")
@@schema("act")
@@ -1107,7 +1106,7 @@ model ActivityAmDetails {
model ActivityPrices {
id Int @id @default(autoincrement())
activityVenueXid Int @map("activity_venue_xid")
activityVenue Activities @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade)
activityVenue ActivityVenues @relation(fields: [activityVenueXid], references: [id], onDelete: Cascade)
noOfSession Int @map("no_of_session")
isPackage Boolean @default(false) @map("is_package")
sessionValidity Int @map("session_validity")
@@ -1302,6 +1301,7 @@ model ActivityEquipments {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
@@map("activity_equipments")
@@schema("act")
@@ -1310,7 +1310,7 @@ model ActivityEquipments {
model ActivityEquipmentTaxes {
id Int @id @default(autoincrement())
activityEquipmentXid Int @map("activity_equipment_xid")
activityEquipment Activities @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade)
activityEquipment ActivityEquipments @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade)
taxXid Int @map("tax_xid")
taxes Taxes @relation(fields: [taxXid], references: [id], onDelete: Restrict)
taxPer Float @map("tax_per")

View File

@@ -21,7 +21,8 @@ provider:
# Reference the layer defined in this stack using CloudFormation Ref
layers:
# Use the exported stack output so deploy function works (expects a string ARN)
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn}
# For offline/local, fall back to an empty string so the CF lookup is optional.
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn, ''}
apiGateway:
binaryMediaTypes:
- '*/*'

View File

@@ -177,6 +177,23 @@ prePopulateNewActivity:
path: /host/Activity_Hub/OnBoarding/prepopulate-new-activity
method: get
createNewActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.handler
memorySize: 1024
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/CreateNewActivity.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/create-new-activity
method: patch
showSuggestion:
handler: src/modules/host/handlers/Host_Admin/onboarding/showSuggestion.handler
memorySize: 384

View File

@@ -1,73 +1,100 @@
export const HOST_STATUS_INTERNAL = {
HOST_SUBMITTED: "Host Submitted",
HOST_TO_UPDATE: "Host To Update",
REJECTED: "Rejected",
APPROVED: "Approved",
DRAFT: "Draft",
}
HOST_SUBMITTED: 'Host Submitted',
HOST_TO_UPDATE: 'Host To Update',
REJECTED: 'Rejected',
APPROVED: 'Approved',
DRAFT: 'Draft',
};
export const HOST_STATUS_DISPLAY = {
DRAFT: "Draft",
UNDER_REVIEW: "Under Review",
ENHANCING: "Enhancing",
REJECTED: "Rejected",
APPROVED: "Approved",
}
DRAFT: 'Draft',
UNDER_REVIEW: 'Under Review',
ENHANCING: 'Enhancing',
REJECTED: 'Rejected',
APPROVED: 'Approved',
};
export const STEPPER = {
NOT_SUBMITTED: 1,
UNDER_REVIEW: 2,
COMPANY_DETAILS_APPROVED: 3,
BANK_DETAILS_UPDATED: 4,
AGREEMENT_ACCEPTED: 5,
REJECTED: 6
}
NOT_SUBMITTED: 1,
UNDER_REVIEW: 2,
COMPANY_DETAILS_APPROVED: 3,
BANK_DETAILS_UPDATED: 4,
AGREEMENT_ACCEPTED: 5,
REJECTED: 6,
};
export const ACTIVITY_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
PQ_TO_UPDATE: 'PQ To Update',
PQ_SUBMITTED: 'PQ Submitted',
PQ_APPROVED: 'PQ Approved'
}
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
PQ_TO_UPDATE: 'PQ To Update',
PQ_SUBMITTED: 'PQ Submitted',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_SUBMITTED: 'Activity Submitted',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_REJECTED: 'Activity Rejected',
ACTIVITY_APPROVED: 'Activity Approved',
ACTIVITY_LISTED: 'Activity Listed',
ACTIVITY_UNLISTED: 'Activity Un Listed By Host',
};
export const ACTIVITY_DISPLAY_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing',
PQ_IN_REVIEW: 'PQ In Review',
PQ_APPROVED: 'PQ Approved'
}
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing',
PQ_IN_REVIEW: 'PQ In Review',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_IN_REVIEW: 'In Review',
ACTIVITY_TO_REVIEW: 'To Review',
ACTIVITY_NOT_LISTED: 'Not Listed',
ACTIVITY_LISTED: 'Listed',
ACTIVITY_UNLISTED: 'Un Listed',
};
export const ACTIVITY_AM_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
PQ_REJECTED: 'PQ Rejected',
PQ_TO_REVIEW: 'PQ To Review',
PQ_APPROVED: 'PQ Approved'
}
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
PQ_REJECTED: 'PQ Rejected',
PQ_TO_REVIEW: 'PQ To Review',
PQ_APPROVED: 'PQ Approved',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_TO_REVIEW: 'Activity To Review',
ACTIVITY_REJECTED: 'Activity Rejected',
ACTIVITY_APPROVED: 'Activity Approved',
ACTIVITY_LISTED: 'Activity Listed',
};
export const ACTIVITY_AM_DISPLAY_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing',
NEW: 'New',
PQ_APPROVED: 'PQ Approved',
REVISED: 'Revised'
}
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQ_FAILED: 'PQ Failed',
ENHANCING: 'Enchancing',
NEW: 'New',
PQ_APPROVED: 'PQ Approved',
REVISED: 'Revised',
ACTIVITY_DRAFT: 'Draft - Activity',
ACTIVITY_NEW: 'To Review',
ACTIVITY_ENHANCING: 'Enhancing',
ACTIVITY_NOT_LISTED: 'Not Listed',
ACTIVITY_LISTED: 'Listed',
};

View File

@@ -34,7 +34,10 @@ export const ACTIVITY_TRACK_STATUS = {
REJECTED_BY_AM: 'Rejected By AM',
ACCEPTED_BY_AM: 'Accepted By AM',
ENHANCING: 'Enhancing',
PQ_SUBMITTED: 'PQ Submitted'
PQ_SUBMITTED: 'PQ Submitted',
UNDER_REVIEW:'Under Review',
SUBMITTED:'Activity Submitted',
DRAFT:'Activity Draft'
}
// export const HOST_SUGGESTION_TITLES = {

View File

@@ -1,19 +1,24 @@
import { z } from 'zod';
/* ================= MEDIA ================= */
export const MediaDto = z.object({
mediaType: z.string().optional(),
mediaFileName: z.string(),
});
/* ================= PRICE =================
* ❌ NO TAX HERE (tax is root-level only)
*/
export const PriceDto = z.object({
noOfSession: z.number().int().optional().default(1),
isPackage: z.boolean().optional().default(false),
sessionValidity: z.number().int().optional().default(0),
sessionValidityFrequency: z.string().optional().default('Days'),
basePrice: z.number().int().optional().default(0),
sellPrice: z.number().int().optional().default(0),
sellPrice: z.number().int(), // REQUIRED
});
/* ================= VENUE ================= */
export const VenueDto = z.object({
venueName: z.string(),
venueCapacity: z.number().int().optional().default(0),
@@ -25,11 +30,12 @@ export const VenueDto = z.object({
prices: z.array(PriceDto).optional().default([]),
});
/* ================= PICKUP / DROP ================= */
export const PickupDetailDto = z.object({
isPickUp: z.boolean().optional().default(false),
locationLat: z.number().optional().nullable(),
locationLong: z.number().optional().nullable(),
locationAddress: z.string().optional().nullable(),
locationLat: z.number().nullable().optional(),
locationLong: z.number().nullable().optional(),
locationAddress: z.string().nullable().optional(),
transportBasePrice: z.number().int().optional().default(0),
transportTotalPrice: z.number().int().optional().default(0),
});
@@ -40,13 +46,7 @@ export const PickupTransportDto = z.object({
pickupDetails: z.array(PickupDetailDto).optional().default([]),
});
export const NavigationModeDto = z.object({
navigationModeXid: z.number().int(),
isInActivityChargeable: z.boolean().optional().default(false),
basePrice: z.number().int().optional().default(0),
totalPrice: z.number().int().optional().default(0),
});
/* ================= EQUIPMENT ================= */
export const EquipmentDto = z.object({
equipmentName: z.string(),
isEquipmentChargeable: z.boolean().optional().default(false),
@@ -54,22 +54,26 @@ export const EquipmentDto = z.object({
equipmentTotalPrice: z.number().int().optional().default(0),
});
/* ================= ELIGIBILITY ================= */
export const EligibilityDto = z.object({
isAgeRestriction: z.boolean().optional().default(false),
ageRestrictionXid: z.number().int().nullable().optional(),
isWeightRestriction: z.boolean().optional().default(false),
weightRestrictionName: z.string().optional().nullable(),
weightRestrictionName: z.string().nullable().optional(),
weightEntered: z.number().int().nullable().optional(),
weightIn: z.string().optional().nullable(),
weightIn: z.string().nullable().optional(),
minWeight: z.number().int().nullable().optional(),
maxWeight: z.number().int().nullable().optional(),
isHeightRestriction: z.boolean().optional().default(false),
heightRestrictionName: z.string().optional().nullable(),
heightRestrictionName: z.string().nullable().optional(),
heightEntered: z.number().int().nullable().optional(),
minHeight: z.number().int().nullable().optional(),
maxHeight: z.number().int().nullable().optional(),
});
/* ================= OTHER DETAILS ================= */
export const OtherDetailsDto = z.object({
exclusiveNotes: z.string().optional(),
dosNotes: z.string().optional(),
@@ -78,46 +82,75 @@ export const OtherDetailsDto = z.object({
termsAndCondition: z.string().optional(),
});
/* ================= CREATE ACTIVITY ================= */
export const CreateActivityDto = z.object({
activityTypeXid: z.number().int(),
/* 🔑 REQUIRED */
activityXid: z.number().int(),
/* OPTIONAL CORE */
activityTypeXid: z.number().int().optional(),
frequenciesXid: z.number().int().nullable().optional(),
activityTitle: z.string().optional(),
activityDescription: z.string().optional(),
checkInLat: z.number().optional().nullable(),
checkInLong: z.number().optional().nullable(),
checkInAddress: z.string().optional().nullable(),
/* LOCATION */
checkInLat: z.number().nullable().optional(),
checkInLong: z.number().nullable().optional(),
checkInAddress: z.string().nullable().optional(),
isCheckOutSame: z.boolean().optional().default(true),
checkOutLat: z.number().optional().nullable(),
checkOutLong: z.number().optional().nullable(),
checkOutAddress: z.string().optional().nullable(),
checkOutLat: z.number().nullable().optional(),
checkOutLong: z.number().nullable().optional(),
checkOutAddress: z.string().nullable().optional(),
/* DURATION / ENERGY */
energyLevelXid: z.number().int().nullable().optional(),
activityDurationMins: z.number().int().nullable().optional(),
durationHours: z.number().int().optional(),
durationMins: z.number().int().optional(),
/* FLAGS */
foodAvailable: z.boolean().optional().default(false),
foodIsChargeable: z.boolean().optional().default(false),
alcoholAvailable: z.boolean().optional().default(false),
trainerAvailable: z.boolean().optional().default(false),
trainerIsChargeable: z.boolean().optional().default(false),
pickUpDropAvailable: z.boolean().optional().default(false),
pickUpDropIsChargeable: z.boolean().optional().default(false),
inActivityAvailable: z.boolean().optional().default(false),
inActivityIsChargeable: z.boolean().optional().default(false),
equipmentAvailable: z.boolean().optional().default(false),
equipmentIsChargeable: z.boolean().optional().default(false),
cancellationAvailable: z.boolean().optional().default(false),
/* MONEY */
currencyXid: z.number().int().nullable().optional(),
sustainabilityScore: z.number().int().optional().nullable(),
safetyScore: z.number().int().optional().nullable(),
sustainabilityScore: z.number().int().nullable().optional(),
safetyScore: z.number().int().nullable().optional(),
isInstantBooking: z.boolean().optional().default(false),
/* 🔥 ROOT-LEVEL TAX (SINGLE SOURCE OF TRUTH) */
taxXids: z.array(z.number().int()).optional().default([]),
/* ARRAYS */
media: z.array(MediaDto).optional().default([]),
venues: z.array(VenueDto).optional().default([]),
foodTypeIds: z.array(z.number().int()).optional().default([]),
cuisineIds: z.array(z.number().int()).optional().default([]),
pickupTransports: z.array(PickupTransportDto).optional().default([]),
navigationModes: z.array(NavigationModeDto).optional().default([]),
/* 🔥 NAVIGATION = IDs ONLY */
navigationModes: z.array(z.number().int()).optional().default([]),
equipments: z.array(EquipmentDto).optional().default([]),
amenitiesIds: z.array(z.number().int()).optional().default([]),
eligibility: EligibilityDto.optional(),
otherDetails: OtherDetailsDto.optional(),
});

View File

@@ -1,64 +1,256 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
import { CreateActivityDto } from '../../../dto/createActivity.schema';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import {
CreateActivityDto,
CreateActivityInput,
} from '../../../dto/createActivity.schema';
import { HostService } from '../../../services/host.service';
const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region });
/* ------------------------------- Utilities ------------------------------- */
function getExtensionFromMime(mimeType: string) {
const map: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/webp': 'webp',
// ✅ Common video formats
'video/mp4': 'mp4',
'video/quicktime': 'mov',
'video/x-msvideo': 'avi',
'video/x-matroska': 'mkv',
};
return map[mimeType] || 'bin';
}
function normalizeJsonField(fields: any, key: string) {
if (!fields[key]) return undefined;
if (typeof fields[key] === 'object') return fields[key];
try {
return JSON.parse(fields[key]);
} catch {
throw new ApiError(400, `Invalid JSON in field: ${key}`);
}
}
/* -------------------------------- Handler -------------------------------- */
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.',
);
}
async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
/* 1⃣ AUTH */
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 verifyHostToken(token);
const userInfo = await verifyHostToken(token);
let rawBody: any = {};
try {
rawBody = event.body ? JSON.parse(event.body) : {};
} catch (err) {
throw new ApiError(400, 'Invalid JSON in request body');
}
/* 2⃣ CONTENT TYPE */
const contentType =
event.headers['content-type'] || event.headers['Content-Type'];
if (!contentType?.includes('multipart/form-data')) {
throw new ApiError(400, 'Content-Type must be multipart/form-data');
}
// Validate request body with Zod DTO
let dto: any;
try {
dto = CreateActivityDto.parse(rawBody);
} catch (err: any) {
throw new ApiError(400, 'Invalid payload: ' + (err?.message || 'validation failed'));
}
/* 3⃣ BODY BUFFER */
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body as string, 'base64')
: Buffer.from(event.body as string);
// Create full activity and related records
const createdData = await hostService.createFullActivity(userInfo.id, dto as any);
const fields: Record<string, any> = {};
const files: Array<{
buffer: Buffer;
mimeType: string;
fileName: string;
fieldName: string;
}> = [];
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Activity created successfully',
data: createdData
}),
};
},
await new Promise<void>((resolve, reject) => {
const bb = Busboy({
headers: {
...event.headers,
'content-type': contentType,
},
});
bb.on('field', (name, value) => {
fields[name] = value;
});
bb.on('file', (fieldName, file, info) => {
const { filename, mimeType } = info;
const chunks: Buffer[] = [];
let size = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on('data', (chunk) => {
size += chunk.length;
if (size > MAX_SIZE) {
file.destroy(new Error('File exceeds 5MB limit'));
return;
}
chunks.push(chunk);
});
file.on('end', () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType: mimeType || 'application/octet-stream',
fileName: filename || 'unknown',
fieldName,
});
}
});
});
bb.on('finish', () => resolve());
bb.on('error', (err) => reject(new ApiError(400, err.message)));
bb.end(bodyBuffer);
});
/* 4⃣ FLAGS */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
/* 5⃣ ACTIVITY PAYLOAD */
const activityPayload: any = normalizeJsonField(fields, 'activity');
if (!activityPayload) {
throw new ApiError(400, 'activity payload is required');
}
/* 6⃣ NORMALIZE IDS */
if (activityPayload.activityXid) {
activityPayload.activityXid = Number(activityPayload.activityXid);
}
const numberKeys = [
'currencyXid',
'energyLevelXid',
'activityDurationMins',
'trainerTotalAmount',
'pickupDropTotalPrice',
'navigationModeTotalPrice',
];
for (const key of numberKeys) {
if (activityPayload[key] !== undefined && activityPayload[key] !== null) {
activityPayload[key] = Number(activityPayload[key]);
}
}
/* 7⃣ NORMALIZE BOOLEANS */
const booleanKeys = [
'isInstantBooking',
'foodAvailable',
'foodIsChargeable',
'alcoholAvailable',
'trainerAvailable',
'trainerIsChargeable',
'pickUpDropAvailable',
'pickUpDropIsChargeable',
'inActivityAvailable',
'inActivityIsChargeable',
'equipmentAvailable',
'equipmentIsChargeable',
'cancellationAvailable',
'isCheckOutSame',
];
for (const key of booleanKeys) {
if (activityPayload[key] === 'true') activityPayload[key] = true;
if (activityPayload[key] === 'false') activityPayload[key] = false;
}
/* 8⃣ UPLOAD MEDIA */
const uploadedMedia: Array<{ mediaType?: string; mediaFileName: string }> =
[];
// ✅ Accept both images and videos under multipart fields `images` or `videos`
for (const file of files.filter(
(f) => f.fieldName === 'images' || f.fieldName === 'videos',
)) {
const ext = getExtensionFromMime(file.mimeType);
const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Artifacts/${file.fileName}`;
if (s3Key.length > 900) {
throw new ApiError(400, 'Generated S3 key too long');
}
await s3
.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: file.buffer,
ContentType: file.mimeType,
ACL: 'private',
})
.promise();
uploadedMedia.push({
mediaType: file.mimeType,
mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
});
}
/* 🔥 MERGE MEDIA (DO NOT OVERWRITE) */
const existingMedia = Array.isArray(activityPayload.media)
? activityPayload.media
: [];
activityPayload.media = [...existingMedia, ...uploadedMedia];
/* 9⃣ VALIDATION */
let parsedDto: CreateActivityInput;
if (!isDraft) {
const parsed = CreateActivityDto.safeParse(activityPayload);
if (!parsed.success) {
throw new ApiError(
400,
parsed.error.issues.map((i) => i.message).join(', '),
);
}
parsedDto = parsed.data;
} else {
parsedDto = activityPayload as CreateActivityInput;
}
/* 🔟 SAVE ACTIVITY */
const createdActivity = await hostService.createOrUpdateActivity(
userInfo.id,
parsedDto,
isDraft,
);
/* 1⃣1⃣ RESPONSE */
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: isDraft
? 'Activity saved as draft successfully'
: 'Activity created successfully',
data: createdActivity,
}),
};
},
);

View File

@@ -68,6 +68,30 @@ function round2(value: number) {
return Math.round(value);
}
function computeBasePriceAndTaxes(
sellPrice: number,
taxes: Array<{ id: number; taxPer: number }>,
) {
if (!taxes?.length) {
return {
basePrice: round2(sellPrice),
taxDetails: [] as Array<{ taxXid: number; taxPer: number; taxAmount: number }>,
};
}
const totalTaxPer = taxes.reduce((sum, t) => sum + (Number(t.taxPer) || 0), 0);
const denominator = 1 + totalTaxPer / 100;
const basePrice = denominator > 0 ? round2(sellPrice / denominator) : round2(sellPrice);
const taxDetails = taxes.map((t) => ({
taxXid: t.id,
taxPer: t.taxPer,
taxAmount: round2(basePrice * (t.taxPer / 100)),
}));
return { basePrice, taxDetails };
}
const bucket = config.aws.bucketName;
@Injectable()
@@ -1872,276 +1896,366 @@ export class HostService {
* This method will create Activities + ActivityOtherDetails + ActivitiesMedia +
* ActivityVenues + ActivityPrices + ActivityFoodTypes + ActivityCuisine +
* ActivityPickUpTransport/Details + ActivityNavigationModes + ActivityEquipments +
* ActivityAmenities + ActivityEligibility and also seed PQQ headers.
* ActivityAmenities + ActivityEligibility
*/
async createFullActivity(userId: number, payload: import('../dto/createActivity.schema').CreateActivityInput) {
return await this.prisma.$transaction(async (tx) => {
const host = await tx.hostHeader.findFirst({ where: { userXid: userId, isActive: true } });
if (!host) throw new ApiError(404, 'Host not found for the user');
async createOrUpdateActivity(
userId: number,
payload: any,
isDraft: boolean,
) {
/* =====================================================
* HELPERS
* ===================================================== */
const toBool = (v: any) =>
v === true || v === 'true' || v === 1 || v === '1';
const activityTypeXid = payload.activityTypeXid;
if (!activityTypeXid) throw new ApiError(400, 'activityTypeXid is required');
const toNumber = (v: any) =>
v === undefined || v === null || v === '' ? undefined : Number(v);
const activityType = await tx.activityTypes.findUnique({ where: { id: Number(activityTypeXid) } });
if (!activityType) throw new ApiError(404, 'Activity type not found');
const round2 = (v: number) => Math.round(v);
const frequenciesXid = payload.frequenciesXid ?? null;
if (frequenciesXid) {
const freq = await tx.frequencies.findUnique({ where: { id: Number(frequenciesXid) } });
if (!freq) throw new ApiError(404, 'Frequency not found');
}
const computeBasePriceAndTaxes = (
sellPrice: number,
taxes: Array<{ id: number; taxPer: number }>,
) => {
if (!taxes.length) {
return { basePrice: round2(sellPrice), taxDetails: [] };
}
const referenceNumber = await generateActivityRefNumber(tx);
const totalTaxPer = taxes.reduce((s, t) => s + Number(t.taxPer || 0), 0);
const basePrice = round2(sellPrice / (1 + totalTaxPer / 100));
const activityData: any = {
return {
basePrice,
taxDetails: taxes.map((t) => ({
taxXid: t.id,
taxPer: t.taxPer,
taxAmount: round2(basePrice * (t.taxPer / 100)),
})),
};
};
/* =====================================================
* BASIC GUARDS
* ===================================================== */
if (!payload.activityXid) {
throw new ApiError(400, 'activityXid is required');
}
/* =====================================================
* HARD NORMALIZATION (SERVICE-LEVEL)
* ===================================================== */
payload.foodAvailable = toBool(payload.foodAvailable);
payload.alcoholAvailable = toBool(payload.alcoholAvailable);
payload.trainerAvailable = toBool(payload.trainerAvailable);
payload.pickUpDropAvailable = toBool(payload.pickUpDropAvailable);
payload.inActivityAvailable = toBool(payload.inActivityAvailable);
payload.equipmentAvailable = toBool(payload.equipmentAvailable);
payload.cancellationAvailable = toBool(payload.cancellationAvailable);
payload.isInstantBooking = toBool(payload.isInstantBooking);
payload.isCheckOutSame = toBool(payload.isCheckOutSame);
payload.trainerTotalAmount = toNumber(payload.trainerTotalAmount);
if (payload.trainerAvailable) {
if (
typeof payload.trainerTotalAmount !== 'number' ||
Number.isNaN(payload.trainerTotalAmount) ||
payload.trainerTotalAmount <= 0
) {
throw new ApiError(400, 'trainerTotalAmount must be > 0');
}
} else {
delete payload.trainerTotalAmount;
}
if (payload.venues && !Array.isArray(payload.venues)) {
throw new ApiError(400, 'venues must be an array');
}
payload.venues?.forEach((v, idx) => {
v.isMinPeopleReqMandatory = toBool(v.isMinPeopleReqMandatory);
if (!v.venueName) {
throw new ApiError(400, `venues[${idx}] venueName required`);
}
if (v.isMinPeopleReqMandatory && !v.minPeopleRequired) {
throw new ApiError(
400,
`venues[${idx}] min people requirement missing`,
);
}
if (!Array.isArray(v.prices) || !v.prices.length) {
throw new ApiError(400, `venues[${idx}] must have at least one price`);
}
});
/* =====================================================
* ROOT TAX
* ===================================================== */
const taxIds = Array.isArray(payload.taxXids)
? payload.taxXids.map(Number)
: [];
const rootTaxes =
taxIds.length > 0
? await this.prisma.taxes.findMany({
where: { id: { in: taxIds }, isActive: true },
select: { id: true, taxPer: true },
})
: [];
if (taxIds.length !== rootTaxes.length) {
throw new ApiError(400, 'Invalid or inactive tax provided');
}
/* =====================================================
* TRANSACTION
* ===================================================== */
return await this.prisma.$transaction(async (tx) => {
/* --------------------------------
* 1⃣ HOST
* -------------------------------- */
const host = await tx.hostHeader.findFirst({
where: { userXid: userId, isActive: true },
});
if (!host) throw new ApiError(404, 'Host not found');
/* --------------------------------
* 2⃣ ACTIVITY
* -------------------------------- */
const existingActivity = await tx.activities.findFirst({
where: {
id: Number(payload.activityXid),
hostXid: host.id,
activityTypeXid: Number(activityTypeXid),
frequenciesXid: frequenciesXid ? Number(frequenciesXid) : null,
activityRefNumber: referenceNumber,
};
isActive: true,
},
});
if (!existingActivity) {
throw new ApiError(404, 'Activity not found');
}
// Simple mappings
if (payload.activityTitle) activityData.activityTitle = String(payload.activityTitle).substring(0, 30);
if (payload.activityDescription) activityData.activityDescription = String(payload.activityDescription).substring(0, 80);
if (payload.checkInLat !== undefined && payload.checkInLat !== null) activityData.checkInLat = Number(payload.checkInLat);
if (payload.checkInLong !== undefined && payload.checkInLong !== null) activityData.checkInLong = Number(payload.checkInLong);
if (payload.checkInAddress) activityData.checkInAddress = String(payload.checkInAddress).substring(0,150);
if (payload.isCheckOutSame !== undefined) activityData.isCheckOutSame = Boolean(payload.isCheckOutSame);
if (payload.checkOutLat !== undefined && payload.checkOutLat !== null) activityData.checkOutLat = Number(payload.checkOutLat);
if (payload.checkOutLong !== undefined && payload.checkOutLong !== null) activityData.checkOutLong = Number(payload.checkOutLong);
if (payload.checkOutAddress) activityData.checkOutAddress = String(payload.checkOutAddress).substring(0,150);
if (payload.energyLevelXid) activityData.energyLevelXid = Number(payload.energyLevelXid);
/* --------------------------------
* 3⃣ STATUS DECISION (YOUR LOGIC)
* -------------------------------- */
let activityInternalStatus;
let activityDisplayStatus;
let amInternalStatus;
let amDisplayStatus;
// duration: accept minutes or hours+minutes
if (payload.activityDurationMins) activityData.activityDurationMins = Number(payload.activityDurationMins);
else if (payload.durationHours || payload.durationMins) {
const hrs = Number(payload.durationHours || 0);
const mins = Number(payload.durationMins || 0);
activityData.activityDurationMins = hrs * 60 + mins;
const wasRejected =
existingActivity.activityInternalStatus ===
ACTIVITY_INTERNAL_STATUS.ACTIVITY_REJECTED;
if (wasRejected) {
if (isDraft) {
activityInternalStatus =
existingActivity.activityInternalStatus;
activityDisplayStatus =
existingActivity.activityDisplayStatus;
amInternalStatus =
existingActivity.amInternalStatus;
amDisplayStatus =
existingActivity.amDisplayStatus;
} else {
activityInternalStatus =
ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED;
activityDisplayStatus =
ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW;
amInternalStatus =
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW;
amDisplayStatus =
ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW;
}
} else {
if (isDraft) {
activityInternalStatus =
ACTIVITY_INTERNAL_STATUS.ACTIVITY_DRAFT;
activityDisplayStatus =
ACTIVITY_DISPLAY_STATUS.ACTIVITY_DRAFT;
amInternalStatus =
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_DRAFT;
amDisplayStatus =
ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_DRAFT;
} else {
activityInternalStatus =
ACTIVITY_INTERNAL_STATUS.ACTIVITY_SUBMITTED;
activityDisplayStatus =
ACTIVITY_DISPLAY_STATUS.ACTIVITY_IN_REVIEW;
amInternalStatus =
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_TO_REVIEW;
amDisplayStatus =
ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_NEW;
}
}
// Booleans (assign explicitly for type-safety)
if (payload.foodAvailable !== undefined) activityData.foodAvailable = payload.foodAvailable;
if (payload.foodIsChargeable !== undefined) activityData.foodIsChargeable = payload.foodIsChargeable;
if (payload.alcoholAvailable !== undefined) activityData.alcoholAvailable = payload.alcoholAvailable;
if (payload.trainerAvailable !== undefined) activityData.trainerAvailable = payload.trainerAvailable;
if (payload.trainerIsChargeable !== undefined) activityData.trainerIsChargeable = payload.trainerIsChargeable;
if (payload.pickUpDropAvailable !== undefined) activityData.pickUpDropAvailable = payload.pickUpDropAvailable;
if (payload.pickUpDropIsChargeable !== undefined) activityData.pickUpDropIsChargeable = payload.pickUpDropIsChargeable;
if (payload.inActivityAvailable !== undefined) activityData.inActivityAvailable = payload.inActivityAvailable;
if (payload.inActivityIsChargeable !== undefined) activityData.inActivityIsChargeable = payload.inActivityIsChargeable;
if (payload.equipmentAvailable !== undefined) activityData.equipmentAvailable = payload.equipmentAvailable;
if (payload.equipmentIsChargeable !== undefined) activityData.equipmentIsChargeable = payload.equipmentIsChargeable;
if (payload.cancellationAvailable !== undefined) activityData.cancellationAvailable = payload.cancellationAvailable;
if (payload.isInstantBooking !== undefined) activityData.isInstantBooking = payload.isInstantBooking;
/* --------------------------------
* 4⃣ UPDATE ACTIVITY CORE + FLAGS
* -------------------------------- */
const activity = await tx.activities.update({
where: { id: existingActivity.id },
data: {
activityTitle: payload.activityTitle ?? undefined,
activityDescription: payload.activityDescription ?? undefined,
currencyXid: payload.currencyXid ?? undefined,
isInstantBooking: payload.isInstantBooking ?? undefined,
if (payload.currencyXid) activityData.currencyXid = Number(payload.currencyXid);
if (payload.sustainabilityScore !== undefined && payload.sustainabilityScore !== null) activityData.sustainabilityScore = Number(payload.sustainabilityScore);
if (payload.safetyScore !== undefined && payload.safetyScore !== null) activityData.safetyScore = Number(payload.safetyScore);
foodAvailable: payload.foodAvailable,
alcoholAvailable: payload.alcoholAvailable,
trainerAvailable: payload.trainerAvailable,
pickUpDropAvailable: payload.pickUpDropAvailable,
inActivityAvailable: payload.inActivityAvailable,
equipmentAvailable: payload.equipmentAvailable,
cancellationAvailable: payload.cancellationAvailable,
isCheckOutSame: payload.isCheckOutSame,
// Create activity
const createdActivity = await tx.activities.create({ data: activityData });
activityInternalStatus,
activityDisplayStatus,
amInternalStatus,
amDisplayStatus,
},
});
// Other details (nested DTO)
const od = (payload as any).otherDetails;
if (od && (od.exclusiveNotes || od.dosNotes || od.dontsNotes || od.tipsNotes || od.termsAndCondition)) {
await tx.activityOtherDetails.create({
data: {
activityXid: createdActivity.id,
exclusiveNotes: od.exclusiveNotes ? String(od.exclusiveNotes).substring(0,50) : null,
dosNotes: od.dosNotes ? String(od.dosNotes).substring(0,200) : null,
dontsNotes: od.dontsNotes ? String(od.dontsNotes).substring(0,200) : null,
tipsNotes: od.tipsNotes ? String(od.tipsNotes).substring(0,100) : null,
termsAndCondition: od.termsAndCondition ? String(od.termsAndCondition).substring(0,500) : null,
}
const activityXid = activity.id;
/* --------------------------------
* 5⃣ CLEAN OLD VENUES
* -------------------------------- */
const oldVenueIds = (
await tx.activityVenues.findMany({
where: { activityXid },
select: { id: true },
})
).map(v => v.id);
if (oldVenueIds.length) {
const priceIds = (
await tx.activityPrices.findMany({
where: { activityVenueXid: { in: oldVenueIds } },
select: { id: true },
})
).map(p => p.id);
if (priceIds.length) {
await tx.activityPriceTaxes.deleteMany({
where: { activityPriceXid: { in: priceIds } },
});
await tx.activityPrices.deleteMany({
where: { id: { in: priceIds } },
});
}
// Media
if (Array.isArray(payload.media) && payload.media.length) {
const mediaData = payload.media.map((m: any, idx: number) => ({
activityXid: createdActivity.id,
mediaType: String(m.mediaType || 'image'),
mediaFileName: String(m.mediaFileName),
displayOrder: idx + 1,
}));
await tx.activitiesMedia.createMany({ data: mediaData });
}
await tx.activityVenues.deleteMany({
where: { id: { in: oldVenueIds } },
});
}
// Venues + Prices
if (Array.isArray(payload.venues) && payload.venues.length) {
for (const venue of payload.venues) {
const v = await tx.activityVenues.create({
data: {
activityXid: createdActivity.id,
venueName: String(venue.venueName).substring(0,50),
venueCapacity: venue.venueCapacity ? Number(venue.venueCapacity) : 0,
availableSeats: venue.availableSeats ? Number(venue.availableSeats) : 0,
isMinPeopleReqMandatory: Boolean(venue.isMinPeopleReqMandatory || false),
minPeopleRequired: venue.minPeopleRequired ? Number(venue.minPeopleRequired) : null,
minReqfullfilledBeforeMins: venue.minReqfullfilledBeforeMins ? Number(venue.minReqfullfilledBeforeMins) : null,
venueDescription: venue.venueDescription ? String(venue.venueDescription).substring(0,200) : null,
}
});
if (Array.isArray(venue.prices) && venue.prices.length) {
const pricesData = venue.prices.map((p: any) => ({
activityVenueXid: v.id,
noOfSession: Number(p.noOfSession || 1),
isPackage: Boolean(p.isPackage || false),
sessionValidity: Number(p.sessionValidity || 0),
sessionValidityFrequency: String(p.sessionValidityFrequency || 'Days'),
basePrice: Number(p.basePrice || 0),
sellPrice: Number(p.sellPrice || 0),
}));
await tx.activityPrices.createMany({ data: pricesData });
}
}
}
// Food types
if (Array.isArray(payload.foodTypeIds) && payload.foodTypeIds.length) {
const ft = payload.foodTypeIds.map((id: any) => ({ activityXid: createdActivity.id, foodTypeXid: Number(id) }));
await tx.activityFoodTypes.createMany({ data: ft });
}
// Cuisines
if (Array.isArray(payload.cuisineIds) && payload.cuisineIds.length) {
const cs = payload.cuisineIds.map((id: any) => ({ activityXid: createdActivity.id, foodCuisineXid: Number(id) }));
await tx.activityCuisine.createMany({ data: cs });
}
// Pick up transport + details
if (Array.isArray(payload.pickupTransports) && payload.pickupTransports.length) {
for (const pt of payload.pickupTransports) {
const transport = await tx.activityPickUpTransport.create({
data: {
activityXid: createdActivity.id,
transportModeXid: Number(pt.transportModeXid),
isTransportModeChargeable: Boolean(pt.isTransportModeChargeable || false),
}
});
if (Array.isArray(pt.pickupDetails) && pt.pickupDetails.length) {
const pd = pt.pickupDetails.map((d: any) => ({
activityPickUpTransportXid: transport.id,
isPickUp: Boolean(d.isPickUp || false),
locationLat: d.locationLat ? Number(d.locationLat) : null,
locationLong: d.locationLong ? Number(d.locationLong) : null,
locationAddress: d.locationAddress ? String(d.locationAddress).substring(0,150) : null,
transportBasePrice: d.transportBasePrice ? Number(d.transportBasePrice) : 0,
transportTotalPrice: d.transportTotalPrice ? Number(d.transportTotalPrice) : 0,
}));
await tx.activityPickUpDetails.createMany({ data: pd });
}
}
}
// Navigation modes
if (Array.isArray(payload.navigationModes) && payload.navigationModes.length) {
const navs = payload.navigationModes.map((n: any) => ({
activityXid: createdActivity.id,
navigationModeXid: Number(n.navigationModeXid),
isInActivityChargeable: Boolean(n.isInActivityChargeable || false),
navigationModesBasePrice: Number(n.basePrice || 0),
navigationModesTotalPrice: Number(n.totalPrice || 0),
}));
await tx.activityNavigationModes.createMany({ data: navs });
}
// Equipments
if (Array.isArray(payload.equipments) && payload.equipments.length) {
const eqs = payload.equipments.map((e: any) => ({
activityXid: createdActivity.id,
equipmentName: String(e.equipmentName).substring(0,30),
isEquipmentChargeable: Boolean(e.isEquipmentChargeable || false),
equipmentBasePrice: Number(e.equipmentBasePrice || 0),
equipmentTotalPrice: Number(e.equipmentTotalPrice || 0),
}));
await tx.activityEquipments.createMany({ data: eqs });
}
// Amenities
if (Array.isArray(payload.amenitiesIds) && payload.amenitiesIds.length) {
const ams = payload.amenitiesIds.map((id: any) => ({ activityXid: createdActivity.id, amenitiesXid: Number(id) }));
await tx.activityAmenities.createMany({ data: ams });
}
// Eligibility
if (payload.eligibility) {
const e = payload.eligibility;
await tx.activityEligibility.create({
data: {
activityXid: createdActivity.id,
isAgeRestriction: Boolean(e.isAgeRestriction || false),
ageRestrictionXid: e.ageRestrictionXid ? Number(e.ageRestrictionXid) : null,
isWeightRestriction: Boolean(e.isWeightRestriction || false),
weightRestrictionName: e.weightRestrictionName ? String(e.weightRestrictionName).substring(0,30) : null,
weightEntered: e.weightEntered ? Number(e.weightEntered) : null,
weightIn: e.weightIn ? String(e.weightIn).substring(0,30) : null,
minWeight: e.minWeight ? Number(e.minWeight) : null,
maxWeight: e.maxWeight ? Number(e.maxWeight) : null,
isHeightRestriction: Boolean(e.isHeightRestriction || false),
heightRestrictionName: e.heightRestrictionName ? String(e.heightRestrictionName).substring(0,30) : null,
heightEntered: e.heightEntered ? Number(e.heightEntered) : null,
minHeight: e.minHeight ? Number(e.minHeight) : null,
maxHeight: e.maxHeight ? Number(e.maxHeight) : null,
}
});
}
// ----------------- PQQ seeding (copy of existing logic) -----------------
const questions = await tx.pQQCategories.findMany({
where: { isActive: true },
select: {
id: true,
categoryName: true,
displayOrder: true,
pqqsubCategories: {
where: { isActive: true },
select: {
id: true,
subCategoryName: true,
displayOrder: true,
questions: {
where: { isActive: true },
select: {
id: true,
questionName: true,
maxPoints: true,
displayOrder: true,
},
orderBy: { displayOrder: 'asc' },
},
},
orderBy: { displayOrder: 'asc' },
},
/* --------------------------------
* 6⃣ CREATE VENUES (MULTIPLE)
* -------------------------------- */
for (const venue of payload.venues ?? []) {
const venueRow = await tx.activityVenues.create({
data: {
activityXid,
venueName: venue.venueName,
venueCapacity: venue.venueCapacity ?? 0,
availableSeats: venue.availableSeats ?? 0,
isMinPeopleReqMandatory: venue.isMinPeopleReqMandatory,
minPeopleRequired: venue.minPeopleRequired ?? null,
minReqfullfilledBeforeMins:
venue.minReqfullfilledBeforeMins ?? null,
},
orderBy: { displayOrder: 'asc' },
});
const allQuestions: number[] = [];
for (const cat of questions) {
for (const sub of cat.pqqsubCategories) {
for (const q of sub.questions) {
allQuestions.push(q.id);
}
for (const price of venue.prices) {
const sellPrice = Number(price.sellPrice);
const { basePrice, taxDetails } =
computeBasePriceAndTaxes(sellPrice, rootTaxes);
const priceRow = await tx.activityPrices.create({
data: {
activityVenueXid: venueRow.id,
noOfSession: price.noOfSession ?? 1,
isPackage: price.isPackage ?? false,
sessionValidity: price.sessionValidity ?? 0,
sessionValidityFrequency:
price.sessionValidityFrequency ?? 'Days',
basePrice,
sellPrice,
},
});
if (taxDetails.length) {
await tx.activityPriceTaxes.createMany({
data: taxDetails.map(t => ({
activityPriceXid: priceRow.id,
taxXid: t.taxXid,
taxPer: t.taxPer,
taxAmount: t.taxAmount,
})),
});
}
}
}
if (allQuestions.length) {
await tx.activityPQQheader.createMany({
data: allQuestions.map((id) => ({
activityXid: createdActivity.id,
pqqQuestionXid: id,
pqqAnswerXid: null,
})),
/* --------------------------------
* 7⃣ TRAINER
* -------------------------------- */
if (payload.trainerAvailable) {
const { basePrice, taxDetails } =
computeBasePriceAndTaxes(
payload.trainerTotalAmount,
rootTaxes,
);
const trainer = await tx.activityTrainers.create({
data: {
activityXid,
baseAmount: basePrice,
totalAmount: payload.trainerTotalAmount,
},
});
for (const t of taxDetails) {
await tx.activityTrainerTaxes.create({
data: {
activityTrainerXid: trainer.id,
taxXid: t.taxXid,
taxPer: t.taxPer,
taxAmount: t.taxAmount,
},
});
}
}
return { activity_xid: createdActivity.id };
/* --------------------------------
* 8⃣ ACTIVITY TRACK
* -------------------------------- */
await tx.activityTrack.create({
data: {
activityXid,
trackType: 'ACTIVITY',
trackStatus: activityInternalStatus,
updatedByXid: userId,
updatedByRole: ROLE_NAME.HOST,
updatedOn: new Date(),
},
});
}
/* --------------------------------
* 9⃣ RESPONSE
* -------------------------------- */
return {
activityXid,
activityRefNumber: activity.activityRefNumber,
status: isDraft
? 'ACTIVITY_SAVED_AS_DRAFT'
: 'ACTIVITY_SUBMITTED',
};
});
}
async getAllPQUpdatedResponse(activityXid: number) {