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:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user