1488 lines
36 KiB
TypeScript
1488 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 } 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);
|
|
|
|
let packageQueryBuilder = orm.em
|
|
.createQueryBuilder(Package, "_package")
|
|
.select(["*"])
|
|
.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 === "1") {
|
|
packageQueryBuilder = packageQueryBuilder.distinctOn(["class"]).orderBy({
|
|
class: "ASC",
|
|
});
|
|
}
|
|
|
|
switch (query.sort_by) {
|
|
case "newest":
|
|
packageQueryBuilder = packageQueryBuilder.orderBy({
|
|
createdAt: "DESC",
|
|
});
|
|
break;
|
|
case "oldest":
|
|
packageQueryBuilder = packageQueryBuilder.orderBy({
|
|
createdAt: "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;
|
|
}
|
|
}
|