add hotel schedule (again)

This commit is contained in:
ItsMalma
2025-12-05 09:45:09 +07:00
parent bbfc022395
commit a5794e9a1e
8 changed files with 367 additions and 59 deletions

View File

@@ -0,0 +1,36 @@
import { Hotel } from "@/database/entities/hotel.entity";
import {
Entity,
ManyToOne,
PrimaryKey,
Property,
type Rel,
} from "@mikro-orm/core";
@Entity()
export class HotelSchedule {
@PrimaryKey({ type: "varchar", length: 30 })
id!: string;
@ManyToOne(() => Hotel)
hotel!: Rel<Hotel>;
@Property({ type: "date" })
checkIn!: Date;
@Property({ type: "date" })
checkOut!: Date;
@Property({
type: "timestamp",
onCreate: () => new Date(),
})
createdAt!: Date;
@Property({
type: "timestamp",
onCreate: () => new Date(),
onUpdate: () => new Date(),
})
updatedAt!: Date;
}

View File

@@ -1,5 +1,5 @@
import { FlightSchedule } from "@/database/entities/flight-schedule.entity";
import { Hotel } from "@/database/entities/hotel.entity";
import { HotelSchedule } from "@/database/entities/hotel-schedule.entity";
import { PackageItinerary } from "@/database/entities/package-itinerary.entity";
import { Package } from "@/database/entities/package.entity";
import { TransportationClass } from "@/database/entities/transportation-class.entity";
@@ -43,21 +43,21 @@ export class PackageDetail {
})
inboundFlight!: Rel<FlightSchedule>;
@ManyToMany(() => Hotel, undefined, {
@ManyToMany(() => HotelSchedule, undefined, {
owner: true,
cascade: [Cascade.REMOVE],
})
tourHotels = new Collection<Hotel>(this);
tourHotels = new Collection<HotelSchedule>(this);
@ManyToOne(() => Hotel, {
@ManyToOne(() => HotelSchedule, {
cascade: [Cascade.REMOVE],
})
makkahHotel!: Rel<Hotel>;
makkahHotel!: Rel<HotelSchedule>;
@ManyToOne(() => Hotel, {
@ManyToOne(() => HotelSchedule, {
cascade: [Cascade.REMOVE],
})
madinahHotel!: Rel<Hotel>;
madinahHotel!: Rel<HotelSchedule>;
@ManyToOne(() => TransportationClass)
transportation!: Rel<TransportationClass>;

View File

@@ -1318,6 +1318,100 @@
},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 30,
"mappedType": "string"
},
"hotel_id": {
"name": "hotel_id",
"type": "varchar(30)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 30,
"mappedType": "string"
},
"check_in": {
"name": "check_in",
"type": "date",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 0,
"mappedType": "date"
},
"check_out": {
"name": "check_out",
"type": "date",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 0,
"mappedType": "date"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
}
},
"name": "hotel_schedule",
"schema": "public",
"indexes": [
{
"keyName": "hotel_schedule_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"hotel_schedule_hotel_id_foreign": {
"constraintName": "hotel_schedule_hotel_id_foreign",
"columnNames": [
"hotel_id"
],
"localTableName": "public.hotel_schedule",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.hotel",
"updateRule": "cascade"
}
},
"nativeEnums": {}
},
{
"columns": {
"id": {
@@ -2852,7 +2946,7 @@
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.hotel",
"referencedTableName": "public.hotel_schedule",
"deleteRule": "cascade"
},
"package_detail_madinah_hotel_id_foreign": {
@@ -2864,7 +2958,7 @@
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.hotel",
"referencedTableName": "public.hotel_schedule",
"deleteRule": "cascade"
},
"package_detail_transportation_id_foreign": {
@@ -2906,8 +3000,8 @@
"length": 30,
"mappedType": "string"
},
"hotel_id": {
"name": "hotel_id",
"hotel_schedule_id": {
"name": "hotel_schedule_id",
"type": "varchar(30)",
"unsigned": false,
"autoincrement": false,
@@ -2924,7 +3018,7 @@
"keyName": "package_detail_tour_hotels_pkey",
"columnNames": [
"package_detail_id",
"hotel_id"
"hotel_schedule_id"
],
"composite": true,
"constraint": true,
@@ -2947,16 +3041,16 @@
"deleteRule": "cascade",
"updateRule": "cascade"
},
"package_detail_tour_hotels_hotel_id_foreign": {
"constraintName": "package_detail_tour_hotels_hotel_id_foreign",
"package_detail_tour_hotels_hotel_schedule_id_foreign": {
"constraintName": "package_detail_tour_hotels_hotel_schedule_id_foreign",
"columnNames": [
"hotel_id"
"hotel_schedule_id"
],
"localTableName": "public.package_detail_tour_hotels",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.hotel",
"referencedTableName": "public.hotel_schedule",
"deleteRule": "cascade",
"updateRule": "cascade"
}

View File

@@ -0,0 +1,107 @@
import { Migration } from "@mikro-orm/migrations";
export class Migration20251205022442 extends Migration {
override async up(): Promise<void> {
const knex = this.getKnex();
await knex.schema.alterTable("package_detail", (table) => {
table.dropColumns("makkah_hotel_id", "madinah_hotel_id");
});
await knex.schema.alterTable("package_detail_tour_hotels", (table) => {
table.dropColumn("hotel_id");
});
await knex.schema.createTable("hotel_schedule", (table) => {
// Columns
table.string("id", 30).notNullable();
table.string("hotel_id", 30).notNullable();
table.date("check_in").notNullable();
table.date("check_out").notNullable();
table.timestamp("created_at", { useTz: true }).notNullable();
table.timestamp("updated_at", { useTz: true }).notNullable();
// Constraints
table.primary(["id"], { constraintName: "hotel_schedule_pkey" });
table
.foreign("hotel_id", "hotel_schedule_hotel_id_foreign")
.references("hotel.id")
.onUpdate("NO ACTION")
.onDelete("CASCADE");
});
await knex.schema.alterTable("package_detail", (table) => {
table.string("makkah_hotel_id", 30).notNullable();
table.string("madinah_hotel_id", 30).notNullable();
table
.foreign("makkah_hotel_id", "package_detail_makkah_hotel_id_foreign")
.references("hotel_schedule.id")
.onUpdate("NO ACTION")
.onDelete("CASCADE");
table
.foreign("madinah_hotel_id", "package_detail_madinah_hotel_id_foreign")
.references("hotel_schedule.id")
.onUpdate("NO ACTION")
.onDelete("CASCADE");
});
await knex.schema.alterTable("package_detail_tour_hotels", (table) => {
table.string("hotel_id", 30).notNullable();
table
.foreign("hotel_id", "package_detail_tour_hotels_hotel_id_foreign")
.references("hotel_schedule.id")
.onUpdate("NO ACTION")
.onDelete("CASCADE");
});
}
override async down(): Promise<void> {
const knex = this.getKnex();
await knex.schema.alterTable("package_detail_tour_hotels", (table) => {
table.dropForeign(
"hotel_id",
"package_detail_tour_hotels_hotel_id_foreign",
);
table.dropColumn("hotel_id");
});
await knex.schema.alterTable("package_detail", (table) => {
table.dropForeign(
"madinah_hotel_id",
"package_detail_madinah_hotel_id_foreign",
);
table.dropForeign(
"makkah_hotel_id",
"package_detail_makkah_hotel_id_foreign",
);
table.dropColumn("madinah_hotel_id");
table.dropColumn("makkah_hotel_id");
});
await knex.schema.dropTable("hotel_schedule");
await knex.schema.alterTable("package_detail_tour_hotels", (table) => {
table.string("hotel_id", 30).notNullable();
table
.foreign("hotel_id", "package_detail_tour_hotels_hotel_id_foreign")
.references("hotel.id")
.onUpdate("NO ACTION")
.onDelete("CASCADE");
});
await knex.schema.alterTable("package_detail", (table) => {
table.string("makkah_hotel_id", 30).notNullable();
table.string("madinah_hotel_id", 30).notNullable();
table
.foreign("makkah_hotel_id", "package_detail_makkah_hotel_id_foreign")
.references("hotel.id")
.onUpdate("NO ACTION")
.onDelete("CASCADE");
table
.foreign("madinah_hotel_id", "package_detail_madinah_hotel_id_foreign")
.references("hotel.id")
.onUpdate("NO ACTION")
.onDelete("CASCADE");
});
}
}

View File

@@ -465,7 +465,7 @@ export class PackageController extends Controller {
const makkahHotel = await orm.em.findOne(
Hotel,
{
id: body.makkah_hotel_id,
id: body.makkah_hotel.hotel_id,
},
{
populate: ["*"],
@@ -487,7 +487,7 @@ export class PackageController extends Controller {
const madinahHotel = await orm.em.findOne(
Hotel,
{
id: body.madinah_hotel_id,
id: body.madinah_hotel.hotel_id,
},
{
populate: ["*"],
@@ -671,8 +671,22 @@ export class PackageController extends Controller {
tourFlight: tourFlightSchedule,
outboundFlight: outboundFlightSchedule,
inboundFlight: inboundFlightSchedule,
makkahHotel: makkahHotel,
madinahHotel: madinahHotel,
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,
@@ -687,22 +701,22 @@ export class PackageController extends Controller {
updatedAt: new Date(),
});
for (const [index, tourHotelId] of (body.tour_hotel_ids ?? []).entries()) {
const tourHotel = await orm.em.findOne(
for (const [index, tourHotel] of (body.tour_hotels ?? []).entries()) {
const tourHotelEntity = await orm.em.findOne(
Hotel,
{
id: tourHotelId,
id: tourHotel.hotel_id,
},
{
populate: ["*"],
},
);
if (!tourHotel) {
if (!tourHotelEntity) {
return res.status(404).json({
data: null,
errors: [
{
path: `tour_hotel_ids.${index}`,
path: `tour_hotels.${index}.hotel_id`,
location: "body",
message: "Hotel not found.",
},
@@ -710,7 +724,14 @@ export class PackageController extends Controller {
});
}
packageDetail.tourHotels.add(tourHotel);
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();
@@ -1029,7 +1050,7 @@ export class PackageController extends Controller {
const makkahHotel = await orm.em.findOne(
Hotel,
{
id: body.makkah_hotel_id,
id: body.makkah_hotel.hotel_id,
},
{
populate: ["*"],
@@ -1051,7 +1072,7 @@ export class PackageController extends Controller {
const madinahHotel = await orm.em.findOne(
Hotel,
{
id: body.madinah_hotel_id,
id: body.madinah_hotel.hotel_id,
},
{
populate: ["*"],
@@ -1258,8 +1279,18 @@ export class PackageController extends Controller {
tourFlight: tourFlightSchedule,
outboundFlight: outboundFlightSchedule,
inboundFlight: inboundFlightSchedule,
makkahHotel: makkahHotel,
madinahHotel: madinahHotel,
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,
@@ -1274,22 +1305,22 @@ export class PackageController extends Controller {
});
packageDetail.tourHotels.set([]);
for (const [index, tourHotelId] of (body.tour_hotel_ids ?? []).entries()) {
const tourHotel = await orm.em.findOne(
for (const [index, tourHotel] of (body.tour_hotels ?? []).entries()) {
const tourHotelEntity = await orm.em.findOne(
Hotel,
{
id: tourHotelId,
id: tourHotel.hotel_id,
},
{
populate: ["*"],
},
);
if (!tourHotel) {
if (!tourHotelEntity) {
return res.status(404).json({
data: null,
errors: [
{
path: `tour_hotel_ids.${index}`,
path: `tour_hotels.${index}.hotel_id`,
location: "body",
message: "Hotel not found.",
},
@@ -1297,7 +1328,14 @@ export class PackageController extends Controller {
});
}
packageDetail.tourHotels.add(tourHotel);
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();

View File

@@ -1,4 +1,5 @@
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 {
@@ -15,6 +16,7 @@ import type { FlightClassResponse } from "@/modules/flight/flight.types";
import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
import type {
PackageDetailResponse,
PackageHotelResponse,
PackageItineraryDayResponse,
PackageItineraryResponse,
PackageItineraryWidgetResponse,
@@ -65,6 +67,14 @@ export class PackageMapper {
return flightClassResponses;
}
private mapHotelSchedule(hotelSchedule: HotelSchedule): PackageHotelResponse {
return {
hotel: this.hotelMapper.mapEntityToResponse(hotelSchedule.hotel),
check_in: dateFns.format(hotelSchedule.checkIn, "yyyy-MM-dd"),
check_out: dateFns.format(hotelSchedule.checkOut, "yyyy-MM-dd"),
};
}
private mapItineraryWidget(
packageItineraryWidget: PackageItineraryWidget,
): PackageItineraryWidgetResponse {
@@ -161,14 +171,10 @@ export class PackageMapper {
outbound_flights: this.mapFlightSchedule(packageDetail.outboundFlight),
inbound_flights: this.mapFlightSchedule(packageDetail.inboundFlight),
tour_hotels: packageDetail.tourHotels.map(
this.hotelMapper.mapEntityToResponse,
),
makkah_hotel: this.hotelMapper.mapEntityToResponse(
packageDetail.makkahHotel,
),
madinah_hotel: this.hotelMapper.mapEntityToResponse(
packageDetail.madinahHotel,
this.mapHotelSchedule.bind(this),
),
makkah_hotel: this.mapHotelSchedule(packageDetail.makkahHotel),
madinah_hotel: this.mapHotelSchedule(packageDetail.madinahHotel),
transportation: this.transportationMapper.mapClassEntityToResponse(
packageDetail.transportation,
),

View File

@@ -51,24 +51,45 @@ export const packageDetailRequestSchema = z.object({
"Must be array.",
)
.nonempty("Must not empty."),
tour_hotel_ids: z
tour_hotels: z
.array(
z
z.object(
{
hotel_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(30, "Max 30 characters."),
check_in: dateSchema,
check_out: dateSchema,
},
"Must be object.",
),
"Must be array.",
)
.nonempty("Must not empty.")
.nullable(),
makkah_hotel_id: z
makkah_hotel: z.object(
{
hotel_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(30, "Max 30 characters."),
madinah_hotel_id: z
check_in: dateSchema,
check_out: dateSchema,
},
"Must be object.",
),
madinah_hotel: z.object(
{
hotel_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")
.max(30, "Max 30 characters."),
check_in: dateSchema,
check_out: dateSchema,
},
"Must be object.",
),
transportation_id: z
.ulid("Must be ulid string.")
.nonempty("Must not empty.")

View File

@@ -37,6 +37,12 @@ export type PackageConsultResponse = {
session_code: string;
};
export type PackageHotelResponse = {
hotel: HotelResponse;
check_in: string;
check_out: string;
};
export type PackageItineraryWidgetResponse =
| {
type: "transport";
@@ -73,9 +79,9 @@ export type PackageDetailResponse = {
tour_flights: FlightClassResponse[] | null;
outbound_flights: FlightClassResponse[];
inbound_flights: FlightClassResponse[];
tour_hotels: HotelResponse[] | null;
makkah_hotel: HotelResponse;
madinah_hotel: HotelResponse;
tour_hotels: PackageHotelResponse[] | null;
makkah_hotel: PackageHotelResponse;
madinah_hotel: PackageHotelResponse;
transportation: TransportationClassResponse;
quad_price: number;
quad_discount: number;