Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1
This commit is contained in:
@@ -861,7 +861,7 @@ model Activities {
|
||||
frequency Frequencies? @relation(fields: [frequenciesXid], references: [id], onDelete: Restrict)
|
||||
activityRefNumber String? @map("activity_ref_number") @db.VarChar(30)
|
||||
activityTitle String? @map("activity_title") @db.VarChar(30)
|
||||
activityDescription String? @map("activity_description") @db.VarChar(80)
|
||||
activityDescription String? @map("activity_description") @db.VarChar(255)
|
||||
checkInLat Float? @map("check_in_lat")
|
||||
checkInLong Float? @map("check_in_long")
|
||||
checkInAddress String? @map("check_in_address") @db.VarChar(150)
|
||||
@@ -913,8 +913,6 @@ model Activities {
|
||||
ActivityEligibility ActivityEligibility[]
|
||||
ActivitySuggestions ActivitySuggestions[]
|
||||
ActivityAmDetails ActivityAmDetails[]
|
||||
ActivityPrices ActivityPrices[]
|
||||
ActivityVenueArtifacts ActivityVenueArtifacts[]
|
||||
ActivityPQQheader ActivityPQQheader[]
|
||||
ActivityAllowedEntry ActivityAllowedEntry[]
|
||||
ActivityFoodCost ActivityFoodCost[]
|
||||
@@ -922,7 +920,6 @@ model Activities {
|
||||
ActivityNavigationModes ActivityNavigationModes[]
|
||||
ActivityPickUpDetails ActivityPickUpDetails[]
|
||||
ActivityAmenities ActivityAmenities[]
|
||||
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
|
||||
ScheduleHeader ScheduleHeader[]
|
||||
ItineraryActivities ItineraryActivities[]
|
||||
activityTracks ActivityTrack[]
|
||||
@@ -938,7 +935,7 @@ model ActivityOtherDetails {
|
||||
id Int @id @default(autoincrement())
|
||||
activityXid Int @map("activity_xid")
|
||||
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
|
||||
exclusiveNotes String? @map("exclusive_notes") @db.VarChar(50)
|
||||
exclusiveNotes String? @map("exclusive_notes") @db.VarChar(500)
|
||||
dosNotes String? @map("dos_notes") @db.VarChar(200)
|
||||
dontsNotes String? @map("donts_notes") @db.VarChar(200)
|
||||
tipsNotes String? @map("tips_notes") @db.VarChar(100)
|
||||
@@ -1004,6 +1001,8 @@ model ActivityVenues {
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
ScheduleHeader ScheduleHeader[]
|
||||
ItineraryActivities ItineraryActivities[]
|
||||
ActivityPrices ActivityPrices[] // <-- Added opposite relation
|
||||
ActivityVenueArtifacts ActivityVenueArtifacts[] // <-- 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")
|
||||
@@ -1144,7 +1143,7 @@ model ActivityPriceTaxes {
|
||||
model ActivityVenueArtifacts {
|
||||
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)
|
||||
mediaType String @map("media_type") @db.VarChar(30)
|
||||
mediaFileName String @map("media_file_name") @db.VarChar(400)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
- '*/*'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/* ================= MEDIA ================= */
|
||||
export const MediaDto = z.object({
|
||||
mediaType: z.string().optional(),
|
||||
mediaFileName: z.string(),
|
||||
mediaType: z.string().optional(), // "image/jpeg", "video/mp4", etc.
|
||||
mediaFileName: z.string(), // S3 file URL
|
||||
});
|
||||
|
||||
/* ================= PRICE =================
|
||||
* ❌ No tax info here; 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),
|
||||
@@ -22,14 +27,20 @@ export const VenueDto = z.object({
|
||||
minPeopleRequired: z.number().int().nullable().optional(),
|
||||
minReqfullfilledBeforeMins: z.number().int().nullable().optional(),
|
||||
venueDescription: z.string().optional(),
|
||||
|
||||
// ✅ new: media per venue (for ActivityVenueArtifacts)
|
||||
media: z.array(MediaDto).optional().default([]),
|
||||
|
||||
// price list per venue
|
||||
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 +51,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 +59,27 @@ 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(),
|
||||
heightIn: z.string().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 +88,73 @@ 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 / CURRENCY */
|
||||
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),
|
||||
media: z.array(MediaDto).optional().default([]),
|
||||
venues: z.array(VenueDto).optional().default([]),
|
||||
|
||||
/* 🔥 ROOT-LEVEL TAX (SINGLE SOURCE OF TRUTH) */
|
||||
taxXids: z.array(z.number().int()).optional().default([]),
|
||||
|
||||
/* 🔥 MEDIA ARRAYS */
|
||||
media: z.array(MediaDto).optional().default([]), // Activity-level media
|
||||
venues: z.array(VenueDto).optional().default([]), // Each venue’s media + prices
|
||||
|
||||
/* RELATION ARRAYS */
|
||||
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([]),
|
||||
navigationModes: z.array(z.number().int()).optional().default([]),
|
||||
equipments: z.array(EquipmentDto).optional().default([]),
|
||||
amenitiesIds: z.array(z.number().int()).optional().default([]),
|
||||
|
||||
/* EXTRA OBJECTS */
|
||||
eligibility: EligibilityDto.optional(),
|
||||
otherDetails: OtherDetailsDto.optional(),
|
||||
});
|
||||
|
||||
@@ -1,64 +1,311 @@
|
||||
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',
|
||||
'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.',
|
||||
);
|
||||
}
|
||||
|
||||
const userInfo = await verifyHostToken(token);
|
||||
|
||||
/* 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');
|
||||
}
|
||||
|
||||
/* 3️⃣ BODY BUFFER */
|
||||
const bodyBuffer = event.isBase64Encoded
|
||||
? Buffer.from(event.body as string, 'base64')
|
||||
: Buffer.from(event.body as string);
|
||||
|
||||
const fields: Record<string, any> = {};
|
||||
const files: Array<{
|
||||
buffer: Buffer;
|
||||
mimeType: string;
|
||||
fileName: string;
|
||||
fieldName: string;
|
||||
}> = [];
|
||||
|
||||
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',
|
||||
'activityTypeXid',
|
||||
'frequenciesXid',
|
||||
'trainerTotalAmount',
|
||||
'pickupDropTotalPrice',
|
||||
'navigationModeTotalPrice',
|
||||
'sustainabilityScore',
|
||||
'safetyScore',
|
||||
'checkInLat',
|
||||
'checkInLong',
|
||||
'checkOutLat',
|
||||
'checkOutLong',
|
||||
];
|
||||
|
||||
for (const key of numberKeys) {
|
||||
if (activityPayload[key] !== undefined && activityPayload[key] !== null && activityPayload[key] !== '') {
|
||||
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 ACTIVITY-LEVEL MEDIA (images/videos) */
|
||||
const uploadedActivityMedia: Array<{ mediaType?: string; mediaFileName: string }> = [];
|
||||
|
||||
for (const file of files.filter(
|
||||
(f) => f.fieldName === 'activityImages' || f.fieldName === 'activityVideos',
|
||||
)) {
|
||||
const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Media/${Date.now()}_${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();
|
||||
|
||||
uploadedActivityMedia.push({
|
||||
mediaType: file.mimeType,
|
||||
mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
|
||||
});
|
||||
}
|
||||
|
||||
/* 🔥 MERGE ACTIVITY MEDIA */
|
||||
const existingMedia = Array.isArray(activityPayload.media)
|
||||
? activityPayload.media
|
||||
: [];
|
||||
activityPayload.media = [...existingMedia, ...uploadedActivityMedia];
|
||||
|
||||
/* 9️⃣ PROCESS VENUE MEDIA UPLOADS */
|
||||
// Group venue files by index: venueImages[0], venueImages[1], etc.
|
||||
const venueFilesMap: Map<number, Array<{ buffer: Buffer; mimeType: string; fileName: string }>> = new Map();
|
||||
|
||||
for (const file of files) {
|
||||
// Match patterns like: venueImages[0], venueVideos[1], etc.
|
||||
const match = file.fieldName.match(/^venue(Images|Videos)\[(\d+)\]$/);
|
||||
if (match) {
|
||||
const venueIndex = parseInt(match[2], 10);
|
||||
if (!venueFilesMap.has(venueIndex)) {
|
||||
venueFilesMap.set(venueIndex, []);
|
||||
}
|
||||
venueFilesMap.get(venueIndex)!.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload venue files and attach to corresponding venues
|
||||
if (Array.isArray(activityPayload.venues)) {
|
||||
for (let i = 0; i < activityPayload.venues.length; i++) {
|
||||
const venue = activityPayload.venues[i];
|
||||
const venueFiles = venueFilesMap.get(i) || [];
|
||||
|
||||
const uploadedVenueMedia: Array<{ mediaType?: string; mediaFileName: string }> = [];
|
||||
|
||||
for (const file of venueFiles) {
|
||||
const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Venue_${i}/Media/${Date.now()}_${file.fileName}`;
|
||||
|
||||
if (s3Key.length > 900) {
|
||||
throw new ApiError(400, 'Generated S3 key too long for venue media');
|
||||
}
|
||||
|
||||
await s3
|
||||
.upload({
|
||||
Bucket: config.aws.bucketName,
|
||||
Key: s3Key,
|
||||
Body: file.buffer,
|
||||
ContentType: file.mimeType,
|
||||
ACL: 'private',
|
||||
})
|
||||
.promise();
|
||||
|
||||
uploadedVenueMedia.push({
|
||||
mediaType: file.mimeType,
|
||||
mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Verify token and get user info
|
||||
const userInfo = await verifyHostToken(token);
|
||||
// Merge with existing venue media
|
||||
const existingVenueMedia = Array.isArray(venue.media) ? venue.media : [];
|
||||
venue.media = [...existingVenueMedia, ...uploadedVenueMedia];
|
||||
}
|
||||
}
|
||||
|
||||
let rawBody: any = {};
|
||||
try {
|
||||
rawBody = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (err) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
/* 🔟 VALIDATION */
|
||||
let parsedDto: CreateActivityInput;
|
||||
|
||||
// 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'));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Create full activity and related records
|
||||
const createdData = await hostService.createFullActivity(userInfo.id, dto as any);
|
||||
/* 1️⃣1️⃣ SAVE ACTIVITY */
|
||||
const createdActivity = await hostService.createOrUpdateActivity(
|
||||
userInfo.id,
|
||||
parsedDto,
|
||||
isDraft,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Activity created successfully',
|
||||
data: createdData
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
/* 1️⃣2️⃣ 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,
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user