diff --git a/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx index a0373645b..b3ef81538 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[section]/page.tsx @@ -1,6 +1,6 @@ +import { notFound } from "next/navigation" + import { serverClient } from "@/lib/trpc/server" -import { getHotelDataSchema } from "@/server/routers/hotels/output" -import tempHotelData from "@/server/routers/hotels/tempHotelData.json" import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import BedSelection from "@/components/HotelReservation/SelectRate/BedSelection" @@ -80,12 +80,19 @@ export default async function SectionsPage({ }: PageArgs) { setLang(params.lang) - // TODO: Use real endpoint. - const hotel = getHotelDataSchema.parse(tempHotelData) + const hotel = await serverClient().hotel.hotelData.get({ + hotelId: "811", + language: params.lang, + }) + + if (!hotel) { + // TODO: handle case with hotel missing + return notFound() + } const rooms = await serverClient().hotel.rates.get({ // TODO: pass the correct hotel ID and all other parameters that should be included in the search - hotelId: "1", + hotelId: hotel.data.id, }) const intl = await getIntl() @@ -170,7 +177,9 @@ export default async function SectionsPage({ header={intl.formatMessage({ id: "Payment info" })} path={`payment?${currentSearchParams}`} > - {params.section === "payment" && } + {params.section === "payment" && ( + + )}
diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx new file mode 100644 index 000000000..c739b6635 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx index 46f941bbc..0a22c5bc0 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx @@ -1,20 +1,67 @@ +"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 - return ( -
-
- - - -
-
+ 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/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts new file mode 100644 index 000000000..0b8133c43 --- /dev/null +++ b/app/api/web/payment-callback/[lang]/[status]/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from "next/server" +import { env } from "process" + +import { Lang } from "@/constants/languages" +import { + bookingConfirmation, + payment, +} from "@/constants/routes/hotelReservation" + +export async function GET( + request: NextRequest, + { params }: { params: { lang: string; status: string } } +): Promise { + console.log(`[payment-callback] callback started`) + const lang = params.lang as Lang + const status = params.status + const returnUrl = new URL(`${env.PUBLIC_URL}/${payment[lang]}`) + + if (status === "success") { + const confirmationUrl = new URL( + `${env.PUBLIC_URL}/${bookingConfirmation[lang]}` + ) + console.log(`[payment-callback] redirecting to: ${confirmationUrl}`) + return NextResponse.redirect(confirmationUrl) + } + + if (status === "cancel") { + returnUrl.searchParams.set("cancel", "true") + } + + if (status === "error") { + returnUrl.searchParams.set("error", "true") + } + + console.log(`[payment-callback] redirecting to: ${returnUrl}`) + return NextResponse.redirect(returnUrl) +} diff --git a/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx b/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx index 9482f51fe..448dc82e1 100644 --- a/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx +++ b/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx @@ -1,16 +1,17 @@ +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 { getIntl } from "@/i18n" import styles from "./introSection.module.css" import { IntroSectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" -export default async function IntroSection({ email }: IntroSectionProps) { - const intl = await getIntl() +export default function IntroSection({ email }: IntroSectionProps) { + const intl = useIntl() return (
diff --git a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx b/components/HotelReservation/BookingConfirmation/StaySection/index.tsx index 99ecae8ca..7907ac191 100644 --- a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx +++ b/components/HotelReservation/BookingConfirmation/StaySection/index.tsx @@ -1,16 +1,17 @@ +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 { getIntl } from "@/i18n" import styles from "./staySection.module.css" import { StaySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" -export default async function StaySection({ hotel, stay }: StaySectionProps) { - const intl = await getIntl() +export default function StaySection({ hotel, stay }: StaySectionProps) { + const intl = useIntl() const nightsText = stay.nights > 1 diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx b/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx index 509af9c52..16eb84330 100644 --- a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx +++ b/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx @@ -1,13 +1,14 @@ +import { useIntl } from "react-intl" + import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" -import { getIntl } from "@/i18n" import styles from "./summarySection.module.css" import { SummarySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" -export default async function SummarySection({ summary }: SummarySectionProps) { - const intl = await getIntl() +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}` diff --git a/components/HotelReservation/SelectRate/Payment/index.tsx b/components/HotelReservation/SelectRate/Payment/index.tsx index ed8068076..3ddf73c01 100644 --- a/components/HotelReservation/SelectRate/Payment/index.tsx +++ b/components/HotelReservation/SelectRate/Payment/index.tsx @@ -1,62 +1,161 @@ "use client" +import { useRouter } from "next/navigation" +import { useEffect, useState } from "react" +import { useIntl } from "react-intl" + +import { + BOOKING_CONFIRMATION_NUMBER, + BookingStatusEnum, +} from "@/constants/booking" import { trpc } from "@/lib/trpc/client" +import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" +import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" +import useLang from "@/hooks/useLang" + +import styles from "./payment.module.css" + +import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" + +const maxRetries = 40 +const retryInterval = 2000 + +export default function Payment({ hotel }: PaymentProps) { + const router = useRouter() + const lang = useLang() + const intl = useIntl() + const [confirmationNumber, setConfirmationNumber] = useState("") + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState("") -export default function Payment() { const initiateBooking = trpc.booking.booking.create.useMutation({ onSuccess: (result) => { - // TODO: Handle success, poll for payment link and redirect the user to the payment - console.log("Res", result) + if (result?.confirmationNumber) { + // Planet doesn't support query params so we have to store values in session storage + sessionStorage.setItem( + BOOKING_CONFIRMATION_NUMBER, + result.confirmationNumber + ) + + setConfirmationNumber(result.confirmationNumber) + } else { + // TODO: add proper error message + toast.error("Failed to create booking") + } }, - onError: () => { - // TODO: Handle error - console.log("Error") + onError: (error) => { + console.error("Error", error) + // TODO: add proper error message + toast.error("Failed to create booking") }, }) - return ( - + packages: { + breakfast: true, + allergyFriendly: true, + petFriendly: true, + accessibility: true, + }, + smsConfirmationRequested: true, + }, + ], + payment: { + paymentMethod: selectedPaymentMethod, + cardHolder: { + email: "test.user@scandichotels.com", + name: "Test User", + phoneCountryCode: "", + phoneSubscriber: "", + }, + success: `api/web/payment-callback/${lang}/success`, + error: `api/web/payment-callback/${lang}/error`, + cancel: `api/web/payment-callback/${lang}/cancel`, + }, + }) + } + + if ( + initiateBooking.isPending || + (confirmationNumber && !bookingStatus.data?.paymentUrl) + ) { + return + } + + return ( +
+
+
+ + {hotel.merchantInformationData.alternatePaymentOptions.map( + (paymentOption) => ( + + ) + )} +
+
+ +
) } diff --git a/components/HotelReservation/SelectRate/Payment/payment.module.css b/components/HotelReservation/SelectRate/Payment/payment.module.css new file mode 100644 index 000000000..9200ce8f3 --- /dev/null +++ b/components/HotelReservation/SelectRate/Payment/payment.module.css @@ -0,0 +1,18 @@ +.paymentItemContainer { + max-width: 480px; + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); + padding-bottom: var(--Spacing-x4); +} + +.paymentItem { + background-color: var(--Base-Background-Normal); + padding: var(--Spacing-x3); + border: 1px solid var(--Base-Border-Normal); + border-radius: var(--Corner-radius-Medium); + display: flex; + align-items: center; + gap: var(--Spacing-x2); + cursor: pointer; +} diff --git a/constants/booking.ts b/constants/booking.ts new file mode 100644 index 000000000..664240a79 --- /dev/null +++ b/constants/booking.ts @@ -0,0 +1,7 @@ +export enum BookingStatusEnum { + CreatedInOhip = "CreatedInOhip", + PaymentRegistered = "PaymentRegistered", + BookingCompleted = "BookingCompleted", +} + +export const BOOKING_CONFIRMATION_NUMBER = "bookingConfirmationNumber" diff --git a/constants/routes/hotelReservation.js b/constants/routes/hotelReservation.js index 72d298965..5fdb1b6b1 100644 --- a/constants/routes/hotelReservation.js +++ b/constants/routes/hotelReservation.js @@ -28,4 +28,24 @@ export const selectHotelMap = { de: `${selectHotel.de}/map`, } +/** @type {import('@/types/routes').LangRoute} */ +export const payment = { + en: `${hotelReservation.en}/payment`, + sv: `${hotelReservation.sv}/betalning`, + no: `${hotelReservation.no}/betaling`, + fi: `${hotelReservation.fi}/maksu`, + da: `${hotelReservation.da}/payment`, + de: `${hotelReservation.de}/bezahlung`, +} + +/** @type {import('@/types/routes').LangRoute} */ +export const bookingConfirmation = { + en: `${hotelReservation.en}/booking-confirmation`, + sv: `${hotelReservation.sv}/bokningsbekraftelse`, + no: `${hotelReservation.no}/booking-confirmation`, + fi: `${hotelReservation.fi}/varausvahvistus`, + da: `${hotelReservation.da}/booking-confirmation`, + de: `${hotelReservation.de}/buchungsbesttigung`, +} + export const bookingFlow = [...Object.values(hotelReservation)] diff --git a/hooks/booking/useHandleBookingStatus.ts b/hooks/booking/useHandleBookingStatus.ts new file mode 100644 index 000000000..5373057a8 --- /dev/null +++ b/hooks/booking/useHandleBookingStatus.ts @@ -0,0 +1,35 @@ +"use client" + +import { BookingStatusEnum } from "@/constants/booking" +import { trpc } from "@/lib/trpc/client" + +export function useHandleBookingStatus( + confirmationNumber: string | null, + expectedStatus: BookingStatusEnum, + maxRetries: number, + retryInterval: number +) { + const query = trpc.booking.status.useQuery( + { confirmationNumber: confirmationNumber ?? "" }, + { + enabled: !!confirmationNumber, + refetchInterval: (query) => { + if (query.state.error || query.state.dataUpdateCount >= maxRetries) { + return false + } + + if (query.state.data?.reservationStatus === expectedStatus) { + return false + } + + return retryInterval + }, + refetchIntervalInBackground: true, + refetchOnWindowFocus: false, + refetchOnMount: false, + retry: false, + } + ) + + return query +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index f19f18f62..39f49c9a8 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -14,6 +14,7 @@ "Any changes you've made will be lost.": "Alle ændringer, du har foretaget, går tabt.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?", "Arrival date": "Ankomstdato", + "as of today": "fra idag", "As our": "Som vores {level}", "As our Close Friend": "Som vores nære ven", "At latest": "Senest", @@ -23,14 +24,16 @@ "Bed type": "Seng type", "Book": "Book", "Book reward night": "Book bonusnat", - "Code / Voucher": "Bookingkoder / voucher", "Booking number": "Bookingnummer", + "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "Breakfast": "Morgenmad", "Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast included": "Morgenmad inkluderet", "Bus terminal": "Busstation", "Business": "Forretning", + "by": "inden", "Cancel": "Afbestille", + "characters": "tegn", "Check in": "Check ind", "Check out": "Check ud", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.", @@ -45,8 +48,10 @@ "Close menu": "Luk menu", "Close my pages menu": "Luk mine sider menu", "Close the map": "Luk kortet", + "Code / Voucher": "Bookingkoder / voucher", "Coming up": "Er lige om hjørnet", "Compare all levels": "Sammenlign alle niveauer", + "Complete booking & go to payment": "Udfyld booking & gå til betaling", "Contact us": "Kontakt os", "Continue": "Blive ved", "Copyright all rights reserved": "Scandic AB Alle rettigheder forbeholdes", @@ -74,9 +79,9 @@ "Explore all levels and benefits": "Udforsk alle niveauer og fordele", "Explore nearby": "Udforsk i nærheden", "Extras to your booking": "Tillæg til din booking", - "FAQ": "Ofte stillede spørgsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.", "Fair": "Messe", + "FAQ": "Ofte stillede spørgsmål", "Find booking": "Find booking", "Find hotels": "Find hotel", "Flexibility": "Fleksibilitet", @@ -87,17 +92,22 @@ "Get inspired": "Bliv inspireret", "Go back to edit": "Gå tilbage til redigering", "Go back to overview": "Gå tilbage til oversigten", + "Guests & Rooms": "Gæster & værelser", "Hi": "Hei", "Highest level": "Højeste niveau", "Hospital": "Hospital", "Hotel": "Hotel", "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det virker", "Image gallery": "Billedgalleri", "Join Scandic Friends": "Tilmeld dig Scandic Friends", + "km to city center": "km til byens centrum", "Language": "Sprog", "Latest searches": "Seneste søgninger", "Level": "Niveau", @@ -124,9 +134,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", + "Membership cards": "Medlemskort", "Membership ID": "Medlems-id", "Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder", - "Membership cards": "Medlemskort", "Menu": "Menu", "Modify": "Ændre", "Month": "Måned", @@ -141,6 +151,9 @@ "Nearby companies": "Nærliggende virksomheder", "New password": "Nyt kodeord", "Next": "Næste", + "next level:": "Næste niveau:", + "night": "nat", + "nights": "nætter", "Nights needed to level up": "Nætter nødvendige for at komme i niveau", "No content published": "Intet indhold offentliggjort", "No matching location found": "Der blev ikke fundet nogen matchende placering", @@ -151,11 +164,13 @@ "Non-refundable": "Ikke-refunderbart", "Not found": "Ikke fundet", "Nr night, nr adult": "{nights, number} nat, {adults, number} voksen", + "number": "nummer", "On your journey": "På din rejse", "Open": "Åben", "Open language menu": "Åbn sprogmenuen", "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", + "or": "eller", "Overview": "Oversigt", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", @@ -167,6 +182,7 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", + "points": "Point", "Points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", @@ -185,7 +201,6 @@ "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", "Rooms": "Værelser", - "Guests & Rooms": "Gæster & værelser", "Save": "Gemme", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -210,25 +225,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.", "Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.", "Something went wrong!": "Noget gik galt!", + "special character": "speciel karakter", + "spendable points expiring by": "{points} Brugbare point udløber den {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gade", "Successfully updated profile!": "Profilen er opdateret med succes!", "Summary": "Opsummering", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.", "Thank you": "Tak", "Theatre": "Teater", "There are no transactions to display": "Der er ingen transaktioner at vise", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", + "to": "til", "Total Points": "Samlet antal point", "Tourist": "Turist", "Transaction date": "Overførselsdato", "Transactions": "Transaktioner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", + "TUI Points": "TUI Points", "Type of bed": "Sengtype", "Type of room": "Værelsestype", + "uppercase letter": "stort bogstav", "Use bonus cheque": "Brug Bonus Cheque", "Use code/voucher": "Brug kode/voucher", "User information": "Brugeroplysninger", @@ -255,9 +274,9 @@ "You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.", "You have no previous stays.": "Du har ingen tidligere ophold.", "You have no upcoming stays.": "Du har ingen kommende ophold.", - "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your card was successfully removed!": "Dit kort blev fjernet!", "Your card was successfully saved!": "Dit kort blev gemt!", + "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your current level": "Dit nuværende niveau", "Your details": "Dine oplysninger", "Your level": "Dit niveau", @@ -265,23 +284,5 @@ "Zip code": "Postnummer", "Zoo": "Zoo", "Zoom in": "Zoom ind", - "Zoom out": "Zoom ud", - "as of today": "pr. dags dato", - "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", - "by": "inden", - "characters": "tegn", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", - "km to city center": "km til byens centrum", - "next level:": "Næste niveau:", - "night": "nat", - "nights": "nætter", - "number": "nummer", - "or": "eller", - "points": "Point", - "special character": "speciel karakter", - "spendable points expiring by": "{points} Brugbare point udløber den {date}", - "to": "til", - "uppercase letter": "stort bogstav" + "Zoom out": "Zoom ud" } diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 296486732..9035612bc 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -14,6 +14,7 @@ "Any changes you've made will be lost.": "Alle Änderungen, die Sie vorgenommen haben, gehen verloren.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?", "Arrival date": "Ankunftsdatum", + "as of today": "Stand heute", "As our": "Als unser {level}", "As our Close Friend": "Als unser enger Freund", "At latest": "Spätestens", @@ -23,14 +24,16 @@ "Bed type": "Bettentyp", "Book": "Buchen", "Book reward night": "Bonusnacht buchen", - "Code / Voucher": "Buchungscodes / Gutscheine", "Booking number": "Buchungsnummer", + "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", "Breakfast": "Frühstück", "Breakfast excluded": "Frühstück nicht inbegriffen", "Breakfast included": "Frühstück inbegriffen", "Bus terminal": "Busbahnhof", "Business": "Geschäft", + "by": "bis", "Cancel": "Stornieren", + "characters": "figuren", "Check in": "Einchecken", "Check out": "Auschecken", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.", @@ -45,8 +48,10 @@ "Close menu": "Menü schließen", "Close my pages menu": "Meine Seiten Menü schließen", "Close the map": "Karte schließen", + "Code / Voucher": "Buchungscodes / Gutscheine", "Coming up": "Demnächst", "Compare all levels": "Vergleichen Sie alle Levels", + "Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen", "Contact us": "Kontaktieren Sie uns", "Continue": "Weitermachen", "Copyright all rights reserved": "Scandic AB Alle Rechte vorbehalten", @@ -74,9 +79,9 @@ "Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile", "Explore nearby": "Erkunden Sie die Umgebung", "Extras to your booking": "Extras zu Ihrer Buchung", - "FAQ": "Häufig gestellte Fragen", "Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.", "Fair": "Messe", + "FAQ": "Häufig gestellte Fragen", "Find booking": "Buchung finden", "Find hotels": "Hotels finden", "Flexibility": "Flexibilität", @@ -87,17 +92,22 @@ "Get inspired": "Lassen Sie sich inspieren", "Go back to edit": "Zurück zum Bearbeiten", "Go back to overview": "Zurück zur Übersicht", + "Guests & Rooms": "Gäste & Zimmer", "Hi": "Hallo", "Highest level": "Höchstes Level", "Hospital": "Krankenhaus", "Hotel": "Hotel", "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personen", + "hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen", "Hotels": "Hotels", "How do you want to sleep?": "Wie möchtest du schlafen?", "How it works": "Wie es funktioniert", "Image gallery": "Bildergalerie", "Join Scandic Friends": "Treten Sie Scandic Friends bei", + "km to city center": "km bis zum Stadtzentrum", "Language": "Sprache", "Latest searches": "Letzte Suchanfragen", "Level": "Level", @@ -124,9 +134,9 @@ "Member price": "Mitgliederpreis", "Member price from": "Mitgliederpreis ab", "Members": "Mitglieder", + "Membership cards": "Mitgliedskarten", "Membership ID": "Mitglieds-ID", "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", - "Membership cards": "Mitgliedskarten", "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", @@ -141,6 +151,9 @@ "Nearby companies": "Nahe gelegene Unternehmen", "New password": "Neues Kennwort", "Next": "Nächste", + "next level:": "Nächstes Level:", + "night": "nacht", + "nights": "Nächte", "Nights needed to level up": "Nächte, die zum Levelaufstieg benötigt werden", "No content published": "Kein Inhalt veröffentlicht", "No matching location found": "Kein passender Standort gefunden", @@ -151,11 +164,13 @@ "Non-refundable": "Nicht erstattungsfähig", "Not found": "Nicht gefunden", "Nr night, nr adult": "{nights, number} Nacht, {adults, number} Erwachsener", + "number": "nummer", "On your journey": "Auf deiner Reise", "Open": "Offen", "Open language menu": "Sprachmenü öffnen", "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", + "or": "oder", "Overview": "Übersicht", "Parking": "Parken", "Parking / Garage": "Parken / Garage", @@ -166,6 +181,7 @@ "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", + "points": "Punkte", "Points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", @@ -184,7 +200,6 @@ "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", "Rooms": "Räume", - "Guests & Rooms": "Gäste & Zimmer", "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -209,25 +224,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.", "Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.", "Something went wrong!": "Etwas ist schief gelaufen!", + "special character": "sonderzeichen", + "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", "Sports": "Sport", "Standard price": "Standardpreis", "Street": "Straße", "Successfully updated profile!": "Profil erfolgreich aktualisiert!", "Summary": "Zusammenfassung", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.", "Thank you": "Danke", "Theatre": "Theater", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", + "to": "zu", "Total Points": "Gesamtpunktzahl", "Tourist": "Tourist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", "Transportations": "Transportmittel", "Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)", + "TUI Points": "TUI Points", "Type of bed": "Bettentyp", "Type of room": "Zimmerart", + "uppercase letter": "großbuchstabe", "Use bonus cheque": "Bonusscheck nutzen", "Use code/voucher": "Code/Gutschein nutzen", "User information": "Nutzerinformation", @@ -254,9 +273,9 @@ "You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.", "You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.", "You have no upcoming stays.": "Sie haben keine bevorstehenden Aufenthalte.", - "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!", "Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!", + "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your current level": "Ihr aktuelles Level", "Your details": "Ihre Angaben", "Your level": "Dein level", @@ -264,23 +283,5 @@ "Zip code": "PLZ", "Zoo": "Zoo", "Zoom in": "Vergrößern", - "Zoom out": "Verkleinern", - "as of today": "Stand heute", - "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", - "by": "bis", - "characters": "figuren", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personen", - "hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen", - "km to city center": "km bis zum Stadtzentrum", - "next level:": "Nächstes Level:", - "night": "nacht", - "nights": "Nächte", - "number": "nummer", - "or": "oder", - "points": "Punkte", - "special character": "sonderzeichen", - "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", - "to": "zu", - "uppercase letter": "großbuchstabe" + "Zoom out": "Verkleinern" } diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index f044f990e..d5f7e010c 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -14,6 +14,7 @@ "Any changes you've made will be lost.": "Any changes you've made will be lost.", "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", @@ -23,14 +24,18 @@ "Bed type": "Bed type", "Book": "Book", "Book reward night": "Book reward night", - "Code / Voucher": "Code / Voucher", "Booking number": "Booking number", + "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", + "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", + "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", "Breakfast": "Breakfast", "Breakfast excluded": "Breakfast excluded", "Breakfast included": "Breakfast included", "Bus terminal": "Bus terminal", "Business": "Business", + "by": "by", "Cancel": "Cancel", + "characters": "characters", "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.", @@ -45,8 +50,10 @@ "Close menu": "Close menu", "Close my pages menu": "Close my pages menu", "Close the map": "Close the map", + "Code / Voucher": "Code / Voucher", "Coming up": "Coming up", "Compare all levels": "Compare all levels", + "Complete booking & go to payment": "Complete booking & go to payment", "Contact us": "Contact us", "Continue": "Continue", "Copyright all rights reserved": "Scandic AB All rights reserved", @@ -59,6 +66,7 @@ "Date of Birth": "Date of Birth", "Day": "Day", "Description": "Description", + "Destination": "Destination", "Destinations & hotels": "Destinations & hotels", "Destination": "Destination", "Disabled booking options header": "We're sorry", @@ -75,9 +83,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", "Flexibility": "Flexibility", @@ -88,17 +96,22 @@ "Get inspired": "Get inspired", "Go back to edit": "Go back to edit", "Go back to overview": "Go back to overview", + "Guests & Rooms": "Guests & Rooms", "Hi": "Hi", "Highest level": "Highest level", "Hospital": "Hospital", "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", "Image gallery": "Image gallery", "Join Scandic Friends": "Join Scandic Friends", + "km to city center": "km to city center", "Language": "Language", "Latest searches": "Latest searches", "Level": "Level", @@ -125,9 +138,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", @@ -142,6 +155,9 @@ "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 content published": "No content published", "No matching location found": "No matching location found", @@ -152,11 +168,13 @@ "Non-refundable": "Non-refundable", "Not found": "Not found", "Nr night, nr adult": "{nights, number} night, {adults, number} adult", + "number": "number", "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", "Overview": "Overview", "Parking": "Parking", "Parking / Garage": "Parking / Garage", @@ -168,6 +186,7 @@ "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", @@ -186,12 +205,11 @@ "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", "Rooms": "Rooms", - "Guests & Rooms": "Guests & Rooms", "Save": "Save", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", - "See all photos": "See all photos", "Search": "Search", + "See all photos": "See all photos", "See hotel details": "See hotel details", "See room details": "See room details", "See rooms": "See rooms", @@ -212,25 +230,29 @@ "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.", "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 Points": "Total Points", "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", @@ -257,9 +279,9 @@ "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 level": "Your level", @@ -267,25 +289,5 @@ "Zip code": "Zip code", "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.nights": "{totalNights, plural, one {# night} other {# nights}}", - "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", - "by": "by", - "characters": "characters", - "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", - "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" + "Zoom out": "Zoom out" } diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 1aa5165b7..25b04ef90 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -14,6 +14,7 @@ "Any changes you've made will be lost.": "Kaikki tekemäsi muutokset menetetään.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?", "Arrival date": "Saapumispäivä", + "as of today": "tänään", "As our": "{level}-etu", "As our Close Friend": "Läheisenä ystävänämme", "At latest": "Viimeistään", @@ -24,12 +25,15 @@ "Book": "Varaa", "Book reward night": "Kirjapalkinto-ilta", "Booking number": "Varausnumero", + "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", "Breakfast": "Aamiainen", "Breakfast excluded": "Aamiainen ei sisälly", "Breakfast included": "Aamiainen sisältyy", "Bus terminal": "Bussiasema", "Business": "Business", + "by": "mennessä", "Cancel": "Peruuttaa", + "characters": "hahmoja", "Check in": "Sisäänkirjautuminen", "Check out": "Uloskirjautuminen", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.", @@ -47,6 +51,7 @@ "Code / Voucher": "Varauskoodit / kupongit", "Coming up": "Tulossa", "Compare all levels": "Vertaa kaikkia tasoja", + "Complete booking & go to payment": "Täydennä varaus & siirry maksamaan", "Contact us": "Ota meihin yhteyttä", "Continue": "Jatkaa", "Copyright all rights reserved": "Scandic AB Kaikki oikeudet pidätetään", @@ -74,9 +79,9 @@ "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore nearby": "Tutustu lähialueeseen", "Extras to your booking": "Varauksessa lisäpalveluita", - "FAQ": "UKK", "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Fair": "Messukeskus", + "FAQ": "UKK", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", "Flexibility": "Joustavuus", @@ -94,11 +99,15 @@ "Hotel": "Hotelli", "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", + "hotelPages.rooms.roomCard.person": "henkilö", + "hotelPages.rooms.roomCard.persons": "Henkilöä", + "hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot", "Hotels": "Hotellit", "How do you want to sleep?": "Kuinka haluat nukkua?", "How it works": "Kuinka se toimii", "Image gallery": "Kuvagalleria", "Join Scandic Friends": "Liity jäseneksi", + "km to city center": "km keskustaan", "Language": "Kieli", "Latest searches": "Viimeisimmät haut", "Level": "Level", @@ -125,9 +134,9 @@ "Member price": "Jäsenhinta", "Member price from": "Jäsenhinta alkaen", "Members": "Jäsenet", + "Membership cards": "Jäsenkortit", "Membership ID": "Jäsentunnus", "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", - "Membership cards": "Jäsenkortit", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", @@ -142,6 +151,9 @@ "Nearby companies": "Läheiset yritykset", "New password": "Uusi salasana", "Next": "Seuraava", + "next level:": "pistettä tasolle:", + "night": "yö", + "nights": "yötä", "Nights needed to level up": "Yöt, joita tarvitaan tasolle", "No content published": "Ei julkaistua sisältöä", "No matching location found": "Vastaavaa sijaintia ei löytynyt", @@ -152,11 +164,13 @@ "Non-refundable": "Ei palautettavissa", "Not found": "Ei löydetty", "Nr night, nr adult": "{nights, number} yö, {adults, number} aikuinen", + "number": "määrä", "On your journey": "Matkallasi", "Open": "Avata", "Open language menu": "Avaa kielivalikko", "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", + "or": "tai", "Overview": "Yleiskatsaus", "Parking": "Pysäköinti", "Parking / Garage": "Pysäköinti / Autotalli", @@ -168,6 +182,7 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", + "points": "pistettä", "Points": "Pisteet", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", @@ -210,25 +225,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.", "Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.", "Something went wrong!": "Jotain meni pieleen!", + "special character": "erikoishahmo", + "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "Sports": "Urheilu", "Standard price": "Normaali hinta", "Street": "Katu", "Successfully updated profile!": "Profiilin päivitys onnistui!", "Summary": "Yhteenveto", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.", "Thank you": "Kiitos", "Theatre": "Teatteri", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", + "to": "to", "Total Points": "Kokonaispisteet", "Tourist": "Turisti", "Transaction date": "Tapahtuman päivämäärä", "Transactions": "Tapahtumat", "Transportations": "Kuljetukset", "Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)", + "TUI Points": "TUI Points", "Type of bed": "Vuodetyyppi", "Type of room": "Huonetyyppi", + "uppercase letter": "iso kirjain", "Use bonus cheque": "Käytä bonussekkiä", "Use code/voucher": "Käytä koodia/voucheria", "User information": "Käyttäjän tiedot", @@ -255,9 +274,9 @@ "You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.", "You have no previous stays.": "Sinulla ei ole aiempia majoituksia.", "You have no upcoming stays.": "Sinulla ei ole tulevia majoituksia.", - "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!", "Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!", + "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your current level": "Nykyinen tasosi", "Your details": "Tietosi", "Your level": "Tasosi", @@ -265,23 +284,5 @@ "Zip code": "Postinumero", "Zoo": "Eläintarha", "Zoom in": "Lähennä", - "Zoom out": "Loitonna", - "as of today": "tänään", - "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", - "by": "mennessä", - "characters": "hahmoja", - "hotelPages.rooms.roomCard.person": "henkilö", - "hotelPages.rooms.roomCard.persons": "Henkilöä", - "hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot", - "km to city center": "km keskustaan", - "next level:": "pistettä tasolle:", - "night": "yö", - "nights": "yötä", - "number": "määrä", - "or": "tai", - "points": "pistettä", - "special character": "erikoishahmo", - "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", - "to": "to", - "uppercase letter": "iso kirjain" + "Zoom out": "Loitonna" } diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 304920b27..efb73b3d1 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -14,6 +14,7 @@ "Any changes you've made will be lost.": "Eventuelle endringer du har gjort, går tapt.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?", "Arrival date": "Ankomstdato", + "as of today": "per idag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nære venn", "At latest": "Senest", @@ -23,14 +24,16 @@ "Bed type": "Seng type", "Book": "Bestill", "Book reward night": "Bestill belønningskveld", - "Code / Voucher": "Bestillingskoder / kuponger", "Booking number": "Bestillingsnummer", + "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "Breakfast": "Frokost", "Breakfast excluded": "Frokost ekskludert", "Breakfast included": "Frokost inkludert", "Bus terminal": "Bussterminal", "Business": "Forretnings", + "by": "innen", "Cancel": "Avbryt", + "characters": "tegn", "Check in": "Sjekk inn", "Check out": "Sjekk ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.", @@ -45,8 +48,10 @@ "Close menu": "Lukk meny", "Close my pages menu": "Lukk mine sidermenyn", "Close the map": "Lukk kartet", + "Code / Voucher": "Bestillingskoder / kuponger", "Coming up": "Kommer opp", "Compare all levels": "Sammenlign alle nivåer", + "Complete booking & go to payment": "Fullfør bestilling & gå til betaling", "Contact us": "Kontakt oss", "Continue": "Fortsette", "Copyright all rights reserved": "Scandic AB Alle rettigheter forbeholdt", @@ -74,9 +79,9 @@ "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "Explore nearby": "Utforsk i nærheten", "Extras to your booking": "Tilvalg til bestillingen din", - "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", + "FAQ": "FAQ", "Find booking": "Finn booking", "Find hotels": "Finn hotell", "Flexibility": "Fleksibilitet", @@ -87,17 +92,22 @@ "Get inspired": "Bli inspirert", "Go back to edit": "Gå tilbake til redigering", "Go back to overview": "Gå tilbake til oversikten", + "Guests & Rooms": "Gjester & rom", "Hi": "Hei", "Highest level": "Høyeste nivå", "Hospital": "Sykehus", "Hotel": "Hotel", "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", "Image gallery": "Bildegalleri", "Join Scandic Friends": "Bli med i Scandic Friends", + "km to city center": "km til sentrum", "Language": "Språk", "Latest searches": "Siste søk", "Level": "Nivå", @@ -124,9 +134,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris fra", "Members": "Medlemmer", + "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen", - "Membership cards": "Medlemskort", "Menu": "Menu", "Modify": "Endre", "Month": "Måned", @@ -141,6 +151,9 @@ "Nearby companies": "Nærliggende selskaper", "New password": "Nytt passord", "Next": "Neste", + "next level:": "Neste nivå:", + "night": "natt", + "nights": "netter", "Nights needed to level up": "Netter som trengs for å komme opp i nivå", "No content published": "Ingen innhold publisert", "No matching location found": "Fant ingen samsvarende plassering", @@ -151,11 +164,13 @@ "Non-refundable": "Ikke-refunderbart", "Not found": "Ikke funnet", "Nr night, nr adult": "{nights, number} natt, {adults, number} voksen", + "number": "antall", "On your journey": "På reisen din", "Open": "Åpen", "Open language menu": "Åpne språkmenyen", "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", + "or": "eller", "Overview": "Oversikt", "Parking": "Parkering", "Parking / Garage": "Parkering / Garasje", @@ -167,6 +182,7 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", + "points": "poeng", "Points": "Poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", @@ -185,7 +201,6 @@ "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", "Rooms": "Rom", - "Guests & Rooms": "Gjester & rom", "Save": "Lagre", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -210,25 +225,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.", "Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.", "Something went wrong!": "Noe gikk galt!", + "special character": "spesiell karakter", + "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gate", "Successfully updated profile!": "Vellykket oppdatert profil!", "Summary": "Sammendrag", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.", "Thank you": "Takk", "Theatre": "Teater", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", + "to": "til", "Total Points": "Totale poeng", "Tourist": "Turist", "Transaction date": "Transaksjonsdato", "Transactions": "Transaksjoner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", + "TUI Points": "TUI Points", "Type of bed": "Sengtype", "Type of room": "Romtype", + "uppercase letter": "stor bokstav", "Use bonus cheque": "Bruk bonussjekk", "Use code/voucher": "Bruk kode/voucher", "User information": "Brukerinformasjon", @@ -255,9 +274,9 @@ "You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.", "You have no previous stays.": "Du har ingen tidligere opphold.", "You have no upcoming stays.": "Du har ingen kommende opphold.", - "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your card was successfully removed!": "Kortet ditt ble fjernet!", "Your card was successfully saved!": "Kortet ditt ble lagret!", + "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your current level": "Ditt nåværende nivå", "Your details": "Dine detaljer", "Your level": "Ditt nivå", @@ -265,23 +284,5 @@ "Zip code": "Post kode", "Zoo": "Dyrehage", "Zoom in": "Zoom inn", - "Zoom out": "Zoom ut", - "as of today": "per i dag", - "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", - "by": "innen", - "characters": "tegn", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet", - "km to city center": "km til sentrum", - "next level:": "Neste nivå:", - "night": "natt", - "nights": "netter", - "number": "antall", - "or": "eller", - "points": "poeng", - "special character": "spesiell karakter", - "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", - "to": "til", - "uppercase letter": "stor bokstav" + "Zoom out": "Zoom ut" } diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index bb99789bb..266ecdcfb 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -14,6 +14,7 @@ "Any changes you've made will be lost.": "Alla ändringar du har gjort kommer att gå förlorade.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?", "Arrival date": "Ankomstdatum", + "as of today": "från och med idag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nära vän", "At latest": "Senast", @@ -23,14 +24,16 @@ "Bed type": "Sängtyp", "Book": "Boka", "Book reward night": "Boka frinatt", - "Code / Voucher": "Bokningskoder / kuponger", "Booking number": "Bokningsnummer", + "booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}", "Breakfast": "Frukost", "Breakfast excluded": "Frukost ingår ej", "Breakfast included": "Frukost ingår", "Bus terminal": "Bussterminal", "Business": "Business", + "by": "innan", "Cancel": "Avbryt", + "characters": "tecken", "Check in": "Checka in", "Check out": "Checka ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.", @@ -45,8 +48,10 @@ "Close menu": "Stäng menyn", "Close my pages menu": "Stäng mina sidor menyn", "Close the map": "Stäng kartan", + "Code / Voucher": "Bokningskoder / kuponger", "Coming up": "Kommer härnäst", "Compare all levels": "Jämför alla nivåer", + "Complete booking & go to payment": "Fullför bokning & gå till betalning", "Contact us": "Kontakta oss", "Continue": "Fortsätt", "Copyright all rights reserved": "Scandic AB Alla rättigheter förbehålls", @@ -74,9 +79,9 @@ "Explore all levels and benefits": "Utforska alla nivåer och fördelar", "Explore nearby": "Utforska i närheten", "Extras to your booking": "Extra tillval till din bokning", - "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.", "Fair": "Mässa", + "FAQ": "FAQ", "Find booking": "Hitta bokning", "Find hotels": "Hitta hotell", "Flexibility": "Flexibilitet", @@ -87,17 +92,22 @@ "Get inspired": "Bli inspirerad", "Go back to edit": "Gå tillbaka till redigeringen", "Go back to overview": "Gå tillbaka till översikten", + "Guests & Rooms": "Gäster & rum", "Hi": "Hej", "Highest level": "Högsta nivå", "Hospital": "Sjukhus", "Hotel": "Hotell", "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet", "Hotels": "Hotell", "How do you want to sleep?": "Hur vill du sova?", "How it works": "Hur det fungerar", "Image gallery": "Bildgalleri", "Join Scandic Friends": "Gå med i Scandic Friends", + "km to city center": "km till stadens centrum", "Language": "Språk", "Latest searches": "Senaste sökningarna", "Level": "Nivå", @@ -124,9 +134,9 @@ "Member price": "Medlemspris", "Member price from": "Medlemspris från", "Members": "Medlemmar", + "Membership cards": "Medlemskort", "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp", - "Membership cards": "Medlemskort", "Menu": "Meny", "Modify": "Ändra", "Month": "Månad", @@ -141,6 +151,9 @@ "Nearby companies": "Närliggande företag", "New password": "Nytt lösenord", "Next": "Nästa", + "next level:": "Nästa nivå:", + "night": "natt", + "nights": "nätter", "Nights needed to level up": "Nätter som behövs för att gå upp i nivå", "No content published": "Inget innehåll publicerat", "No matching location found": "Ingen matchande plats hittades", @@ -151,11 +164,13 @@ "Non-refundable": "Ej återbetalningsbar", "Not found": "Hittades inte", "Nr night, nr adult": "{nights, number} natt, {adults, number} vuxen", + "number": "nummer", "On your journey": "På din resa", "Open": "Öppna", "Open language menu": "Öppna språkmenyn", "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", + "or": "eller", "Overview": "Översikt", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", @@ -167,6 +182,7 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", + "points": "poäng", "Points": "Poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", @@ -185,7 +201,6 @@ "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", "Rooms": "Rum", - "Guests & Rooms": "Gäster & rum", "Save": "Spara", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", @@ -210,25 +225,29 @@ "Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.", "Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.", "Something went wrong!": "Något gick fel!", + "special character": "speciell karaktär", + "spendable points expiring by": "{points} poäng förfaller {date}", "Sports": "Sport", "Standard price": "Standardpris", "Street": "Gata", "Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!", "Summary": "Sammanfattning", - "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.", "Thank you": "Tack", "Theatre": "Teater", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", + "to": "till", "Total Points": "Poäng totalt", "Tourist": "Turist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktioner", "Transportations": "Transport", "Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)", + "TUI Points": "TUI Points", "Type of bed": "Sängtyp", "Type of room": "Rumstyp", + "uppercase letter": "stor bokstav", "Use bonus cheque": "Använd bonuscheck", "Use code/voucher": "Använd kod/voucher", "User information": "Användarinformation", @@ -255,9 +274,9 @@ "You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.", "You have no previous stays.": "Du har inga tidigare vistelser.", "You have no upcoming stays.": "Du har inga planerade resor.", - "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your card was successfully removed!": "Ditt kort har tagits bort!", "Your card was successfully saved!": "Ditt kort har sparats!", + "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your current level": "Din nuvarande nivå", "Your details": "Dina uppgifter", "Your level": "Din nivå", @@ -265,23 +284,5 @@ "Zip code": "Postnummer", "Zoo": "Djurpark", "Zoom in": "Zooma in", - "Zoom out": "Zooma ut", - "as of today": "per idag", - "booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}", - "by": "innan", - "characters": "tecken", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", - "hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet", - "km to city center": "km till stadens centrum", - "next level:": "Nästa nivå:", - "night": "natt", - "nights": "nätter", - "number": "nummer", - "or": "eller", - "points": "poäng", - "special character": "speciell karaktär", - "spendable points expiring by": "{points} poäng förfaller {date}", - "to": "till", - "uppercase letter": "stor bokstav" + "Zoom out": "Zooma ut" } diff --git a/server/routers/booking/index.ts b/server/routers/booking/index.ts index 65b968733..f3c0b45ad 100644 --- a/server/routers/booking/index.ts +++ b/server/routers/booking/index.ts @@ -1,5 +1,9 @@ import { mergeRouters } from "@/server/trpc" import { bookingMutationRouter } from "./mutation" +import { bookingQueryRouter } from "./query" -export const bookingRouter = mergeRouters(bookingMutationRouter) +export const bookingRouter = mergeRouters( + bookingMutationRouter, + bookingQueryRouter +) diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index 46a88110e..7e9b9b2c8 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -1,38 +1,68 @@ import { z } from "zod" -// Query +const roomsSchema = z.array( + z.object({ + adults: z.number().int().nonnegative(), + childrenAges: z + .array( + z.object({ + age: z.number().int().nonnegative(), + bedType: z.string(), + }) + ) + .default([]), + rateCode: z.string(), + roomTypeCode: z.string(), + guest: z.object({ + title: z.string(), + firstName: z.string(), + lastName: z.string(), + email: z.string().email(), + phoneCountryCodePrefix: z.string(), + phoneNumber: z.string(), + countryCode: z.string(), + membershipNumber: z.string().optional(), + }), + smsConfirmationRequested: z.boolean(), + packages: z.object({ + breakfast: z.boolean(), + allergyFriendly: z.boolean(), + petFriendly: z.boolean(), + accessibility: z.boolean(), + }), + }) +) + +const paymentSchema = z.object({ + paymentMethod: z.string(), + card: z + .object({ + alias: z.string(), + expiryDate: z.string(), + cardType: z.string(), + }) + .optional(), + cardHolder: z.object({ + email: z.string().email(), + name: z.string(), + phoneCountryCode: z.string(), + phoneSubscriber: z.string(), + }), + success: z.string(), + error: z.string(), + cancel: z.string(), +}) + // Mutation export const createBookingInput = z.object({ hotelId: z.string(), checkInDate: z.string(), checkOutDate: z.string(), - rooms: z.array( - z.object({ - adults: z.number().int().nonnegative(), - children: z.number().int().nonnegative(), - rateCode: z.string(), - roomTypeCode: z.string(), - guest: z.object({ - title: z.string(), - firstName: z.string(), - lastName: z.string(), - email: z.string().email(), - phoneCountryCodePrefix: z.string(), - phoneNumber: z.string(), - countryCode: z.string(), - }), - smsConfirmationRequested: z.boolean(), - }) - ), - payment: z.object({ - cardHolder: z.object({ - Email: z.string().email(), - Name: z.string(), - PhoneCountryCode: z.string(), - PhoneSubscriber: z.string(), - }), - success: z.string(), - error: z.string(), - cancel: z.string(), - }), + rooms: roomsSchema, + payment: paymentSchema, +}) + +// Query +export const getBookingStatusInput = z.object({ + confirmationNumber: z.string(), }) diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 2b35f56d4..53595b2d0 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api" import * as api from "@/lib/api" import { getVerifiedUser } from "@/server/routers/user/query" -import { router, safeProtectedProcedure } from "@/server/trpc" +import { bookingServiceProcedure, router } from "@/server/trpc" import { getMembership } from "@/utils/user" @@ -36,13 +36,15 @@ async function getMembershipNumber( export const bookingMutationRouter = router({ booking: router({ - create: safeProtectedProcedure + create: bookingServiceProcedure .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), + // membershipNumber: await getMembershipNumber(ctx.session), checkInDate, checkOutDate, hotelId, @@ -56,11 +58,10 @@ export const bookingMutationRouter = router({ query: loggingAttributes, }) ) - const headers = ctx.session - ? { - Authorization: `Bearer ${ctx.session?.token.access_token}`, - } - : undefined + const headers = { + Authorization: `Bearer ${ctx.serviceToken}`, + } + const apiResponse = await api.post(api.endpoints.v1.booking, { headers, body: input, diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index 8fedd8716..dbc8101df 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -5,9 +5,9 @@ export const createBookingSchema = z data: z.object({ attributes: z.object({ confirmationNumber: z.string(), - cancellationNumber: z.string().nullable(), + cancellationNumber: z.string().optional(), reservationStatus: z.string(), - paymentUrl: z.string().nullable(), + paymentUrl: z.string().optional(), }), type: z.string(), id: z.string(), diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts new file mode 100644 index 000000000..f7f439b90 --- /dev/null +++ b/server/routers/booking/query.ts @@ -0,0 +1,85 @@ +import { metrics } from "@opentelemetry/api" + +import * as api from "@/lib/api" +import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" +import { bookingServiceProcedure, router } from "@/server/trpc" + +import { getBookingStatusInput } from "./input" +import { createBookingSchema } from "./output" + +const meter = metrics.getMeter("trpc.booking") +const getBookingStatusCounter = meter.createCounter("trpc.booking.status") +const getBookingStatusSuccessCounter = meter.createCounter( + "trpc.booking.status-success" +) +const getBookingStatusFailCounter = meter.createCounter( + "trpc.booking.status-fail" +) + +export const bookingQueryRouter = router({ + status: bookingServiceProcedure + .input(getBookingStatusInput) + .query(async function ({ ctx, input }) { + const { confirmationNumber } = input + getBookingStatusCounter.add(1, { confirmationNumber }) + + const apiResponse = await api.get( + `${api.endpoints.v1.booking}/${confirmationNumber}/status`, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + } + ) + + if (!apiResponse.ok) { + const responseMessage = await apiResponse.text() + getBookingStatusFailCounter.add(1, { + confirmationNumber, + error_type: "http_error", + error: responseMessage, + }) + console.error( + "api.booking.status 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 verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + getBookingStatusFailCounter.add(1, { + confirmationNumber, + error_type: "validation_error", + error: JSON.stringify(verifiedData.error), + }) + console.error( + "api.booking.status validation error", + JSON.stringify({ + query: { confirmationNumber }, + error: verifiedData.error, + }) + ) + throw badRequestError() + } + + getBookingStatusSuccessCounter.add(1, { confirmationNumber }) + console.info( + "api.booking.status success", + JSON.stringify({ + query: { confirmationNumber }, + }) + ) + + return verifiedData.data + }), +}) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index d0d95903a..d1fb2dfe8 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -436,6 +436,22 @@ export const roomSchema = z.object({ type: z.enum(["roomcategories"]), }) +const merchantInformationSchema = z.object({ + webMerchantId: z.string(), + cards: z.record(z.string(), z.boolean()).transform((val) => { + return Object.entries(val) + .filter(([_, enabled]) => enabled) + .map(([key]) => key) + }), + alternatePaymentOptions: z + .record(z.string(), z.boolean()) + .transform((val) => { + return Object.entries(val) + .filter(([_, enabled]) => enabled) + .map(([key]) => key) + }), +}) + // NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html export const getHotelDataSchema = z.object({ data: z.object({ @@ -471,6 +487,7 @@ export const getHotelDataSchema = z.object({ hotelContent: hotelContentSchema, detailedFacilities: z.array(detailedFacilitySchema), healthFacilities: z.array(healthFacilitySchema), + merchantInformationData: merchantInformationSchema, rewardNight: rewardNightSchema, pointsOfInterest: z .array(pointOfInterestSchema) diff --git a/server/trpc.ts b/server/trpc.ts index 4f5409a01..e8f9c26f1 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -121,29 +121,24 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { }) }) -export const profileServiceProcedure = t.procedure.use(async (opts) => { - const { access_token } = await fetchServiceToken(["profile"]) - if (!access_token) { - throw internalServerError("Failed to obtain profile service token") - } - return opts.next({ - ctx: { - serviceToken: access_token, - }, +function createServiceProcedure(serviceName: string) { + return t.procedure.use(async (opts) => { + const { access_token } = await fetchServiceToken([serviceName]) + if (!access_token) { + throw internalServerError(`Failed to obtain ${serviceName} service token`) + } + return opts.next({ + ctx: { + serviceToken: access_token, + }, + }) }) -}) +} + +export const bookingServiceProcedure = createServiceProcedure("booking") +export const hotelServiceProcedure = createServiceProcedure("hotel") +export const profileServiceProcedure = createServiceProcedure("profile") -export const hotelServiceProcedure = t.procedure.use(async (opts) => { - const { access_token } = await fetchServiceToken(["hotel"]) - if (!access_token) { - throw internalServerError("Failed to obtain hotel service token") - } - return opts.next({ - ctx: { - serviceToken: access_token, - }, - }) -}) export const serverActionProcedure = t.procedure.experimental_caller( experimental_nextAppDirCaller({ createContext, diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index e64e51506..69460962b 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -1,5 +1,7 @@ import { Rate } from "@/server/routers/hotels/output" +import { Hotel } from "@/types/hotel" + export interface SectionProps { nextPath: string } @@ -33,6 +35,10 @@ export interface RoomSelectionProps extends SectionProps { export interface DetailsProps extends SectionProps {} +export interface PaymentProps { + hotel: Hotel +} + export interface SectionPageProps { breakfast?: string bed?: string