From 8be2eebaba842642cc416d5982f511781aaad191 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Thu, 29 Jan 2026 17:47:48 +0530 Subject: [PATCH] made cancel slot api and modified get by id api to send all the data of scheduling and taking listNow flag in create scheduling api while deleting all the records before creating new ones --- prisma/schema.prisma | 2 +- serverless/functions/host.yml | 17 +- .../host/createSchedulingOfAct.validation.ts | 1 + .../Activity_Hub/Scheduling/cancelSlot.ts | 83 +++++++ .../services/activityScheduling.service.ts | 215 +++++++++++++++++- 5 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 src/modules/host/handlers/Activity_Hub/Scheduling/cancelSlot.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4b5a978..3bf0505 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1523,7 +1523,7 @@ model Cancellations { id Int @id @default(autoincrement()) scheduleHeaderXid Int @map("schedule_header_xid") scheduleHeader ScheduleHeader @relation(fields: [scheduleHeaderXid], references: [id], onDelete: Cascade) - occurenceDate DateTime @map("occurence_date") + occurenceDate DateTime? @map("occurence_date") slotXid Int @map("slot_xid") slot ScheduleDetails @relation(fields: [slotXid], references: [id], onDelete: Cascade) cancellationReason String @map("cancellation_reason") diff --git a/serverless/functions/host.yml b/serverless/functions/host.yml index 377fe38..0be9b32 100644 --- a/serverless/functions/host.yml +++ b/serverless/functions/host.yml @@ -492,4 +492,19 @@ getVenueDurationByAct: events: - httpApi: path: /scheduling/get-venue-duration/{activityXid} - method: get \ No newline at end of file + method: get + +cancelSlotForActivity: + handler: src/modules/host/handlers/Activity_Hub/Scheduling/cancelSlot.handler + memorySize: 512 + package: + patterns: + - 'src/modules/host/handlers/Activity_Hub/Scheduling/**' + - ${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: /scheduling/cancel-slot + method: post \ No newline at end of file diff --git a/src/common/utils/validation/host/createSchedulingOfAct.validation.ts b/src/common/utils/validation/host/createSchedulingOfAct.validation.ts index 082e538..c769aac 100644 --- a/src/common/utils/validation/host/createSchedulingOfAct.validation.ts +++ b/src/common/utils/validation/host/createSchedulingOfAct.validation.ts @@ -13,6 +13,7 @@ const WeekdayEnum = z.enum([ export const scheduleActivity = z.object({ activityXid: z.number(), + listNow: z.boolean(), scheduleType: z.enum([ SCHEDULING_TYPE.ONCE, diff --git a/src/modules/host/handlers/Activity_Hub/Scheduling/cancelSlot.ts b/src/modules/host/handlers/Activity_Hub/Scheduling/cancelSlot.ts new file mode 100644 index 0000000..b236eb9 --- /dev/null +++ b/src/modules/host/handlers/Activity_Hub/Scheduling/cancelSlot.ts @@ -0,0 +1,83 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { safeHandler } from '../../../../../common/utils/handlers/safeHandler'; +import { prismaClient } from '../../../../../common/database/prisma.lambda.service'; +import { SchedulingService } from '../../../services/activityScheduling.service'; +import { HostService } from '../../../services/host.service'; +import ApiError from '../../../../../common/utils/helper/ApiError'; +import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost'; +import { scheduleActivity } from '../../../../../common/utils/validation/host/createSchedulingOfAct.validation'; +import { z } from 'zod'; + +const schedulingService = new SchedulingService(prismaClient); +const hostService = new HostService(prismaClient); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Extract token from headers + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'] + if (!token) { + throw new ApiError(400, 'This is a protected route. Please provide a valid token.'); + } + + // Authenticate user using the shared authForHost function + const userInfo = await verifyHostToken(token); + const hostId = userInfo.id; + + if (Number.isNaN(hostId)) { + throw new ApiError(400, 'Host id must be a number'); + } + + const host = await hostService.getHostIdByUserXid(hostId); + if (!host) { + throw new ApiError(404, 'Host not found'); + } + + let body: { activityXid: number; venueXid: number; scheduleHeaderXid: number; slotXid?: number; cancellationReason?: string }; + + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch { + throw new ApiError(400, 'Invalid JSON payload'); + } + + const activity = await schedulingService.getActivityByXid(body.activityXid); + if (!activity) { + throw new ApiError(404, "Activity not found"); + } + + const venueExists = await schedulingService.getVenueFromVenueXid( + body.venueXid, + body.activityXid + ); + + if (!venueExists) { + throw new ApiError( + 404, + `Venue not found for this activity` + ) + } + + + await schedulingService.cancelSlotForActivity( + Number(body.scheduleHeaderXid), + Number(body.slotXid), + body.cancellationReason || 'No reason provided' + ); + + const result = await schedulingService.getVenueDurationByAct(Number(body.activityXid), Number(hostId)); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Scheduling details updated successfully', + data: result + }), + }; +}); \ No newline at end of file diff --git a/src/modules/host/services/activityScheduling.service.ts b/src/modules/host/services/activityScheduling.service.ts index 428cb99..9e4818c 100644 --- a/src/modules/host/services/activityScheduling.service.ts +++ b/src/modules/host/services/activityScheduling.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; -import { ACTIVITY_INTERNAL_STATUS, SCHEDULING_TYPE } from '../../../common/utils/constants/host.constant'; +import { ACTIVITY_AM_DISPLAY_STATUS, ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_DISPLAY_STATUS, ACTIVITY_INTERNAL_STATUS, SCHEDULING_TYPE } from '../../../common/utils/constants/host.constant'; import ApiError from '../../../common/utils/helper/ApiError'; import { ScheduleActivityDTO } from '../../../common/utils/validation/host/createSchedulingOfAct.validation'; import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; @@ -16,6 +16,7 @@ export class SchedulingService { async addSchedulingForActivity(data: ScheduleActivityDTO) { const { activityXid, + listNow, scheduleType, dateRange, rules, @@ -25,6 +26,50 @@ export class SchedulingService { } = data; return this.prisma.$transaction(async (tx) => { + + const venueXids = venues.map(v => v.venueXid); + + /* ---------------------------------- + ๐Ÿงน 0๏ธโƒฃ DELETE OLD SCHEDULING (PER ACTIVITY + VENUES) + ---------------------------------- */ + + const oldHeaders = await tx.scheduleHeader.findMany({ + where: { + activityXid, + activityVenueXid: { in: venueXids }, + isActive: true, + }, + select: { id: true }, + }); + + const headerIds = oldHeaders.map(h => h.id); + + if (headerIds.length) { + // Delete in correct FK order + await tx.cancellations.deleteMany({ + where: { scheduleHeaderXid: { in: headerIds } }, + }); + + await tx.scheduleDetails.deleteMany({ + where: { scheduleHeaderXid: { in: headerIds } }, + }); + + await tx.scheduleOccurences.deleteMany({ + where: { scheduleHeaderXid: { in: headerIds } }, + }); + + await tx.scheduleRecurrence.deleteMany({ + where: { scheduleHeaderXid: { in: headerIds } }, + }); + + await tx.scheduleHeader.deleteMany({ + where: { id: { in: headerIds } }, + }); + } + + /* ---------------------------------- + โž• 1๏ธโƒฃ CREATE NEW SCHEDULING + ---------------------------------- */ const createdHeaders: number[] = []; for (const venue of venues) { @@ -92,6 +137,18 @@ export class SchedulingService { }, }); } + + if (listNow) { + await tx.activities.update({ + where: { id: activityXid, isActive: true }, + data: { + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.ACTIVITY_LISTED + } + }) + } } return { success: true, scheduleHeaderIds: createdHeaders }; @@ -293,15 +350,171 @@ export class SchedulingService { select: { id: true, activityDurationMins: true, + activityTitle: true, + activityRefNumber: true, ActivityVenues: { select: { id: true, venueName: true, venueLabel: true, + ScheduleHeader: { + select: { + id: true, + scheduleType: true, + startDate: true, + endDate: true, + earlyCheckInMins: true, + bookingCutOffMins: true, + ScheduleDetails: { + select: { + id: true, + occurenceDate: true, + weekDay: true, + dayOfMonth: true, + startTime: true, + endTime: true, + } + }, + Cancellations: { + select: { + id: true, + slotXid: true, + cancellationReason: true, + slot: { + select: { + id: true, + occurenceDate: true, + startTime: true, + endTime: true, + weekDay: true, + dayOfMonth: true, + } + } + } + }, + scheduleOccurences: { + select: { + id: true, + occurenceDate: true + } + }, + scheduleRecurrences: { + select: { + id: true, + weekDay: true, + dayOfMonth: true + } + } + } + } + } + }, + ActivitiesMedia: { + select: { + id: true, + mediaFileName: true, + mediaType: true, } } + } }) + + if (!result) return null; + + if (result.ActivitiesMedia?.length) { + for (const media of result.ActivitiesMedia) { + if (!media.mediaFileName) continue; + + const key = media.mediaFileName.startsWith('http') + ? media.mediaFileName.split('.com/')[1] + : media.mediaFileName; + + (media as any).presignedUrl = await getPresignedUrl(bucket, key); + } + } + + for (const venue of result.ActivityVenues ?? []) { + for (const header of venue.ScheduleHeader ?? []) { + + /* ------------------------------- + ๐Ÿšซ SLOT CANCELLATION FLAG + -------------------------------- */ + const cancelledSlotIds = new Set( + header.Cancellations?.map(c => c.slotXid) + ); + + for (const slot of header.ScheduleDetails ?? []) { + (slot as any).isCancelled = cancelledSlotIds.has(slot.id); + } + + /* ------------------------------- + ๐Ÿ“… FRONTEND FRIENDLY META + -------------------------------- */ + + // WEEKLY โ†’ send weekdays + if (header.scheduleType === 'WEEKLY') { + (header as any).scheduleDays = [ + ...new Set( + header.scheduleRecurrences + ?.map(r => r.weekDay) + .filter(Boolean) + ), + ]; + } + + // MONTHLY โ†’ send dates (1โ€“31) + if (header.scheduleType === 'MONTHLY') { + (header as any).scheduleDates = [ + ...new Set( + header.scheduleRecurrences + ?.map(r => r.dayOfMonth) + .filter(Boolean) + ), + ]; + } + + // CUSTOM / ONCE โ†’ send exact dates + if ( + header.scheduleType === 'CUSTOM' || + header.scheduleType === 'ONCE' + ) { + (header as any).scheduleDates = [ + ...new Set( + header.scheduleOccurences + ?.map(o => + o.occurenceDate + ?.toISOString() + .split('T')[0] + ) + .filter(Boolean) + ), + ]; + } + } + } + + (result as any).availableScheduleTypes = [ + 'ONCE', + 'WEEKLY', + 'MONTHLY', + 'CUSTOM', + ]; + return result; } + + async cancelSlotForActivity( + scheduleHeaderXid: number, + slotXid?: number, + cancellationReason?: string + ){ + return await this.prisma.cancellations.create({ + data: { + scheduleHeaderXid, + slotXid, + cancellationReason + } + }) + } } \ No newline at end of file