Merged in feat/SW-431-payment-flow (pull request #635)

Feat/SW-431 payment flow

* feat(SW-431): Update mock hotel data

* feat(SW-431): Added route handler and trpc routes

* feat(SW-431): List payment methods and handle booking status and redirection

* feat(SW-431): Updated booking page to poll for booking status

* feat(SW-431): Updated create booking contract

* feat(SW-431): small fix

* fix(SW-431): Added intl string and sorted dictionaries

* fix(SW-431): Changes from PR

* fix(SW-431): fixes from PR

* fix(SW-431): add todo comments

* fix(SW-431): update schema prop

Approved-by: Simon.Emanuelsson
This commit is contained in:
Tobias Johansson
2024-10-04 09:37:09 +00:00
committed by Pontus Dreij
parent c7e829cd02
commit d44451d2dc
26 changed files with 708 additions and 283 deletions

View File

@@ -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<LangParams & { section: string }, SectionPageProps>) {
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" && <Payment />}
{params.section === "payment" && (
<Payment hotel={hotel.data.attributes} />
)}
</SectionAccordion>
</div>
<div className={styles.summary}>

View File

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

View File

@@ -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 (
<main className={styles.main}>
<section className={styles.section}>
<IntroSection email={email} />
<StaySection hotel={hotel} stay={stay} />
<SummarySection summary={summary} />
</section>
</main>
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 (
<main className={styles.main}>
<section className={styles.section}>
<IntroSection email={email} />
<StaySection hotel={hotel} stay={stay} />
<SummarySection summary={summary} />
</section>
</main>
)
}
return <LoadingSpinner />
}

View File

@@ -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<NextResponse> {
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)
}

View File

@@ -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 (
<section className={styles.section}>

View File

@@ -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

View File

@@ -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}`

View File

@@ -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<string>("")
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>("")
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 (
<Button
onClick={() =>
// TODO: Use real values
initiateBooking.mutate({
hotelId: "811",
checkInDate: "2024-12-10",
checkOutDate: "2024-12-11",
rooms: [
{
adults: 1,
children: 0,
rateCode: "SAVEEU",
roomTypeCode: "QC",
guest: {
title: "Mr",
firstName: "Test",
lastName: "User",
email: "test.user@scandichotels.com",
phoneCountryCodePrefix: "string",
phoneNumber: "string",
countryCode: "string",
},
smsConfirmationRequested: true,
},
],
payment: {
cardHolder: {
Email: "test.user@scandichotels.com",
Name: "Test User",
PhoneCountryCode: "",
PhoneSubscriber: "",
},
success: "success/handle",
error: "error/handle",
cancel: "cancel/handle",
const bookingStatus = useHandleBookingStatus(
confirmationNumber,
BookingStatusEnum.PaymentRegistered,
maxRetries,
retryInterval
)
useEffect(() => {
if (bookingStatus?.data?.paymentUrl) {
router.push(bookingStatus.data.paymentUrl)
}
}, [bookingStatus, router])
function handleSubmit() {
initiateBooking.mutate({
hotelId: hotel.operaId,
checkInDate: "2024-12-10",
checkOutDate: "2024-12-11",
rooms: [
{
adults: 1,
childrenAges: [],
rateCode: "SAVEEU",
roomTypeCode: "QC",
guest: {
title: "Mr",
firstName: "Test",
lastName: "User",
email: "test.user@scandichotels.com",
phoneCountryCodePrefix: "string",
phoneNumber: "string",
countryCode: "string",
},
})
}
>
Create booking
</Button>
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 <LoadingSpinner />
}
return (
<div>
<div>
<div className={styles.paymentItemContainer}>
<button
className={styles.paymentItem}
onClick={() => setSelectedPaymentMethod("card")}
>
<input
type="radio"
name="payment-method"
id="card"
value="card"
checked={selectedPaymentMethod === "card"}
/>
<label htmlFor="card">card</label>
</button>
{hotel.merchantInformationData.alternatePaymentOptions.map(
(paymentOption) => (
<button
key={paymentOption}
className={styles.paymentItem}
onClick={() => setSelectedPaymentMethod(paymentOption)}
>
<input
type="radio"
name="payment-method"
id={paymentOption}
value={paymentOption}
checked={selectedPaymentMethod === paymentOption}
/>
<label htmlFor={paymentOption}>{paymentOption}</label>
</button>
)
)}
</div>
</div>
<Button disabled={!selectedPaymentMethod} onClick={handleSubmit}>
{intl.formatMessage({ id: "Complete booking & go to payment" })}
</Button>
</div>
)
}

View File

@@ -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;
}

7
constants/booking.ts Normal file
View File

@@ -0,0 +1,7 @@
export enum BookingStatusEnum {
CreatedInOhip = "CreatedInOhip",
PaymentRegistered = "PaymentRegistered",
BookingCompleted = "BookingCompleted",
}
export const BOOKING_CONFIRMATION_NUMBER = "bookingConfirmationNumber"

View File

@@ -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)]

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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
)

View File

@@ -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(),
})

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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
}),
})

View File

@@ -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)

View File

@@ -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,

View File

@@ -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