feat: get breakfast package from API

This commit is contained in:
Simon Emanuelsson
2024-10-28 10:12:03 +01:00
parent fc8844eb96
commit 62f549e85d
47 changed files with 718 additions and 210 deletions

View File

@@ -148,7 +148,7 @@ export const editProfile = protectedServerActionProcedure
) )
} }
const apiResponse = await api.patch(api.endpoints.v1.profile, { const apiResponse = await api.patch(api.endpoints.v1.Profile.profile, {
body, body,
cache: "no-store", cache: "no-store",
headers: { headers: {

View File

@@ -55,7 +55,7 @@ export const registerUser = serviceServerActionProcedure
let apiResponse let apiResponse
try { try {
apiResponse = await api.post(api.endpoints.v1.profile, { apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
body: parsedPayload.data, body: parsedPayload.data,
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,

View File

@@ -33,7 +33,7 @@ export const registerUserBookingFlow = serviceServerActionProcedure
// TODO: Consume the API to register the user as soon as passwordless signup is enabled. // TODO: Consume the API to register the user as soon as passwordless signup is enabled.
// let apiResponse // let apiResponse
// try { // try {
// apiResponse = await api.post(api.endpoints.v1.profile, { // apiResponse = await api.post(api.endpoints.v1.Profile.profile, {
// body: payload, // body: payload,
// headers: { // headers: {
// Authorization: `Bearer ${ctx.serviceToken}`, // Authorization: `Bearer ${ctx.serviceToken}`,

View File

@@ -0,0 +1 @@
export { default } from "../page"

View File

@@ -0,0 +1,5 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function LoadingHotelHeader() {
return <LoadingSpinner />
}

View File

@@ -0,0 +1,22 @@
import { redirect } from "next/navigation"
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
import type { LangParams, PageArgs } from "@/types/params"
export default async function HotelHeader({
params,
searchParams,
}: PageArgs<LangParams, { hotel: string }>) {
const home = `/${params.lang}`
if (!searchParams.hotel) {
redirect(home)
}
const hotel = await getHotelData(searchParams.hotel, params.lang)
if (!hotel?.data) {
redirect(home)
}
return <HotelSelectionHeader hotel={hotel.data.attributes} />
}

View File

@@ -0,0 +1 @@
export { default } from "../page"

View File

@@ -0,0 +1,5 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function LoadingHotelSidePeek() {
return <LoadingSpinner />
}

View File

@@ -0,0 +1,21 @@
import { redirect } from "next/navigation"
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek"
import type { LangParams, PageArgs } from "@/types/params"
export default async function HotelSidePeek({
params,
searchParams,
}: PageArgs<LangParams, { hotel: string }>) {
if (!searchParams.hotel) {
redirect(`/${params.lang}`)
}
const hotel = await getHotelData(searchParams.hotel, params.lang)
if (!hotel?.data) {
redirect(`/${params.lang}`)
}
return <SidePeek hotel={hotel.data.attributes} />
}

View File

@@ -1,46 +1,32 @@
import { redirect } from "next/navigation"
import {
getCreditCardsSafely,
getHotelData,
getProfileSafely,
} from "@/lib/trpc/memoizedRequests"
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek"
import Summary from "@/components/HotelReservation/EnterDetails/Summary" import Summary from "@/components/HotelReservation/EnterDetails/Summary"
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import { preload } from "./page"
import styles from "./layout.module.css" import styles from "./layout.module.css"
import { StepEnum } from "@/types/components/enterDetails/step" import { StepEnum } from "@/types/components/enterDetails/step"
import type { LangParams, LayoutArgs } from "@/types/params" import type { LangParams, LayoutArgs } from "@/types/params"
function preload(id: string, lang: string) {
void getHotelData(id, lang)
void getProfileSafely()
void getCreditCardsSafely()
}
export default async function StepLayout({ export default async function StepLayout({
children, children,
hotelHeader,
params, params,
}: React.PropsWithChildren<LayoutArgs<LangParams & { step: StepEnum }>>) { sidePeek,
setLang(params.lang) }: React.PropsWithChildren<
preload("811", params.lang) LayoutArgs<LangParams & { step: StepEnum }> & {
hotelHeader: React.ReactNode
const hotel = await getHotelData("811", params.lang) sidePeek: React.ReactNode
if (!hotel?.data) {
redirect(`/${params.lang}`)
} }
>) {
setLang(params.lang)
preload()
return ( return (
<EnterDetailsProvider step={params.step}> <EnterDetailsProvider step={params.step}>
<main className={styles.layout}> <main className={styles.layout}>
<HotelSelectionHeader hotel={hotel.data.attributes} /> {hotelHeader}
<div className={styles.content}> <div className={styles.content}>
<SelectedRoom /> <SelectedRoom />
{children} {children}
@@ -48,7 +34,7 @@ export default async function StepLayout({
<Summary /> <Summary />
</aside> </aside>
</div> </div>
<SidePeek hotel={hotel.data.attributes} /> {sidePeek}
</main> </main>
</EnterDetailsProvider> </EnterDetailsProvider>
) )

View File

@@ -1,6 +1,7 @@
import { notFound } from "next/navigation" import { notFound, redirect } from "next/navigation"
import { import {
getBreakfastPackages,
getCreditCardsSafely, getCreditCardsSafely,
getHotelData, getHotelData,
getProfileSafely, getProfileSafely,
@@ -17,22 +18,32 @@ import { getIntl } from "@/i18n"
import { StepEnum } from "@/types/components/enterDetails/step" import { StepEnum } from "@/types/components/enterDetails/step"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export function preload() {
void getProfileSafely()
void getCreditCardsSafely()
}
function isValidStep(step: string): step is StepEnum { function isValidStep(step: string): step is StepEnum {
return Object.values(StepEnum).includes(step as StepEnum) return Object.values(StepEnum).includes(step as StepEnum)
} }
export default async function StepPage({ export default async function StepPage({
params, params,
}: PageArgs<LangParams & { step: StepEnum }>) { searchParams,
const { step, lang } = params }: PageArgs<LangParams & { step: StepEnum }, { hotel: string }>) {
if (!searchParams.hotel) {
redirect(`/${params.lang}`)
}
void getBreakfastPackages(searchParams.hotel)
const intl = await getIntl() const intl = await getIntl()
const hotel = await getHotelData("811", lang) const hotel = await getHotelData(searchParams.hotel, params.lang)
const user = await getProfileSafely() const user = await getProfileSafely()
const savedCreditCards = await getCreditCardsSafely() const savedCreditCards = await getCreditCardsSafely()
const breakfastPackages = await getBreakfastPackages(searchParams.hotel)
if (!isValidStep(step) || !hotel) { if (!isValidStep(params.step) || !hotel) {
return notFound() return notFound()
} }
@@ -51,7 +62,7 @@ export default async function StepPage({
step={StepEnum.breakfast} step={StepEnum.breakfast}
label={intl.formatMessage({ id: "Select breakfast options" })} label={intl.formatMessage({ id: "Select breakfast options" })}
> >
<Breakfast /> <Breakfast packages={breakfastPackages} />
</SectionAccordion> </SectionAccordion>
<SectionAccordion <SectionAccordion
header="Details" header="Details"

View File

@@ -15,7 +15,7 @@ import { bedTypeSchema } from "./schema"
import styles from "./bedOptions.module.css" import styles from "./bedOptions.module.css"
import type { BedTypeSchema } from "@/types/components/enterDetails/bedType" import type { BedTypeSchema } from "@/types/components/enterDetails/bedType"
import { bedTypeEnum } from "@/types/enums/bedType" import { BedTypeEnum } from "@/types/enums/bedType"
export default function BedType() { export default function BedType() {
const intl = useIntl() const intl = useIntl()
@@ -61,7 +61,7 @@ export default function BedType() {
<RadioCard <RadioCard
Icon={KingBedIcon} Icon={KingBedIcon}
iconWidth={46} iconWidth={46}
id={bedTypeEnum.KING} id={BedTypeEnum.KING}
name="bedType" name="bedType"
subtitle={intl.formatMessage( subtitle={intl.formatMessage(
{ id: "{width} cm × {length} cm" }, { id: "{width} cm × {length} cm" },
@@ -72,12 +72,12 @@ export default function BedType() {
)} )}
text={text} text={text}
title={intl.formatMessage({ id: "King bed" })} title={intl.formatMessage({ id: "King bed" })}
value={bedTypeEnum.KING} value={BedTypeEnum.KING}
/> />
<RadioCard <RadioCard
Icon={KingBedIcon} Icon={KingBedIcon}
iconWidth={46} iconWidth={46}
id={bedTypeEnum.QUEEN} id={BedTypeEnum.QUEEN}
name="bedType" name="bedType"
subtitle={intl.formatMessage( subtitle={intl.formatMessage(
{ id: "{width} cm × {length} cm" }, { id: "{width} cm × {length} cm" },
@@ -88,7 +88,7 @@ export default function BedType() {
)} )}
text={text} text={text}
title={intl.formatMessage({ id: "Queen bed" })} title={intl.formatMessage({ id: "Queen bed" })}
value={bedTypeEnum.QUEEN} value={BedTypeEnum.QUEEN}
/> />
</form> </form>
</FormProvider> </FormProvider>

View File

@@ -1,7 +1,7 @@
import { z } from "zod" import { z } from "zod"
import { bedTypeEnum } from "@/types/enums/bedType" import { BedTypeEnum } from "@/types/enums/bedType"
export const bedTypeSchema = z.object({ export const bedTypeSchema = z.object({
bedType: z.nativeEnum(bedTypeEnum), bedType: z.nativeEnum(BedTypeEnum),
}) })

View File

@@ -7,36 +7,50 @@ import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details" import { useEnterDetailsStore } from "@/stores/enter-details"
import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons" import { Highlight } from "@/components/TempDesignSystem/Form/ChoiceCard/_Card"
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
import { breakfastSchema } from "./schema" import { breakfastFormSchema } from "./schema"
import styles from "./breakfast.module.css" import styles from "./breakfast.module.css"
import type { BreakfastSchema } from "@/types/components/enterDetails/breakfast" import type {
import { breakfastEnum } from "@/types/enums/breakfast" BreakfastFormSchema,
BreakfastProps,
} from "@/types/components/enterDetails/breakfast"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function Breakfast() { export default function Breakfast({ packages }: BreakfastProps) {
const intl = useIntl() const intl = useIntl()
const breakfast = useEnterDetailsStore((state) => state.data.breakfast) const breakfast = useEnterDetailsStore((state) => state.data.breakfast)
const methods = useForm<BreakfastSchema>({ let defaultValues = undefined
defaultValues: breakfast ? { breakfast } : undefined, if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {
defaultValues = { breakfast: BreakfastPackageEnum.NO_BREAKFAST }
} else if (breakfast?.code) {
defaultValues = { breakfast: breakfast.code }
}
const methods = useForm<BreakfastFormSchema>({
defaultValues,
criteriaMode: "all", criteriaMode: "all",
mode: "all", mode: "all",
resolver: zodResolver(breakfastSchema), resolver: zodResolver(breakfastFormSchema),
reValidateMode: "onChange", reValidateMode: "onChange",
}) })
const completeStep = useEnterDetailsStore((state) => state.completeStep) const completeStep = useEnterDetailsStore((state) => state.completeStep)
const onSubmit = useCallback( const onSubmit = useCallback(
(values: BreakfastSchema) => { (values: BreakfastFormSchema) => {
completeStep(values) const pkg = packages?.find((p) => p.code === values.breakfast)
if (pkg) {
completeStep({ breakfast: pkg })
} else {
completeStep({ breakfast: BreakfastPackageEnum.NO_BREAKFAST })
}
}, },
[completeStep] [completeStep, packages]
) )
useEffect(() => { useEffect(() => {
@@ -47,30 +61,46 @@ export default function Breakfast() {
return () => subscription.unsubscribe() return () => subscription.unsubscribe()
}, [methods, onSubmit]) }, [methods, onSubmit])
if (!packages) {
return null
}
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
<RadioCard {packages.map((pkg) => (
Icon={BreakfastIcon} <RadioCard
id={breakfastEnum.BREAKFAST} key={pkg.code}
name="breakfast" id={pkg.code}
subtitle={intl.formatMessage<React.ReactNode>( name="breakfast"
{ id: "<b>{amount} {currency}</b>/night per adult" }, subtitle={
{ pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
amount: "150", ? intl.formatMessage<React.ReactNode>(
b: (str) => <b>{str}</b>, { id: "breakfast.price.free" },
currency: "SEK", {
amount: pkg.originalPrice,
currency: pkg.currency,
free: (str) => <Highlight>{str}</Highlight>,
strikethrough: (str) => <s>{str}</s>,
}
)
: intl.formatMessage(
{ id: "breakfast.price" },
{
amount: pkg.packagePrice,
currency: pkg.currency,
}
)
} }
)} text={intl.formatMessage({
text={intl.formatMessage({ id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", })}
})} title={intl.formatMessage({ id: "Breakfast buffet" })}
title={intl.formatMessage({ id: "Breakfast buffet" })} value={pkg.code}
value={breakfastEnum.BREAKFAST} />
/> ))}
<RadioCard <RadioCard
Icon={NoBreakfastIcon} id={BreakfastPackageEnum.NO_BREAKFAST}
id={breakfastEnum.NO_BREAKFAST}
name="breakfast" name="breakfast"
subtitle={intl.formatMessage( subtitle={intl.formatMessage(
{ id: "{amount} {currency}" }, { id: "{amount} {currency}" },
@@ -83,7 +113,7 @@ export default function Breakfast() {
id: "You can always change your mind later and add breakfast at the hotel.", id: "You can always change your mind later and add breakfast at the hotel.",
})} })}
title={intl.formatMessage({ id: "No breakfast" })} title={intl.formatMessage({ id: "No breakfast" })}
value={breakfastEnum.NO_BREAKFAST} value={BreakfastPackageEnum.NO_BREAKFAST}
/> />
</form> </form>
</FormProvider> </FormProvider>

View File

@@ -1,7 +1,15 @@
import { z } from "zod" import { z } from "zod"
import { breakfastEnum } from "@/types/enums/breakfast" import { breakfastPackageSchema } from "@/server/routers/hotels/output"
export const breakfastSchema = z.object({ import { BreakfastPackageEnum } from "@/types/enums/breakfast"
breakfast: z.nativeEnum(breakfastEnum),
export const breakfastStoreSchema = z.object({
breakfast: breakfastPackageSchema.or(
z.literal(BreakfastPackageEnum.NO_BREAKFAST)
),
})
export const breakfastFormSchema = z.object({
breakfast: z.string().or(z.literal(BreakfastPackageEnum.NO_BREAKFAST)),
}) })

View File

@@ -70,3 +70,7 @@
.listItem:nth-of-type(n + 2) { .listItem:nth-of-type(n + 2) {
margin-top: var(--Spacing-x-quarter); margin-top: var(--Spacing-x-quarter);
} }
.highlight {
color: var(--Scandic-Brand-Scandic-Red);
}

View File

@@ -34,3 +34,12 @@ export type CheckboxProps =
export type RadioProps = export type RadioProps =
| Omit<ListCardProps, "type"> | Omit<ListCardProps, "type">
| Omit<TextCardProps, "type"> | Omit<TextCardProps, "type">
export interface ListProps extends Pick<ListCardProps, "declined"> {
list?: ListCardProps["list"]
}
export interface SubtitleProps
extends Pick<BaseCardProps, "highlightSubtitle" | "subtitle"> {}
export interface TextProps extends Pick<TextCardProps, "text"> {}

View File

@@ -2,16 +2,16 @@
import { useFormContext } from "react-hook-form" import { useFormContext } from "react-hook-form"
import { CheckIcon, CloseIcon, HeartIcon } from "@/components/Icons" import { CheckIcon, CloseIcon } from "@/components/Icons"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import styles from "./card.module.css" import styles from "./card.module.css"
import type { CardProps } from "./card" import type { CardProps, ListProps, SubtitleProps, TextProps } from "./card"
export default function Card({ export default function Card({
Icon = HeartIcon, Icon,
iconHeight = 32, iconHeight = 32,
iconWidth = 32, iconWidth = 32,
declined = false, declined = false,
@@ -26,56 +26,79 @@ export default function Card({
value, value,
}: CardProps) { }: CardProps) {
const { register } = useFormContext() const { register } = useFormContext()
return ( return (
<label className={styles.label} data-declined={declined} tabIndex={0}> <label className={styles.label} data-declined={declined} tabIndex={0}>
<Caption className={styles.title} type="label" uppercase> <Caption className={styles.title} color="burgundy" type="label" uppercase>
{title} {title}
</Caption> </Caption>
{subtitle ? ( <Subtitle highlightSubtitle={highlightSubtitle} subtitle={subtitle} />
<Caption {Icon ? (
className={styles.subtitle} <Icon
color={highlightSubtitle ? "baseTextAccent" : "uiTextHighContrast"} className={styles.icon}
type="regular" color="uiTextHighContrast"
> height={iconHeight}
{subtitle} width={iconWidth}
</Caption> />
) : null}
<Icon
className={styles.icon}
color="uiTextHighContrast"
height={iconHeight}
width={iconWidth}
/>
{list
? list.map((listItem) => (
<span key={listItem.title} className={styles.listItem}>
{declined ? (
<CloseIcon
color="uiTextMediumContrast"
height={20}
width={20}
/>
) : (
<CheckIcon color="baseIconLowContrast" height={20} width={20} />
)}
<Footnote color="uiTextMediumContrast">{listItem.title}</Footnote>
</span>
))
: null}
{text ? (
<Footnote className={styles.text} color="uiTextMediumContrast">
{text}
</Footnote>
) : null} ) : null}
<List declined={declined} list={list} />
<Text text={text} />
<input <input
{...register(name)}
aria-hidden aria-hidden
id={id || name} id={id || name}
hidden hidden
type={type} type={type}
value={value} value={value}
{...register(name)}
/> />
</label> </label>
) )
} }
function List({ declined, list }: ListProps) {
if (!list) {
return null
}
return list.map((listItem) => (
<span key={listItem.title} className={styles.listItem}>
{declined ? (
<CloseIcon color="uiTextMediumContrast" height={20} width={20} />
) : (
<CheckIcon color="baseIconLowContrast" height={20} width={20} />
)}
<Footnote color="uiTextMediumContrast">{listItem.title}</Footnote>
</span>
))
}
function Subtitle({ highlightSubtitle, subtitle }: SubtitleProps) {
if (!subtitle) {
return null
}
return (
<Caption
className={styles.subtitle}
color={highlightSubtitle ? "baseTextAccent" : "uiTextMediumContrast"}
type="label"
uppercase
>
{subtitle}
</Caption>
)
}
function Text({ text }: TextProps) {
if (!text) {
return null
}
return (
<Footnote className={styles.text} color="uiTextMediumContrast">
{text}
</Footnote>
)
}
export function Highlight({ children }: React.PropsWithChildren) {
return <span className={styles.highlight}>{children}</span>
}

View File

@@ -1,7 +1,18 @@
export enum BookingStatusEnum { export enum BookingStatusEnum {
CreatedInOhip = "CreatedInOhip",
PaymentRegistered = "PaymentRegistered",
BookingCompleted = "BookingCompleted", BookingCompleted = "BookingCompleted",
Cancelled = "Cancelled",
CheckedOut = "CheckedOut",
ConfirmedInScorpio = "ConfirmedInScorpio",
CreatedInOhip = "CreatedInOhip",
PaymentAuthorized = "PaymentAuthorized",
PaymentCancelled = "PaymentCancelled",
PaymentError = "PaymentError",
PaymentFailed = "PaymentFailed",
PaymentRegistered = "PaymentRegistered",
PaymentSucceeded = "PaymentSucceeded",
PendingAcceptPriceChange = "PendingAcceptPriceChange",
PendingGuarantee = "PendingGuarantee",
PendingPayment = "PendingPayment",
} }
export enum BedTypeEnum { export enum BedTypeEnum {

View File

@@ -1,6 +1,5 @@
{ {
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)", "<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/nat pr. voksen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.", "A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.",
"A photo of the room": "Et foto af værelset", "A photo of the room": "Et foto af værelset",
"ACCE": "Tilgængelighed", "ACCE": "Tilgængelighed",
@@ -366,6 +365,8 @@
"booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}",
"booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.",
"booking.thisRoomIsEquippedWith": "Dette værelse er udstyret med", "booking.thisRoomIsEquippedWith": "Dette værelse er udstyret med",
"breakfast.price": "{amount} {currency}/nat",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/nat",
"by": "inden", "by": "inden",
"characters": "tegn", "characters": "tegn",
"guest": "gæst", "guest": "gæst",

View File

@@ -1,6 +1,5 @@
{ {
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)", "<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/Nacht pro Erwachsener",
"A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.", "A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.",
"A photo of the room": "Ein Foto des Zimmers", "A photo of the room": "Ein Foto des Zimmers",
"ACCE": "Zugänglichkeit", "ACCE": "Zugänglichkeit",
@@ -367,6 +366,8 @@
"booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}",
"booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.",
"booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit", "booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit",
"breakfast.price": "{amount} {currency}/Nacht",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/Nacht",
"by": "bis", "by": "bis",
"characters": "figuren", "characters": "figuren",
"guest": "gast", "guest": "gast",

View File

@@ -1,6 +1,5 @@
{ {
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)", "<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/night per adult",
"A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.", "A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.",
"A photo of the room": "A photo of the room", "A photo of the room": "A photo of the room",
"ACCE": "Accessibility", "ACCE": "Accessibility",
@@ -385,6 +384,8 @@
"booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}",
"booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.", "booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
"booking.thisRoomIsEquippedWith": "This room is equipped with", "booking.thisRoomIsEquippedWith": "This room is equipped with",
"breakfast.price": "{amount} {currency}/night",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/night",
"by": "by", "by": "by",
"characters": "characters", "characters": "characters",
"from": "from", "from": "from",

View File

@@ -1,6 +1,5 @@
{ {
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)", "<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/yö per aikuinen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.", "A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.",
"A photo of the room": "Kuva huoneesta", "A photo of the room": "Kuva huoneesta",
"ACCE": "Saavutettavuus", "ACCE": "Saavutettavuus",
@@ -367,6 +366,8 @@
"booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}", "booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}",
"booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", "booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.",
"booking.thisRoomIsEquippedWith": "Tämä huone on varustettu", "booking.thisRoomIsEquippedWith": "Tämä huone on varustettu",
"breakfast.price": "{amount} {currency}/yö",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/yö",
"by": "mennessä", "by": "mennessä",
"characters": "hahmoja", "characters": "hahmoja",
"guest": "Vieras", "guest": "Vieras",

View File

@@ -1,6 +1,5 @@
{ {
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)", "<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per voksen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.", "A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.",
"A photo of the room": "Et bilde av rommet", "A photo of the room": "Et bilde av rommet",
"ACCE": "Tilgjengelighet", "ACCE": "Tilgjengelighet",
@@ -363,6 +362,8 @@
"booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}",
"booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}",
"booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", "booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med",
"breakfast.price": "{amount} {currency}/natt",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt",
"by": "innen", "by": "innen",
"characters": "tegn", "characters": "tegn",
"guest": "gjest", "guest": "gjest",

View File

@@ -1,6 +1,5 @@
{ {
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)", "<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per vuxen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.", "A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.",
"A photo of the room": "Ett foto av rummet", "A photo of the room": "Ett foto av rummet",
"ACCE": "Tillgänglighet", "ACCE": "Tillgänglighet",
@@ -364,6 +363,8 @@
"booking.rooms": "{totalRooms, plural, one {# rum} other {# rum}}", "booking.rooms": "{totalRooms, plural, one {# rum} other {# rum}}",
"booking.terms": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsLink>Villkoren och villkoren</termsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyLink>Scandics integritetspolicy</privacyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.", "booking.terms": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsLink>Villkoren och villkoren</termsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyLink>Scandics integritetspolicy</privacyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.",
"booking.thisRoomIsEquippedWith": "Detta rum är utrustat med", "booking.thisRoomIsEquippedWith": "Detta rum är utrustat med",
"breakfast.price": "{amount} {currency}/natt",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt",
"by": "innan", "by": "innan",
"characters": "tecken", "characters": "tecken",
"guest": "gäst", "guest": "gäst",

View File

@@ -2,29 +2,190 @@
* Nested enum requires namespace * Nested enum requires namespace
*/ */
export namespace endpoints { export namespace endpoints {
export const enum v0 { namespace base {
profile = "profile/v0/Profile", export const enum path {
availability = "availability",
booking = "booking",
hotel = "hotel",
package = "package",
profile = "profile",
}
export const enum enitity {
Ancillary = "Ancillary",
Availabilities = "availabilities",
Bookings = "Bookings",
Breakfast = "breakfast",
Cities = "Cities",
Countries = "Countries",
Hotels = "Hotels",
Locations = "Locations",
Packages = "packages",
Profile = "Profile",
Reward = "Reward",
Stays = "Stays",
Transaction = "Transaction",
}
} }
export const enum v1 {
hotelsAvailability = "availability/v1/availabilities/city", export namespace v1 {
roomsAvailability = "availability/v1/availabilities/hotel", const version = "v1"
profile = "profile/v1/Profile", /**
booking = "booking/v1/Bookings", * availability (Swagger)
creditCards = `${profile}/creditCards`, * https://tstapi.scandichotels.com/availability/swagger/v1/index.html
city = "hotel/v1/Cities", */
citiesCountry = `${city}/country`, export namespace Availability {
countries = "hotel/v1/Countries", export function city(cityId: string) {
friendTransactions = "profile/v1/Transaction/friendTransactions", return `${base.path.availability}/${version}/${base.enitity.Availabilities}/city/${cityId}`
hotels = "hotel/v1/Hotels", }
initiateSaveCard = `${creditCards}/initiateSaveCard`, export function hotel(hotelId: string) {
locations = "hotel/v1/Locations", return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel/${hotelId}`
previousStays = "booking/v1/Stays/past", }
upcomingStays = "booking/v1/Stays/future", }
rewards = `${profile}/reward`,
tierRewards = `${profile}/TierRewards`, /**
subscriberId = `${profile}/SubscriberId`, * booking (Swagger)
packages = "package/v1/packages/hotel", * https://tstapi.scandichotels.com/booking/swagger/v1/index.html
*/
export namespace Booking {
export const bookings = `${base.path.booking}/${version}/${base.enitity.Bookings}`
export function booking(confirmationNumber: string) {
return `${bookings}/${confirmationNumber}`
}
export function cancel(confirmationNumber: string) {
return `${bookings}/${confirmationNumber}/cancel`
}
export function status(confirmationNumber: string) {
return `${bookings}/${confirmationNumber}/status`
}
export const enum Stays {
future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`,
past = `${base.path.booking}/${version}/${base.enitity.Stays}/past`,
}
}
/**
* hotel (Swagger)
* https://tstapi.scandichotels.com/hotel/swagger/v1/index.html
*/
export namespace Hotel {
export const cities = `${base.path.hotel}/${version}/${base.enitity.Cities}`
export namespace Cities {
export function city(cityId: string) {
return `${cities}/${cityId}`
}
export function country(countryId: string) {
return `${cities}/country/${countryId}`
}
export function hotel(hotelId: string) {
return `${cities}/hotel/${hotelId}`
}
}
export const countries = `${base.path.hotel}/${version}/${base.enitity.Countries}`
export namespace Countries {
export function country(countryId: string) {
return `${countries}/${countryId}`
}
}
export const hotels = `${base.path.hotel}/${version}/${base.enitity.Hotels}`
export namespace Hotels {
export function hotel(hotelId: string) {
return `${hotels}/${hotelId}`
}
export function meetingRooms(hotelId: string) {
return `${hotels}/${hotelId}/meetingRooms`
}
export function merchantInformation(hotelId: string) {
return `${hotels}/${hotelId}/merchantInformation`
}
export function nearbyHotels(hotelId: string) {
return `${hotels}/${hotelId}/nearbyHotels`
}
export function restaurants(hotelId: string) {
return `${hotels}/${hotelId}/restaurants`
}
export function roomCategories(hotelId: string) {
return `${hotels}/${hotelId}/roomCategories`
}
}
export const locations = `${base.path.hotel}/${version}/${base.enitity.Locations}`
}
/**
* package (Swagger)
* https://tstapi.scandichotels.com/package/swagger/v1/index.html
*/
export namespace Package {
export namespace Ancillary {
export function hotel(hotelId: string) {
return `${base.path.package}/${version}/${base.enitity.Ancillary}/hotel/${hotelId}`
}
export function hotelAncillaries(hotelId: string) {
return `${base.path.package}/${version}/${base.enitity.Ancillary}/hotel/${hotelId}/ancillaries`
}
}
export namespace Breakfast {
export function hotel(hotelId: string) {
return `${base.path.package}/${version}/${base.enitity.Breakfast}/hotel/${hotelId}`
}
}
export namespace Packages {
export function hotel(hotelId: string) {
return `${base.path.package}/${version}/${base.enitity.Packages}/hotel/${hotelId}`
}
}
}
/**
* profile (Swagger)
* https://tstapi.scandichotels.com/profile/swagger/v1/index.html
*/
export namespace Profile {
export const invalidateSessions = `${base.path.profile}/${version}/${base.enitity.Profile}/invalidateSessions`
export const membership = `${base.path.profile}/${version}/${base.enitity.Profile}/membership`
export const profile = `${base.path.profile}/${version}/${base.enitity.Profile}`
export const reward = `${base.path.profile}/${version}/${base.enitity.Profile}/reward`
export const subscriberId = `${base.path.profile}/${version}/${base.enitity.Profile}/SubscriberId`
export const tierRewards = `${base.path.profile}/${version}/${base.enitity.Profile}/tierRewards`
export function deleteProfile(profileId: string) {
return `${profile}/${profileId}`
}
export const creditCards = `${base.path.profile}/${version}/${base.enitity.Profile}/creditCards`
export namespace CreditCards {
export const initiateSaveCard = `${creditCards}/initiateSaveCard`
export function deleteCreditCard(creditCardId: string) {
return `${creditCards}/${creditCardId}`
}
export function transaction(transactionId: string) {
return `${creditCards}/${transactionId}`
}
}
export namespace Reward {
export const allTiers = `${base.path.profile}/${version}/${base.enitity.Reward}/AllTiers`
export const reward = `${base.path.profile}/${version}/${base.enitity.Reward}`
export const unwrap = `${base.path.profile}/${version}/${base.enitity.Reward}/Unwrap`
export function claim(rewardId: string) {
return `${base.path.profile}/${version}/${base.enitity.Reward}/Claim/${rewardId}`
}
}
export const enum Transaction {
friendTransactions = `${base.path.profile}/${version}/${base.enitity.Transaction}/friendTransactions`,
}
}
} }
} }
export type Endpoint = endpoints.v0 | endpoints.v1 export type Endpoint = string

View File

@@ -28,7 +28,7 @@ const wrappedFetch = fetchRetry(fetch, {
}) })
export async function get( export async function get(
endpoint: Endpoint | `${Endpoint}/${string}`, endpoint: Endpoint,
options: RequestOptionsWithOutBody, options: RequestOptionsWithOutBody,
params = {} params = {}
) { ) {

View File

@@ -89,3 +89,9 @@ export const getLanguageSwitcher = cache(
export const getSiteConfig = cache(async function getMemoizedSiteConfig() { export const getSiteConfig = cache(async function getMemoizedSiteConfig() {
return serverClient().contentstack.base.siteConfig() return serverClient().contentstack.base.siteConfig()
}) })
export const getBreakfastPackages = cache(async function getMemoizedPackages(
hotelId: string
) {
return serverClient().hotel.packages.breakfast({ hotelId })
})

View File

@@ -6,7 +6,6 @@ import { login } from "@/constants/routes/handleAuth"
import { webviews } from "@/constants/routes/webviews" import { webviews } from "@/constants/routes/webviews"
import { appRouter } from "@/server" import { appRouter } from "@/server"
import { createContext } from "@/server/context" import { createContext } from "@/server/context"
import { internalServerError } from "@/server/errors/next"
import { createCallerFactory } from "@/server/trpc" import { createCallerFactory } from "@/server/trpc"
const createCaller = createCallerFactory(appRouter) const createCaller = createCallerFactory(appRouter)

View File

@@ -1,5 +1,6 @@
import { cookies, headers } from "next/headers" import { cookies, headers } from "next/headers"
import { type Session } from "next-auth" import { type Session } from "next-auth"
import { cache } from "react"
import { Lang } from "@/constants/languages" import { Lang } from "@/constants/languages"
@@ -37,7 +38,7 @@ export function createContextInner(opts: CreateContextOptions) {
* This is the actual context you'll use in your router * This is the actual context you'll use in your router
* @link https://trpc.io/docs/context * @link https://trpc.io/docs/context
**/ **/
export function createContext() { export const createContext = cache(function () {
const h = headers() const h = headers()
const cookie = cookies() const cookie = cookies()
@@ -66,6 +67,6 @@ export function createContext() {
webToken: webviewTokenCookie?.value, webToken: webviewTokenCookie?.value,
contentType: h.get("x-contenttype")!, contentType: h.get("x-contenttype")!,
}) })
} })
export type Context = ReturnType<typeof createContext> export type Context = ReturnType<typeof createContext>

View File

@@ -62,7 +62,7 @@ export const bookingMutationRouter = router({
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,
} }
const apiResponse = await api.post(api.endpoints.v1.booking, { const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, {
headers, headers,
body: input, body: input,
}) })

View File

@@ -33,7 +33,7 @@ export const bookingQueryRouter = router({
getBookingConfirmationCounter.add(1, { confirmationNumber }) getBookingConfirmationCounter.add(1, { confirmationNumber })
const apiResponse = await api.get( const apiResponse = await api.get(
`${api.endpoints.v1.booking}/${confirmationNumber}`, api.endpoints.v1.Booking.booking(confirmationNumber),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,
@@ -142,7 +142,7 @@ export const bookingQueryRouter = router({
getBookingStatusCounter.add(1, { confirmationNumber }) getBookingStatusCounter.add(1, { confirmationNumber })
const apiResponse = await api.get( const apiResponse = await api.get(
`${api.endpoints.v1.booking}/${confirmationNumber}/status`, api.endpoints.v1.Booking.status(confirmationNumber),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,

View File

@@ -75,7 +75,7 @@ function getUniqueRewardIds(rewardIds: string[]) {
const getAllCachedApiRewards = unstable_cache( const getAllCachedApiRewards = unstable_cache(
async function (token) { async function (token) {
const apiResponse = await api.get(api.endpoints.v1.tierRewards, { const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
@@ -194,7 +194,7 @@ export const rewardQueryRouter = router({
const { limit, cursor } = input const { limit, cursor } = input
const apiResponse = await api.get(api.endpoints.v1.rewards, { const apiResponse = await api.get(api.endpoints.v1.Profile.reward, {
cache: undefined, // override defaultOptions cache: undefined, // override defaultOptions
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,
@@ -393,7 +393,7 @@ export const rewardQueryRouter = router({
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1) getCurrentRewardCounter.add(1)
const apiResponse = await api.get(api.endpoints.v1.rewards, { const apiResponse = await api.get(api.endpoints.v1.Profile.reward, {
cache: undefined, // override defaultOptions cache: undefined, // override defaultOptions
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,

View File

@@ -39,3 +39,7 @@ export const getlHotelDataInputSchema = z.object({
.array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"]))
.optional(), .optional(),
}) })
export const getBreakfastPackageInput = z.object({
hotelId: z.string().min(1, { message: "hotelId is required" }),
})

View File

@@ -9,7 +9,9 @@ import { getPoiGroupByCategoryName } from "./utils"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"
import { CurrencyEnum } from "@/types/enums/currency"
import { FacilityEnum } from "@/types/enums/facilities" import { FacilityEnum } from "@/types/enums/facilities"
import { PackageTypeEnum } from "@/types/enums/packages"
import { PointOfInterestCategoryNameEnum } from "@/types/hotel" import { PointOfInterestCategoryNameEnum } from "@/types/hotel"
const ratingsSchema = z const ratingsSchema = z
@@ -653,7 +655,7 @@ export const apiCountriesSchema = z.object({
name: z.string(), name: z.string(),
}), }),
hotelInformationSystemId: z.number().optional(), hotelInformationSystemId: z.number().optional(),
id: z.string().optional(), id: z.string().optional().default(""),
language: z.string().optional(), language: z.string().optional(),
type: z.literal("countries"), type: z.literal("countries"),
}) })
@@ -794,3 +796,30 @@ export const apiLocationsSchema = z.object({
}) })
), ),
}) })
export const breakfastPackageSchema = z.object({
code: z.string(),
currency: z.nativeEnum(CurrencyEnum),
description: z.string(),
originalPrice: z.number().default(0),
packagePrice: z.number(),
packageType: z.enum([
PackageTypeEnum.BreakfastAdult,
PackageTypeEnum.BreakfastChildren,
]),
totalPrice: z.number(),
})
export const breakfastPackagesSchema = z
.object({
data: z.object({
attributes: z.object({
hotelId: z.number(),
packages: z.array(breakfastPackageSchema),
}),
type: z.literal("breakfastpackage"),
}),
})
.transform(({ data }) =>
data.attributes.packages.filter((pkg) => pkg.code.match(/^(BRF\d+)$/gm))
)

View File

@@ -13,6 +13,7 @@ import {
contentStackUidWithServiceProcedure, contentStackUidWithServiceProcedure,
publicProcedure, publicProcedure,
router, router,
safeProtectedServiceProcedure,
serviceProcedure, serviceProcedure,
} from "@/server/trpc" } from "@/server/trpc"
import { toApiLang } from "@/server/utils" import { toApiLang } from "@/server/utils"
@@ -24,11 +25,13 @@ import {
getHotelPageCounter, getHotelPageCounter,
validateHotelPageRefs, validateHotelPageRefs,
} from "../contentstack/hotelPage/utils" } from "../contentstack/hotelPage/utils"
import { getVerifiedUser, parsedUser } from "../user/query"
import { import {
getRoomPackagesInputSchema, getRoomPackagesInputSchema,
getRoomPackagesSchema, getRoomPackagesSchema,
} from "./schemas/packages" } from "./schemas/packages"
import { import {
getBreakfastPackageInput,
getHotelInputSchema, getHotelInputSchema,
getHotelsAvailabilityInputSchema, getHotelsAvailabilityInputSchema,
getlHotelDataInputSchema, getlHotelDataInputSchema,
@@ -36,6 +39,7 @@ import {
getRoomsAvailabilityInputSchema, getRoomsAvailabilityInputSchema,
} from "./input" } from "./input"
import { import {
breakfastPackagesSchema,
getHotelDataSchema, getHotelDataSchema,
getHotelsAvailabilitySchema, getHotelsAvailabilitySchema,
getRatesSchema, getRatesSchema,
@@ -51,6 +55,7 @@ import {
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { Facility } from "@/types/hotel" import type { Facility } from "@/types/hotel"
import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage" import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
@@ -88,6 +93,14 @@ const roomsAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.rooms-fail" "trpc.hotel.availability.rooms-fail"
) )
const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast")
const breakfastPackagesSuccessCounter = meter.createCounter(
"trpc.package.breakfast-success"
)
const breakfastPackagesFailCounter = meter.createCounter(
"trpc.package.breakfast-fail"
)
async function getContentstackData(lang: Lang, uid?: string | null) { async function getContentstackData(lang: Lang, uid?: string | null) {
if (!uid) { if (!uid) {
return null return null
@@ -169,7 +182,7 @@ export const hotelQueryRouter = router({
}) })
) )
const apiResponse = await api.get( const apiResponse = await api.get(
`${api.endpoints.v1.hotels}/${hotelId}`, api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,
@@ -320,7 +333,7 @@ export const hotelQueryRouter = router({
JSON.stringify({ query: { cityId, params } }) JSON.stringify({ query: { cityId, params } })
) )
const apiResponse = await api.get( const apiResponse = await api.get(
`${api.endpoints.v1.hotelsAvailability}/${cityId}`, api.endpoints.v1.Availability.city(cityId),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,
@@ -444,7 +457,7 @@ export const hotelQueryRouter = router({
JSON.stringify({ query: { hotelId, params } }) JSON.stringify({ query: { hotelId, params } })
) )
const apiResponse = await api.get( const apiResponse = await api.get(
`${api.endpoints.v1.roomsAvailability}/${hotelId}`, api.endpoints.v1.Availability.hotel(hotelId.toString()),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,
@@ -587,7 +600,7 @@ export const hotelQueryRouter = router({
) )
const apiResponse = await api.get( const apiResponse = await api.get(
`${api.endpoints.v1.hotels}/${hotelId}`, api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,
@@ -734,7 +747,7 @@ export const hotelQueryRouter = router({
) )
const apiResponse = await api.get( const apiResponse = await api.get(
`${api.endpoints.v1.packages}/${hotelId}`, api.endpoints.v1.Package.Packages.hotel(hotelId),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.serviceToken}`, Authorization: `Bearer ${ctx.serviceToken}`,
@@ -789,5 +802,114 @@ export const hotelQueryRouter = router({
return validatedPackagesData.data return validatedPackagesData.data
}), }),
breakfast: safeProtectedServiceProcedure
.input(getBreakfastPackageInput)
.query(async function ({ ctx, input }) {
const params = {
Adults: 2,
EndDate: "2024-10-28",
StartDate: "2024-10-25",
}
const metricsData = { ...input, ...params }
breakfastPackagesCounter.add(1, metricsData)
console.info(
"api.package.breakfast start",
JSON.stringify({ query: metricsData })
)
const apiResponse = await api.get(
api.endpoints.v1.Package.Breakfast.hotel(input.hotelId),
{
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: 60,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
breakfastPackagesFailCounter.add(1, {
...metricsData,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelsAvailability error",
JSON.stringify({
query: metricsData,
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson)
if (!breakfastPackages.success) {
hotelsAvailabilityFailCounter.add(1, {
...metricsData,
error_type: "validation_error",
error: JSON.stringify(breakfastPackages.error),
})
console.error(
"api.package.breakfast validation error",
JSON.stringify({
query: metricsData,
error: breakfastPackages.error,
})
)
return null
}
breakfastPackagesSuccessCounter.add(1, metricsData)
console.info(
"api.package.breakfast success",
JSON.stringify({
query: metricsData,
})
)
if (ctx.session?.token) {
const apiUser = await getVerifiedUser({ session: ctx.session })
if (apiUser && !("error" in apiUser)) {
const user = parsedUser(apiUser.data, false)
if (
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) {
if (originalBreakfastPackage) {
freeBreakfastPackage.originalPrice =
originalBreakfastPackage.packagePrice
}
return [freeBreakfastPackage]
}
}
}
}
return breakfastPackages.data.filter(
(pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
)
}),
}), }),
}) })

View File

@@ -96,7 +96,7 @@ export async function getCountries(
return unstable_cache( return unstable_cache(
async function (searchParams) { async function (searchParams) {
const countryResponse = await api.get( const countryResponse = await api.get(
api.endpoints.v1.countries, api.endpoints.v1.Hotel.countries,
options, options,
searchParams searchParams
) )
@@ -136,7 +136,7 @@ export async function getCitiesByCountry(
await Promise.all( await Promise.all(
searchedCountries.data.map(async (country) => { searchedCountries.data.map(async (country) => {
const countryResponse = await api.get( const countryResponse = await api.get(
`${api.endpoints.v1.citiesCountry}/${country.name}`, api.endpoints.v1.Hotel.Cities.country(country.name),
options, options,
searchParams searchParams
) )
@@ -182,7 +182,7 @@ export async function getLocations(
groupedCitiesByCountry: CitiesGroupedByCountry | null groupedCitiesByCountry: CitiesGroupedByCountry | null
) { ) {
const apiResponse = await api.get( const apiResponse = await api.get(
api.endpoints.v1.locations, api.endpoints.v1.Hotel.locations,
options, options,
searchParams searchParams
) )

View File

@@ -35,16 +35,19 @@ export const userMutationRouter = router({
"api.user.creditCard.add start", "api.user.creditCard.add start",
JSON.stringify({ query: { language: input.language } }) JSON.stringify({ query: { language: input.language } })
) )
const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, { const apiResponse = await api.post(
headers: { api.endpoints.v1.Profile.CreditCards.initiateSaveCard,
Authorization: `Bearer ${ctx.session.token.access_token}`, {
}, headers: {
body: { Authorization: `Bearer ${ctx.session.token.access_token}`,
language: input.language, },
mobileToken: false, body: {
redirectUrl: `api/web/add-card-callback/${input.language}`, language: input.language,
}, mobileToken: false,
}) redirectUrl: `api/web/add-card-callback/${input.language}`,
},
}
)
if (!apiResponse.ok) { if (!apiResponse.ok) {
const text = await apiResponse.text() const text = await apiResponse.text()
@@ -85,7 +88,7 @@ export const userMutationRouter = router({
.mutation(async function ({ ctx, input }) { .mutation(async function ({ ctx, input }) {
console.info("api.user.creditCard.save start", JSON.stringify({})) console.info("api.user.creditCard.save start", JSON.stringify({}))
const apiResponse = await api.post( const apiResponse = await api.post(
`${api.endpoints.v1.creditCards}/${input.transactionId}`, api.endpoints.v1.Profile.CreditCards.transaction(input.transactionId),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,
@@ -118,7 +121,9 @@ export const userMutationRouter = router({
JSON.stringify({ query: {} }) JSON.stringify({ query: {} })
) )
const apiResponse = await api.remove( const apiResponse = await api.remove(
`${api.endpoints.v1.creditCards}/${input.creditCardId}`, api.endpoints.v1.Profile.CreditCards.deleteCreditCard(
input.creditCardId
),
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,
@@ -149,7 +154,7 @@ export const userMutationRouter = router({
ctx, ctx,
}) { }) {
generatePreferencesLinkCounter.add(1) generatePreferencesLinkCounter.add(1)
const apiResponse = await api.get(api.endpoints.v1.subscriberId, { const apiResponse = await api.get(api.endpoints.v1.Profile.subscriberId, {
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,
}, },

View File

@@ -89,7 +89,7 @@ export const getVerifiedUser = cache(
} }
getVerifiedUserCounter.add(1) getVerifiedUserCounter.add(1)
console.info("api.user.profile getVerifiedUser start", JSON.stringify({})) console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.profile, { const apiResponse = await api.get(api.endpoints.v1.Profile.profile, {
headers: { headers: {
Authorization: `Bearer ${session.token.access_token}`, Authorization: `Bearer ${session.token.access_token}`,
}, },
@@ -163,7 +163,7 @@ export const getVerifiedUser = cache(
} }
) )
function parsedUser(data: User, isMFA: boolean) { export function parsedUser(data: User, isMFA: boolean) {
const country = countries.find((c) => c.code === data.address.countryCode) const country = countries.find((c) => c.code === data.address.countryCode)
const user = { const user = {
@@ -211,7 +211,7 @@ function parsedUser(data: User, isMFA: boolean) {
async function getCreditCards(session: Session) { async function getCreditCards(session: Session) {
getCreditCardsCounter.add(1) getCreditCardsCounter.add(1)
console.info("api.profile.creditCards start", JSON.stringify({})) console.info("api.profile.creditCards start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.creditCards, { const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, {
headers: { headers: {
Authorization: `Bearer ${session.token.access_token}`, Authorization: `Bearer ${session.token.access_token}`,
}, },
@@ -354,7 +354,7 @@ export const userQueryRouter = router({
JSON.stringify({ query: { params } }) JSON.stringify({ query: { params } })
) )
const previousStaysResponse = await api.get( const previousStaysResponse = await api.get(
api.endpoints.v1.previousStays, api.endpoints.v1.Booking.Stays.past,
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,
@@ -430,7 +430,7 @@ export const userQueryRouter = router({
) )
const apiResponse = await api.get( const apiResponse = await api.get(
api.endpoints.v1.previousStays, api.endpoints.v1.Booking.Stays.past,
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,
@@ -492,7 +492,7 @@ export const userQueryRouter = router({
) )
const nextCursor = const nextCursor =
verifiedData.data.links && verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset ? verifiedData.data.links.offset
: undefined : undefined
@@ -525,7 +525,7 @@ export const userQueryRouter = router({
JSON.stringify({ query: { params } }) JSON.stringify({ query: { params } })
) )
const apiResponse = await api.get( const apiResponse = await api.get(
api.endpoints.v1.upcomingStays, api.endpoints.v1.Booking.Stays.future,
{ {
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,
@@ -585,7 +585,7 @@ export const userQueryRouter = router({
}) })
const nextCursor = const nextCursor =
verifiedData.data.links && verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset ? verifiedData.data.links.offset
: undefined : undefined
@@ -611,13 +611,16 @@ export const userQueryRouter = router({
"api.transaction.friendTransactions start", "api.transaction.friendTransactions start",
JSON.stringify({}) JSON.stringify({})
) )
const apiResponse = await api.get(api.endpoints.v1.friendTransactions, { const apiResponse = await api.get(
cache: undefined, // override defaultOptions api.endpoints.v1.Profile.Transaction.friendTransactions,
headers: { {
Authorization: `Bearer ${ctx.session.token.access_token}`, cache: undefined, // override defaultOptions
}, headers: {
next: { revalidate: 30 * 60 * 1000 }, Authorization: `Bearer ${ctx.session.token.access_token}`,
}) },
next: { revalidate: 30 * 60 * 1000 },
}
)
if (!apiResponse.ok) { if (!apiResponse.ok) {
// switch (apiResponse.status) { // switch (apiResponse.status) {
@@ -740,7 +743,7 @@ export const userQueryRouter = router({
membershipCards: protectedProcedure.query(async function ({ ctx }) { membershipCards: protectedProcedure.query(async function ({ ctx }) {
getProfileCounter.add(1) getProfileCounter.add(1)
console.info("api.profile start", JSON.stringify({})) console.info("api.profile start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.profile, { const apiResponse = await api.get(api.endpoints.v1.Profile.profile, {
cache: "no-store", cache: "no-store",
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,

View File

@@ -35,7 +35,7 @@ async function updateStaysBookingUrl(
// Temporary API call needed till we have user name in ctx session data // Temporary API call needed till we have user name in ctx session data
getProfileCounter.add(1) getProfileCounter.add(1)
console.info("api.user.profile updatebookingurl start", JSON.stringify({})) console.info("api.user.profile updatebookingurl start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.profile, { const apiResponse = await api.get(api.endpoints.v1.Profile.profile, {
cache: "no-store", cache: "no-store",
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,

View File

@@ -176,7 +176,7 @@ export const protectedServerActionProcedure = serverActionProcedure.use(
} }
) )
// NOTE: This is actually save to use, just the implementation could change // NOTE: This is actually safe to use, just the implementation could change
// in minor version bumps. Please read: https://trpc.io/docs/faq#unstable // in minor version bumps. Please read: https://trpc.io/docs/faq#unstable
export const contentStackUidWithServiceProcedure = export const contentStackUidWithServiceProcedure =
contentstackExtendedProcedureUID.unstable_concat(serviceProcedure) contentstackExtendedProcedureUID.unstable_concat(serviceProcedure)
@@ -186,3 +186,6 @@ export const contentStackBaseWithServiceProcedure =
export const contentStackBaseWithProtectedProcedure = export const contentStackBaseWithProtectedProcedure =
contentstackBaseProcedure.unstable_concat(protectedProcedure) contentstackBaseProcedure.unstable_concat(protectedProcedure)
export const safeProtectedServiceProcedure =
safeProtectedProcedure.unstable_concat(serviceProcedure)

View File

@@ -3,21 +3,22 @@ import { createContext, useContext } from "react"
import { create, useStore } from "zustand" import { create, useStore } from "zustand"
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema"
import { BreakfastPackage } from "@/types/components/enterDetails/breakfast"
import { DetailsSchema } from "@/types/components/enterDetails/details" import { DetailsSchema } from "@/types/components/enterDetails/details"
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek"
import { StepEnum } from "@/types/components/enterDetails/step" import { StepEnum } from "@/types/components/enterDetails/step"
import { bedTypeEnum } from "@/types/enums/bedType" import { BedTypeEnum } from "@/types/enums/bedType"
import { breakfastEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
const SESSION_STORAGE_KEY = "enterDetails" const SESSION_STORAGE_KEY = "enterDetails"
interface EnterDetailsState { interface EnterDetailsState {
data: { data: {
bedType: bedTypeEnum | undefined bedType: BedTypeEnum | undefined
breakfast: breakfastEnum | undefined breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined
} & DetailsSchema } & DetailsSchema
steps: StepEnum[] steps: StepEnum[]
currentStep: StepEnum currentStep: StepEnum
@@ -26,7 +27,7 @@ interface EnterDetailsState {
completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void
navigate: ( navigate: (
step: StepEnum, step: StepEnum,
updatedData?: Record<string, string | boolean> updatedData?: Record<string, string | boolean | BreakfastPackage>
) => void ) => void
setCurrentStep: (step: StepEnum) => void setCurrentStep: (step: StepEnum) => void
openSidePeek: (key: SidePeekEnum | null) => void openSidePeek: (key: SidePeekEnum | null) => void
@@ -75,7 +76,7 @@ export function initEditDetailsState(currentStep: StepEnum) {
initialData = { ...initialData, ...validatedBedType.data } initialData = { ...initialData, ...validatedBedType.data }
isValid[StepEnum.selectBed] = true isValid[StepEnum.selectBed] = true
} }
const validatedBreakfast = breakfastSchema.safeParse(inputData) const validatedBreakfast = breakfastStoreSchema.safeParse(inputData)
if (validatedBreakfast.success) { if (validatedBreakfast.success) {
validPaths.push(StepEnum.details) validPaths.push(StepEnum.details)
initialData = { ...initialData, ...validatedBreakfast.data } initialData = { ...initialData, ...validatedBreakfast.data }

View File

@@ -1,5 +1,21 @@
import { z } from "zod" import { z } from "zod"
import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" import {
breakfastPackageSchema,
breakfastPackagesSchema,
} from "@/server/routers/hotels/output"
export interface BreakfastSchema extends z.output<typeof breakfastSchema> {} import { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
export interface BreakfastFormSchema
extends z.output<typeof breakfastFormSchema> {}
export interface BreakfastPackages
extends z.output<typeof breakfastPackagesSchema> {}
export interface BreakfastPackage
extends z.output<typeof breakfastPackageSchema> {}
export interface BreakfastProps {
packages: BreakfastPackages | null
}

View File

@@ -1,4 +1,4 @@
export enum bedTypeEnum { export enum BedTypeEnum {
KING = "KING", KING = "KING",
QUEEN = "QUEEN", QUEEN = "QUEEN",
} }

View File

@@ -1,4 +1,5 @@
export enum breakfastEnum { export enum BreakfastPackageEnum {
BREAKFAST = "BREAKFAST", FREE_MEMBER_BREAKFAST = "BRF0",
REGULAR_BREAKFAST = "BRF1",
NO_BREAKFAST = "NO_BREAKFAST", NO_BREAKFAST = "NO_BREAKFAST",
} }

7
types/enums/currency.ts Normal file
View File

@@ -0,0 +1,7 @@
export enum CurrencyEnum {
DKK = "DKK",
EUR = "EUR",
NOK = "NOK",
PLN = "PLN",
SEK = "SEK",
}

7
types/enums/packages.ts Normal file
View File

@@ -0,0 +1,7 @@
export enum PackageTypeEnum {
AccessibleFriendlyRoom = "AccessibleFriendlyRoom",
AllergyRoom = "AllergyRoom",
BreakfastAdult = "BreakfastAdult",
BreakfastChildren = "BreakfastChildren",
PetRoom = "PetRoom",
}