diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/loading.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/loading.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css new file mode 100644 index 000000000..06b39ef41 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css @@ -0,0 +1,157 @@ +.details, +.guest, +.header, +.hgroup, +.hotel, +.list, +.main, +.section, +.receipt, +.total { + display: flex; + flex-direction: column; +} + +.main { + gap: var(--Spacing-x5); + margin: 0 auto; + width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 708px); +} + +.header, +.hgroup { + align-items: center; +} + +.header { + gap: var(--Spacing-x3); +} + +.hgroup { + gap: var(--Spacing-x-half); +} + +.body { + max-width: 560px; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x9); +} + +.booking { + display: grid; + gap: var(--Spacing-x-one-and-half); + grid-template-areas: + "image" + "details" + "actions"; +} + +.actions, +.details { + background-color: var(--Base-Surface-Subtle-Normal); + border-radius: var(--Corner-radius-Medium); +} + +.details { + gap: var(--Spacing-x3); + grid-area: details; + padding: var(--Spacing-x2); +} + +.tempImage { + align-items: center; + background-color: lightgrey; + border-radius: var(--Corner-radius-Medium); + display: flex; + grid-area: image; + justify-content: center; +} + +.actions { + display: grid; + grid-area: actions; + padding: var(--Spacing-x1) var(--Spacing-x2); +} + +.list { + gap: var(--Spacing-x-one-and-half); + list-style: none; + margin: 0; + padding: 0; +} + +.listItem { + align-items: center; + display: flex; + gap: var(--Spacing-x1); + justify-content: space-between; +} + +.summary { + display: grid; + gap: var(--Spacing-x3); +} + +.guest, +.hotel { + gap: var(--Spacing-x-half); +} + +.receipt, +.total { + gap: var(--Spacing-x2); +} + +.divider { + grid-column: 1 / -1; +} + +@media screen and (max-width: 767px) { + .actions { + & > button[class*="btn"][class*="icon"][class*="small"] { + border-bottom: 1px solid var(--Base-Border-Subtle); + border-radius: 0; + justify-content: space-between; + + &:last-of-type { + border-bottom: none; + } + + & > svg { + order: 2; + } + } + } + + .tempImage { + min-height: 250px; + } +} + +@media screen and (min-width: 768px) { + .booking { + grid-template-areas: + "details image" + "actions actions"; + grid-template-columns: 1fr minmax(256px, min(256px, 100%)); + } + + .actions { + gap: var(--Spacing-x7); + grid-template-columns: repeat(auto-fit, minmax(50px, auto)); + justify-content: center; + padding: var(--Spacing-x1) var(--Spacing-x3); + } + + .details { + padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x2); + } + + .summary { + grid-template-columns: 1fr 1fr; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx new file mode 100644 index 000000000..23b5270d4 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -0,0 +1,279 @@ +import { dt } from "@/lib/dt" +import { serverClient } from "@/lib/trpc/server" + +import { + CalendarIcon, + DownloadIcon, + ImageIcon, + PrinterIcon, +} from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./page.module.css" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function BookingConfirmationPage({ + params, +}: PageArgs) { + const booking = await serverClient().booking.confirmation({ + confirmationNumber: "991697377", + }) + + if (!booking) { + return null + } + + const intl = await getIntl() + const text = intl.formatMessage( + { id: "booking.confirmation.text" }, + { + emailLink: (str) => ( + + {str} + + ), + } + ) + + const fromDate = dt(booking.temp.fromDate).locale(params.lang) + const toDate = dt(booking.temp.toDate).locale(params.lang) + const nights = intl.formatMessage( + { id: "booking.nights" }, + { + totalNights: dt(toDate.format("YYYY-MM-DD")).diff( + dt(fromDate.format("YYYY-MM-DD")), + "days" + ), + } + ) + + return ( +
+
+
+ + {intl.formatMessage({ id: "booking.confirmation.title" })} + + + {booking.hotel.name} + +
+ + {text} + +
+
+
+
+
+ + {intl.formatMessage( + { id: "Reference #{bookingNr}" }, + { bookingNr: "A92320VV" } + )} + +
+
    +
  • + {intl.formatMessage({ id: "Check-in" })} + + {`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`} + +
  • +
  • + {intl.formatMessage({ id: "Check-out" })} + + {`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`} + +
  • +
  • + {intl.formatMessage({ id: "Breakfast" })} + + {booking.temp.breakfastFrom} - {booking.temp.breakfastTo} + +
  • +
  • + {intl.formatMessage({ id: "Cancellation policy" })} + + {intl.formatMessage({ id: booking.temp.cancelPolicy })} + +
  • +
  • + {intl.formatMessage({ id: "Rebooking" })} + {`${intl.formatMessage({ id: "Free until" })} ${fromDate.subtract(3, "day").format("ddd, D MMM")}`} +
  • +
+
+ +
+ + + +
+
+
+
+ + {intl.formatMessage({ id: "Guest" })} + +
+ + {`${booking.guest.firstName} ${booking.guest.lastName}${booking.guest.memberbershipNumber ? ` (${intl.formatMessage({ id: "member no" })} ${booking.guest.memberbershipNumber})` : ""}`} + + {booking.guest.email} + + {booking.guest.phoneNumber} + +
+
+
+ + {intl.formatMessage({ id: "Your hotel" })} + +
+ + {booking.hotel.name} + + {booking.hotel.email} + + {booking.hotel.phoneNumber} + +
+
+ +
+
+ + {`${booking.temp.room.type}, ${nights}`} + + {booking.temp.room.price} +
+ {booking.temp.packages.map((pkg) => ( +
+ + {pkg.name} + + {pkg.price} +
+ ))} +
+
+
+ + {intl.formatMessage({ id: "VAT" })} + + {booking.temp.room.vat} +
+
+ + {intl.formatMessage({ id: "Total cost" })} + + {booking.temp.total} + + {`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`} + +
+
+ +
+ + {`${intl.formatMessage({ id: "Payment received" })} ${dt(booking.temp.payment).locale(params.lang).format("D MMM YYYY, h:mm z")}`} + + + {intl.formatMessage( + { id: "{card} ending with {cardno}" }, + { + card: "Mastercard", + cardno: "2202", + } + )} + +
+
+
+
+ ) +} + +// const { email, hotel, stay, summary } = tempConfirmationData + +// const confirmationNumber = useMemo(() => { +// if (typeof window === "undefined") return "" + +// const storedConfirmationNumber = sessionStorage.getItem( +// BOOKING_CONFIRMATION_NUMBER +// ) +// TODO: cleanup stored values +// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER) +// return storedConfirmationNumber +// }, []) + +// const bookingStatus = useHandleBookingStatus( +// confirmationNumber, +// BookingStatusEnum.BookingCompleted, +// maxRetries, +// retryInterval +// ) + +// if ( +// confirmationNumber === null || +// bookingStatus.isError || +// (bookingStatus.isFetched && !bookingStatus.data) +// ) { +// // TODO: handle error +// throw new Error("Error fetching booking status") +// } + +// if ( +// bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted +// ) { +// return diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css new file mode 100644 index 000000000..3f89e6f51 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css @@ -0,0 +1,5 @@ +.layout { + background-color: var(--Base-Surface-Primary-light-Normal); + min-height: 100dvh; + padding: 80px 0 160px; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx new file mode 100644 index 000000000..971c66e0d --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx @@ -0,0 +1,15 @@ +import { notFound } from "next/navigation" + +import { env } from "@/env/server" + +import styles from "./layout.module.css" + +// route groups needed as layouts have different bgc +export default function ConfirmedBookingLayout({ + children, +}: React.PropsWithChildren) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return
{children}
+} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/layout.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/layout.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx similarity index 95% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index 89f4e62ca..8dca8f4c7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -3,7 +3,7 @@ import { env } from "@/env/server" import { fetchAvailableHotels, getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils" +} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" import { setLang } from "@/i18n/serverContext" diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx similarity index 95% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index b6548122c..7dcec9130 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -3,7 +3,7 @@ import { selectHotelMap } from "@/constants/routes/hotelReservation" import { fetchAvailableHotels, getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils" +} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" import { ChevronRightIcon } from "@/components/Icons" diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils.ts rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css deleted file mode 100644 index 2771aef75..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.main { - display: flex; - justify-content: center; - padding: var(--Spacing-x4); - background-color: var(--Scandic-Brand-Warm-White); - min-height: 100dvh; - max-width: var(--max-width); - margin: 0 auto; -} - -.section { - display: flex; - flex-direction: column; - gap: var(--Spacing-x4); - width: 100%; -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx deleted file mode 100644 index 0a22c5bc0..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client" - -import { useMemo } from "react" - -import { - BOOKING_CONFIRMATION_NUMBER, - BookingStatusEnum, -} from "@/constants/booking" - -import IntroSection from "@/components/HotelReservation/BookingConfirmation/IntroSection" -import StaySection from "@/components/HotelReservation/BookingConfirmation/StaySection" -import SummarySection from "@/components/HotelReservation/BookingConfirmation/SummarySection" -import { tempConfirmationData } from "@/components/HotelReservation/BookingConfirmation/tempConfirmationData" -import LoadingSpinner from "@/components/LoadingSpinner" -import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" - -import styles from "./page.module.css" - -const maxRetries = 10 -const retryInterval = 2000 - -export default function BookingConfirmationPage() { - const { email, hotel, stay, summary } = tempConfirmationData - - const confirmationNumber = useMemo(() => { - if (typeof window === "undefined") return "" - - const storedConfirmationNumber = sessionStorage.getItem( - BOOKING_CONFIRMATION_NUMBER - ) - // TODO: cleanup stored values - // sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER) - return storedConfirmationNumber - }, []) - - const bookingStatus = useHandleBookingStatus( - confirmationNumber, - BookingStatusEnum.BookingCompleted, - maxRetries, - retryInterval - ) - - if ( - confirmationNumber === null || - bookingStatus.isError || - (bookingStatus.isFetched && !bookingStatus.data) - ) { - // TODO: handle error - throw new Error("Error fetching booking status") - } - - if ( - bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted - ) { - return ( -
-
- - - -
-
- ) - } - - return -} diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx new file mode 100644 index 000000000..e0ff199ee --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx @@ -0,0 +1,3 @@ +export default function ConfirmedBookingSlot() { + return null +} diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx b/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx deleted file mode 100644 index 448dc82e1..000000000 --- a/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useIntl } from "react-intl" - -import Button from "@/components/TempDesignSystem/Button" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Title from "@/components/TempDesignSystem/Text/Title" - -import styles from "./introSection.module.css" - -import { IntroSectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export default function IntroSection({ email }: IntroSectionProps) { - const intl = useIntl() - - return ( -
-
- - {intl.formatMessage({ id: "Thank you" })} - - - {intl.formatMessage({ id: "We look forward to your visit!" })} - -
- - {intl.formatMessage({ - id: "We have sent a detailed confirmation of your booking to your email: ", - })} - {email} - -
- - -
-
- ) -} diff --git a/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css b/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css deleted file mode 100644 index 5b79c3796..000000000 --- a/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.section { - display: flex; - flex-direction: column; - gap: var(--Spacing-x3); - width: 100%; -} - -.buttons { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--Spacing-x2); -} - -.button { - width: 100%; - max-width: 240px; - justify-content: center; -} - -@media screen and (min-width: 1367px) { - .buttons { - flex-direction: row; - justify-content: space-around; - } -} diff --git a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx b/components/HotelReservation/BookingConfirmation/StaySection/index.tsx deleted file mode 100644 index 7907ac191..000000000 --- a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useIntl } from "react-intl" - -import { ArrowRightIcon, ScandicLogoIcon } from "@/components/Icons" -import Image from "@/components/Image" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Title from "@/components/TempDesignSystem/Text/Title" - -import styles from "./staySection.module.css" - -import { StaySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export default function StaySection({ hotel, stay }: StaySectionProps) { - const intl = useIntl() - - const nightsText = - stay.nights > 1 - ? intl.formatMessage({ id: "nights" }) - : intl.formatMessage({ id: "night" }) - - return ( - <> -
- -
-
- - - {hotel.name} - - - {hotel.address} - {hotel.phone} - -
- - {`${stay.nights} ${nightsText}`} - - {stay.start} - - {stay.end} - - -
-
-
-
- - {intl.formatMessage({ id: "Breakfast" })} - - - {`${intl.formatMessage({ id: "Weekdays" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`} - {`${intl.formatMessage({ id: "Weekends" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`} - -
-
- {intl.formatMessage({ id: "Check in" })} - - {intl.formatMessage({ id: "From" })} - {hotel.checkIn} - -
-
- - {intl.formatMessage({ id: "Check out" })} - - - {intl.formatMessage({ id: "At latest" })} - {hotel.checkOut} - -
-
- - ) -} diff --git a/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css b/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css deleted file mode 100644 index 1eae5c732..000000000 --- a/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css +++ /dev/null @@ -1,78 +0,0 @@ -.card { - display: flex; - width: 100%; - background-color: var(--Base-Surface-Primary-light-Normal); - border: 1px solid var(--Base-Border-Subtle); - border-radius: var(--Corner-radius-Small); - overflow: hidden; -} - -.image { - height: 100%; - width: 105px; - object-fit: cover; -} - -.info { - display: flex; - flex-direction: column; - width: 100%; - gap: var(--Spacing-x1); - padding: var(--Spacing-x2); -} - -.hotel, -.stay { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-half); -} - -.caption { - display: flex; - flex-direction: column; -} - -.dates { - display: flex; - align-items: center; - gap: var(--Spacing-x-half); -} - -.table { - display: flex; - justify-content: space-between; - padding: var(--Spacing-x2); - border-radius: var(--Corner-radius-Small); - background-color: var(--Base-Surface-Primary-dark-Normal); - width: 100%; -} - -.breakfast, -.checkIn, -.checkOut { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-half); -} - -@media screen and (min-width: 1367px) { - .card { - flex-direction: column; - } - .image { - width: 100%; - max-height: 195px; - } - - .info { - flex-direction: row; - justify-content: space-between; - } - - .hotel, - .stay { - width: 100%; - max-width: 230px; - } -} diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx b/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx deleted file mode 100644 index 16eb84330..000000000 --- a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useIntl } from "react-intl" - -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Title from "@/components/TempDesignSystem/Text/Title" - -import styles from "./summarySection.module.css" - -import { SummarySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export default function SummarySection({ summary }: SummarySectionProps) { - const intl = useIntl() - const roomType = `${intl.formatMessage({ id: "Type of room" })}: ${summary.roomType}` - const bedType = `${intl.formatMessage({ id: "Type of bed" })}: ${summary.bedType}` - const breakfast = `${intl.formatMessage({ id: "Breakfast" })}: ${summary.breakfast}` - const flexibility = `${intl.formatMessage({ id: "Flexibility" })}: ${summary.flexibility}` - - return ( -
- - {intl.formatMessage({ id: "Summary" })} - - - {roomType} - 1648 SEK - - - {bedType} - 0 SEK - - - {breakfast} - 198 SEK - - - {flexibility} - 200 SEK - -
- ) -} diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css b/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css deleted file mode 100644 index b65d92e76..000000000 --- a/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.section { - width: 100%; -} - -.summary { - display: flex; - justify-content: space-between; - border-bottom: 1px solid var(--Base-Border-Subtle); -} - -.summary span { - padding: var(--Spacing-x2) var(--Spacing-x0); -} diff --git a/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts b/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts deleted file mode 100644 index 2dbf572e7..000000000 --- a/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BookingConfirmation } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export const tempConfirmationData: BookingConfirmation = { - email: "lisa.andersson@outlook.com", - hotel: { - name: "Helsinki Hub", - address: "Kaisaniemenkatu 7, Helsinki", - location: "Helsinki", - phone: "+358 300 870680", - image: - "https://test3.scandichotels.com/imagevault/publishedmedia/i11isd60bh119s9486b7/downtown-camper-by-scandic-lobby-reception-desk-ch.jpg?w=640", - checkIn: "15.00", - checkOut: "12.00", - breakfast: { start: "06:30", end: "10:00" }, - }, - stay: { - nights: 1, - start: "2024.03.09", - end: "2024.03.10", - }, - summary: { - roomType: "Standard Room", - bedType: "King size", - breakfast: "Yes", - flexibility: "Yes", - }, -} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 3d582e7e3..5ad6bdaf9 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -66,7 +66,7 @@ export default function Payment({ resolver: zodResolver(paymentSchema), }) - const initiateBooking = trpc.booking.booking.create.useMutation({ + const initiateBooking = trpc.booking.create.useMutation({ onSuccess: (result) => { if (result?.confirmationNumber) { setConfirmationNumber(result.confirmationNumber) diff --git a/components/Icons/Download.tsx b/components/Icons/Download.tsx new file mode 100644 index 000000000..7c1e9017a --- /dev/null +++ b/components/Icons/Download.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function DownloadIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Printer.tsx b/components/Icons/Printer.tsx new file mode 100644 index 000000000..d703940da --- /dev/null +++ b/components/Icons/Printer.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function PrinterIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 8a7764dd6..e7547b178 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -28,6 +28,7 @@ export { default as CrossCircle } from "./CrossCircle" export { default as CulturalIcon } from "./Cultural" export { default as DeleteIcon } from "./Delete" export { default as DoorOpenIcon } from "./DoorOpen" +export { default as DownloadIcon } from "./Download" export { default as DresserIcon } from "./Dresser" export { default as EditIcon } from "./Edit" export { default as ElectricBikeIcon } from "./ElectricBike" @@ -76,6 +77,7 @@ export { default as PhoneIcon } from "./Phone" export { default as PlusIcon } from "./Plus" export { default as PlusCircleIcon } from "./PlusCircle" export { default as PriceTagIcon } from "./PriceTag" +export { default as PrinterIcon } from "./Printer" export { default as RestaurantIcon } from "./Restaurant" export { default as RoomServiceIcon } from "./RoomService" export { default as SaunaIcon } from "./Sauna" diff --git a/constants/booking.ts b/constants/booking.ts index 9f5b6fed9..81e23cd23 100644 --- a/constants/booking.ts +++ b/constants/booking.ts @@ -4,6 +4,13 @@ export enum BookingStatusEnum { BookingCompleted = "BookingCompleted", } +export enum BedTypeEnum { + Crib = "Crib", + ExtraBed = "ExtraBed", + ParentsBed = "ParentsBed", + Unknown = "Unknown", +} + export const BOOKING_CONFIRMATION_NUMBER = "bookingConfirmationNumber" export enum PaymentMethodEnum { diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 759c424d0..3500a8d01 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -9,6 +9,7 @@ "Add Room": "Add room", "Add code": "Add code", "Add new card": "Add new card", + "Add to calendar": "Add to calendar", "Address": "Address", "Adults": "Adults", "Age": "Age", @@ -25,7 +26,6 @@ "Approx.": "Approx.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?", "Arrival date": "Arrival date", - "as of today": "as of today", "As our": "As our {level}", "As our Close Friend": "As our Close Friend", "At latest": "At latest", @@ -39,12 +39,6 @@ "Book": "Book", "Book reward night": "Book reward night", "Booking number": "Booking number", - "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", - "booking.children": "{totalChildren, plural, one {# child} other {# children}}", - "booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}", - "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", - "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.", "Breakfast": "Breakfast", "Breakfast buffet": "Breakfast buffet", "Breakfast excluded": "Breakfast excluded", @@ -53,12 +47,13 @@ "Breakfast selection in next step.": "Breakfast selection in next step.", "Bus terminal": "Bus terminal", "Business": "Business", - "by": "by", "Cancel": "Cancel", - "characters": "characters", + "Cancellation policy": "Cancellation policy", "Check in": "Check in", "Check out": "Check out", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.", + "Check-in": "Check-in", + "Check-out": "Check-out", "Child age is required": "Child age is required", "Children": "Children", "Choose room": "Choose room", @@ -100,6 +95,7 @@ "Distance to city centre": "{number}km to city centre", "Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?", "Done": "Done", + "Download invoice": "Download invoice", "Download the Scandic app": "Download the Scandic app", "Driving directions": "Driving directions", "Earn bonus nights & points": "Earn bonus nights & points", @@ -114,9 +110,9 @@ "Explore all levels and benefits": "Explore all levels and benefits", "Explore nearby": "Explore nearby", "Extras to your booking": "Extras to your booking", + "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.", "Fair": "Fair", - "FAQ": "FAQ", "Find booking": "Find booking", "Find hotels": "Find hotels", "First name": "First name", @@ -125,14 +121,14 @@ "Former Scandic Hotel": "Former Scandic Hotel", "Free cancellation": "Free cancellation", "Free rebooking": "Free rebooking", + "Free until": "Free until", "From": "From", "Get inspired": "Get inspired", "Get member benefits & offers": "Get member benefits & offers", "Go back to edit": "Go back to edit", "Go back to overview": "Go back to overview", - "guest": "guest", + "Guest": "Guest", "Guest information": "Guest information", - "guests": "guests", "Guests & Rooms": "Guests & Rooms", "Hi": "Hi", "Highest level": "Highest level", @@ -140,9 +136,6 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "persons", - "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "Hotels": "Hotels", "How do you want to sleep?": "How do you want to sleep?", "How it works": "How it works", @@ -153,10 +146,9 @@ "In extra bed": "In extra bed", "Included": "Included", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", - "Join at no cost": "Join at no cost", "Join Scandic Friends": "Join Scandic Friends", + "Join at no cost": "Join at no cost", "King bed": "King bed", - "km to city center": "km to city center", "Language": "Language", "Last name": "Last name", "Latest searches": "Latest searches", @@ -176,7 +168,7 @@ "Log in here": "Log in here", "Log in/Join": "Log in/Join", "Log out": "Log out", - "lowercase letter": "lowercase letter", + "MY SAVED CARDS": "MY SAVED CARDS", "Main menu": "Main menu", "Manage preferences": "Manage preferences", "Map": "Map", @@ -186,9 +178,9 @@ "Member price": "Member price", "Member price from": "Member price from", "Members": "Members", - "Membership cards": "Membership cards", "Membership ID": "Membership ID", "Membership ID copied to clipboard": "Membership ID copied to clipboard", + "Membership cards": "Membership cards", "Menu": "Menu", "Modify": "Modify", "Month": "Month", @@ -198,16 +190,11 @@ "My pages": "My pages", "My pages menu": "My pages menu", "My payment cards": "My payment cards", - "MY SAVED CARDS": "MY SAVED CARDS", "My wishes": "My wishes", - "n/a": "n/a", "Nearby": "Nearby", "Nearby companies": "Nearby companies", "New password": "New password", "Next": "Next", - "next level:": "next level:", - "night": "night", - "nights": "nights", "Nights needed to level up": "Nights needed to level up", "No breakfast": "No breakfast", "No content published": "No content published", @@ -220,14 +207,12 @@ "Nordic Swan Ecolabel": "Nordic Swan Ecolabel", "Not found": "Not found", "Nr night, nr adult": "{nights, number} night, {adults, number} adult", - "number": "number", + "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", "On your journey": "On your journey", "Open": "Open", "Open language menu": "Open language menu", "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", - "or": "or", - "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", "Overview": "Overview", "Parking": "Parking", "Parking / Garage": "Parking / Garage", @@ -235,11 +220,11 @@ "Pay later": "Pay later", "Pay now": "Pay now", "Payment info": "Payment info", + "Payment received": "Payment received", "Phone": "Phone", "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", - "points": "Points", "Points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", @@ -247,6 +232,7 @@ "Points needed to level up": "Points needed to level up", "Points needed to stay on level": "Points needed to stay on level", "Previous victories": "Previous victories", + "Print confirmation": "Print confirmation", "Proceed to login": "Proceed to login", "Proceed to payment method": "Proceed to payment method", "Public price from": "Public price from", @@ -256,6 +242,8 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Read more about the hotel", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Rebooking": "Rebooking", + "Reference #{bookingNr}": "Reference #{bookingNr}", "Remove card from member profile": "Remove card from member profile", "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -301,34 +289,32 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.", "Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.", "Something went wrong!": "Something went wrong!", - "special character": "special character", - "spendable points expiring by": "{points} spendable points expiring by {date}", "Sports": "Sports", "Standard price": "Standard price", "Street": "Street", "Successfully updated profile!": "Successfully updated profile!", "Summary": "Summary", + "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.", "Terms and conditions": "Terms and conditions", "Thank you": "Thank you", "Theatre": "Theatre", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", - "to": "to", - "Total incl VAT": "Total incl VAT", "Total Points": "Total Points", + "Total cost": "Total cost", + "Total incl VAT": "Total incl VAT", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", "Transportations": "Transportations", "Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)", - "TUI Points": "TUI Points", "Type of bed": "Type of bed", "Type of room": "Type of room", - "uppercase letter": "uppercase letter", "Use bonus cheque": "Use bonus cheque", "Use code/voucher": "Use code/voucher", "User information": "User information", + "VAT": "VAT", "View as list": "View as list", "View as map": "View as map", "View your booking": "View your booking", @@ -347,18 +333,19 @@ "Where to": "Where to", "Which room class suits you the best?": "Which room class suits you the best?", "Year": "Year", - "Yes, discard changes": "Yes, discard changes", "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", + "Yes, discard changes": "Yes, discard changes", "Yes, remove my card": "Yes, remove my card", "You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.", "You canceled adding a new credit card.": "You canceled adding a new credit card.", "You have no previous stays.": "You have no previous stays.", "You have no upcoming stays.": "You have no upcoming stays.", + "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your card was successfully removed!": "Your card was successfully removed!", "Your card was successfully saved!": "Your card was successfully saved!", - "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your current level": "Your current level", "Your details": "Your details", + "Your hotel": "Your hotel", "Your level": "Your level", "Your points to spend": "Your points to spend", "Your room": "Your room", @@ -366,7 +353,39 @@ "Zoo": "Zoo", "Zoom in": "Zoom in", "Zoom out": "Zoom out", + "as of today": "as of today", + "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", + "booking.children": "{totalChildren, plural, one {# child} other {# children}}", + "booking.confirmation.text": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please email us.", + "booking.confirmation.title": "Your booking is confirmed", + "booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}", + "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", + "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.", + "by": "by", + "characters": "characters", + "from": "from", + "guest": "guest", + "guests": "guests", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "persons", + "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", + "km to city center": "km to city center", + "lowercase letter": "lowercase letter", + "member no": "member no", + "n/a": "n/a", + "next level:": "next level:", + "night": "night", + "nights": "nights", + "number": "number", + "or": "or", + "points": "Points", + "special character": "special character", + "spendable points expiring by": "{points} spendable points expiring by {date}", + "to": "to", + "uppercase letter": "uppercase letter", "{amount} {currency}": "{amount} {currency}", + "{card} ending with {cardno}": "{card} ending with {cardno}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" -} +} \ No newline at end of file diff --git a/i18n/index.ts b/i18n/index.ts index be2d744bb..0a8ebb67d 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -8,7 +8,7 @@ import { Lang } from "@/constants/languages" const cache = createIntlCache() async function initIntl(lang: Lang) { - return createIntl( + return createIntl( { defaultLocale: Lang.en, locale: lang, diff --git a/lib/dt.ts b/lib/dt.ts index b90938475..5cbe77692 100644 --- a/lib/dt.ts +++ b/lib/dt.ts @@ -7,6 +7,7 @@ import d from "dayjs" import advancedFormat from "dayjs/plugin/advancedFormat" import isToday from "dayjs/plugin/isToday" import relativeTime from "dayjs/plugin/relativeTime" +import timezone from "dayjs/plugin/timezone" import utc from "dayjs/plugin/utc" /** @@ -59,6 +60,7 @@ d.locale("no", { d.extend(advancedFormat) d.extend(isToday) d.extend(relativeTime) +d.extend(timezone) d.extend(utc) export const dt = d diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index 7e9b9b2c8..cbda7d3ef 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -63,6 +63,10 @@ export const createBookingInput = z.object({ }) // Query -export const getBookingStatusInput = z.object({ +const confirmationNumberInput = z.object({ confirmationNumber: z.string(), }) + +export const bookingConfirmationInput = confirmationNumberInput + +export const getBookingStatusInput = confirmationNumberInput diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 1ff3422af..9e5677d32 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -35,96 +35,95 @@ async function getMembershipNumber( } export const bookingMutationRouter = router({ - booking: router({ - create: serviceProcedure - .input(createBookingInput) - .mutation(async function ({ ctx, input }) { - const { checkInDate, checkOutDate, hotelId } = input + create: serviceProcedure.input(createBookingInput).mutation(async function ({ + ctx, + input, + }) { + const { checkInDate, checkOutDate, hotelId } = input - // TODO: add support for user token OR service token in procedure - // then we can fetch membership number if user token exists - const loggingAttributes = { - // membershipNumber: await getMembershipNumber(ctx.session), - checkInDate, - checkOutDate, - hotelId, - } + // TODO: add support for user token OR service token in procedure + // then we can fetch membership number if user token exists + const loggingAttributes = { + // membershipNumber: await getMembershipNumber(ctx.session), + checkInDate, + checkOutDate, + hotelId, + } - createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate }) + createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate }) - console.info( - "api.booking.booking.create start", - JSON.stringify({ - query: loggingAttributes, - }) - ) - const headers = { - Authorization: `Bearer ${ctx.serviceToken}`, - } + console.info( + "api.booking.create start", + JSON.stringify({ + query: loggingAttributes, + }) + ) + const headers = { + Authorization: `Bearer ${ctx.serviceToken}`, + } - const apiResponse = await api.post(api.endpoints.v1.booking, { - headers, - body: input, + const apiResponse = await api.post(api.endpoints.v1.booking, { + headers, + body: input, + }) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + createBookingFailCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + }), + }) + console.error( + "api.booking.create error", + JSON.stringify({ + query: loggingAttributes, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + error: text, + }, }) + ) + return null + } - if (!apiResponse.ok) { - const text = await apiResponse.text() - createBookingFailCounter.add(1, { - hotelId, - checkInDate, - checkOutDate, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - }), - }) - console.error( - "api.booking.booking.create error", - JSON.stringify({ - query: loggingAttributes, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - error: text, - }, - }) - ) - return null - } + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + createBookingFailCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + error_type: "validation_error", + }) - const apiJson = await apiResponse.json() - const verifiedData = createBookingSchema.safeParse(apiJson) - if (!verifiedData.success) { - createBookingFailCounter.add(1, { - hotelId, - checkInDate, - checkOutDate, - error_type: "validation_error", - }) - - console.error( - "api.booking.booking.create validation error", - JSON.stringify({ - query: loggingAttributes, - error: verifiedData.error, - }) - ) - return null - } - - createBookingSuccessCounter.add(1, { - hotelId, - checkInDate, - checkOutDate, + console.error( + "api.booking.create validation error", + JSON.stringify({ + query: loggingAttributes, + error: verifiedData.error, }) + ) + return null + } - console.info( - "api.booking.booking.create success", - JSON.stringify({ - query: loggingAttributes, - }) - ) - return verifiedData.data - }), + createBookingSuccessCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + }) + + console.info( + "api.booking.create success", + JSON.stringify({ + query: loggingAttributes, + }) + ) + return verifiedData.data }), }) diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index 7535aac6e..aacf1ca6b 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -1,5 +1,8 @@ import { z } from "zod" +import { BedTypeEnum } from "@/constants/booking" + +// MUTATION export const createBookingSchema = z .object({ data: z.object({ @@ -32,4 +35,46 @@ export const createBookingSchema = z paymentUrl: d.data.attributes.paymentUrl, })) -type CreateBookingData = z.infer +// QUERY +const childrenAgesSchema = z.object({ + age: z.number(), + bedType: z.nativeEnum(BedTypeEnum), +}) + +const guestSchema = z.object({ + firstName: z.string(), + lastName: z.string(), +}) + +const packagesSchema = z.object({ + accessibility: z.boolean(), + allergyFriendly: z.boolean(), + breakfast: z.boolean(), + petFriendly: z.boolean(), +}) + +export const bookingConfirmationSchema = z + .object({ + data: z.object({ + attributes: z.object({ + adults: z.number(), + checkInDate: z.date({ coerce: true }), + checkOutDate: z.date({ coerce: true }), + createDateTime: z.date({ coerce: true }), + childrenAges: z.array(childrenAgesSchema), + computedReservationStatus: z.string(), + confirmationNumber: z.string(), + currencyCode: z.string(), + guest: guestSchema, + hasPayRouting: z.boolean(), + hotelId: z.string(), + packages: packagesSchema, + rateCode: z.string(), + reservationStatus: z.string(), + totalPrice: z.number(), + }), + id: z.string(), + type: z.literal("booking"), + }), + }) + .transform(({ data }) => data.attributes) diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index d053782bd..76b874ba3 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -4,10 +4,20 @@ import * as api from "@/lib/api" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" import { router, serviceProcedure } from "@/server/trpc" -import { getBookingStatusInput } from "./input" -import { createBookingSchema } from "./output" +import { bookingConfirmationInput, getBookingStatusInput } from "./input" +import { bookingConfirmationSchema, createBookingSchema } from "./output" const meter = metrics.getMeter("trpc.booking") +const getBookingConfirmationCounter = meter.createCounter( + "trpc.booking.confirmation" +) +const getBookingConfirmationSuccessCounter = meter.createCounter( + "trpc.booking.confirmation-success" +) +const getBookingConfirmationFailCounter = meter.createCounter( + "trpc.booking.confirmation-fail" +) + const getBookingStatusCounter = meter.createCounter("trpc.booking.status") const getBookingStatusSuccessCounter = meter.createCounter( "trpc.booking.status-success" @@ -17,6 +27,113 @@ const getBookingStatusFailCounter = meter.createCounter( ) export const bookingQueryRouter = router({ + confirmation: serviceProcedure + .input(bookingConfirmationInput) + .query(async function ({ ctx, input: { confirmationNumber } }) { + getBookingConfirmationCounter.add(1, { confirmationNumber }) + + const apiResponse = await api.get( + `${api.endpoints.v1.booking}/${confirmationNumber}`, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + } + ) + + if (!apiResponse.ok) { + const responseMessage = await apiResponse.text() + getBookingConfirmationFailCounter.add(1, { + confirmationNumber, + error_type: "http_error", + error: responseMessage, + }) + console.error( + "api.booking.confirmation error", + JSON.stringify({ + query: { confirmationNumber }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text: responseMessage, + }, + }) + ) + + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + + const apiJson = await apiResponse.json() + const booking = bookingConfirmationSchema.safeParse(apiJson) + if (!booking.success) { + getBookingConfirmationFailCounter.add(1, { + confirmationNumber, + error_type: "validation_error", + error: JSON.stringify(booking.error), + }) + console.error( + "api.booking.confirmation validation error", + JSON.stringify({ + query: { confirmationNumber }, + error: booking.error, + }) + ) + throw badRequestError() + } + + getBookingConfirmationSuccessCounter.add(1, { confirmationNumber }) + console.info( + "api.booking.confirmation success", + JSON.stringify({ + query: { confirmationNumber }, + }) + ) + + return { + ...booking.data, + temp: { + breakfastFrom: "06:30", + breakfastTo: "11:00", + cancelPolicy: "Free rebooking", + fromDate: "2024-10-21 14:00", + packages: [ + { + name: "Breakfast buffet", + price: "150 SEK", + }, + { + name: "Member discount", + price: "-297 SEK", + }, + { + name: "Points used / remaining", + price: "0 / 1044", + }, + ], + payment: "2024-08-09 1:47", + room: { + price: "2 589 SEK", + type: "Cozy Cabin", + vat: "684,79 SEK", + }, + toDate: "2024-10-22 11:00", + total: "2 739 SEK", + totalInEuro: "265 EUR", + }, + guest: { + email: "sarah.obrian@gmail.com", + firstName: "Sarah", + lastName: "O'Brian", + memberbershipNumber: "19822", + phoneNumber: "+46702446688", + }, + hotel: { + email: "bookings@scandichotels.com", + name: "Downtown Camper by Scandic", + phoneNumber: "+4689001350", + }, + } + }), status: serviceProcedure.input(getBookingStatusInput).query(async function ({ ctx, input,