diff --git a/actions/editProfile.ts b/actions/editProfile.ts index 8f0791c48..acf5d1b12 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -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, cache: "no-store", headers: { diff --git a/actions/registerUser.ts b/actions/registerUser.ts index e65fc357f..ecd2318b4 100644 --- a/actions/registerUser.ts +++ b/actions/registerUser.ts @@ -55,7 +55,7 @@ export const registerUser = serviceServerActionProcedure let apiResponse try { - apiResponse = await api.post(api.endpoints.v1.profile, { + apiResponse = await api.post(api.endpoints.v1.Profile.profile, { body: parsedPayload.data, headers: { Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/actions/registerUserBookingFlow.ts b/actions/registerUserBookingFlow.ts index a0539d351..a34cad231 100644 --- a/actions/registerUserBookingFlow.ts +++ b/actions/registerUserBookingFlow.ts @@ -33,7 +33,7 @@ export const registerUserBookingFlow = serviceServerActionProcedure // TODO: Consume the API to register the user as soon as passwordless signup is enabled. // let apiResponse // try { - // apiResponse = await api.post(api.endpoints.v1.profile, { + // apiResponse = await api.post(api.endpoints.v1.Profile.profile, { // body: payload, // headers: { // Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx new file mode 100644 index 000000000..0fad268cc --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingHotelHeader() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx new file mode 100644 index 000000000..58a216006 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx @@ -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) { + const home = `/${params.lang}` + if (!searchParams.hotel) { + redirect(home) + } + const hotel = await getHotelData(searchParams.hotel, params.lang) + if (!hotel?.data) { + redirect(home) + } + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx new file mode 100644 index 000000000..67515d4f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingHotelSidePeek() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx new file mode 100644 index 000000000..13b770699 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx @@ -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) { + if (!searchParams.hotel) { + redirect(`/${params.lang}`) + } + const hotel = await getHotelData(searchParams.hotel, params.lang) + if (!hotel?.data) { + redirect(`/${params.lang}`) + } + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 0e8edd50e..271d19e6d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -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 SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" -import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" import Summary from "@/components/HotelReservation/EnterDetails/Summary" -import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import { setLang } from "@/i18n/serverContext" +import { preload } from "./page" + import styles from "./layout.module.css" import { StepEnum } from "@/types/components/enterDetails/step" 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({ children, + hotelHeader, params, -}: React.PropsWithChildren>) { - setLang(params.lang) - preload("811", params.lang) - - const hotel = await getHotelData("811", params.lang) - - if (!hotel?.data) { - redirect(`/${params.lang}`) + sidePeek, +}: React.PropsWithChildren< + LayoutArgs & { + hotelHeader: React.ReactNode + sidePeek: React.ReactNode } - +>) { + setLang(params.lang) + preload() return ( - + {hotelHeader} {children} @@ -48,7 +34,7 @@ export default async function StepLayout({ - + {sidePeek} ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 8e8d3c891..77ec20203 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,6 +1,7 @@ -import { notFound } from "next/navigation" +import { notFound, redirect } from "next/navigation" import { + getBreakfastPackages, getCreditCardsSafely, getHotelData, getProfileSafely, @@ -17,22 +18,32 @@ import { getIntl } from "@/i18n" import { StepEnum } from "@/types/components/enterDetails/step" import type { LangParams, PageArgs } from "@/types/params" +export function preload() { + void getProfileSafely() + void getCreditCardsSafely() +} + function isValidStep(step: string): step is StepEnum { return Object.values(StepEnum).includes(step as StepEnum) } export default async function StepPage({ params, -}: PageArgs) { - const { step, lang } = params + searchParams, +}: PageArgs) { + if (!searchParams.hotel) { + redirect(`/${params.lang}`) + } + void getBreakfastPackages(searchParams.hotel) const intl = await getIntl() - const hotel = await getHotelData("811", lang) + const hotel = await getHotelData(searchParams.hotel, params.lang) const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() + const breakfastPackages = await getBreakfastPackages(searchParams.hotel) - if (!isValidStep(step) || !hotel) { + if (!isValidStep(params.step) || !hotel) { return notFound() } @@ -51,7 +62,7 @@ export default async function StepPage({ step={StepEnum.breakfast} label={intl.formatMessage({ id: "Select breakfast options" })} > - + diff --git a/components/HotelReservation/EnterDetails/BedType/schema.ts b/components/HotelReservation/EnterDetails/BedType/schema.ts index d9f52a407..8f77ba768 100644 --- a/components/HotelReservation/EnterDetails/BedType/schema.ts +++ b/components/HotelReservation/EnterDetails/BedType/schema.ts @@ -1,7 +1,7 @@ import { z } from "zod" -import { bedTypeEnum } from "@/types/enums/bedType" +import { BedTypeEnum } from "@/types/enums/bedType" export const bedTypeSchema = z.object({ - bedType: z.nativeEnum(bedTypeEnum), + bedType: z.nativeEnum(BedTypeEnum), }) diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index c2ea00754..603a3aaad 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -7,36 +7,50 @@ import { useIntl } from "react-intl" 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 { breakfastSchema } from "./schema" +import { breakfastFormSchema } from "./schema" import styles from "./breakfast.module.css" -import type { BreakfastSchema } from "@/types/components/enterDetails/breakfast" -import { breakfastEnum } from "@/types/enums/breakfast" +import type { + 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 breakfast = useEnterDetailsStore((state) => state.data.breakfast) - const methods = useForm({ - defaultValues: breakfast ? { breakfast } : undefined, + let defaultValues = undefined + if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { + defaultValues = { breakfast: BreakfastPackageEnum.NO_BREAKFAST } + } else if (breakfast?.code) { + defaultValues = { breakfast: breakfast.code } + } + const methods = useForm({ + defaultValues, criteriaMode: "all", mode: "all", - resolver: zodResolver(breakfastSchema), + resolver: zodResolver(breakfastFormSchema), reValidateMode: "onChange", }) const completeStep = useEnterDetailsStore((state) => state.completeStep) const onSubmit = useCallback( - (values: BreakfastSchema) => { - completeStep(values) + (values: BreakfastFormSchema) => { + const pkg = packages?.find((p) => p.code === values.breakfast) + if (pkg) { + completeStep({ breakfast: pkg }) + } else { + completeStep({ breakfast: BreakfastPackageEnum.NO_BREAKFAST }) + } }, - [completeStep] + [completeStep, packages] ) useEffect(() => { @@ -47,30 +61,46 @@ export default function Breakfast() { return () => subscription.unsubscribe() }, [methods, onSubmit]) + if (!packages) { + return null + } + return ( - ( - { id: "{amount} {currency}/night per adult" }, - { - amount: "150", - b: (str) => {str}, - currency: "SEK", + {packages.map((pkg) => ( + ( + { id: "breakfast.price.free" }, + { + amount: pkg.originalPrice, + currency: pkg.currency, + free: (str) => {str}, + strikethrough: (str) => {str}, + } + ) + : intl.formatMessage( + { id: "breakfast.price" }, + { + amount: pkg.packagePrice, + currency: pkg.currency, + } + ) } - )} - text={intl.formatMessage({ - id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", - })} - title={intl.formatMessage({ id: "Breakfast buffet" })} - value={breakfastEnum.BREAKFAST} - /> + text={intl.formatMessage({ + id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", + })} + title={intl.formatMessage({ id: "Breakfast buffet" })} + value={pkg.code} + /> + ))} diff --git a/components/HotelReservation/EnterDetails/Breakfast/schema.ts b/components/HotelReservation/EnterDetails/Breakfast/schema.ts index 34cc5efca..5f8c1f354 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/schema.ts +++ b/components/HotelReservation/EnterDetails/Breakfast/schema.ts @@ -1,7 +1,15 @@ import { z } from "zod" -import { breakfastEnum } from "@/types/enums/breakfast" +import { breakfastPackageSchema } from "@/server/routers/hotels/output" -export const breakfastSchema = z.object({ - breakfast: z.nativeEnum(breakfastEnum), +import { BreakfastPackageEnum } from "@/types/enums/breakfast" + +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)), }) diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css index 1044596f6..fa7d6d13a 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css @@ -70,3 +70,7 @@ .listItem:nth-of-type(n + 2) { margin-top: var(--Spacing-x-quarter); } + +.highlight { + color: var(--Scandic-Brand-Scandic-Red); +} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts index d1a961150..145116409 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts @@ -34,3 +34,12 @@ export type CheckboxProps = export type RadioProps = | Omit | Omit + +export interface ListProps extends Pick { + list?: ListCardProps["list"] +} + +export interface SubtitleProps + extends Pick {} + +export interface TextProps extends Pick {} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index c57d56cc4..2a3faf57b 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -2,16 +2,16 @@ 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 Footnote from "@/components/TempDesignSystem/Text/Footnote" import styles from "./card.module.css" -import type { CardProps } from "./card" +import type { CardProps, ListProps, SubtitleProps, TextProps } from "./card" export default function Card({ - Icon = HeartIcon, + Icon, iconHeight = 32, iconWidth = 32, declined = false, @@ -26,56 +26,79 @@ export default function Card({ value, }: CardProps) { const { register } = useFormContext() - return ( - + {title} - {subtitle ? ( - - {subtitle} - - ) : null} - - {list - ? list.map((listItem) => ( - - {declined ? ( - - ) : ( - - )} - {listItem.title} - - )) - : null} - {text ? ( - - {text} - + + {Icon ? ( + ) : null} + + ) } + +function List({ declined, list }: ListProps) { + if (!list) { + return null + } + + return list.map((listItem) => ( + + {declined ? ( + + ) : ( + + )} + {listItem.title} + + )) +} + +function Subtitle({ highlightSubtitle, subtitle }: SubtitleProps) { + if (!subtitle) { + return null + } + + return ( + + {subtitle} + + ) +} + +function Text({ text }: TextProps) { + if (!text) { + return null + } + return ( + + {text} + + ) +} + +export function Highlight({ children }: React.PropsWithChildren) { + return {children} +} diff --git a/constants/booking.ts b/constants/booking.ts index 8f5acb120..da6b30695 100644 --- a/constants/booking.ts +++ b/constants/booking.ts @@ -1,7 +1,18 @@ export enum BookingStatusEnum { - CreatedInOhip = "CreatedInOhip", - PaymentRegistered = "PaymentRegistered", 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 { diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index e47484466..c66448f17 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -1,6 +1,5 @@ { "Included (based on availability)": "Inkluderet (baseret på tilgængelighed)", - "{amount} {currency}/night per adult": "{amount} {currency}/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 photo of the room": "Et foto af værelset", "ACCE": "Tilgængelighed", @@ -366,6 +365,8 @@ "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 Vilkår og betingelser, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til Scandics Privatlivspolitik. 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", + "breakfast.price": "{amount} {currency}/nat", + "breakfast.price.free": "{amount} {currency} 0 {currency}/nat", "by": "inden", "characters": "tegn", "guest": "gæst", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 88a5092e5..1959939ee 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -1,6 +1,5 @@ { "Included (based on availability)": "Inbegriffen (je nach Verfügbarkeit)", - "{amount} {currency}/night per adult": "{amount} {currency}/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 photo of the room": "Ein Foto des Zimmers", "ACCE": "Zugänglichkeit", @@ -367,6 +366,8 @@ "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 Vilkår og betingelser, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til Scandics Privatlivspolitik. 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", + "breakfast.price": "{amount} {currency}/Nacht", + "breakfast.price.free": "{amount} {currency} 0 {currency}/Nacht", "by": "bis", "characters": "figuren", "guest": "gast", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 75fbbd805..2fa6793d7 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -1,6 +1,5 @@ { "Included (based on availability)": "Included (based on availability)", - "{amount} {currency}/night per adult": "{amount} {currency}/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 photo of the room": "A photo of the room", "ACCE": "Accessibility", @@ -385,6 +384,8 @@ "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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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", + "breakfast.price": "{amount} {currency}/night", + "breakfast.price.free": "{amount} {currency} 0 {currency}/night", "by": "by", "characters": "characters", "from": "from", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 6f2f28b63..883b9d277 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -1,6 +1,5 @@ { "Included (based on availability)": "Sisältyy (saatavuuden mukaan)", - "{amount} {currency}/night per adult": "{amount} {currency}/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 photo of the room": "Kuva huoneesta", "ACCE": "Saavutettavuus", @@ -367,6 +366,8 @@ "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 ehdot ja ehtoja, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti Scandicin tietosuojavaltuuden mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", "booking.thisRoomIsEquippedWith": "Tämä huone on varustettu", + "breakfast.price": "{amount} {currency}/yö", + "breakfast.price.free": "{amount} {currency} 0 {currency}/yö", "by": "mennessä", "characters": "hahmoja", "guest": "Vieras", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 0b45a4673..075f84783 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -1,6 +1,5 @@ { "Included (based on availability)": "Inkludert (basert på tilgjengelighet)", - "{amount} {currency}/night per adult": "{amount} {currency}/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 photo of the room": "Et bilde av rommet", "ACCE": "Tilgjengelighet", @@ -363,6 +362,8 @@ "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", "booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", + "breakfast.price": "{amount} {currency}/natt", + "breakfast.price.free": "{amount} {currency} 0 {currency}/natt", "by": "innen", "characters": "tegn", "guest": "gjest", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 406aa4cb4..0b48022d3 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -1,6 +1,5 @@ { "Included (based on availability)": "Ingår (baserat på tillgänglighet)", - "{amount} {currency}/night per adult": "{amount} {currency}/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 photo of the room": "Ett foto av rummet", "ACCE": "Tillgänglighet", @@ -364,6 +363,8 @@ "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 Villkoren och villkoren, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med Scandics integritetspolicy. 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", + "breakfast.price": "{amount} {currency}/natt", + "breakfast.price.free": "{amount} {currency} 0 {currency}/natt", "by": "innan", "characters": "tecken", "guest": "gäst", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index be1aee2fd..100fb7518 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -2,29 +2,190 @@ * Nested enum requires namespace */ export namespace endpoints { - export const enum v0 { - profile = "profile/v0/Profile", + namespace base { + 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", - roomsAvailability = "availability/v1/availabilities/hotel", - profile = "profile/v1/Profile", - booking = "booking/v1/Bookings", - creditCards = `${profile}/creditCards`, - city = "hotel/v1/Cities", - citiesCountry = `${city}/country`, - countries = "hotel/v1/Countries", - friendTransactions = "profile/v1/Transaction/friendTransactions", - hotels = "hotel/v1/Hotels", - initiateSaveCard = `${creditCards}/initiateSaveCard`, - locations = "hotel/v1/Locations", - previousStays = "booking/v1/Stays/past", - upcomingStays = "booking/v1/Stays/future", - rewards = `${profile}/reward`, - tierRewards = `${profile}/TierRewards`, - subscriberId = `${profile}/SubscriberId`, - packages = "package/v1/packages/hotel", + + export namespace v1 { + const version = "v1" + /** + * availability (Swagger) + * https://tstapi.scandichotels.com/availability/swagger/v1/index.html + */ + export namespace Availability { + export function city(cityId: string) { + return `${base.path.availability}/${version}/${base.enitity.Availabilities}/city/${cityId}` + } + export function hotel(hotelId: string) { + return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel/${hotelId}` + } + } + + /** + * booking (Swagger) + * 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 diff --git a/lib/api/index.ts b/lib/api/index.ts index 475e0da4e..46bae9f88 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -28,7 +28,7 @@ const wrappedFetch = fetchRetry(fetch, { }) export async function get( - endpoint: Endpoint | `${Endpoint}/${string}`, + endpoint: Endpoint, options: RequestOptionsWithOutBody, params = {} ) { diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index d09349a8c..e34f241ce 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -89,3 +89,9 @@ export const getLanguageSwitcher = cache( export const getSiteConfig = cache(async function getMemoizedSiteConfig() { return serverClient().contentstack.base.siteConfig() }) + +export const getBreakfastPackages = cache(async function getMemoizedPackages( + hotelId: string +) { + return serverClient().hotel.packages.breakfast({ hotelId }) +}) diff --git a/lib/trpc/server.ts b/lib/trpc/server.ts index 46c0f8840..5d8539a8a 100644 --- a/lib/trpc/server.ts +++ b/lib/trpc/server.ts @@ -6,7 +6,6 @@ import { login } from "@/constants/routes/handleAuth" import { webviews } from "@/constants/routes/webviews" import { appRouter } from "@/server" import { createContext } from "@/server/context" -import { internalServerError } from "@/server/errors/next" import { createCallerFactory } from "@/server/trpc" const createCaller = createCallerFactory(appRouter) diff --git a/server/context.ts b/server/context.ts index 76ea0ba34..f33d6b1c9 100644 --- a/server/context.ts +++ b/server/context.ts @@ -1,5 +1,6 @@ import { cookies, headers } from "next/headers" import { type Session } from "next-auth" +import { cache } from "react" 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 * @link https://trpc.io/docs/context **/ -export function createContext() { +export const createContext = cache(function () { const h = headers() const cookie = cookies() @@ -66,6 +67,6 @@ export function createContext() { webToken: webviewTokenCookie?.value, contentType: h.get("x-contenttype")!, }) -} +}) export type Context = ReturnType diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 9e5677d32..2edbd5bdd 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -62,7 +62,7 @@ export const bookingMutationRouter = router({ Authorization: `Bearer ${ctx.serviceToken}`, } - const apiResponse = await api.post(api.endpoints.v1.booking, { + const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, { headers, body: input, }) diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index 76b874ba3..5c72bb284 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -33,7 +33,7 @@ export const bookingQueryRouter = router({ getBookingConfirmationCounter.add(1, { confirmationNumber }) const apiResponse = await api.get( - `${api.endpoints.v1.booking}/${confirmationNumber}`, + api.endpoints.v1.Booking.booking(confirmationNumber), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -142,7 +142,7 @@ export const bookingQueryRouter = router({ getBookingStatusCounter.add(1, { confirmationNumber }) const apiResponse = await api.get( - `${api.endpoints.v1.booking}/${confirmationNumber}/status`, + api.endpoints.v1.Booking.status(confirmationNumber), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 419e0a2c2..b4e0e54b2 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -75,7 +75,7 @@ function getUniqueRewardIds(rewardIds: string[]) { const getAllCachedApiRewards = unstable_cache( async function (token) { - const apiResponse = await api.get(api.endpoints.v1.tierRewards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, { headers: { Authorization: `Bearer ${token}`, }, @@ -194,7 +194,7 @@ export const rewardQueryRouter = router({ 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 headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -393,7 +393,7 @@ export const rewardQueryRouter = router({ surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { 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 headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 6deff63a6..61927c3d2 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -39,3 +39,7 @@ export const getlHotelDataInputSchema = z.object({ .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) .optional(), }) + +export const getBreakfastPackageInput = z.object({ + hotelId: z.string().min(1, { message: "hotelId is required" }), +}) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index e4c90c5dc..f8e2b8a6e 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -9,7 +9,9 @@ import { getPoiGroupByCategoryName } from "./utils" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { AlertTypeEnum } from "@/types/enums/alert" +import { CurrencyEnum } from "@/types/enums/currency" import { FacilityEnum } from "@/types/enums/facilities" +import { PackageTypeEnum } from "@/types/enums/packages" import { PointOfInterestCategoryNameEnum } from "@/types/hotel" const ratingsSchema = z @@ -653,7 +655,7 @@ export const apiCountriesSchema = z.object({ name: z.string(), }), hotelInformationSystemId: z.number().optional(), - id: z.string().optional(), + id: z.string().optional().default(""), language: z.string().optional(), 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)) + ) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index c7a1da08f..a7b6c220e 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -13,6 +13,7 @@ import { contentStackUidWithServiceProcedure, publicProcedure, router, + safeProtectedServiceProcedure, serviceProcedure, } from "@/server/trpc" import { toApiLang } from "@/server/utils" @@ -24,11 +25,13 @@ import { getHotelPageCounter, validateHotelPageRefs, } from "../contentstack/hotelPage/utils" +import { getVerifiedUser, parsedUser } from "../user/query" import { getRoomPackagesInputSchema, getRoomPackagesSchema, } from "./schemas/packages" import { + getBreakfastPackageInput, getHotelInputSchema, getHotelsAvailabilityInputSchema, getlHotelDataInputSchema, @@ -36,6 +39,7 @@ import { getRoomsAvailabilityInputSchema, } from "./input" import { + breakfastPackagesSchema, getHotelDataSchema, getHotelsAvailabilitySchema, getRatesSchema, @@ -51,6 +55,7 @@ import { import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { Facility } from "@/types/hotel" import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage" @@ -88,6 +93,14 @@ const roomsAvailabilityFailCounter = meter.createCounter( "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) { if (!uid) { return null @@ -169,7 +182,7 @@ export const hotelQueryRouter = router({ }) ) const apiResponse = await api.get( - `${api.endpoints.v1.hotels}/${hotelId}`, + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -320,7 +333,7 @@ export const hotelQueryRouter = router({ JSON.stringify({ query: { cityId, params } }) ) const apiResponse = await api.get( - `${api.endpoints.v1.hotelsAvailability}/${cityId}`, + api.endpoints.v1.Availability.city(cityId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -444,7 +457,7 @@ export const hotelQueryRouter = router({ JSON.stringify({ query: { hotelId, params } }) ) const apiResponse = await api.get( - `${api.endpoints.v1.roomsAvailability}/${hotelId}`, + api.endpoints.v1.Availability.hotel(hotelId.toString()), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -587,7 +600,7 @@ export const hotelQueryRouter = router({ ) const apiResponse = await api.get( - `${api.endpoints.v1.hotels}/${hotelId}`, + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -734,7 +747,7 @@ export const hotelQueryRouter = router({ ) const apiResponse = await api.get( - `${api.endpoints.v1.packages}/${hotelId}`, + api.endpoints.v1.Package.Packages.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -789,5 +802,114 @@ export const hotelQueryRouter = router({ 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 + ) + }), }), }) diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index 84020bfaa..6d384b3cf 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -96,7 +96,7 @@ export async function getCountries( return unstable_cache( async function (searchParams) { const countryResponse = await api.get( - api.endpoints.v1.countries, + api.endpoints.v1.Hotel.countries, options, searchParams ) @@ -136,7 +136,7 @@ export async function getCitiesByCountry( await Promise.all( searchedCountries.data.map(async (country) => { const countryResponse = await api.get( - `${api.endpoints.v1.citiesCountry}/${country.name}`, + api.endpoints.v1.Hotel.Cities.country(country.name), options, searchParams ) @@ -182,7 +182,7 @@ export async function getLocations( groupedCitiesByCountry: CitiesGroupedByCountry | null ) { const apiResponse = await api.get( - api.endpoints.v1.locations, + api.endpoints.v1.Hotel.locations, options, searchParams ) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index 005941090..b03e6a68e 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -35,16 +35,19 @@ export const userMutationRouter = router({ "api.user.creditCard.add start", JSON.stringify({ query: { language: input.language } }) ) - const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, { - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - body: { - language: input.language, - mobileToken: false, - redirectUrl: `api/web/add-card-callback/${input.language}`, - }, - }) + const apiResponse = await api.post( + api.endpoints.v1.Profile.CreditCards.initiateSaveCard, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + body: { + language: input.language, + mobileToken: false, + redirectUrl: `api/web/add-card-callback/${input.language}`, + }, + } + ) if (!apiResponse.ok) { const text = await apiResponse.text() @@ -85,7 +88,7 @@ export const userMutationRouter = router({ .mutation(async function ({ ctx, input }) { console.info("api.user.creditCard.save start", JSON.stringify({})) const apiResponse = await api.post( - `${api.endpoints.v1.creditCards}/${input.transactionId}`, + api.endpoints.v1.Profile.CreditCards.transaction(input.transactionId), { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -118,7 +121,9 @@ export const userMutationRouter = router({ JSON.stringify({ query: {} }) ) const apiResponse = await api.remove( - `${api.endpoints.v1.creditCards}/${input.creditCardId}`, + api.endpoints.v1.Profile.CreditCards.deleteCreditCard( + input.creditCardId + ), { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -149,7 +154,7 @@ export const userMutationRouter = router({ ctx, }) { generatePreferencesLinkCounter.add(1) - const apiResponse = await api.get(api.endpoints.v1.subscriberId, { + const apiResponse = await api.get(api.endpoints.v1.Profile.subscriberId, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 224f03198..a0d7face0 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -89,7 +89,7 @@ export const getVerifiedUser = cache( } getVerifiedUserCounter.add(1) 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: { 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 user = { @@ -211,7 +211,7 @@ function parsedUser(data: User, isMFA: boolean) { async function getCreditCards(session: Session) { getCreditCardsCounter.add(1) 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: { Authorization: `Bearer ${session.token.access_token}`, }, @@ -354,7 +354,7 @@ export const userQueryRouter = router({ JSON.stringify({ query: { params } }) ) const previousStaysResponse = await api.get( - api.endpoints.v1.previousStays, + api.endpoints.v1.Booking.Stays.past, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -430,7 +430,7 @@ export const userQueryRouter = router({ ) const apiResponse = await api.get( - api.endpoints.v1.previousStays, + api.endpoints.v1.Booking.Stays.past, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -492,7 +492,7 @@ export const userQueryRouter = router({ ) const nextCursor = verifiedData.data.links && - verifiedData.data.links.offset < verifiedData.data.links.totalCount + verifiedData.data.links.offset < verifiedData.data.links.totalCount ? verifiedData.data.links.offset : undefined @@ -525,7 +525,7 @@ export const userQueryRouter = router({ JSON.stringify({ query: { params } }) ) const apiResponse = await api.get( - api.endpoints.v1.upcomingStays, + api.endpoints.v1.Booking.Stays.future, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -585,7 +585,7 @@ export const userQueryRouter = router({ }) const nextCursor = verifiedData.data.links && - verifiedData.data.links.offset < verifiedData.data.links.totalCount + verifiedData.data.links.offset < verifiedData.data.links.totalCount ? verifiedData.data.links.offset : undefined @@ -611,13 +611,16 @@ export const userQueryRouter = router({ "api.transaction.friendTransactions start", JSON.stringify({}) ) - const apiResponse = await api.get(api.endpoints.v1.friendTransactions, { - cache: undefined, // override defaultOptions - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - next: { revalidate: 30 * 60 * 1000 }, - }) + const apiResponse = await api.get( + api.endpoints.v1.Profile.Transaction.friendTransactions, + { + cache: undefined, // override defaultOptions + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + next: { revalidate: 30 * 60 * 1000 }, + } + ) if (!apiResponse.ok) { // switch (apiResponse.status) { @@ -740,7 +743,7 @@ export const userQueryRouter = router({ membershipCards: protectedProcedure.query(async function ({ ctx }) { getProfileCounter.add(1) 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", headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, diff --git a/server/routers/user/utils.ts b/server/routers/user/utils.ts index cde50270f..c720195b1 100644 --- a/server/routers/user/utils.ts +++ b/server/routers/user/utils.ts @@ -35,7 +35,7 @@ async function updateStaysBookingUrl( // Temporary API call needed till we have user name in ctx session data getProfileCounter.add(1) 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", headers: { Authorization: `Bearer ${token}`, diff --git a/server/trpc.ts b/server/trpc.ts index 0079a1ff6..688ea01cf 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -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 export const contentStackUidWithServiceProcedure = contentstackExtendedProcedureUID.unstable_concat(serviceProcedure) @@ -186,3 +186,6 @@ export const contentStackBaseWithServiceProcedure = export const contentStackBaseWithProtectedProcedure = contentstackBaseProcedure.unstable_concat(protectedProcedure) + +export const safeProtectedServiceProcedure = + safeProtectedProcedure.unstable_concat(serviceProcedure) diff --git a/stores/enter-details.ts b/stores/enter-details.ts index d4d2666d1..b5c862d36 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -3,21 +3,22 @@ import { createContext, useContext } from "react" import { create, useStore } from "zustand" 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 { BreakfastPackage } from "@/types/components/enterDetails/breakfast" import { DetailsSchema } from "@/types/components/enterDetails/details" import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" import { StepEnum } from "@/types/components/enterDetails/step" -import { bedTypeEnum } from "@/types/enums/bedType" -import { breakfastEnum } from "@/types/enums/breakfast" +import { BedTypeEnum } from "@/types/enums/bedType" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" const SESSION_STORAGE_KEY = "enterDetails" interface EnterDetailsState { data: { - bedType: bedTypeEnum | undefined - breakfast: breakfastEnum | undefined + bedType: BedTypeEnum | undefined + breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined } & DetailsSchema steps: StepEnum[] currentStep: StepEnum @@ -26,7 +27,7 @@ interface EnterDetailsState { completeStep: (updatedData: Partial) => void navigate: ( step: StepEnum, - updatedData?: Record + updatedData?: Record ) => void setCurrentStep: (step: StepEnum) => void openSidePeek: (key: SidePeekEnum | null) => void @@ -75,7 +76,7 @@ export function initEditDetailsState(currentStep: StepEnum) { initialData = { ...initialData, ...validatedBedType.data } isValid[StepEnum.selectBed] = true } - const validatedBreakfast = breakfastSchema.safeParse(inputData) + const validatedBreakfast = breakfastStoreSchema.safeParse(inputData) if (validatedBreakfast.success) { validPaths.push(StepEnum.details) initialData = { ...initialData, ...validatedBreakfast.data } diff --git a/types/components/enterDetails/breakfast.ts b/types/components/enterDetails/breakfast.ts index 868bc96a1..21ba37bd0 100644 --- a/types/components/enterDetails/breakfast.ts +++ b/types/components/enterDetails/breakfast.ts @@ -1,5 +1,21 @@ 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 {} +import { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" + +export interface BreakfastFormSchema + extends z.output {} + +export interface BreakfastPackages + extends z.output {} + +export interface BreakfastPackage + extends z.output {} + +export interface BreakfastProps { + packages: BreakfastPackages | null +} diff --git a/types/enums/bedType.ts b/types/enums/bedType.ts index 0b4ba284d..2feb6d980 100644 --- a/types/enums/bedType.ts +++ b/types/enums/bedType.ts @@ -1,4 +1,4 @@ -export enum bedTypeEnum { +export enum BedTypeEnum { KING = "KING", QUEEN = "QUEEN", } diff --git a/types/enums/breakfast.ts b/types/enums/breakfast.ts index 567db2860..81ff51a2e 100644 --- a/types/enums/breakfast.ts +++ b/types/enums/breakfast.ts @@ -1,4 +1,5 @@ -export enum breakfastEnum { - BREAKFAST = "BREAKFAST", +export enum BreakfastPackageEnum { + FREE_MEMBER_BREAKFAST = "BRF0", + REGULAR_BREAKFAST = "BRF1", NO_BREAKFAST = "NO_BREAKFAST", } diff --git a/types/enums/currency.ts b/types/enums/currency.ts new file mode 100644 index 000000000..c04ed8450 --- /dev/null +++ b/types/enums/currency.ts @@ -0,0 +1,7 @@ +export enum CurrencyEnum { + DKK = "DKK", + EUR = "EUR", + NOK = "NOK", + PLN = "PLN", + SEK = "SEK", +} diff --git a/types/enums/packages.ts b/types/enums/packages.ts new file mode 100644 index 000000000..f030ccaab --- /dev/null +++ b/types/enums/packages.ts @@ -0,0 +1,7 @@ +export enum PackageTypeEnum { + AccessibleFriendlyRoom = "AccessibleFriendlyRoom", + AllergyRoom = "AllergyRoom", + BreakfastAdult = "BreakfastAdult", + BreakfastChildren = "BreakfastChildren", + PetRoom = "PetRoom", +}