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

This commit is contained in:
2026-01-29 17:47:48 +05:30
parent 1d90675d19
commit 8be2eebaba
5 changed files with 315 additions and 3 deletions

View File

@@ -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")

View File

@@ -492,4 +492,19 @@ getVenueDurationByAct:
events:
- httpApi:
path: /scheduling/get-venue-duration/{activityXid}
method: get
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

View File

@@ -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,

View File

@@ -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<APIGatewayProxyResult> => {
// 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
}),
};
});

View File

@@ -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 (131)
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
}
})
}
}