Files
goumrah-api/src/modules/package/package.controller.ts
2025-12-18 07:51:21 +07:00

1497 lines
36 KiB
TypeScript

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<PackageResponse>);
}
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<PackageConsultResponse>);
}
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<Package> = {};
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<PackageResponse>);
}
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<PackageResponse>);
}
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<PackageResponse>);
}
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<PackageDetailResponse>);
}
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<PackageDetailResponse>);
}
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<PackageDetailResponse>);
}
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<PackageDetailResponse>);
}
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;
}
}