Merge branch 'master' into feature/tracking

This commit is contained in:
Linus Flood
2024-11-11 15:47:30 +01:00
253 changed files with 3837 additions and 2268 deletions

View File

@@ -1,5 +1,7 @@
import { z } from "zod"
import { ChildBedTypeEnum } from "@/constants/booking"
const roomsSchema = z.array(
z.object({
adults: z.number().int().nonnegative(),
@@ -7,7 +9,7 @@ const roomsSchema = z.array(
.array(
z.object({
age: z.number().int().nonnegative(),
bedType: z.string(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
)
.default([]),
@@ -18,7 +20,7 @@ const roomsSchema = z.array(
firstName: z.string(),
lastName: z.string(),
email: z.string().email(),
phoneCountryCodePrefix: z.string(),
phoneCountryCodePrefix: z.string().nullable(),
phoneNumber: z.string(),
countryCode: z.string(),
membershipNumber: z.string().optional(),
@@ -30,6 +32,7 @@ const roomsSchema = z.array(
petFriendly: z.boolean(),
accessibility: z.boolean(),
}),
roomPrice: z.number().or(z.string().transform((val) => Number(val))),
})
)
@@ -42,12 +45,14 @@ const paymentSchema = z.object({
cardType: z.string(),
})
.optional(),
cardHolder: z.object({
email: z.string().email(),
name: z.string(),
phoneCountryCode: z.string(),
phoneSubscriber: z.string(),
}),
cardHolder: z
.object({
email: z.string().email(),
name: z.string(),
phoneCountryCode: z.string(),
phoneSubscriber: z.string(),
})
.optional(),
success: z.string(),
error: z.string(),
cancel: z.string(),

View File

@@ -1,6 +1,10 @@
import { z } from "zod"
import { BedTypeEnum } from "@/constants/booking"
import { ChildBedTypeEnum } from "@/constants/booking"
import { phoneValidator } from "@/utils/phoneValidator"
import { CurrencyEnum } from "@/types/enums/currency"
// MUTATION
export const createBookingSchema = z
@@ -36,26 +40,26 @@ export const createBookingSchema = z
}))
// QUERY
const childrenAgesSchema = z.object({
age: z.number(),
bedType: z.nativeEnum(BedTypeEnum),
const extraBedTypesSchema = z.object({
quantity: z.number(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
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({
@@ -65,17 +69,22 @@ export const bookingConfirmationSchema = z
checkInDate: z.date({ coerce: true }),
checkOutDate: z.date({ coerce: true }),
createDateTime: z.date({ coerce: true }),
childrenAges: z.array(childrenAgesSchema),
childrenAges: z.array(z.number()),
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"),

View File

@@ -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,
},
}
}),

View File

@@ -1,6 +1,6 @@
import { z } from "zod"
import { BedTypeEnum } from "@/constants/booking"
import { ChildBedTypeEnum } from "@/constants/booking"
import { dt } from "@/lib/dt"
import { toLang } from "@/server/utils"
@@ -312,34 +312,35 @@ const socialMediaSchema = z.object({
facebook: z.string().optional(),
})
const metaSpecialAlertSchema = z.object({
const specialAlertSchema = z.object({
type: z.string(),
title: z.string().optional(),
description: z.string().optional(),
displayInBookingFlow: z.boolean(),
startDate: z.string(),
endDate: z.string(),
startDate: z.string().optional(),
endDate: z.string().optional(),
})
const metaSchema = z.object({
specialAlerts: z
.array(metaSpecialAlertSchema)
.transform((data) => {
const now = dt().utc().format("YYYY-MM-DD")
const filteredAlerts = data.filter((alert) => {
const shouldShowNow = alert.startDate <= now && alert.endDate >= now
const hasText = alert.description || alert.title
return shouldShowNow && hasText
})
return filteredAlerts.map((alert, idx) => ({
id: `alert-${alert.type}-${idx}`,
type: AlertTypeEnum.Info,
heading: alert.title || null,
text: alert.description || null,
}))
const specialAlertsSchema = z
.array(specialAlertSchema)
.transform((data) => {
const now = dt().utc().format("YYYY-MM-DD")
const filteredAlerts = data.filter((alert) => {
const shouldShowNow =
alert.startDate && alert.endDate
? alert.startDate <= now && alert.endDate >= now
: true
const hasText = alert.description || alert.title
return shouldShowNow && hasText
})
.default([]),
})
return filteredAlerts.map((alert, idx) => ({
id: `alert-${alert.type}-${idx}`,
type: AlertTypeEnum.Info,
heading: alert.title || null,
text: alert.description || null,
}))
})
.default([])
const relationshipsSchema = z.object({
restaurants: z.object({
@@ -380,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) {
@@ -393,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),
socialMedia: socialMediaSchema,
meta: metaSchema.optional(),
isActive: z.boolean(),
conferencesAndMeetings: facilitySchema.optional(),
healthAndWellness: facilitySchema.optional(),
ratings: ratingsSchema,
rewardNight: rewardNightSchema,
restaurantImages: facilitySchema.optional(),
gallery: gallerySchema.optional(),
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
specialNeedGroups: z.array(specialNeedGroupSchema),
}),
relationships: relationshipsSchema,
}),
@@ -441,7 +482,7 @@ export const getHotelDataSchema = z.object({
export const childrenSchema = z.object({
age: z.number(),
bedType: z.nativeEnum(BedTypeEnum),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
const occupancySchema = z.object({
@@ -801,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

View File

@@ -57,7 +57,7 @@ import {
} from "./utils"
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import type { BedType } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
@@ -294,7 +294,7 @@ export const hotelQueryRouter = router({
const hotelAttributes = hotelData.data.attributes
const images = hotelAttributes.gallery?.smallerImages
const hotelAlerts = hotelAttributes.meta?.specialAlerts || []
const hotelAlerts = hotelAttributes.specialAlerts
const roomCategories = included
? included.filter((item) => item.type === "roomcategories")
@@ -731,13 +731,11 @@ export const hotelQueryRouter = router({
return null
}
const memberRate = selectedRoom.products.find(
(rate) => rate.productType.member?.rateCode === rateCode
)?.productType.member
const publicRate = selectedRoom.products.find(
(rate) => rate.productType.public?.rateCode === rateCode
)?.productType.public
const rateTypes = selectedRoom.products.find(
(rate) =>
rate.productType.public?.rateCode === rateCode ||
rate.productType.member?.rateCode === rateCode
)?.productType
const mustBeGuaranteed =
validateAvailabilityData.data.rateDefinitions.filter(
@@ -765,7 +763,7 @@ export const hotelQueryRouter = router({
}
}
})
.filter((bed): bed is BedType => Boolean(bed))
.filter((bed): bed is BedTypeSelection => Boolean(bed))
selectedRoomAvailabilitySuccessCounter.add(1, {
hotelId,
@@ -787,8 +785,8 @@ export const hotelQueryRouter = router({
selectedRoom,
mustBeGuaranteed,
cancellationText,
memberRate,
publicRate,
memberRate: rateTypes?.member,
publicRate: rateTypes?.public,
bedTypes,
}
}),
@@ -976,11 +974,10 @@ export const hotelQueryRouter = router({
const { lang } = ctx
const apiLang = toApiLang(lang)
const params = {
Adults: input.adults,
EndDate: dt(input.toDate).format("YYYY-MM-DD"),
StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
EndDate: dt(input.toDate).format("YYYY-MM-D"),
StartDate: dt(input.fromDate).format("YYYY-MM-D"),
language: apiLang,
}
@@ -1064,20 +1061,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]
}
}

View File

@@ -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(),