Merged in feature/SW-3505-fetch-eurobonus-points (pull request #2847)

feat(SW-3505): add endpoint for getting eurobonus profile

* feat(SW-3505): add endpoint for getting eurobonus profile

* make sure we add loginType to session

* no need to run zod parsing twice

* Make SAS environment variables mandatory


Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-09-23 12:13:20 +00:00
parent 8cd6f1d3a5
commit c46e71d76e
6 changed files with 90 additions and 5 deletions

View File

@@ -79,6 +79,7 @@ const config: NextAuthConfig = {
}
: undefined,
token: {
loginType: "sas",
access_token: token.access_token,
expires_at: token.expires_at,
error: token.error,

View File

@@ -6,6 +6,7 @@ import Image from "@scandic-hotels/design-system/Image"
import Link from "@scandic-hotels/design-system/Link"
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client"
import useLang from "@/hooks/useLang"
@@ -17,6 +18,14 @@ export function Header() {
const lang = useLang()
const session = useSession()
const {
data: profileData,
isLoading,
isSuccess,
} = trpc.partner.sas.getEuroBonusProfile.useQuery(undefined, {
enabled: session.status === "authenticated",
})
return (
<>
<header className={styles.header}>
@@ -45,6 +54,15 @@ export function Header() {
{session.data?.user && <>{session.data.user.email}</>}
</span>
</Typography>
{isLoading && <SkeletonShimmer width={"6ch"} height={"1ch"} />}
{isSuccess && profileData && (
<Typography variant="Body/Supporting text (caption)/smBold">
<span>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{profileData.points.total} Points
</span>
</Typography>
)}
<Link color={"white"} href={`/${lang}/logout`} prefetch={false}>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{"Logout"}

View File

@@ -6,6 +6,8 @@ import {
configureServerClient,
} from "@scandic-hotels/trpc/serverClient"
import { auth } from "@/auth"
import type { Lang } from "@scandic-hotels/common/constants/language"
export async function createAppContext() {
@@ -18,7 +20,8 @@ export async function createAppContext() {
url: headersList.get("x-url")!,
contentType: headersList.get("x-contenttype")!,
auth: async () => {
return null
const session = await auth()
return session
},
})

View File

@@ -13,10 +13,10 @@ export const env = createEnv({
server: {
API_BASEURL: z.string(),
BOOKING_ENCRYPTION_KEY: z.string(),
SAS_API_ENDPOINT: z.string().default(""),
SAS_AUTH_ENDPOINT: z.string().default(""),
SAS_OCP_APIM: z.string().default(""),
SAS_AUTH_CLIENTID: z.string().default(""),
SAS_API_ENDPOINT: z.string(),
SAS_AUTH_ENDPOINT: z.string(),
SAS_OCP_APIM: z.string(),
SAS_AUTH_CLIENTID: z.string(),
CACHE_TIME_HOTELS: z.coerce
.number()
.default(TWENTYFOUR_HOURS)

View File

@@ -0,0 +1,61 @@
import { z } from "zod"
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import { env } from "../../../../env/server"
import { protectedProcedure } from "../../../procedures"
const outputSchema = z.object({
eurobonusNumber: z.string(),
linkStatus: z.enum(["UNLINKED", "LINKED"]),
isBlocked: z.boolean(),
enrollmentDate: z.string(),
birthdate: z.string(),
mobileNumber: z.string(),
email: z.string().email(),
points: z.object({
balances: z.array(z.unknown()),
total: z.number(),
}),
tier: z.string(),
tierStartDate: z.string(),
tierEndDate: z.string().nullable(),
lifeTimeGold: z.boolean(),
tierBoostRequestedByScandic: z.boolean(),
firstName: z.string(),
lastName: z.string(),
whoBoosted: z.enum(["NO-BOOST", "BOOSTED"]),
})
const sasLogger = createLogger("SAS")
const url = new URL("/api/scandic-partnership/v1/profile", env.SAS_API_ENDPOINT)
export const getEuroBonusProfile = protectedProcedure
.output(outputSchema)
.query(async function ({ ctx }) {
if (ctx.session.token.loginType !== "sas") {
throw new Error(
`Failed to fetch EuroBonus profile, expected loginType to be "sas" but was ${ctx.session.token.loginType}`
)
}
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
"Ocp-Apim-Subscription-Key": env.SAS_OCP_APIM,
Authorization: `Bearer ${ctx.session?.token?.access_token}`,
},
})
if (!response.ok) {
sasLogger.error(
`Failed to get EuroBonus profile, status: ${response.status}, statusText: ${response.statusText}`
)
throw new Error("Failed to fetch EuroBonus profile", {
cause: { status: response.status, statusText: response.statusText },
})
}
const data = await response.json()
return data
})

View File

@@ -1,6 +1,7 @@
import { router } from "../../.."
import { requestOtp } from "./otp/request/requestOtp"
import { verifyOtp } from "./otp/verify/verifyOtp"
import { getEuroBonusProfile } from "./getEuroBonusProfile"
import { linkAccount } from "./linkAccount"
import { performLevelUpgrade } from "./performLevelUpgrade"
import { transferPoints } from "./transferPoints"
@@ -13,4 +14,5 @@ export const sasRouter = router({
unlinkAccount,
performLevelUpgrade,
transferPoints,
getEuroBonusProfile,
})