import { z } from "zod" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { dt } from "@scandic-hotels/common/dt" import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator" import { nullableStringEmailValidator, nullableStringValidator, } from "@scandic-hotels/common/utils/zod/stringValidator" import { BookingStatusEnum } from "../../enums/bookingStatus" import { BreakfastPackageEnum } from "../../enums/breakfast" import { ChildBedTypeEnum } from "../../enums/childBedTypeEnum" import { calculateRefId } from "../../utils/refId" export const guestSchema = z.object({ email: nullableStringEmailValidator, firstName: nullableStringValidator, lastName: nullableStringValidator, membershipNumber: nullableStringValidator, phoneNumber: nullableStringValidator, countryCode: nullableStringValidator, }) export type Guest = z.output // QUERY const childBedPreferencesSchema = z.object({ bedType: z.nativeEnum(ChildBedTypeEnum), quantity: z.number().int(), code: z.string().nullable().default(""), }) const priceSchema = z.object({ currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown), totalPrice: z.number().nullish(), totalUnit: z.number().int().nullish(), unit: z.number().int().nullish(), unitPrice: z.number(), }) export const packageSchema = z .object({ code: nullableStringValidator, comment: z.string().nullish(), description: nullableStringValidator, price: priceSchema, type: z.string().nullish(), }) .transform((packageData) => ({ code: packageData.code, comment: packageData.comment, currency: packageData.price.currency, description: packageData.description, totalPrice: packageData.price.totalPrice ?? 0, totalUnit: packageData.price.totalUnit ?? 0, type: packageData.type, unit: packageData.price.unit ?? 0, unitPrice: packageData.price.unitPrice, })) const ancillarySchema = z .object({ comment: z.string().default(""), deliveryTime: z.string().default(""), }) .nullable() .default({ comment: "", deliveryTime: "", }) const rateDefinitionSchema = z.object({ breakfastIncluded: z.boolean().default(false), cancellationRule: z.string().nullable().default(""), cancellationText: z.string().nullable().default(""), generalTerms: z.array(z.string()).default([]), isMemberRate: z.boolean().default(false), mustBeGuaranteed: z.boolean().default(false), rateCode: z.string().default(""), title: z.string().nullable().default(""), isCampaignRate: z.boolean().default(false), }) export const linkedReservationSchema = z.object({ confirmationNumber: z.string().default(""), hotelId: z.string().default(""), checkinDate: z.string(), checkoutDate: z.string(), cancellationNumber: nullableStringValidator, roomTypeCode: z.string().default(""), adults: z.number().int(), children: z.number().int(), profileId: z.string().default(""), }) const linksSchema = z.object({ addAncillary: z .object({ href: z.string(), meta: z.object({ method: z.string(), }), }) .nullable(), cancel: z .object({ href: z.string(), meta: z.object({ method: z.string(), }), }) .nullable(), guarantee: z .object({ href: z.string(), meta: z.object({ method: z.string(), }), }) .nullable(), modify: z .object({ href: z.string(), meta: z.object({ method: z.string(), }), }) .nullable(), self: z .object({ href: z.string(), meta: z.object({ method: z.string(), }), }) .nullable(), }) export const bookingConfirmationSchema = z .object({ data: z.object({ attributes: z.object({ adults: z.number().int(), ancillary: ancillarySchema, bookingType: z.string().optional(), cancelationNumber: z.string().nullable().default(""), checkInDate: z.string().refine((val) => dt(val).isValid()), checkOutDate: z.string().refine((val) => dt(val).isValid()), childBedPreferences: z.array(childBedPreferencesSchema).default([]), childrenAges: z.array(z.number().int()).default([]), canChangeDate: z.boolean(), bookingCode: z.string().nullable(), cheques: z.number(), vouchers: z.number(), guaranteeInfo: z .object({ maskedCard: z.string(), cardType: z.string(), paymentMethod: z.string(), paymentMethodDescription: z.string(), }) .nullish(), computedReservationStatus: z.string().nullable().default(""), confirmationNumber: nullableStringValidator, createDateTime: z.string().refine((val) => dt(val).isValid()), currencyCode: z.nativeEnum(CurrencyEnum), guest: guestSchema, linkedReservations: nullableArrayObjectValidator( linkedReservationSchema ), hotelId: z.string(), mainRoom: z.boolean(), multiRoom: z.boolean(), packages: z.array(packageSchema).default([]), rateDefinition: rateDefinitionSchema, reservationStatus: z.string().nullable().default(""), roomPoints: z.number(), roomPointType: z .enum(["Scandic", "EuroBonus"]) .nullable() .default(null) .catch(null), roomPrice: z.number(), roomTypeCode: z.string().default(""), totalPoints: z.number(), totalPrice: z.number(), totalPriceExVat: z.number(), vatAmount: z.number(), vatPercentage: z.number(), }), id: z.string(), type: z.literal("booking"), links: linksSchema, }), }) .transform(({ data }) => ({ ...data.attributes, refId: calculateRefId( data.attributes.confirmationNumber, data.attributes.guest.lastName ), linkedReservations: data.attributes.linkedReservations.map( (linkedReservation) => { /** * We lazy load linked reservations in the client. * The problem is that we need to load the reservation in order to * calculate the refId for the reservation as the refId uses the guest's * lastname in it. Ideally we should pass a promise to the React * component that uses `use()` to resolve it. But right now we use tRPC * in the client. That tRPC endpoint only uses the confirmationNumber * from the refId. So that means we can pass whatever as the lastname * here, because it is actually never read. We should change this ASAP. */ return { ...linkedReservation, refId: calculateRefId( linkedReservation.confirmationNumber, "" // TODO: Empty lastname here, see comment above ), } } ), packages: data.attributes.packages.filter((p) => p.type !== "Ancillary"), ancillaries: data.attributes.packages.filter((p) => p.type === "Ancillary"), extraBedTypes: data.attributes.childBedPreferences, showAncillaries: !!( data.links.addAncillary || data.attributes.packages.some( (p) => p.type === "Ancillary" || p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST ) ) && data.attributes.reservationStatus !== BookingStatusEnum.Cancelled, isCancelable: !!data.links.cancel, isModifiable: !!data.links.modify, isGuaranteeable: !!data.links.guarantee, canModifyAncillaries: !!data.links.addAncillary, // Typo from API cancellationNumber: data.attributes.cancelationNumber, }))