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.
service: minglarDev
service: minglar
useDotenv: true

View File

@@ -13,6 +13,40 @@ const hostService = new HostService(prismaClient);
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 getS3KeyFromUrl(url: string): string {
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) => {
console.log(`FIELD RAW: ${fieldname} =`, val);
if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null;
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;
}
}
fields[fieldname] = parseMultipartFieldValue(val);
});
bb.on("close", () => resolve());
@@ -154,7 +173,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
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 (!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 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 */
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) */
const logoFile = files.find(
(f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile'
@@ -449,6 +510,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
parsedParentCompany,
uploadedParentDocs,
isDraft,
{ deleteCompanyLogo, deleteParentCompanyLogo },
);
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');

View File

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

View File

@@ -232,7 +232,7 @@ const getUniqueDatesForScheduleDetail = (
@Injectable()
export class ItineraryService {
constructor(private prisma: PrismaClient) {}
constructor(private prisma: PrismaClient) { }
async getUserItineraryDetails(userXid: number) {
const [userLocation, activityEntries, travellerType, energyLevel] = await Promise.all([
@@ -398,15 +398,15 @@ export class ItineraryService {
),
energyLevel: entry.Activities?.activityType?.energyLevel
? {
id: entry.Activities.activityType.energyLevel.id,
energyLevelName:
entry.Activities.activityType.energyLevel.energyLevelName,
energyIcon:
entry.Activities.activityType.energyLevel.energyIcon,
energyIconPresignedUrl: await attachPresignedUrl(
entry.Activities.activityType.energyLevel.energyIcon,
),
}
id: entry.Activities.activityType.energyLevel.id,
energyLevelName:
entry.Activities.activityType.energyLevel.energyLevelName,
energyIcon:
entry.Activities.activityType.energyLevel.energyIcon,
energyIconPresignedUrl: await attachPresignedUrl(
entry.Activities.activityType.energyLevel.energyIcon,
),
}
: null,
media: await attachMediaWithPresignedUrl(
entry.Activities?.ActivitiesMedia ?? [],
@@ -640,9 +640,9 @@ export class ItineraryService {
const slotEnd = scheduleHeader.activity.activityDurationMins
? addMinutes(
slotStart,
scheduleHeader.activity.activityDurationMins,
)
slotStart,
scheduleHeader.activity.activityDurationMins,
)
: combineDateAndTime(slotDate, slot.endTime);
if (!slotEnd) {
@@ -665,7 +665,7 @@ export class ItineraryService {
if (
activityItem.occurenceDate &&
formatDateKey(slotDate) !==
formatDateKey(parseDateValue(activityItem.occurenceDate))
formatDateKey(parseDateValue(activityItem.occurenceDate))
) {
return null;
}
@@ -879,6 +879,97 @@ export class ItineraryService {
checkInAddress: true,
checkInLat: 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: {
where: {
isActive: true,
@@ -921,9 +1012,8 @@ export class ItineraryService {
const formattedItineraries = await Promise.all(
itineraries.map(async (itinerary) => {
const ownerFullName = `${itinerary.owner.firstName ?? ''} ${
itinerary.owner.lastName ?? ''
}`.trim();
const ownerFullName = `${itinerary.owner.firstName ?? ''} ${itinerary.owner.lastName ?? ''
}`.trim();
const members = await Promise.all(
itinerary.ItineraryMembers.map(async (member) => ({
@@ -932,9 +1022,8 @@ export class ItineraryService {
memberRole: member.memberRole,
memberStatus: member.memberStatus,
invitedByXid: member.invitedByXid,
fullName: `${member.member.firstName ?? ''} ${
member.member.lastName ?? ''
}`.trim(),
fullName: `${member.member.firstName ?? ''} ${member.member.lastName ?? ''
}`.trim(),
firstName: member.member.firstName,
lastName: member.member.lastName,
profileImage: member.member.profileImage,
@@ -985,6 +1074,80 @@ export class ItineraryService {
media: await attachMediaWithPresignedUrl(
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,
scheduleHeader: item.scheduledHeader,
@@ -1150,6 +1313,7 @@ export class ItineraryService {
id: true,
activityTitle: true,
activityDurationMins: true,
activityDescription: true,
checkInLat: true,
checkInLong: true,
checkInAddress: true,
@@ -1246,6 +1410,17 @@ export class ItineraryService {
venueName: true,
venueLabel: true,
venueCapacity: true,
ActivityVenueArtifacts: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
mediaType: true,
mediaFileName: true,
},
},
availableSeats: true,
ScheduleHeader: {
where: {
@@ -1411,6 +1586,8 @@ export class ItineraryService {
venueXid: venue.id,
venueName: venue.venueName,
venueLabel: venue.venueLabel,
mediaFileName: venue.ActivityVenueArtifacts[0]?.mediaFileName ?? null,
mediaType: venue.ActivityVenueArtifacts[0]?.mediaType ?? null,
venueCapacity: venue.venueCapacity,
availableSeats: venue.availableSeats,
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;
}
availableSlots.sort(
availableSlotsWithPresignedUrl.sort(
(first, second) =>
new Date(first!.startDateTime).getTime() -
new Date(second!.startDateTime).getTime(),
@@ -1454,29 +1647,30 @@ export class ItineraryService {
bucketTypeName: entry.bucketTypeName,
distance,
activityTitle: activity.activityTitle,
activityDescription: activity.activityDescription,
activityDurationMins,
activityCoverImage: coverImage?.mediaFileName ?? null,
activityCoverImagePresignedUrl: await attachPresignedUrl(
coverImage?.mediaFileName,
),
venue: availableSlots[0]
venue: availableSlotsWithPresignedUrl[0]
? {
venueXid: availableSlots[0].venueXid,
venueName: availableSlots[0].venueName,
venueLabel: availableSlots[0].venueLabel,
}
venueXid: availableSlotsWithPresignedUrl[0].venueXid,
venueName: availableSlotsWithPresignedUrl[0].venueName,
venueLabel: availableSlotsWithPresignedUrl[0].venueLabel,
}
: null,
availableSlots,
availableSlots: availableSlotsWithPresignedUrl,
entryType: activity.ActivityAllowedEntry[0]?.allowedEntryType ?? null,
energyLevel: energyLevel
? {
energyLevelXid: energyLevel.id,
energyLevelName: energyLevel.energyLevelName,
energyLevelIcon: energyLevel.energyIcon,
energyLevelIconPresignedUrl: await attachPresignedUrl(
energyLevel.energyIcon,
),
}
energyLevelXid: energyLevel.id,
energyLevelName: energyLevel.energyLevelName,
energyLevelIcon: energyLevel.energyIcon,
energyLevelIconPresignedUrl: await attachPresignedUrl(
energyLevel.energyIcon,
),
}
: null,
checkInAddress: activity.checkInAddress,
checkInCity: activity.checkInCity,
@@ -1486,9 +1680,9 @@ export class ItineraryService {
checkInLong: activity.checkInLong,
interest: activity.activityType?.interests
? {
id: activity.activityType.interests.id,
interestName: activity.activityType.interests.interestName,
}
id: activity.activityType.interests.id,
interestName: activity.activityType.interests.interestName,
}
: null,
};
}),