upated createNewActivity handler
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,7 +913,6 @@ model Activities {
|
||||
ActivityEligibility ActivityEligibility[]
|
||||
ActivitySuggestions ActivitySuggestions[]
|
||||
ActivityAmDetails ActivityAmDetails[]
|
||||
ActivityVenueArtifacts ActivityVenueArtifacts[]
|
||||
ActivityPQQheader ActivityPQQheader[]
|
||||
ActivityAllowedEntry ActivityAllowedEntry[]
|
||||
ActivityFoodCost ActivityFoodCost[]
|
||||
@@ -936,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)
|
||||
@@ -1003,6 +1002,7 @@ model ActivityVenues {
|
||||
ScheduleHeader ScheduleHeader[]
|
||||
ItineraryActivities ItineraryActivities[]
|
||||
ActivityPrices ActivityPrices[] // <-- Added opposite relation
|
||||
ActivityVenueArtifacts ActivityVenueArtifacts[] // <-- Added opposite relation
|
||||
|
||||
@@map("activity_venues")
|
||||
@@schema("act")
|
||||
@@ -1143,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")
|
||||
|
||||
@@ -2,12 +2,12 @@ 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 HERE (tax is root-level only)
|
||||
* ❌ No tax info here; root-level only
|
||||
*/
|
||||
export const PriceDto = z.object({
|
||||
noOfSession: z.number().int().optional().default(1),
|
||||
@@ -15,7 +15,7 @@ export const PriceDto = z.object({
|
||||
sessionValidity: z.number().int().optional().default(0),
|
||||
sessionValidityFrequency: z.string().optional().default('Days'),
|
||||
basePrice: z.number().int().optional().default(0),
|
||||
sellPrice: z.number().int(), // REQUIRED
|
||||
sellPrice: z.number().int(), // required
|
||||
});
|
||||
|
||||
/* ================= VENUE ================= */
|
||||
@@ -27,6 +27,11 @@ 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([]),
|
||||
});
|
||||
|
||||
@@ -69,6 +74,7 @@ export const EligibilityDto = z.object({
|
||||
isHeightRestriction: z.boolean().optional().default(false),
|
||||
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(),
|
||||
});
|
||||
@@ -127,7 +133,7 @@ export const CreateActivityDto = z.object({
|
||||
|
||||
cancellationAvailable: z.boolean().optional().default(false),
|
||||
|
||||
/* MONEY */
|
||||
/* MONEY / CURRENCY */
|
||||
currencyXid: z.number().int().nullable().optional(),
|
||||
sustainabilityScore: z.number().int().nullable().optional(),
|
||||
safetyScore: z.number().int().nullable().optional(),
|
||||
@@ -136,21 +142,19 @@ export const CreateActivityDto = z.object({
|
||||
/* 🔥 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([]),
|
||||
/* 🔥 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([]),
|
||||
|
||||
/* 🔥 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([]),
|
||||
|
||||
/* EXTRA OBJECTS */
|
||||
eligibility: EligibilityDto.optional(),
|
||||
otherDetails: OtherDetailsDto.optional(),
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ function getExtensionFromMime(mimeType: string) {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/webp': 'webp',
|
||||
// ✅ Common video formats
|
||||
'video/mp4': 'mp4',
|
||||
'video/quicktime': 'mov',
|
||||
'video/x-msvideo': 'avi',
|
||||
@@ -141,13 +140,21 @@ export const handler = safeHandler(
|
||||
'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) {
|
||||
if (activityPayload[key] !== undefined && activityPayload[key] !== null && activityPayload[key] !== '') {
|
||||
activityPayload[key] = Number(activityPayload[key]);
|
||||
}
|
||||
}
|
||||
@@ -175,17 +182,13 @@ export const handler = safeHandler(
|
||||
if (activityPayload[key] === 'false') activityPayload[key] = false;
|
||||
}
|
||||
|
||||
/* 8️⃣ UPLOAD MEDIA */
|
||||
const uploadedMedia: Array<{ mediaType?: string; mediaFileName: string }> =
|
||||
[];
|
||||
/* 8️⃣ UPLOAD ACTIVITY-LEVEL MEDIA (images/videos) */
|
||||
const uploadedActivityMedia: 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',
|
||||
(f) => f.fieldName === 'activityImages' || f.fieldName === 'activityVideos',
|
||||
)) {
|
||||
const ext = getExtensionFromMime(file.mimeType);
|
||||
|
||||
const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Artifacts/${file.fileName}`;
|
||||
const s3Key = `ActivityOnboarding/Activity_${activityPayload.activityXid}/Media/${Date.now()}_${file.fileName}`;
|
||||
|
||||
if (s3Key.length > 900) {
|
||||
throw new ApiError(400, 'Generated S3 key too long');
|
||||
@@ -201,20 +204,72 @@ export const handler = safeHandler(
|
||||
})
|
||||
.promise();
|
||||
|
||||
uploadedMedia.push({
|
||||
uploadedActivityMedia.push({
|
||||
mediaType: file.mimeType,
|
||||
mediaFileName: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`,
|
||||
});
|
||||
}
|
||||
|
||||
/* 🔥 MERGE MEDIA (DO NOT OVERWRITE) */
|
||||
/* 🔥 MERGE ACTIVITY MEDIA */
|
||||
const existingMedia = Array.isArray(activityPayload.media)
|
||||
? activityPayload.media
|
||||
: [];
|
||||
activityPayload.media = [...existingMedia, ...uploadedActivityMedia];
|
||||
|
||||
activityPayload.media = [...existingMedia, ...uploadedMedia];
|
||||
/* 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();
|
||||
|
||||
/* 9️⃣ VALIDATION */
|
||||
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}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Merge with existing venue media
|
||||
const existingVenueMedia = Array.isArray(venue.media) ? venue.media : [];
|
||||
venue.media = [...existingVenueMedia, ...uploadedVenueMedia];
|
||||
}
|
||||
}
|
||||
|
||||
/* 🔟 VALIDATION */
|
||||
let parsedDto: CreateActivityInput;
|
||||
|
||||
if (!isDraft) {
|
||||
@@ -230,14 +285,14 @@ export const handler = safeHandler(
|
||||
parsedDto = activityPayload as CreateActivityInput;
|
||||
}
|
||||
|
||||
/* 🔟 SAVE ACTIVITY */
|
||||
/* 1️⃣1️⃣ SAVE ACTIVITY */
|
||||
const createdActivity = await hostService.createOrUpdateActivity(
|
||||
userInfo.id,
|
||||
parsedDto,
|
||||
isDraft,
|
||||
);
|
||||
|
||||
/* 1️⃣1️⃣ RESPONSE */
|
||||
/* 1️⃣2️⃣ RESPONSE */
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
@@ -253,4 +308,4 @@ export const handler = safeHandler(
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user