import { Controller } from "@/common/controller"; import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware"; import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware"; import { paginationQuerySchema } from "@/common/schemas"; import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage"; import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service"; import type { ErrorResponse, ListResponse, SingleResponse, } from "@/common/types"; import { generateRandomCode } from "@/common/utils"; import { FlightClass } from "@/database/entities/flight-class.entity"; import { FlightSchedule } from "@/database/entities/flight-schedule.entity"; import { Hotel } from "@/database/entities/hotel.entity"; import { PackageConsultSession } from "@/database/entities/package-consult-session.entity"; import { PackageDetail } from "@/database/entities/package-detail.entity"; import { PackageItineraryDay } from "@/database/entities/package-itinerary-day.entity"; import { PackageItineraryImage } from "@/database/entities/package-itinerary-image.entity"; import { PackageItineraryWidgetHotel, PackageItineraryWidgetInformation, PackageItineraryWidgetTransport, } from "@/database/entities/package-itinerary-widget.entity"; import { PackageItinerary } from "@/database/entities/package-itinerary.entity"; import { Package } from "@/database/entities/package.entity"; import { Partner } from "@/database/entities/partner.entity"; import { TransportationClass } from "@/database/entities/transportation-class.entity"; import { AdminPermission } from "@/database/enums/admin-permission.enum"; import { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum"; import { orm } from "@/database/orm"; import type { PackageMapper } from "@/modules/package/package.mapper"; import { packageDetailParamsSchema, packageDetailRequestSchema, packageParamsSchema, packageQuerySchema, packageRequestSchema, } from "@/modules/package/package.schemas"; import type { PackageConsultResponse, PackageDetailResponse, PackageResponse, } from "@/modules/package/package.types"; import { wrap, type OrderDefinition } from "@mikro-orm/core"; import { Router, type Request, type Response } from "express"; import slugify from "slugify"; import { ulid } from "ulid"; export class PackageController extends Controller { public constructor( private readonly mapper: PackageMapper, private readonly fileStorage: AbstractFileStorage, private readonly jwtService: AbstractJwtService, ) { super(); } async create(req: Request, res: Response) { const parseBodyResult = packageRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const thumbnailFile = await this.fileStorage.storeFile( Buffer.from(body.thumbnail, "base64"), ); const partner = await orm.em.findOne(Partner, { id: body.partner_id }); if (!partner) { return res.status(404).json({ data: null, errors: [ { path: "partner_id", location: "body", message: "Partner not found.", }, ], } satisfies ErrorResponse); } const package_ = orm.em.create(Package, { id: ulid(), slug: slugify(body.name, { lower: true }), name: body.name, type: body.type, class: body.class, thumbnail: thumbnailFile.name, useFastTrain: body.use_fast_train, partner, createdAt: new Date(), updatedAt: new Date(), }); await orm.em.flush(); return res.status(201).json({ data: this.mapper.mapEntityToResponse(package_), errors: null, } satisfies SingleResponse); } async consult(req: Request, res: Response) { const parseParamsResult = packageParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const package_ = await orm.em.findOne( Package, { id: params.id }, { populate: ["*"] }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } const consultSession = orm.em.create(PackageConsultSession, { id: ulid(), code: generateRandomCode(6, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"), package: package_, createdAt: new Date(), updatedAt: new Date(), }); return res.status(200).json({ data: { session_code: consultSession.code, }, errors: null, } satisfies SingleResponse); } async list(req: Request, res: Response) { const parseQueryResult = packageQuerySchema.safeParse(req.query); if (!parseQueryResult.success) { return this.handleZodError(parseQueryResult.error, res, "query"); } const query = parseQueryResult.data; const count = await orm.em.count(Package); const orderBy: OrderDefinition = {}; switch (query.sort_by) { case "newest": orderBy.createdAt = "DESC"; break; case "oldest": orderBy.createdAt = "ASC"; break; } let packageQueryBuilder = orm.em .createQueryBuilder(Package, "_package") .select(["*"], true) .distinctOn(["class"]) .limit(query.per_page) .offset((query.page - 1) * query.per_page) .leftJoinAndSelect("_package.partner", "_partner"); if ("class" in query && query.class) { packageQueryBuilder = packageQueryBuilder.where({ class: query.class }); } if ("by_ideal" in query && query.by_ideal) { packageQueryBuilder = packageQueryBuilder.where({ class: "by_ideal" }); } switch (query.sort_by) { case "newest": packageQueryBuilder = packageQueryBuilder.orderBy({ "_package.created_at": "DESC", }); break; case "oldest": packageQueryBuilder = packageQueryBuilder.orderBy({ "_package.created_at": "ASC", }); break; } const packages = await packageQueryBuilder.getResultList(); return res.status(200).json({ data: packages.map(this.mapper.mapEntityToResponse.bind(this.mapper)), errors: null, meta: { page: query.page, per_page: query.per_page, total_pages: Math.ceil(count / query.per_page), total_items: count, }, } satisfies ListResponse); } async view(req: Request, res: Response) { const parseParamsResult = packageParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const package_ = await orm.em.findOne( Package, { id: params.id }, { populate: ["*"] }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } return res.status(200).json({ data: this.mapper.mapEntityToResponse(package_), errors: null, } satisfies SingleResponse); } async update(req: Request, res: Response) { const parseParamsResult = packageParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const parseBodyResult = packageRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const partner = await orm.em.findOne( Partner, { id: body.partner_id }, { populate: ["*"] }, ); if (!partner) { return res.status(404).json({ data: null, errors: [ { path: "partner_id", location: "body", message: "Partner not found.", }, ], } satisfies ErrorResponse); } const package_ = await orm.em.findOne( Package, { id: params.id }, { populate: ["*"], }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } await this.fileStorage.storeFile( Buffer.from(body.thumbnail, "base64"), package_.thumbnail, ); package_.slug = slugify(body.name, { lower: true }); package_.name = body.name; package_.type = body.type; package_.class = body.class; package_.useFastTrain = body.use_fast_train; package_.partner = partner; package_.updatedAt = new Date(); await orm.em.flush(); return res.status(200).json({ data: this.mapper.mapEntityToResponse(package_), errors: null, } satisfies SingleResponse); } async delete(req: Request, res: Response) { const parseParamsResult = packageParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const package_ = await orm.em.findOne( Package, { id: params.id, }, { populate: ["*"], }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } await this.fileStorage.removeFile(package_.thumbnail); await orm.em.removeAndFlush(package_); return res.status(204).send(); } async createDetail(req: Request, res: Response) { const parseParamsResult = packageParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const parseBodyResult = packageDetailRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const package_ = await orm.em.findOne( Package, { id: params.id }, { populate: ["*"] }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } let tourFlightSchedule: FlightSchedule | null = null; for (const [index, tourFlightId] of (body.tour_flight_ids ?? []) .toReversed() .entries()) { const tourFlight = await orm.em.findOne( FlightClass, { id: tourFlightId }, { populate: ["*"] }, ); if (!tourFlight) { return res.status(404).json({ data: null, errors: [ { path: `tour_flight_ids.${index}`, location: "body", message: "Flight class not found.", }, ], } satisfies ErrorResponse); } tourFlightSchedule = orm.em.create(FlightSchedule, { id: ulid(), flight: tourFlight, next: tourFlightSchedule, createdAt: new Date(), updatedAt: new Date(), }); } let outboundFlightSchedule: FlightSchedule | null = null; for (const [index, outboundFlightId] of body.outbound_flight_ids .toReversed() .entries()) { const outboundFlight = await orm.em.findOne( FlightClass, { id: outboundFlightId }, { populate: ["*"] }, ); if (!outboundFlight) { return res.status(404).json({ data: null, errors: [ { path: `outbound_flight_ids.${index}`, location: "body", message: "Flight class not found.", }, ], } satisfies ErrorResponse); } outboundFlightSchedule = orm.em.create(FlightSchedule, { id: ulid(), flight: outboundFlight, next: outboundFlightSchedule, createdAt: new Date(), updatedAt: new Date(), }); } if (outboundFlightSchedule === null) { return res.status(400).json({ data: null, errors: [ { path: "outbound_flight_ids", location: "body", message: "Must not empty.", }, ], }); } let inboundFlightSchedule: FlightSchedule | null = null; for (const [index, inboundFlightId] of body.inbound_flight_ids .toReversed() .entries()) { const inboundFlight = await orm.em.findOne( FlightClass, { id: inboundFlightId }, { populate: ["*"] }, ); if (!inboundFlight) { return res.status(404).json({ data: null, errors: [ { path: `inbound_flight_ids.${index}`, location: "body", message: "Flight class not found.", }, ], } satisfies ErrorResponse); } inboundFlightSchedule = orm.em.create(FlightSchedule, { id: ulid(), flight: inboundFlight, next: inboundFlightSchedule, createdAt: new Date(), updatedAt: new Date(), }); } if (inboundFlightSchedule === null) { return res.status(400).json({ data: null, errors: [ { path: "inbound_flight_ids", location: "body", message: "Must not empty.", }, ], }); } const makkahHotel = await orm.em.findOne( Hotel, { id: body.makkah_hotel.hotel_id, }, { populate: ["*"], }, ); if (!makkahHotel) { return res.status(404).json({ data: null, errors: [ { path: "makkah_hotel_id", location: "body", message: "Hotel not found.", }, ], }); } const madinahHotel = await orm.em.findOne( Hotel, { id: body.madinah_hotel.hotel_id, }, { populate: ["*"], }, ); if (!madinahHotel) { return res.status(404).json({ data: null, errors: [ { path: "madinah_hotel_id", location: "body", message: "Hotel not found.", }, ], }); } const transportationClass = await orm.em.findOne( TransportationClass, { id: body.transportation_id, }, { populate: ["*"], }, ); if (!transportationClass) { return res.status(404).json({ data: null, errors: [ { path: "transportation_id", location: "body", message: "Transportation class not found.", }, ], }); } let itineraryEntity: PackageItinerary | null = null; for (const [index, itinerary] of body.itineraries.toReversed().entries()) { let itineraryDayEntity: PackageItineraryDay | null = null; for (const [dayIndex, itineraryDay] of itinerary.days .toReversed() .entries()) { itineraryDayEntity = orm.em.create(PackageItineraryDay, { id: ulid(), title: itineraryDay.title, description: itineraryDay.description, next: itineraryDayEntity, createdAt: new Date(), updatedAt: new Date(), }); for (const [ widgetIndex, itineraryWidget, ] of itineraryDay.widgets.entries()) { switch (itineraryWidget.type) { case "transport": const transportWidget = orm.em.create( PackageItineraryWidgetTransport, { id: ulid(), packageItineraryDay: itineraryDayEntity, type: PackageItineraryWidgetType.transport, createdAt: new Date(), updatedAt: new Date(), transportation: itineraryWidget.transportation, from: itineraryWidget.from, to: itineraryWidget.to, }, ); itineraryDayEntity.widgets.add(transportWidget); break; case "hotel": const hotel = await orm.em.findOne( Hotel, { id: itineraryWidget.hotel_id, }, { populate: ["*"], }, ); if (!hotel) { return res.status(404).json({ data: null, errors: [ { path: `itineraries.${index}.days.${dayIndex}.widgets.${widgetIndex}.hotel_id`, location: "body", message: "Hotel not found.", }, ], }); } const hotelWidget = orm.em.create(PackageItineraryWidgetHotel, { id: ulid(), packageItineraryDay: itineraryDayEntity, type: PackageItineraryWidgetType.hotel, createdAt: new Date(), updatedAt: new Date(), hotel, }); itineraryDayEntity.widgets.add(hotelWidget); break; case "information": const informationWidget = orm.em.create( PackageItineraryWidgetInformation, { id: ulid(), packageItineraryDay: itineraryDayEntity, type: PackageItineraryWidgetType.information, createdAt: new Date(), updatedAt: new Date(), description: itineraryWidget.description, }, ); itineraryDayEntity.widgets.add(informationWidget); break; } } } if (itineraryDayEntity === null) { return res.status(400).json({ data: null, errors: [ { path: `itineraries.${index}.days`, location: "body", message: "Must not empty.", }, ], } satisfies ErrorResponse); } itineraryEntity = orm.em.create(PackageItinerary, { id: ulid(), location: itinerary.location, day: itineraryDayEntity, next: itineraryEntity, createdAt: new Date(), updatedAt: new Date(), }); for (const [index, image] of itinerary.images.entries()) { const imageFile = await this.fileStorage.storeFile( Buffer.from(image, "base64"), ); const itineraryImage = orm.em.create(PackageItineraryImage, { id: ulid(), packageItinerary: itineraryEntity, src: imageFile.name, createdAt: new Date(), updatedAt: new Date(), }); itineraryEntity.images.add(itineraryImage); } } if (itineraryEntity === null) { return res.status(400).json({ data: null, errors: [ { path: "itineraries", location: "body", message: "Must not empty.", }, ], }); } const packageDetail = orm.em.create(PackageDetail, { id: ulid(), package: package_, departureDate: body.departure_date, tourFlight: tourFlightSchedule, outboundFlight: outboundFlightSchedule, inboundFlight: inboundFlightSchedule, makkahHotel: { id: ulid(), hotel: makkahHotel, checkIn: body.makkah_hotel.check_in, checkOut: body.makkah_hotel.check_out, createdAt: new Date(), updatedAt: new Date(), }, madinahHotel: { id: ulid(), hotel: madinahHotel, checkIn: body.madinah_hotel.check_in, checkOut: body.madinah_hotel.check_out, createdAt: new Date(), updatedAt: new Date(), }, transportation: transportationClass, quadPrice: body.quad_price, quadDiscount: body.quad_discount, triplePrice: body.triple_price, tripleDiscount: body.triple_discount, doublePrice: body.double_price, doubleDiscount: body.double_discount, infantPrice: body.infant_price, infantDiscount: body.infant_discount, itinerary: itineraryEntity, createdAt: new Date(), updatedAt: new Date(), }); for (const [index, tourHotel] of (body.tour_hotels ?? []).entries()) { const tourHotelEntity = await orm.em.findOne( Hotel, { id: tourHotel.hotel_id, }, { populate: ["*"], }, ); if (!tourHotelEntity) { return res.status(404).json({ data: null, errors: [ { path: `tour_hotels.${index}.hotel_id`, location: "body", message: "Hotel not found.", }, ], }); } packageDetail.tourHotels.add({ id: ulid(), hotel: tourHotelEntity, checkIn: tourHotel.check_in, checkOut: tourHotel.check_out, createdAt: new Date(), updatedAt: new Date(), }); } await orm.em.flush(); return res.status(201).json({ data: this.mapper.mapDetailEntityToResponse(packageDetail), errors: null, } satisfies SingleResponse); } async listDetails(req: Request, res: Response) { const parseQueryResult = paginationQuerySchema.safeParse(req.query); if (!parseQueryResult.success) { return this.handleZodError(parseQueryResult.error, res, "query"); } const query = parseQueryResult.data; const parseParamsResult = packageParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const package_ = await orm.em.findOne( Package, { id: params.id }, { populate: ["*"], }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } const count = await orm.em.count(PackageDetail, { package: package_, }); const details = await orm.em.find( PackageDetail, { package: package_ }, { limit: query.per_page, offset: (query.page - 1) * query.per_page, orderBy: { createdAt: "DESC" }, populate: ["*"], }, ); return res.status(200).json({ data: details.map( this.mapper.mapDetailEntityToResponse.bind(this.mapper), ), errors: null, meta: { page: query.page, per_page: query.per_page, total_pages: Math.ceil(count / query.per_page), total_items: count, }, } satisfies ListResponse); } async viewDetail(req: Request, res: Response) { const parseParamsResult = packageDetailParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const package_ = await orm.em.findOne( Package, { id: params.package_id }, { populate: ["*"] }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } const packageDetail = await orm.em.findOne( PackageDetail, { id: params.id, package: package_, }, { populate: ["*"] }, ); if (!packageDetail) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package detail not found.", }, ], } satisfies ErrorResponse); } return res.status(200).json({ data: this.mapper.mapDetailEntityToResponse(packageDetail), errors: null, } satisfies SingleResponse); } async updateDetail(req: Request, res: Response) { const parseParamsResult = packageDetailParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const parseBodyResult = packageDetailRequestSchema.safeParse(req.body); if (!parseBodyResult.success) { return this.handleZodError(parseBodyResult.error, res, "body"); } const body = parseBodyResult.data; const package_ = await orm.em.findOne( Package, { id: params.package_id }, { populate: ["*"] }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "package_id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } const packageDetail = await orm.em.findOne( PackageDetail, { id: params.id, package: package_, }, { populate: ["*"] }, ); if (!packageDetail) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package detail not found.", }, ], } satisfies ErrorResponse); } for ( let tourFlight: FlightSchedule | null = packageDetail.tourFlight; tourFlight !== null; tourFlight = tourFlight.next ) { orm.em.remove(tourFlight); } let tourFlightSchedule: FlightSchedule | null = null; for (const [index, tourFlightId] of (body.tour_flight_ids ?? []) .toReversed() .entries()) { const tourFlight = await orm.em.findOne( FlightClass, { id: tourFlightId }, { populate: ["*"] }, ); if (!tourFlight) { return res.status(404).json({ data: null, errors: [ { path: `tour_flight_ids.${index}`, location: "body", message: "Flight class not found.", }, ], } satisfies ErrorResponse); } tourFlightSchedule = orm.em.create(FlightSchedule, { id: ulid(), flight: tourFlight, next: tourFlightSchedule, createdAt: new Date(), updatedAt: new Date(), }); } for ( let outboundFlight: FlightSchedule | null = packageDetail.outboundFlight; outboundFlight !== null; outboundFlight = outboundFlight.next ) { orm.em.remove(outboundFlight); } let outboundFlightSchedule: FlightSchedule | null = null; for (const [index, outboundFlightId] of body.outbound_flight_ids .toReversed() .entries()) { const outboundFlight = await orm.em.findOne( FlightClass, { id: outboundFlightId }, { populate: ["*"] }, ); if (!outboundFlight) { return res.status(404).json({ data: null, errors: [ { path: `outbound_flight_ids.${index}`, location: "body", message: "Flight class not found.", }, ], } satisfies ErrorResponse); } outboundFlightSchedule = orm.em.create(FlightSchedule, { id: ulid(), flight: outboundFlight, next: outboundFlightSchedule, createdAt: new Date(), updatedAt: new Date(), }); } if (outboundFlightSchedule === null) { return res.status(400).json({ data: null, errors: [ { path: "outbound_flight_ids", location: "body", message: "Must not empty.", }, ], }); } for ( let inboundFlight: FlightSchedule | null = packageDetail.inboundFlight; inboundFlight !== null; inboundFlight = inboundFlight.next ) { orm.em.remove(inboundFlight); } let inboundFlightSchedule: FlightSchedule | null = null; for (const [index, inboundFlightId] of body.inbound_flight_ids .toReversed() .entries()) { const inboundFlight = await orm.em.findOne( FlightClass, { id: inboundFlightId }, { populate: ["*"] }, ); if (!inboundFlight) { return res.status(404).json({ data: null, errors: [ { path: `inbound_flight_ids.${index}`, location: "body", message: "Flight class not found.", }, ], } satisfies ErrorResponse); } inboundFlightSchedule = orm.em.create(FlightSchedule, { id: ulid(), flight: inboundFlight, next: inboundFlightSchedule, createdAt: new Date(), updatedAt: new Date(), }); } if (inboundFlightSchedule === null) { return res.status(400).json({ data: null, errors: [ { path: "inbound_flight_ids", location: "body", message: "Must not empty.", }, ], }); } const makkahHotel = await orm.em.findOne( Hotel, { id: body.makkah_hotel.hotel_id, }, { populate: ["*"], }, ); if (!makkahHotel) { return res.status(404).json({ data: null, errors: [ { path: "makkah_hotel_id", location: "body", message: "Hotel not found.", }, ], }); } const madinahHotel = await orm.em.findOne( Hotel, { id: body.madinah_hotel.hotel_id, }, { populate: ["*"], }, ); if (!madinahHotel) { return res.status(404).json({ data: null, errors: [ { path: "madinah_hotel_id", location: "body", message: "Hotel not found.", }, ], }); } const transportationClass = await orm.em.findOne( TransportationClass, { id: body.transportation_id, }, { populate: ["*"], }, ); if (!transportationClass) { return res.status(404).json({ data: null, errors: [ { path: "transportation_id", location: "body", message: "Transportation class not found.", }, ], }); } for ( let itinerary: PackageItinerary | null = packageDetail.itinerary; itinerary !== null; itinerary = itinerary.next ) { for ( let itineraryDay: PackageItineraryDay | null = itinerary.day; itineraryDay !== null; itineraryDay = itineraryDay.next ) { for (const itineraryWidget of itineraryDay.widgets) { orm.em.remove(itineraryWidget); } orm.em.remove(itineraryDay); } for (const itineraryImage of itinerary.images) { await this.fileStorage.removeFile(itineraryImage.src); orm.em.remove(itineraryImage); } orm.em.remove(itinerary); } let itineraryEntity: PackageItinerary | null = null; for (const [index, itinerary] of body.itineraries.toReversed().entries()) { let itineraryDayEntity: PackageItineraryDay | null = null; for (const [dayIndex, itineraryDay] of itinerary.days .toReversed() .entries()) { itineraryDayEntity = orm.em.create(PackageItineraryDay, { id: ulid(), title: itineraryDay.title, description: itineraryDay.description, next: itineraryDayEntity, createdAt: new Date(), updatedAt: new Date(), }); for (const [ widgetIndex, itineraryWidget, ] of itineraryDay.widgets.entries()) { switch (itineraryWidget.type) { case "transport": const transportWidget = orm.em.create( PackageItineraryWidgetTransport, { id: ulid(), packageItineraryDay: itineraryDayEntity, type: PackageItineraryWidgetType.transport, createdAt: new Date(), updatedAt: new Date(), transportation: itineraryWidget.transportation, from: itineraryWidget.from, to: itineraryWidget.to, }, ); itineraryDayEntity.widgets.add(transportWidget); break; case "hotel": const hotel = await orm.em.findOne( Hotel, { id: itineraryWidget.hotel_id, }, { populate: ["*"], }, ); if (!hotel) { return res.status(404).json({ data: null, errors: [ { path: `itineraries.${index}.days.${dayIndex}.widgets.${widgetIndex}.hotel_id`, location: "body", message: "Hotel not found.", }, ], }); } const hotelWidget = orm.em.create(PackageItineraryWidgetHotel, { id: ulid(), packageItineraryDay: itineraryDayEntity, type: PackageItineraryWidgetType.hotel, createdAt: new Date(), updatedAt: new Date(), hotel, }); itineraryDayEntity.widgets.add(hotelWidget); break; case "information": const informationWidget = orm.em.create( PackageItineraryWidgetInformation, { id: ulid(), packageItineraryDay: itineraryDayEntity, type: PackageItineraryWidgetType.information, createdAt: new Date(), updatedAt: new Date(), description: itineraryWidget.description, }, ); itineraryDayEntity.widgets.add(informationWidget); break; } } } if (itineraryDayEntity === null) { return res.status(400).json({ data: null, errors: [ { path: `itineraries.${index}.days`, location: "body", message: "Must not empty.", }, ], } satisfies ErrorResponse); } itineraryEntity = orm.em.create(PackageItinerary, { id: ulid(), location: itinerary.location, day: itineraryDayEntity, next: itineraryEntity, createdAt: new Date(), updatedAt: new Date(), }); for (const [index, image] of itinerary.images.entries()) { const imageFile = await this.fileStorage.storeFile( Buffer.from(image, "base64"), ); const itineraryImage = orm.em.create(PackageItineraryImage, { id: ulid(), packageItinerary: itineraryEntity, src: imageFile.name, createdAt: new Date(), updatedAt: new Date(), }); itineraryEntity.images.add(itineraryImage); } } if (itineraryEntity === null) { return res.status(400).json({ data: null, errors: [ { path: "itineraries", location: "body", message: "Must not empty.", }, ], }); } wrap(packageDetail).assign({ departureDate: body.departure_date, tourFlight: tourFlightSchedule, outboundFlight: outboundFlightSchedule, inboundFlight: inboundFlightSchedule, makkahHotel: { hotel: makkahHotel, checkIn: body.makkah_hotel.check_in, checkOut: body.makkah_hotel.check_out, updatedAt: new Date(), }, madinahHotel: { hotel: madinahHotel, checkIn: body.madinah_hotel.check_in, checkOut: body.madinah_hotel.check_out, updatedAt: new Date(), }, transportation: transportationClass, quadPrice: body.quad_price, quadDiscount: body.quad_discount, triplePrice: body.triple_price, tripleDiscount: body.triple_discount, doublePrice: body.double_price, doubleDiscount: body.double_discount, infantPrice: body.infant_price, infantDiscount: body.infant_discount, itinerary: itineraryEntity, updatedAt: new Date(), }); packageDetail.tourHotels.set([]); for (const [index, tourHotel] of (body.tour_hotels ?? []).entries()) { const tourHotelEntity = await orm.em.findOne( Hotel, { id: tourHotel.hotel_id, }, { populate: ["*"], }, ); if (!tourHotelEntity) { return res.status(404).json({ data: null, errors: [ { path: `tour_hotels.${index}.hotel_id`, location: "body", message: "Hotel not found.", }, ], }); } packageDetail.tourHotels.add({ id: ulid(), hotel: tourHotelEntity, checkIn: tourHotel.check_in, checkOut: tourHotel.check_out, createdAt: new Date(), updatedAt: new Date(), }); } await orm.em.flush(); return res.status(200).json({ data: this.mapper.mapDetailEntityToResponse(packageDetail), errors: null, } satisfies SingleResponse); } async deleteDetail(req: Request, res: Response) { const parseParamsResult = packageDetailParamsSchema.safeParse(req.params); if (!parseParamsResult.success) { return this.handleZodError(parseParamsResult.error, res, "params"); } const params = parseParamsResult.data; const package_ = await orm.em.findOne( Package, { id: params.package_id }, { populate: ["*"] }, ); if (!package_) { return res.status(404).json({ data: null, errors: [ { path: "package_id", location: "params", message: "Package not found.", }, ], } satisfies ErrorResponse); } const packageDetail = await orm.em.findOne( PackageDetail, { id: params.id, package: package_, }, { populate: ["*"] }, ); if (!packageDetail) { return res.status(404).json({ data: null, errors: [ { path: "id", location: "params", message: "Package detail not found.", }, ], } satisfies ErrorResponse); } for ( let itinerary: PackageItinerary | null = packageDetail.itinerary; itinerary !== null; itinerary = itinerary.next ) { for (const itineraryImage of itinerary.images) { await this.fileStorage.removeFile(itineraryImage.src); } } await orm.em.removeAndFlush(packageDetail); return res.status(204).send(); } public buildRouter(): Router { const router = Router(); router.post( "/", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.createPackage]), this.create.bind(this), ); router.post( "/:id/consult", createOrmContextMiddleware, this.consult.bind(this), ); router.get("/", createOrmContextMiddleware, this.list.bind(this)); router.get("/:id", createOrmContextMiddleware, this.view.bind(this)); router.put( "/:id", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.updatePackage]), this.update.bind(this), ); router.delete( "/:id", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.deletePackage]), this.delete.bind(this), ); router.post( "/:id/details", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.createPackageDetail]), this.createDetail.bind(this), ); router.get( "/:id/details", createOrmContextMiddleware, this.listDetails.bind(this), ); router.get( "/:package_id/details/:id", createOrmContextMiddleware, this.viewDetail.bind(this), ); router.put( "/:package_id/details/:id", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.updatePackageDetail]), this.updateDetail.bind(this), ); router.delete( "/:package_id/details/:id", createOrmContextMiddleware, isAdminMiddleware(this.jwtService, [AdminPermission.deletePackageDetail]), this.deleteDetail.bind(this), ); return router; } }