Merged in feat/sw-3642-inject-sas-eb-payment (pull request #3243)

feat(SW-3642): Enable SAS EB payments

* Wip add SAS eb payment

* Add validate payment call

* Check booking status payment method to determine validation

* Clean up getPaymentData

* Fix PartnerPoints casing

* Add comment for validatePartnerPayment error handling

* Remove comment


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-12-11 13:23:12 +00:00
parent eb90c382b8
commit 7faa9933a2
19 changed files with 489 additions and 110 deletions

View File

@@ -37,7 +37,7 @@ export default async function SelectHotelPage(props: PageArgs<LangParams>) {
searchParams={searchParams}
config={bookingFlowConfig}
topSlot={
bookingFlowConfig.redemptionEnabled ? (
bookingFlowConfig.redemptionType !== "disabled" ? (
<Alert
heading={intl.formatMessage({
id: "selectHotel.earnEuroBonusPointsAlert.heading",

View File

@@ -52,7 +52,7 @@ export default async function Home(props: PageArgs<LangParams>) {
config={bookingFlowConfig}
/>
</section>
{bookingFlowConfig.redemptionEnabled && (
{bookingFlowConfig.redemptionType !== "disabled" && (
<section className={styles.infoBoxes}>
<InfoBox
heading={intl.formatMessage({

View File

@@ -13,7 +13,7 @@ import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRout
export const bookingFlowConfig: BookingFlowConfig = {
bookingCodeEnabled: false,
savedCreditCardsEnabled: false,
redemptionEnabled: env.REDEMPTION_ENABLED === true,
redemptionType: env.REDEMPTION_ENABLED === true ? "partner" : "disabled",
enterDetailsMembershipIdInputLocation: "join-card",
variant: "partner-sas",
routes: {

View File

@@ -8,7 +8,7 @@ import type { BookingFlowConfig } from "@scandic-hotels/booking-flow/BookingFlow
export const bookingFlowConfig: BookingFlowConfig = {
bookingCodeEnabled: true,
redemptionEnabled: true,
redemptionType: "scandic",
savedCreditCardsEnabled: true,
enterDetailsMembershipIdInputLocation: "form",
variant: "scandic",

View File

@@ -8,9 +8,10 @@ import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRout
import type { BookingFlowVariant } from "./bookingFlowVariants"
export type RedemptionType = "scandic" | "partner" | "disabled"
export type BookingFlowConfig = {
bookingCodeEnabled: boolean
redemptionEnabled: boolean
redemptionType: RedemptionType
savedCreditCardsEnabled: boolean
enterDetailsMembershipIdInputLocation: "form" | "join-card"
variant: BookingFlowVariant

View File

@@ -79,7 +79,7 @@ export default function RewardNight() {
}
}, [resetOnMultiroomError, ref])
if (!config.redemptionEnabled) return null
if (config.redemptionType === "disabled") return null
return (
<div ref={ref} onBlur={(e) => closeOnBlur(e.nativeEvent)}>

View File

@@ -41,7 +41,7 @@ export default function FormContent({
const {
formState: { errors, isDirty },
} = useFormContext<BookingWidgetSchema>()
const { bookingCodeEnabled, redemptionEnabled } = useBookingFlowConfig()
const { bookingCodeEnabled, redemptionType } = useBookingFlowConfig()
const searchParams = useSearchParams()
const focusWidget = searchParams.get(FOCUS_WIDGET) === "true"
useEffect(() => {
@@ -126,7 +126,7 @@ export default function FormContent({
)}
style={{
// TODO: Remove this when redemption is enabled for partner-sas
display: redemptionEnabled ? undefined : "none",
display: redemptionType !== "disabled" ? undefined : "none",
}}
>
<Voucher />

View File

@@ -28,7 +28,7 @@ import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { env } from "../../../../env/client"
import { useBookingFlowConfig } from "../../../bookingFlowConfig/bookingFlowConfigContext"
import { useBookingFlowContext } from "../../../hooks/useBookingFlowContext"
import { clearBookingWidgetState } from "../../../hooks/useBookingWidgetState"
import { useHandleBookingStatus } from "../../../hooks/useHandleBookingStatus"
@@ -40,9 +40,10 @@ import { writeGlaToSessionStorage } from "./PaymentCallback/helpers"
import BookingAlert from "./BookingAlert"
import { GuaranteeInfo } from "./GuaranteeInfo"
import {
getPaymentData,
getPaymentMethod,
hasFlexibleRate,
hasPrepaidRate,
isPaymentMethodEnum,
mustGuaranteeBooking,
writePaymentInfoToSessionStorage,
} from "./helpers"
@@ -51,7 +52,6 @@ import { getPaymentHeadingConfig } from "./utils"
import styles from "./payment.module.css"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
@@ -77,6 +77,7 @@ export default function PaymentClient({
const searchParams = useSearchParams()
const { getTopOffset } = useStickyPosition({})
const { user, isLoggedIn } = useBookingFlowContext()
const { redemptionType } = useBookingFlowConfig()
const [refId, setRefId] = useState("")
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
useState(false)
@@ -273,6 +274,7 @@ export default function PaymentClient({
return firstIncompleteRoomIndex !== -1
}, [runPreSubmitCallbacks, rooms, methods.formState.errors, getTopOffset])
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
const handleSubmit = useCallback(
async (data: PaymentFormData) => {
setIsSubmitting(true)
@@ -283,19 +285,26 @@ export default function PaymentClient({
return
}
const paymentMethod = getPaymentMethod(data.paymentMethod, hasFlexRates)
const savedCreditCard = savedCreditCards?.find(
(card) => card.id === data.paymentMethod
)
const guarantee = data.guarantee
const shouldUsePayment =
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
const payment = shouldUsePayment
? getPaymentData({ paymentMethod, savedCreditCard, lang })
: undefined
const paymentMethod = getPaymentMethod({
paymentMethod: data.paymentMethod,
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
const payment = getPaymentData({
guarantee,
bookingMustBeGuaranteed,
hasOnlyFlexRates,
paymentMethod,
savedCreditCard,
isRedemptionBooking,
lang,
})
const paymentMethodType = savedCreditCard
? savedCreditCard.type
@@ -315,7 +324,7 @@ export default function PaymentClient({
checkOutDate: toDate,
hotelId,
language: lang,
payment,
payment: payment ?? undefined,
rooms: rooms.map(
({ room }, idx): CreateBookingInput["rooms"][number] => {
const isMainRoom = idx === 0
@@ -414,14 +423,16 @@ export default function PaymentClient({
[
setIsSubmitting,
scrollToInvalidField,
hasFlexRates,
savedCreditCards,
hasFlexRates,
redemptionType,
bookingMustBeGuaranteed,
hasOnlyFlexRates,
lang,
isRedemptionBooking,
hotelId,
fromDate,
toDate,
hotelId,
rooms,
initiateBooking,
isLoggedIn,
@@ -440,7 +451,6 @@ export default function PaymentClient({
const { preHeading, heading, subHeading, showLearnMore } =
getPaymentHeadingConfig(intl, bookingMustBeGuaranteed, hasOnlyFlexRates)
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
return (
<section
className={cx(styles.paymentSection, {
@@ -522,22 +532,6 @@ const scrollToElement = (el: HTMLElement, offset: number) => {
input?.focus({ preventScroll: true })
}
const getPaymentMethod = (
paymentMethod: string | null | undefined,
hasFlexRates: boolean
): PaymentMethodEnum => {
if (hasFlexRates) {
return PaymentMethodEnum.card
}
return paymentMethod && isPaymentMethodEnum(paymentMethod)
? paymentMethod
: PaymentMethodEnum.card
}
function createPaymentCallbackUrl(lang: Lang) {
return `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
}
function useBookingStatusRedirect({
refId,
enabled,
@@ -584,32 +578,6 @@ function useBookingStatusRedirect({
}, [bookingStatus.data, bookingStatus.isTimeout, router, intl, lang, onError])
}
function getPaymentData({
paymentMethod,
savedCreditCard,
lang,
}: {
paymentMethod: PaymentMethodEnum
savedCreditCard?: CreditCard
lang: Lang
}) {
const paymentRedirectUrl = createPaymentCallbackUrl(lang)
return {
paymentMethod: paymentMethod,
success: `${paymentRedirectUrl}/success`,
error: `${paymentRedirectUrl}/error`,
cancel: `${paymentRedirectUrl}/cancel`,
card: savedCreditCard
? {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
}
: undefined,
}
}
function isNotNull<T>(value: T | null): value is T {
return value !== null
}

View File

@@ -1,6 +1,13 @@
import { describe, expect, it } from "vitest"
import { mustGuaranteeBooking } from "./helpers"
import { Lang } from "@scandic-hotels/common/constants/language"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import {
getPaymentData,
getPaymentMethod,
mustGuaranteeBooking,
} from "./helpers"
const buildRoom = (
overrides: Partial<{
@@ -96,3 +103,179 @@ describe("mustGuaranteeBooking", () => {
).toBe(true)
})
})
describe("getPaymentData", () => {
it("returns correct URLs and method when guarantee is true", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.swish,
lang: Lang.en,
guarantee: true,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: true,
isRedemptionBooking: false,
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.swish,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
})
})
it("returns correct URLs and method when bookingMustBeGuaranteed", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.swish,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: true,
hasOnlyFlexRates: true,
isRedemptionBooking: false,
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.swish,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
})
})
it("returns correct URLs and method when has only flex rates is false", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.swish,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: false,
isRedemptionBooking: false,
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.swish,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
})
})
it("returns null when payment isn't required", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.swish,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: true,
isRedemptionBooking: false,
})
expect(result).toBeNull()
})
it("returns saved credit card when provided", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.card,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: false,
isRedemptionBooking: false,
savedCreditCard: {
alias: "My Visa",
expirationDate: "12/25",
cardType: "visa",
id: "",
type: "",
truncatedNumber: "",
},
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.card,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
card: {
alias: "My Visa",
expiryDate: "12/25",
cardType: "visa",
},
})
})
it("returns correct URLs and method when isRedemptionBooking is true and type is PartnerPoints", () => {
const result = getPaymentData({
paymentMethod: PaymentMethodEnum.PartnerPoints,
lang: Lang.en,
guarantee: false,
bookingMustBeGuaranteed: false,
hasOnlyFlexRates: true,
isRedemptionBooking: true,
})
expect(result).toEqual({
paymentMethod: PaymentMethodEnum.PartnerPoints,
success: `/en/hotelreservation/payment-callback/success`,
error: `/en/hotelreservation/payment-callback/error`,
cancel: `/en/hotelreservation/payment-callback/cancel`,
})
})
})
describe("getPaymentMethod", () => {
it("returns card when hasFlexRates is true", () => {
const hasFlexRates = true
const isRedemptionBooking = false
const redemptionType = "scandic"
const method = getPaymentMethod({
paymentMethod: PaymentMethodEnum.swish,
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
expect(method).toBe(PaymentMethodEnum.card)
})
it("returns PartnerPoints when is redemption and redemptionType is partner", () => {
const hasFlexRates = false
const isRedemptionBooking = true
const redemptionType = "partner"
const method = getPaymentMethod({
paymentMethod: PaymentMethodEnum.swish,
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
expect(method).toBe(PaymentMethodEnum.PartnerPoints)
})
it("returns paymentMethod when not redemption but redemptionType is partner", () => {
const hasFlexRates = false
const isRedemptionBooking = false
const redemptionType = "partner"
const method = getPaymentMethod({
paymentMethod: PaymentMethodEnum.swish,
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
expect(method).toBe(PaymentMethodEnum.swish)
})
it("returns card when payment method is string", () => {
const hasFlexRates = false
const isRedemptionBooking = false
const redemptionType = "scandic"
const method = getPaymentMethod({
paymentMethod: "something-else",
hasFlexRates,
isRedemptionBooking,
redemptionType,
})
expect(method).toBe(PaymentMethodEnum.card)
})
})

View File

@@ -1,6 +1,12 @@
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import { logger } from "@scandic-hotels/common/logger"
import { env } from "../../../../env/client"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
import type { RedemptionType } from "../../../bookingFlowConfig/bookingFlowConfig"
import type { RoomState } from "../../../stores/enter-details/types"
export function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
@@ -123,3 +129,84 @@ export function mustGuaranteeBooking({
return room.mustBeGuaranteed
})
}
function createPaymentCallbackUrl(lang: Lang) {
return `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
}
export function getPaymentData({
guarantee,
bookingMustBeGuaranteed,
hasOnlyFlexRates,
paymentMethod,
isRedemptionBooking,
savedCreditCard,
lang,
}: {
guarantee: boolean
bookingMustBeGuaranteed: boolean
hasOnlyFlexRates: boolean
paymentMethod: PaymentMethodEnum
isRedemptionBooking: boolean
savedCreditCard?: CreditCard
lang: Lang
}) {
const paymentRedirectUrl = createPaymentCallbackUrl(lang)
const redirectUrls = {
success: `${paymentRedirectUrl}/success`,
error: `${paymentRedirectUrl}/error`,
cancel: `${paymentRedirectUrl}/cancel`,
}
if (
isRedemptionBooking &&
paymentMethod === PaymentMethodEnum.PartnerPoints
) {
return {
paymentMethod: paymentMethod,
...redirectUrls,
}
}
const shouldUsePayment =
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
if (!shouldUsePayment) {
return null
}
return {
paymentMethod: paymentMethod,
...redirectUrls,
card: savedCreditCard
? {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
}
: undefined,
}
}
export const getPaymentMethod = ({
paymentMethod,
hasFlexRates,
isRedemptionBooking,
redemptionType,
}: {
paymentMethod: string | null | undefined
hasFlexRates: boolean
isRedemptionBooking: boolean
redemptionType: RedemptionType
}): PaymentMethodEnum => {
if (isRedemptionBooking && redemptionType === "partner") {
return PaymentMethodEnum.PartnerPoints
}
if (hasFlexRates) {
return PaymentMethodEnum.card
}
return paymentMethod && isPaymentMethodEnum(paymentMethod)
? paymentMethod
: PaymentMethodEnum.card
}

View File

@@ -48,7 +48,7 @@ export async function EnterDetailsPage({
// This should never happen unless a user tampers with the URL
if (
!config.redemptionEnabled &&
config.redemptionType === "disabled" &&
booking.searchType === SEARCH_TYPE_REDEMPTION
) {
throw new Error("Redemptions are disabled")

View File

@@ -19,6 +19,8 @@ import { serverClient } from "../trpc"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { NextSearchParams } from "../types"
import { CreateBookingSchema } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
type PaymentCallbackPageProps = {
lang: Lang
@@ -48,11 +50,28 @@ export async function PaymentCallbackPage({
}
const returnUrl = details(lang)
const searchObject = new URLSearchParams()
let errorMessage = undefined
if (status === PaymentCallbackStatusEnum.Cancel) {
const searchObject = new URLSearchParams()
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
return (
<BookingFlowConfig config={config}>
<HandleErrorCallback
returnUrl={returnUrl.toString()}
searchObject={searchObject}
status={status}
/>
</BookingFlowConfig>
)
}
if (status === PaymentCallbackStatusEnum.Error) {
logger.error(
`[payment-callback] error status received for ${confirmationNumber}, status: ${status}`
)
const searchObject = new URLSearchParams()
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
const errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
return (
<BookingFlowConfig config={config}>
<HandleErrorCallback
@@ -81,14 +100,24 @@ export async function PaymentCallbackPage({
}
const booking = await getBooking(confirmationNumber, lang, token)
const refId = booking?.refId
if (
status === PaymentCallbackStatusEnum.Success &&
confirmationNumber &&
refId
) {
const caller = await serverClient()
const bookingStatus = refId
? await caller.booking.status({
refId,
})
: null
if (status === PaymentCallbackStatusEnum.Success && refId) {
const shouldValidatePayment =
bookingStatus?.booking.paymentMethod === PaymentMethodEnum.PartnerPoints
if (shouldValidatePayment) {
// TODO We probably need better error handling for this mutation,
// but for now we've just implemented the happy path
await caller.booking.validatePartnerPayment({ confirmationNumber })
}
const expire = Math.floor(Date.now() / 1000) + 60
const sig = encrypt(expire.toString())
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(refId)}`
@@ -108,49 +137,67 @@ export async function PaymentCallbackPage({
)
}
if (refId) {
try {
const caller = await serverClient()
const bookingStatus = await caller.booking.status({
refId,
})
return (
<HandleBookingStatusError
status={status}
booking={bookingStatus?.booking ?? null}
returnUrl={returnUrl.toString()}
confirmationNumber={confirmationNumber}
config={config}
/>
)
}
const { booking } = bookingStatus
// TODO: how to handle errors for multiple rooms?
const error = booking.errors.find((e) => e.errorCode)
errorMessage =
error?.description ??
`No error message found for booking ${confirmationNumber}, status: ${status}`
searchObject.set(
"errorCode",
error
? error.errorCode.toString()
: BookingErrorCodeEnum.TransactionFailed
)
} catch {
logger.error(
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
)
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
}
}
if (status === PaymentCallbackStatusEnum.Error) {
function HandleBookingStatusError({
booking,
confirmationNumber,
returnUrl,
config,
status,
}: {
booking: CreateBookingSchema | null
confirmationNumber?: string
returnUrl: string
config: BookingFlowConfig
status: PaymentCallbackStatusEnum
}) {
if (!booking) {
logger.error(
`[payment-callback] error status received for ${confirmationNumber}, status: ${status}`
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
)
const searchObject = new URLSearchParams()
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
const errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
return (
<BookingFlowConfig config={config}>
<HandleErrorCallback
returnUrl={returnUrl}
searchObject={searchObject}
status={status}
errorMessage={errorMessage}
/>
</BookingFlowConfig>
)
}
// TODO: how to handle errors for multiple rooms?
const error = booking.errors.find((e) => e.errorCode)
const errorMessage =
error?.description ??
`No error message found for booking ${confirmationNumber}, status: ${status}`
const searchObject = new URLSearchParams()
searchObject.set(
"errorCode",
error ? error.errorCode.toString() : BookingErrorCodeEnum.TransactionFailed
)
return (
<BookingFlowConfig config={config}>
<HandleErrorCallback
returnUrl={returnUrl.toString()}
returnUrl={returnUrl}
searchObject={searchObject}
status={status}
errorMessage={errorMessage}

View File

@@ -20,6 +20,7 @@ export enum PaymentMethodEnum {
maestro = "maestro",
chinaUnionPay = "chinaUnionPay",
discover = "discover",
PartnerPoints = "PartnerPoints",
}
export const PAYMENT_METHOD_TITLES: Record<
@@ -45,4 +46,5 @@ export const PAYMENT_METHOD_TITLES: Record<
maestro: "Maestro",
chinaUnionPay: "China UnionPay",
discover: "Discover",
PartnerPoints: "N/A",
}

View File

@@ -40,6 +40,7 @@ const paymentMethods: Partial<
maestro: (props) => <MaestroIcon {...props} />,
chinaUnionPay: (props) => <ChinaUnionPayIcon {...props} />,
discover: (props) => <DiscoverIcon {...props} />,
PartnerPoints: () => null,
}
type PaymentMethodIconProps = {

View File

@@ -82,6 +82,9 @@ export namespace endpoints {
export function confirmNotification(confirmationNumber: string) {
return `${bookings}/${confirmationNumber}/confirmNotification`
}
export function validatePartnerPayment(confirmationNumber: string) {
return `${bookings}/${confirmationNumber}/validate`
}
export const enum Stays {
future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`,

View File

@@ -1,5 +1,6 @@
import "server-only"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import { createCounter } from "@scandic-hotels/common/telemetry"
import * as api from "../../../../api"
@@ -38,6 +39,23 @@ export const create = safeProtectedServiceProcedure
Authorization: `Bearer ${ctx.token ?? ctx.serviceToken}`,
}
const includePartnerSpecific =
inputWithoutLang.payment?.paymentMethod ===
PaymentMethodEnum.PartnerPoints
if (includePartnerSpecific) {
const session = await ctx.auth()
const token = session?.token.access_token
if (!token) {
throw new Error(
"Cannot create booking with partner points without partner token"
)
}
inputWithoutLang.partnerSpecific = {
eurobonusAccessToken: session?.token.access_token,
}
}
const apiResponse = await api.post(
api.endpoints.v1.Booking.bookings,
{
@@ -60,7 +78,6 @@ export const create = safeProtectedServiceProcedure
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsCreateBooking.validationError(verifiedData.error)

View File

@@ -105,6 +105,11 @@ export const createBookingInput = z.object({
rooms: roomsSchema,
payment: paymentSchema.optional(),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
partnerSpecific: z
.object({
eurobonusAccessToken: z.string(),
})
.optional(),
})
export const createBookingSchema = z
@@ -114,6 +119,7 @@ export const createBookingSchema = z
reservationStatus: z.string(),
guest: guestSchema.optional(),
paymentUrl: z.string().nullable().optional(),
paymentMethod: z.string().nullable().optional(),
rooms: z
.array(
z.object({
@@ -161,6 +167,7 @@ export const createBookingSchema = z
type: d.data.type,
reservationStatus: d.data.attributes.reservationStatus,
paymentUrl: d.data.attributes.paymentUrl,
paymentMethod: d.data.attributes.paymentMethod,
rooms: d.data.attributes.rooms.map((room) => {
const lastName = d.data.attributes.guest?.lastName ?? ""
return {
@@ -171,3 +178,4 @@ export const createBookingSchema = z
errors: d.data.attributes.errors,
guest: d.data.attributes.guest,
}))
export type CreateBookingSchema = z.infer<typeof createBookingSchema>

View File

@@ -17,12 +17,14 @@ import { bookingConfirmationSchema } from "../output"
import { cancelBooking } from "../utils"
import { createBookingSchema } from "./create/schema"
import { create } from "./create"
import { validatePartnerPayment } from "./validatePartnerPayment"
const refIdPlugin = createRefIdPlugin()
const bookingLogger = createLogger("trpc.booking")
export const bookingMutationRouter = router({
create,
validatePartnerPayment,
priceChange: safeProtectedServiceProcedure
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, next }) => {

View File

@@ -0,0 +1,60 @@
import "server-only"
import z from "zod"
import { createCounter } from "@scandic-hotels/common/telemetry"
import * as api from "../../../api"
import { serverErrorByStatus } from "../../../errors"
import { safeProtectedServiceProcedure } from "../../../procedures"
import { toApiLang } from "../../../utils"
const validatePartnerPaymentInput = z.object({
confirmationNumber: z.string(),
})
export const validatePartnerPayment = safeProtectedServiceProcedure
.input(validatePartnerPaymentInput)
.use(async ({ ctx, next }) => {
const token = await ctx.getScandicUserToken()
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx, input }) {
const { confirmationNumber } = input
const getValidateBooking = createCounter("booking.validate")
const metricsValidateBooking = getValidateBooking.init({
confirmationNumber,
})
metricsValidateBooking.start()
const apiResponse = await api.put(
api.endpoints.v1.Booking.validatePartnerPayment(confirmationNumber),
{
headers: {
Authorization: `Bearer ${ctx.token ?? ctx.serviceToken}`,
},
},
{ language: toApiLang(ctx.lang) }
)
if (!apiResponse.ok) {
await metricsValidateBooking.httpError(apiResponse)
// If the booking is not found, return null.
if (apiResponse.status === 404) {
return null
}
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
metricsValidateBooking.success()
return null
})