4 Commits

5 changed files with 349 additions and 65 deletions

View File

@@ -1,5 +1,5 @@
# Legacy monolith config. For new deployments use serverless.*.yml files. # Legacy monolith config. For new deployments use serverless.*.yml files.
service: minglarDev service: minglar
useDotenv: true useDotenv: true

View File

@@ -13,6 +13,40 @@ const hostService = new HostService(prismaClient);
const s3 = new AWS.S3({ region: config.aws.region }); const s3 = new AWS.S3({ region: config.aws.region });
function parseMultipartFieldValue(val: string) {
if (val === '' || val === 'null' || val === 'undefined') return null;
const cleaned = val.trim();
const looksLikeJson =
(cleaned.startsWith('{') && cleaned.endsWith('}')) ||
(cleaned.startsWith('[') && cleaned.endsWith(']')) ||
(cleaned.startsWith('"') && cleaned.endsWith('"'));
if (!looksLikeJson) return val;
try {
return JSON.parse(cleaned);
} catch {
return val;
}
}
function normalizeComments(comments: unknown): string | null {
if (comments === null || comments === undefined || comments === '') return null;
const value = String(comments).trim();
if (!value) return null;
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
return value.slice(1, -1);
}
return value;
}
// Function to extract S3 key from URL // Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string { function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`; const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
@@ -122,22 +156,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
bb.on("field", (fieldname, val) => { bb.on("field", (fieldname, val) => {
console.log(`FIELD RAW: ${fieldname} =`, val); console.log(`FIELD RAW: ${fieldname} =`, val);
if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null; fields[fieldname] = parseMultipartFieldValue(val);
else {
try {
const cleaned = val.trim();
// If it starts and ends with quotes, remove them
const withoutQuotes =
(cleaned.startsWith('"') && cleaned.endsWith('"'))
? cleaned.slice(1, -1)
: cleaned;
fields[fieldname] = JSON.parse(withoutQuotes);
} catch {
fields[fieldname] = val;
}
}
}); });
bb.on("close", () => resolve()); bb.on("close", () => resolve());
@@ -154,7 +173,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const activityXid = Number(fields.activityXid); const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid); const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid); const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = fields.comments || null; const comments = normalizeComments(fields.comments);
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Please provide a valid activity"); if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Please provide a valid activity");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Please select a valid question"); if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Please select a valid question");

View File

@@ -142,6 +142,10 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const deletedFiles = normalizeJsonField(fields, "deletedFiles") || []; const deletedFiles = normalizeJsonField(fields, "deletedFiles") || [];
const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || []; const parentDeletedFiles = normalizeJsonField(fields, "parentDeletedFiles") || [];
const deleteCompanyLogo =
fields.deleteCompanyLogo === 'true' || fields.deleteCompanyLogo === true;
const deleteParentCompanyLogo =
fields.deleteParentCompanyLogo === 'true' || fields.deleteParentCompanyLogo === true;
/** 4) Extract and clean isDraft flag */ /** 4) Extract and clean isDraft flag */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true; const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
@@ -379,6 +383,63 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}); });
} }
/** DELETE EXISTING LOGO IF REQUESTED */
if (deleteCompanyLogo) {
const existingHost = await prismaClient.hostHeader.findFirst({
where: { userXid: userInfo.id },
select: { logoPath: true },
});
if (existingHost?.logoPath) {
try {
const s3Key = getS3KeyFromUrl(existingHost.logoPath);
await deleteFromS3(s3Key);
} catch (e) {
console.error('S3 delete failed for company logo:', existingHost.logoPath, e);
}
}
parsedCompany.logoPath = null;
}
/** DELETE EXISTING PARENT COMPANY LOGO IF REQUESTED */
if (deleteParentCompanyLogo && parsedCompany.isSubsidairy) {
const existingHost = await prismaClient.hostHeader.findFirst({
where: { userXid: userInfo.id },
select: {
id: true,
hostParent: {
select: {
id: true,
logoPath: true,
},
take: 1,
},
},
});
const existingParent = Array.isArray(existingHost?.hostParent)
? existingHost.hostParent[0]
: existingHost?.hostParent;
if (existingParent?.logoPath) {
try {
const s3Key = getS3KeyFromUrl(existingParent.logoPath);
await deleteFromS3(s3Key);
} catch (e) {
console.error('S3 delete failed for parent company logo:', existingParent.logoPath, e);
}
}
if (parsedParentCompany) {
parsedParentCompany.logoPath = null;
} else {
parsedParentCompany = {
logoPath: null,
};
}
}
/** UPLOAD LOGO (if provided) */ /** UPLOAD LOGO (if provided) */
const logoFile = files.find( const logoFile = files.find(
(f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile' (f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
@@ -449,6 +510,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
parsedParentCompany, parsedParentCompany,
uploadedParentDocs, uploadedParentDocs,
isDraft, isDraft,
{ deleteCompanyLogo, deleteParentCompanyLogo },
); );
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.'); if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');

