add core api

This commit is contained in:
ItsMalma
2025-11-15 22:28:58 +07:00
parent e6386648be
commit 8f91994f29
78 changed files with 6701 additions and 904 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,216 @@
import type { FlightSchedule } from "@/database/entities/flight-schedule.entity";
import type { HotelSchedule } from "@/database/entities/hotel-schedule.entity";
import type { PackageDetail } from "@/database/entities/package-detail.entity";
import type { PackageItineraryDay } from "@/database/entities/package-itinerary-day.entity";
import type {
PackageItineraryWidget,
PackageItineraryWidgetHotel,
PackageItineraryWidgetInformation,
PackageItineraryWidgetTransport,
} from "@/database/entities/package-itinerary-widget.entity";
import type { PackageItinerary } from "@/database/entities/package-itinerary.entity";
import type { Package } from "@/database/entities/package.entity";
import { PackageClass } from "@/database/enums/package-class.enum";
import { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum";
import { PackageType } from "@/database/enums/package-type.enum";
import type { FlightMapper } from "@/modules/flight/flight.mapper";
import type { FlightClassResponse } from "@/modules/flight/flight.types";
import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
import type {
PackageDetailResponse,
PackageHotelResponse,
PackageItineraryDayResponse,
PackageItineraryResponse,
PackageItineraryWidgetResponse,
PackageRequest,
PackageResponse,
} from "@/modules/package/package.types";
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
import * as dateFns from "date-fns";
export class PackageMapper {
public constructor(
private readonly flightMapper: FlightMapper,
private readonly hotelMapper: HotelMapper,
private readonly transportationMapper: TransportationMapper,
) {}
public mapPackageType(packageType: PackageRequest["type"]): PackageType {
switch (packageType) {
case "reguler":
return PackageType.reguler;
case "plus":
return PackageType.plus;
}
}
public mapPackageClass(packageClass: PackageRequest["class"]): PackageClass {
switch (packageClass) {
case "silver":
return PackageClass.silver;
case "gold":
return PackageClass.gold;
case "platinum":
return PackageClass.platinum;
}
}
public mapEntityToResponse(package_: Package): PackageResponse {
return {
id: package_.id,
name: package_.name,
type: package_.type,
class: package_.class,
thumbnail: package_.thumbnail,
use_fast_train: package_.useFastTrain,
created_at: package_.createdAt,
updated_at: package_.updatedAt,
};
}
private mapFlightSchedule(
flightSchedule: FlightSchedule,
): FlightClassResponse[] {
const flightClassResponses: FlightClassResponse[] = [];
let currentFlightSchedule: FlightSchedule | null = flightSchedule;
while (currentFlightSchedule !== null) {
flightClassResponses.push(
this.flightMapper.mapClassEntityToResponse(
currentFlightSchedule.flight,
),
);
currentFlightSchedule = currentFlightSchedule.next;
}
return flightClassResponses;
}
private mapHotelSchedule(hotelSchedule: HotelSchedule): PackageHotelResponse {
console.log(hotelSchedule.checkIn);
const checkIn = dateFns.parse(hotelSchedule.checkIn, "H:mm:ss", new Date());
const checkOut = dateFns.parse(
hotelSchedule.checkOut,
"H:mm:ss",
new Date(),
);
return {
hotel: this.hotelMapper.mapEntityToResponse(hotelSchedule.hotel),
check_in: dateFns.format(checkIn, "HH:mm"),
check_out: dateFns.format(checkOut, "HH:mm"),
};
}
private mapItineraryWidget(
packageItineraryWidget: PackageItineraryWidget,
): PackageItineraryWidgetResponse {
switch (packageItineraryWidget.type) {
case PackageItineraryWidgetType.transport:
const transportWidget =
packageItineraryWidget as PackageItineraryWidgetTransport;
return {
type: "transport",
transportation: transportWidget.transportation,
from: transportWidget.from,
to: transportWidget.to,
};
case PackageItineraryWidgetType.hotel:
const hotelWidget =
packageItineraryWidget as PackageItineraryWidgetHotel;
return {
type: "hotel",
hotel: this.hotelMapper.mapEntityToResponse(hotelWidget.hotel),
};
case PackageItineraryWidgetType.information:
const informationWidget =
packageItineraryWidget as PackageItineraryWidgetInformation;
return {
type: "information",
description: informationWidget.description,
};
}
}
private mapItineraryDay(
packageItineraryDay: PackageItineraryDay,
nth: number,
): PackageItineraryDayResponse {
return {
nth,
title: packageItineraryDay.title,
description: packageItineraryDay.description,
widgets: packageItineraryDay.widgets
.getItems()
.map((widget) => this.mapItineraryWidget(widget)),
};
}
private mapItinerary(
packageItinerary: PackageItinerary,
nthDay: number = 1,
): PackageItineraryResponse {
const days: PackageItineraryDayResponse[] = [];
for (
let currentItineraryDay: PackageItineraryDay | null =
packageItinerary.day,
index = 0;
currentItineraryDay !== null;
currentItineraryDay = currentItineraryDay.next, index++
) {
days.push(this.mapItineraryDay(currentItineraryDay, nthDay + index));
}
return {
location: packageItinerary.location,
images: packageItinerary.images
.getItems()
.map((packageItineraryImage) => packageItineraryImage.src),
days,
};
}
public mapDetailEntityToResponse(
packageDetail: PackageDetail,
): PackageDetailResponse {
const itineraries: PackageItineraryResponse[] = [];
for (
let currentItinerary: PackageItinerary | null = packageDetail.itinerary,
nthDay = 1;
currentItinerary !== null;
currentItinerary = currentItinerary.next
) {
const itineraryResponse = this.mapItinerary(currentItinerary, nthDay);
itineraries.push(itineraryResponse);
nthDay += itineraryResponse.days.length;
}
return {
id: packageDetail.id,
package: this.mapEntityToResponse(packageDetail.package),
departure_date: dateFns.format(packageDetail.departureDate, "yyyy-MM-dd"),
tour_flights: packageDetail.tourFlight
? this.mapFlightSchedule(packageDetail.tourFlight)
: null,
outbound_flights: this.mapFlightSchedule(packageDetail.outboundFlight),
inbound_flights: this.mapFlightSchedule(packageDetail.inboundFlight),
tour_hotels: packageDetail.tourHotels.map(this.mapHotelSchedule),
makkah_hotel: this.mapHotelSchedule(packageDetail.makkahHotel),
madinah_hotel: this.mapHotelSchedule(packageDetail.madinahHotel),
transportation: this.transportationMapper.mapClassEntityToResponse(
packageDetail.transportation,
),
quad_price: packageDetail.quadPrice,
triple_price: packageDetail.triplePrice,
double_price: packageDetail.doublePrice,
infant_price: packageDetail.infantPrice,
itineraries,
created_at: packageDetail.createdAt,
updated_at: packageDetail.updatedAt,
};
}
}

View File

@@ -17,53 +17,57 @@ export const packageRequestSchema = z.object({
export const packageDetailRequestSchema = z.object({
departure_date: dateSchema,
tour_flight_slugs: z
tour_flight_ids: z
.array(
z
.string("Must be string.")
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(420, "Max 420 characters."),
.max(30, "Max 30 characters."),
"Must be array.",
)
.nonempty("Must not empty.")
.nullable(),
outbound_flight_ids: z
.array(
z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(30, "Max 30 characters."),
"Must be array.",
)
.nonempty("Must not empty."),
outbound_flight_slugs: z
inbound_flight_ids: z
.array(
z
.string("Must be string.")
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(420, "Max 420 characters."),
.max(30, "Max 30 characters."),
"Must be array.",
)
.nonempty("Must not empty."),
inbound_flight_slugs: z
tour_hotels: z
.array(
z
.string("Must be string.")
.nonempty("Must not empty.")
.max(420, "Max 420 characters."),
z.object(
{
hotel_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(30, "Max 30 characters."),
check_in: timeSchema,
check_out: timeSchema,
},
"Must be object.",
),
"Must be array.",
)
.nonempty("Must not empty."),
tour_hotels: z.array(
z.object(
{
hotel_slug: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(200, "Max 200 characters."),
check_in: timeSchema,
check_out: timeSchema,
},
"Must be object.",
),
"Must be array.",
),
.nonempty("Must not empty.")
.nullable(),
makkah_hotel: z.object(
{
hotel_slug: z
.string("Must be string.")
hotel_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(200, "Max 200 characters."),
.max(30, "Max 30 characters."),
check_in: timeSchema,
check_out: timeSchema,
},
@@ -71,19 +75,19 @@ export const packageDetailRequestSchema = z.object({
),
madinah_hotel: z.object(
{
hotel_slug: z
.string("Must be string.")
hotel_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(200, "Max 200 characters."),
.max(30, "Max 30 characters."),
check_in: timeSchema,
check_out: timeSchema,
},
"Must be object.",
),
transportation_slug: z
.string("Must be string.")
transportation_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(200, "Max 200 characters."),
.max(30, "Max 30 characters."),
quad_price: z
.number("Must be number.")
.int("Must be integer.")
@@ -101,22 +105,97 @@ export const packageDetailRequestSchema = z.object({
.int("Must be integer.")
.positive("Must be positive.")
.nullable(),
itineraries: z.array(
z.object(
{
location: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(100, "Max 100 characters."),
images: z.array(
z.base64("Must be base64 string.").nonempty("Must not empty."),
"Must be array.",
),
days: z.array(
z.object(
{
title: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(100, "Max 100 characters."),
description: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(1000, "Max 1000 characters."),
widgets: z.array(
z.discriminatedUnion("type", [
z.object(
{
type: z.literal("transport"),
transportation: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(100, "Max 100 characters."),
from: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(100, "Max 100 characters."),
to: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(100, "Max 100 characters."),
},
"Must be object.",
),
z.object(
{
type: z.literal("hotel"),
hotel_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(30, "Max 30 characters."),
},
"Must be object.",
),
z.object(
{
type: z.literal("information"),
description: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(1000, "Max 1000 characters."),
},
"Must be object.",
),
]),
"Must be array.",
),
},
"Must be object.",
),
"Must be array.",
),
},
"Must be object.",
),
"Must be array.",
),
});
export const packageQuerySchema = z.object({
detail_slug: z
.string("Must be string.")
export const packageParamsSchema = z.object({
id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(200, "Max 200 characters."),
slug: z
.string("Must be string.")
.nonempty("Must not empty.")
.max(200, "Max 200 characters."),
.max(30, "Max 30 characters."),
});
export const packageDetailQuerySchema = z.object({
slug: z
.string("Must be string.")
export const packageDetailParamsSchema = z.object({
package_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(200, "Max 200 characters."),
.max(30, "Max 30 characters."),
id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(30, "Max 30 characters."),
});

View File

@@ -1,9 +1,9 @@
import type { FlightClassResponse } from "@/modules/flight/flight.types";
import type { HotelResponse } from "@/modules/hotel/hotel.types";
import type {
packageDetailQuerySchema,
packageDetailParamsSchema,
packageDetailRequestSchema,
packageQuerySchema,
packageParamsSchema,
packageRequestSchema,
} from "@/modules/package/package.schemas";
import type { TransportationClassResponse } from "@/modules/transportation/transportation.types";
@@ -13,13 +13,12 @@ export type PackageRequest = z.infer<typeof packageRequestSchema>;
export type PackageDetailRequest = z.infer<typeof packageDetailRequestSchema>;
export type PackageQuery = z.infer<typeof packageQuerySchema>;
export type PackageParams = z.infer<typeof packageParamsSchema>;
export type PackageDetailQuery = z.infer<typeof packageDetailQuerySchema>;
export type PackageDetailParams = z.infer<typeof packageDetailParamsSchema>;
export type PackageResponse = {
id: string;
slug: string;
name: string;
type: "reguler" | "plus";
class: "silver" | "gold" | "platinum";
@@ -35,22 +34,51 @@ export type PackageHotelResponse = {
check_out: string;
};
export type PackageItineraryWidgetResponse =
| {
type: "transport";
transportation: string;
from: string;
to: string;
}
| {
type: "hotel";
hotel: HotelResponse;
}
| {
type: "information";
description: string;
};
export type PackageItineraryDayResponse = {
nth: number;
title: string;
description: string;
widgets: PackageItineraryWidgetResponse[];
};
export type PackageItineraryResponse = {
location: string;
images: string[];
days: PackageItineraryDayResponse[];
};
export type PackageDetailResponse = {
id: string;
slug: string;
package: PackageResponse;
departure_date: string;
tour_flights: FlightClassResponse[];
tour_flights: FlightClassResponse[] | null;
outbound_flights: FlightClassResponse[];
inbound_flights: FlightClassResponse[];
tour_hotels: PackageHotelResponse[];
tour_hotels: PackageHotelResponse[] | null;
makkah_hotel: PackageHotelResponse;
medina_hotel: PackageHotelResponse;
madinah_hotel: PackageHotelResponse;
transportation: TransportationClassResponse;
quad_price: number;
triple_price: number;
double_price: number;
infant_price: number | null;
itineraries: PackageItineraryResponse[];
created_at: Date;
updated_at: Date;
};