feat(SW-791): make confirmation page dynamic
This commit is contained in:
@@ -2,6 +2,10 @@ import { z } from "zod"
|
||||
|
||||
import { ChildBedTypeEnum } from "@/constants/booking"
|
||||
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
|
||||
// MUTATION
|
||||
export const createBookingSchema = z
|
||||
.object({
|
||||
@@ -42,20 +46,20 @@ const extraBedTypesSchema = z.object({
|
||||
})
|
||||
|
||||
const guestSchema = z.object({
|
||||
email: z.string().email().nullable().default(""),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
email: z.string().nullable(),
|
||||
phoneNumber: z.string().nullable(),
|
||||
phoneNumber: phoneValidator().nullable().default(""),
|
||||
})
|
||||
|
||||
const packagesSchema = z.array(
|
||||
z.object({
|
||||
accessibility: z.boolean().optional(),
|
||||
allergyFriendly: z.boolean().optional(),
|
||||
breakfast: z.boolean().optional(),
|
||||
petFriendly: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
const packageSchema = z.object({
|
||||
code: z.string().default(""),
|
||||
currency: z.nativeEnum(CurrencyEnum),
|
||||
quantity: z.number().int(),
|
||||
totalPrice: z.number(),
|
||||
totalQuantity: z.number().int(),
|
||||
unitPrice: z.number(),
|
||||
})
|
||||
|
||||
export const bookingConfirmationSchema = z
|
||||
.object({
|
||||
@@ -66,17 +70,21 @@ export const bookingConfirmationSchema = z
|
||||
checkOutDate: z.date({ coerce: true }),
|
||||
createDateTime: z.date({ coerce: true }),
|
||||
childrenAges: z.array(z.number()),
|
||||
extraBedTypes: z.array(extraBedTypesSchema),
|
||||
extraBedTypes: z.array(extraBedTypesSchema).default([]),
|
||||
computedReservationStatus: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
currencyCode: z.string(),
|
||||
currencyCode: z.nativeEnum(CurrencyEnum),
|
||||
guest: guestSchema,
|
||||
hasPayRouting: z.boolean().optional(),
|
||||
hotelId: z.string(),
|
||||
packages: packagesSchema,
|
||||
packages: z.array(packageSchema),
|
||||
rateCode: z.string(),
|
||||
reservationStatus: z.string(),
|
||||
roomPrice: z.number().int(),
|
||||
roomTypeCode: z.string(),
|
||||
totalPrice: z.number(),
|
||||
totalPriceExVat: z.number(),
|
||||
vatAmount: z.number(),
|
||||
vatPercentage: z.number(),
|
||||
}),
|
||||
id: z.string(),
|
||||
type: z.literal("booking"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
|
||||
import { router, serviceProcedure } from "@/server/trpc"
|
||||
|
||||
@@ -87,6 +88,28 @@ export const bookingQueryRouter = router({
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
if (!hotelData) {
|
||||
getBookingConfirmationFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
hotelId: booking.data.hotelId,
|
||||
error_type: "http_error",
|
||||
error: "Couldn`t get hotel",
|
||||
})
|
||||
console.error(
|
||||
"api.booking.confirmation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber, hotelId: booking.data.hotelId },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text: "Couldn`t get hotel",
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw serverErrorByStatus(404)
|
||||
}
|
||||
|
||||
getBookingConfirmationSuccessCounter.add(1, { confirmationNumber })
|
||||
console.info(
|
||||
"api.booking.confirmation success",
|
||||
@@ -95,44 +118,31 @@ export const bookingQueryRouter = router({
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Add hotels check in and out times to booking check in and out date
|
||||
* as that is date only (YYYY-MM-DD)
|
||||
*/
|
||||
const checkInTime =
|
||||
hotelData.data.attributes.hotelFacts.checkin.checkInTime
|
||||
const [checkInHour, checkInMinute] = checkInTime.split(":")
|
||||
const checkIn = dt(booking.data.checkInDate)
|
||||
.set("hour", Number(checkInHour))
|
||||
.set("minute", Number(checkInMinute))
|
||||
const checkOutTime =
|
||||
hotelData.data.attributes.hotelFacts.checkin.checkOutTime
|
||||
const [checkOutHour, checkOutMinute] = checkOutTime.split(":")
|
||||
const checkOut = dt(booking.data.checkOutDate)
|
||||
.set("hour", Number(checkOutHour))
|
||||
.set("minute", Number(checkOutMinute))
|
||||
|
||||
booking.data.checkInDate = checkIn.toDate()
|
||||
booking.data.checkOutDate = checkOut.toDate()
|
||||
|
||||
return {
|
||||
...booking.data,
|
||||
hotel: hotelData,
|
||||
temp: {
|
||||
breakfastFrom: "06:30",
|
||||
breakfastTo: "11:00",
|
||||
cancelPolicy: "Free rebooking",
|
||||
fromDate: "2024-10-21 14:00",
|
||||
packages: [
|
||||
{
|
||||
name: "Breakfast buffet",
|
||||
price: "150 SEK",
|
||||
},
|
||||
{
|
||||
name: "Member discount",
|
||||
price: "-297 SEK",
|
||||
},
|
||||
{
|
||||
name: "Points used / remaining",
|
||||
price: "0 / 1044",
|
||||
},
|
||||
],
|
||||
payment: "2024-08-09 1:47",
|
||||
room: {
|
||||
price: "2 589 SEK",
|
||||
type: "Cozy Cabin",
|
||||
vat: "684,79 SEK",
|
||||
},
|
||||
toDate: "2024-10-22 11:00",
|
||||
total: "2 739 SEK",
|
||||
totalInEuro: "265 EUR",
|
||||
},
|
||||
guest: {
|
||||
email: "sarah.obrian@gmail.com",
|
||||
firstName: "Sarah",
|
||||
lastName: "O'Brian",
|
||||
memberbershipNumber: "19822",
|
||||
phoneNumber: "+46702446688",
|
||||
booking: booking.data,
|
||||
hotel: {
|
||||
...hotelData.data.attributes,
|
||||
included: hotelData.included,
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -381,11 +381,54 @@ const merchantInformationSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
const hotelFacilityDetailSchema = z
|
||||
.object({
|
||||
description: z.string(),
|
||||
heading: z.string(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
/** Possibly more values */
|
||||
const hotelFacilityDetailsSchema = z.object({
|
||||
breakfast: hotelFacilityDetailSchema,
|
||||
checkout: hotelFacilityDetailSchema,
|
||||
gym: hotelFacilityDetailSchema,
|
||||
internet: hotelFacilityDetailSchema,
|
||||
laundry: hotelFacilityDetailSchema,
|
||||
luggage: hotelFacilityDetailSchema,
|
||||
shop: hotelFacilityDetailSchema,
|
||||
telephone: hotelFacilityDetailSchema,
|
||||
})
|
||||
|
||||
const hotelInformationSchema = z
|
||||
.object({
|
||||
description: z.string(),
|
||||
heading: z.string(),
|
||||
link: z.string().optional(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
const hotelInformationsSchema = z.object({
|
||||
accessibility: hotelInformationSchema,
|
||||
safety: hotelInformationSchema,
|
||||
sustainability: hotelInformationSchema,
|
||||
})
|
||||
|
||||
const hotelFactsSchema = z.object({
|
||||
checkin: checkinSchema,
|
||||
ecoLabels: ecoLabelsSchema,
|
||||
hotelFacilityDetail: hotelFacilityDetailsSchema.default({}),
|
||||
hotelInformation: hotelInformationsSchema.default({}),
|
||||
interior: interiorSchema,
|
||||
receptionHours: receptionHoursSchema,
|
||||
yearBuilt: z.string(),
|
||||
})
|
||||
|
||||
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
||||
export const getHotelDataSchema = z.object({
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
type: z.string(), // No enum here but the standard return appears to be "hotels".
|
||||
type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
|
||||
language: z.string().transform((val) => {
|
||||
const lang = toLang(val)
|
||||
if (!lang) {
|
||||
@@ -394,44 +437,41 @@ export const getHotelDataSchema = z.object({
|
||||
return lang
|
||||
}),
|
||||
attributes: z.object({
|
||||
name: z.string(),
|
||||
operaId: z.string(),
|
||||
keywords: z.array(z.string()),
|
||||
isPublished: z.boolean(),
|
||||
accessibilityElevatorPitchText: z.string().optional(),
|
||||
address: addressSchema,
|
||||
cityId: z.string(),
|
||||
cityName: z.string(),
|
||||
ratings: ratingsSchema,
|
||||
address: addressSchema,
|
||||
conferencesAndMeetings: facilitySchema.optional(),
|
||||
contactInformation: contactInformationSchema,
|
||||
hotelFacts: z.object({
|
||||
checkin: checkinSchema,
|
||||
ecoLabels: ecoLabelsSchema,
|
||||
interior: interiorSchema,
|
||||
receptionHours: receptionHoursSchema,
|
||||
yearBuilt: z.string(),
|
||||
}),
|
||||
location: locationSchema,
|
||||
hotelContent: hotelContentSchema,
|
||||
detailedFacilities: z
|
||||
.array(detailedFacilitySchema)
|
||||
.transform((facilities) =>
|
||||
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
),
|
||||
gallery: gallerySchema.optional(),
|
||||
healthAndWellness: facilitySchema.optional(),
|
||||
healthFacilities: z.array(healthFacilitySchema),
|
||||
hotelContent: hotelContentSchema,
|
||||
hotelFacts: hotelFactsSchema,
|
||||
hotelRoomElevatorPitchText: z.string().optional(),
|
||||
hotelType: z.string().optional(),
|
||||
isActive: z.boolean(),
|
||||
isPublished: z.boolean(),
|
||||
keywords: z.array(z.string()),
|
||||
location: locationSchema,
|
||||
merchantInformationData: merchantInformationSchema,
|
||||
rewardNight: rewardNightSchema,
|
||||
name: z.string(),
|
||||
operaId: z.string(),
|
||||
parking: z.array(parkingSchema),
|
||||
pointsOfInterest: z
|
||||
.array(pointOfInterestSchema)
|
||||
.transform((pois) => pois.sort((a, b) => a.distance - b.distance)),
|
||||
parking: z.array(parkingSchema),
|
||||
specialNeedGroups: z.array(specialNeedGroupSchema),
|
||||
ratings: ratingsSchema,
|
||||
rewardNight: rewardNightSchema,
|
||||
restaurantImages: facilitySchema.optional(),
|
||||
socialMedia: socialMediaSchema,
|
||||
specialAlerts: specialAlertsSchema,
|
||||
isActive: z.boolean(),
|
||||
conferencesAndMeetings: facilitySchema.optional(),
|
||||
healthAndWellness: facilitySchema.optional(),
|
||||
restaurantImages: facilitySchema.optional(),
|
||||
gallery: gallerySchema.optional(),
|
||||
specialNeedGroups: z.array(specialNeedGroupSchema),
|
||||
}),
|
||||
relationships: relationshipsSchema,
|
||||
}),
|
||||
@@ -631,7 +671,7 @@ export const apiCitiesByCountrySchema = z.object({
|
||||
})
|
||||
|
||||
export interface CitiesByCountry
|
||||
extends z.output<typeof apiCitiesByCountrySchema> {}
|
||||
extends z.output<typeof apiCitiesByCountrySchema> { }
|
||||
export type CitiesGroupedByCountry = Record<string, CitiesByCountry["data"]>
|
||||
|
||||
export const apiCountriesSchema = z.object({
|
||||
@@ -661,7 +701,7 @@ export const apiCountriesSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
export interface Countries extends z.output<typeof apiCountriesSchema> {}
|
||||
export interface Countries extends z.output<typeof apiCountriesSchema> { }
|
||||
|
||||
export const apiLocationCitySchema = z.object({
|
||||
attributes: z.object({
|
||||
@@ -802,10 +842,7 @@ export const breakfastPackageSchema = z.object({
|
||||
description: z.string(),
|
||||
localPrice: breakfastPackagePriceSchema,
|
||||
requestedPrice: breakfastPackagePriceSchema,
|
||||
packageType: z.enum([
|
||||
PackageTypeEnum.BreakfastAdult,
|
||||
PackageTypeEnum.BreakfastChildren,
|
||||
]),
|
||||
packageType: z.literal(PackageTypeEnum.BreakfastAdult),
|
||||
})
|
||||
|
||||
export const breakfastPackagesSchema = z
|
||||
|
||||
@@ -1062,20 +1062,10 @@ export const hotelQueryRouter = router({
|
||||
user.membership &&
|
||||
["L6", "L7"].includes(user.membership.membershipLevel)
|
||||
) {
|
||||
const originalBreakfastPackage = breakfastPackages.data.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
const freeBreakfastPackage = breakfastPackages.data.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||
)
|
||||
if (freeBreakfastPackage && freeBreakfastPackage.localPrice) {
|
||||
if (
|
||||
originalBreakfastPackage &&
|
||||
originalBreakfastPackage.localPrice
|
||||
) {
|
||||
freeBreakfastPackage.localPrice.price =
|
||||
originalBreakfastPackage.localPrice.price
|
||||
}
|
||||
if (freeBreakfastPackage?.localPrice) {
|
||||
return [freeBreakfastPackage]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const getStaysSchema = z.object({
|
||||
relationships: z.object({
|
||||
hotel: z.object({
|
||||
links: z.object({
|
||||
related: z.string(),
|
||||
related: z.string().nullable().optional(),
|
||||
}),
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user