View File

@@ -518,6 +518,7 @@ export class HostService {
select: { select: {
id: true, id: true,
emailAddress: true, emailAddress: true,
dateOfBirth: true,
firstName: true, firstName: true,
lastName: true, lastName: true,
mobileNumber: true, mobileNumber: true,
@@ -1390,6 +1391,10 @@ export class HostService {
parentCompanyData?: any | null, parentCompanyData?: any | null,
parentDocuments?: HostDocumentInput[], parentDocuments?: HostDocumentInput[],
isDraft: boolean = false, isDraft: boolean = false,
options?: {
deleteCompanyLogo?: boolean;
deleteParentCompanyLogo?: boolean;
},
) { ) {
return await this.prisma.$transaction(async (tx) => { return await this.prisma.$transaction(async (tx) => {
// Check if host already has a company // Check if host already has a company
@@ -1650,7 +1655,9 @@ export class HostService {
? { connect: { id: Number(companyData.countryXid) } } ? { connect: { id: Number(companyData.countryXid) } }
: undefined, : undefined,
pinCode: companyData.pinCode, pinCode: companyData.pinCode,
logoPath: companyData.logoPath || existingHostCompany.logoPath, logoPath: options?.deleteCompanyLogo
? companyData.logoPath ?? null
: companyData.logoPath || existingHostCompany.logoPath,
isSubsidairy: companyData.isSubsidairy, isSubsidairy: companyData.isSubsidairy,
registrationNumber: companyData.registrationNumber, registrationNumber: companyData.registrationNumber,
panNumber: companyData.panNumber, panNumber: companyData.panNumber,
@@ -1791,10 +1798,11 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } } ? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined, : undefined,
pinCode: parentCompanyData.pinCode || null, pinCode: parentCompanyData.pinCode || null,
logoPath: logoPath: options?.deleteParentCompanyLogo
parentCompanyData?.logoPath || ? parentCompanyData?.logoPath ?? null
existingParentCompany?.logoPath || : parentCompanyData?.logoPath ||
null, existingParentCompany?.logoPath ||
null,
registrationNumber: parentCompanyData.registrationNumber || null, registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null, panNumber: parentCompanyData.panNumber || null,
gstNumber: parentCompanyData.gstNumber || null, gstNumber: parentCompanyData.gstNumber || null,
@@ -1849,10 +1857,11 @@ export class HostService {
? { connect: { id: Number(parentCompanyData.countryXid) } } ? { connect: { id: Number(parentCompanyData.countryXid) } }
: undefined, : undefined,
pinCode: parentCompanyData.pinCode || null, pinCode: parentCompanyData.pinCode || null,
logoPath: logoPath: options?.deleteParentCompanyLogo
parentCompanyData?.logoPath || ? parentCompanyData?.logoPath ?? null
existingParentCompany?.logoPath || : parentCompanyData?.logoPath ||
null, existingParentCompany?.logoPath ||
null,
registrationNumber: parentCompanyData.registrationNumber || null, registrationNumber: parentCompanyData.registrationNumber || null,
panNumber: parentCompanyData.panNumber || null, panNumber: parentCompanyData.panNumber || null,
gstNumber: parentCompanyData.gstNumber || null, gstNumber: parentCompanyData.gstNumber || null,

View File

@@ -232,7 +232,7 @@ const getUniqueDatesForScheduleDetail = (
@Injectable() @Injectable()
export class ItineraryService { export class ItineraryService {
constructor(private prisma: PrismaClient) {} constructor(private prisma: PrismaClient) { }
async getUserItineraryDetails(userXid: number) { async getUserItineraryDetails(userXid: number) {
const [userLocation, activityEntries, travellerType, energyLevel] = await Promise.all([ const [userLocation, activityEntries, travellerType, energyLevel] = await Promise.all([
@@ -398,15 +398,15 @@ export class ItineraryService {
), ),
energyLevel: entry.Activities?.activityType?.energyLevel energyLevel: entry.Activities?.activityType?.energyLevel
? { ? {
id: entry.Activities.activityType.energyLevel.id, id: entry.Activities.activityType.energyLevel.id,
energyLevelName: energyLevelName:
entry.Activities.activityType.energyLevel.energyLevelName, entry.Activities.activityType.energyLevel.energyLevelName,
energyIcon: energyIcon:
entry.Activities.activityType.energyLevel.energyIcon, entry.Activities.activityType.energyLevel.energyIcon,
energyIconPresignedUrl: await attachPresignedUrl( energyIconPresignedUrl: await attachPresignedUrl(
entry.Activities.activityType.energyLevel.energyIcon, entry.Activities.activityType.energyLevel.energyIcon,
), ),
} }
: null, : null,
media: await attachMediaWithPresignedUrl( media: await attachMediaWithPresignedUrl(
entry.Activities?.ActivitiesMedia ?? [], entry.Activities?.ActivitiesMedia ?? [],
@@ -640,9 +640,9 @@ export class ItineraryService {
const slotEnd = scheduleHeader.activity.activityDurationMins const slotEnd = scheduleHeader.activity.activityDurationMins
? addMinutes( ? addMinutes(
slotStart, slotStart,
scheduleHeader.activity.activityDurationMins, scheduleHeader.activity.activityDurationMins,
) )
: combineDateAndTime(slotDate, slot.endTime); : combineDateAndTime(slotDate, slot.endTime);
if (!slotEnd) { if (!slotEnd) {
@@ -665,7 +665,7 @@ export class ItineraryService {
if ( if (
activityItem.occurenceDate && activityItem.occurenceDate &&
formatDateKey(slotDate) !== formatDateKey(slotDate) !==
formatDateKey(parseDateValue(activityItem.occurenceDate)) formatDateKey(parseDateValue(activityItem.occurenceDate))
) { ) {
return null; return null;
} }
@@ -879,6 +879,97 @@ export class ItineraryService {
checkInAddress: true, checkInAddress: true,
checkInLat: true, checkInLat: true,
checkInLong: true, checkInLong: true,
foodAvailable: true,
foodIsChargeable: true,
trainerAvailable: true,
trainerIsChargeable: true,
inActivityAvailable: true,
inActivityIsChargeable: true,
pickUpDropAvailable: true,
pickUpDropIsChargeable: true,
activityFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
ActivityFoodCost: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
baseAmount: true,
totalAmount: true,
},
},
ActivityTrainers: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
baseAmount: true,
totalAmount: true,
},
},
ActivityNavigationModes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
ActivityPickUpDetails: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
isPickUp: true,
locationLat: true,
locationLong: true,
locationAddress: true,
transportBasePrice: true,
transportTotalPrice: true,
},
},
activityPickUpTransports: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
transportModeXid: true,
transportMode: {
select: {
id: true,
transportModeName: true,
transportModeIcon: true,
},
},
},
},
ActivitiesMedia: { ActivitiesMedia: {
where: { where: {
isActive: true, isActive: true,
@@ -921,9 +1012,8 @@ export class ItineraryService {
const formattedItineraries = await Promise.all( const formattedItineraries = await Promise.all(
itineraries.map(async (itinerary) => { itineraries.map(async (itinerary) => {
const ownerFullName = `${itinerary.owner.firstName ?? ''} ${ const ownerFullName = `${itinerary.owner.firstName ?? ''} ${itinerary.owner.lastName ?? ''
itinerary.owner.lastName ?? '' }`.trim();
}`.trim();
const members = await Promise.all( const members = await Promise.all(
itinerary.ItineraryMembers.map(async (member) => ({ itinerary.ItineraryMembers.map(async (member) => ({
@@ -932,9 +1022,8 @@ export class ItineraryService {
memberRole: member.memberRole, memberRole: member.memberRole,
memberStatus: member.memberStatus, memberStatus: member.memberStatus,
invitedByXid: member.invitedByXid, invitedByXid: member.invitedByXid,
fullName: `${member.member.firstName ?? ''} ${ fullName: `${member.member.firstName ?? ''} ${member.member.lastName ?? ''
member.member.lastName ?? '' }`.trim(),
}`.trim(),
firstName: member.member.firstName, firstName: member.member.firstName,
lastName: member.member.lastName, lastName: member.member.lastName,
profileImage: member.member.profileImage, profileImage: member.member.profileImage,
@@ -985,6 +1074,80 @@ export class ItineraryService {
media: await attachMediaWithPresignedUrl( media: await attachMediaWithPresignedUrl(
item.activity.ActivitiesMedia, item.activity.ActivitiesMedia,
), ),
foodDetails: {
foodAvailable: item.activity.foodAvailable,
foodIsChargeable: item.activity.foodIsChargeable,
foodTypes: item.activity.activityFoodTypes.map((foodType) => ({
id: foodType.id,
foodTypeXid: foodType.foodTypeXid,
foodType: foodType.foodType,
})),
foodCost: item.activity.ActivityFoodCost.map((foodCost) => ({
id: foodCost.id,
baseAmount: foodCost.baseAmount,
totalAmount: foodCost.totalAmount,
})),
},
trainerDetails: {
trainerAvailable: item.activity.trainerAvailable,
trainerIsChargeable: item.activity.trainerIsChargeable,
trainerCost: item.activity.ActivityTrainers.map((trainer) => ({
id: trainer.id,
baseAmount: trainer.baseAmount,
totalAmount: trainer.totalAmount,
})),
},
navigationDetails: {
inActivityAvailable: item.activity.inActivityAvailable,
inActivityIsChargeable: item.activity.inActivityIsChargeable,
navigationModes: item.activity.ActivityNavigationModes.map(
(navigationMode) => ({
id: navigationMode.id,
navigationModeName: navigationMode.navigationModeName,
isInActivityChargeable:
navigationMode.isInActivityChargeable,
navigationModesBasePrice:
navigationMode.navigationModesBasePrice,
navigationModesTotalPrice:
navigationMode.navigationModesTotalPrice,
}),
),
},
pickUpDetails: {
pickUpDropAvailable: item.activity.pickUpDropAvailable,
pickUpDropIsChargeable:
item.activity.pickUpDropIsChargeable,
details: item.activity.ActivityPickUpDetails.map(
(pickUpDetail) => ({
id: pickUpDetail.id,
isPickUp: pickUpDetail.isPickUp,
locationLat: pickUpDetail.locationLat,
locationLong: pickUpDetail.locationLong,
locationAddress: pickUpDetail.locationAddress,
transportBasePrice: pickUpDetail.transportBasePrice,
transportTotalPrice: pickUpDetail.transportTotalPrice,
}),
),
transportModes: await Promise.all(
item.activity.activityPickUpTransports.map(
async (transport) => ({
id: transport.id,
transportModeXid: transport.transportModeXid,
transportMode: {
id: transport.transportMode.id,
transportModeName:
transport.transportMode.transportModeName,
transportModeIcon:
transport.transportMode.transportModeIcon,
transportModeIconPresignedUrl:
await attachPresignedUrl(
transport.transportMode.transportModeIcon,
),
},
}),
),
),
},
}, },
venue: item.venue, venue: item.venue,
scheduleHeader: item.scheduledHeader, scheduleHeader: item.scheduledHeader,
@@ -1150,6 +1313,7 @@ export class ItineraryService {
id: true, id: true,
activityTitle: true, activityTitle: true,
activityDurationMins: true, activityDurationMins: true,
activityDescription: true,
checkInLat: true, checkInLat: true,
checkInLong: true, checkInLong: true,
checkInAddress: true, checkInAddress: true,
@@ -1246,6 +1410,17 @@ export class ItineraryService {
venueName: true, venueName: true,
venueLabel: true, venueLabel: true,
venueCapacity: true, venueCapacity: true,
ActivityVenueArtifacts: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
mediaType: true,
mediaFileName: true,
},
},
availableSeats: true, availableSeats: true,
ScheduleHeader: { ScheduleHeader: {
where: { where: {
@@ -1411,6 +1586,8 @@ export class ItineraryService {
venueXid: venue.id, venueXid: venue.id,
venueName: venue.venueName, venueName: venue.venueName,
venueLabel: venue.venueLabel, venueLabel: venue.venueLabel,
mediaFileName: venue.ActivityVenueArtifacts[0]?.mediaFileName ?? null,
mediaType: venue.ActivityVenueArtifacts[0]?.mediaType ?? null,
venueCapacity: venue.venueCapacity, venueCapacity: venue.venueCapacity,
availableSeats: venue.availableSeats, availableSeats: venue.availableSeats,
slotDate: formatDateKey(slotDate), slotDate: formatDateKey(slotDate),
@@ -1430,11 +1607,27 @@ export class ItineraryService {
}), }),
); );
if (!availableSlots.length) { const sanitizedAvailableSlots = availableSlots.filter(
(
slot,
): slot is NonNullable<(typeof availableSlots)[number]> =>
Boolean(slot),
);
const availableSlotsWithPresignedUrl = await Promise.all(
sanitizedAvailableSlots.map(async (slot) => ({
...slot,
mediaFileNamePresignedUrl: await attachPresignedUrl(
slot.mediaFileName,
),
})),
);
if (!availableSlotsWithPresignedUrl.length) {
return null; return null;
} }
availableSlots.sort( availableSlotsWithPresignedUrl.sort(
(first, second) => (first, second) =>
new Date(first!.startDateTime).getTime() - new Date(first!.startDateTime).getTime() -
new Date(second!.startDateTime).getTime(), new Date(second!.startDateTime).getTime(),
@@ -1454,29 +1647,30 @@ export class ItineraryService {
bucketTypeName: entry.bucketTypeName, bucketTypeName: entry.bucketTypeName,
distance, distance,
activityTitle: activity.activityTitle, activityTitle: activity.activityTitle,
activityDescription: activity.activityDescription,
activityDurationMins, activityDurationMins,
activityCoverImage: coverImage?.mediaFileName ?? null, activityCoverImage: coverImage?.mediaFileName ?? null,
activityCoverImagePresignedUrl: await attachPresignedUrl( activityCoverImagePresignedUrl: await attachPresignedUrl(
coverImage?.mediaFileName, coverImage?.mediaFileName,
), ),
venue: availableSlots[0] venue: availableSlotsWithPresignedUrl[0]
? { ? {
venueXid: availableSlots[0].venueXid, venueXid: availableSlotsWithPresignedUrl[0].venueXid,
venueName: availableSlots[0].venueName, venueName: availableSlotsWithPresignedUrl[0].venueName,
venueLabel: availableSlots[0].venueLabel, venueLabel: availableSlotsWithPresignedUrl[0].venueLabel,
} }
: null, : null,
availableSlots, availableSlots: availableSlotsWithPresignedUrl,
entryType: activity.ActivityAllowedEntry[0]?.allowedEntryType ?? null, entryType: activity.ActivityAllowedEntry[0]?.allowedEntryType ?? null,
energyLevel: energyLevel energyLevel: energyLevel
? { ? {
energyLevelXid: energyLevel.id, energyLevelXid: energyLevel.id,
energyLevelName: energyLevel.energyLevelName, energyLevelName: energyLevel.energyLevelName,
energyLevelIcon: energyLevel.energyIcon, energyLevelIcon: energyLevel.energyIcon,
energyLevelIconPresignedUrl: await attachPresignedUrl( energyLevelIconPresignedUrl: await attachPresignedUrl(
energyLevel.energyIcon, energyLevel.energyIcon,
), ),
} }
: null, : null,
checkInAddress: activity.checkInAddress, checkInAddress: activity.checkInAddress,
checkInCity: activity.checkInCity, checkInCity: activity.checkInCity,
@@ -1486,9 +1680,9 @@ export class ItineraryService {
checkInLong: activity.checkInLong, checkInLong: activity.checkInLong,
interest: activity.activityType?.interests interest: activity.activityType?.interests
? { ? {
id: activity.activityType.interests.id, id: activity.activityType.interests.id,
interestName: activity.activityType.interests.interestName, interestName: activity.activityType.interests.interestName,
} }
: null, : null,
}; };
}), }),