Merged in feat/sw-2864-move-hotels-router-to-trpc-package (pull request #2410)

feat (SW-2864): Move booking router to trpc package

* Add env to trpc package

* Add eslint to trpc package

* Apply lint rules

* Use direct imports from trpc package

* Add lint-staged config to trpc

* Move lang enum to common

* Restructure trpc package folder structure

* WIP first step

* update internal imports in trpc

* Fix most errors in scandic-web

Just 100 left...

* Move Props type out of trpc

* Fix CategorizedFilters types

* Move more schemas in hotel router

* Fix deps

* fix getNonContentstackUrls

* Fix import error

* Fix entry error handling

* Fix generateMetadata metrics

* Fix alertType enum

* Fix duplicated types

* lint:fix

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package

* Fix broken imports

* Move booking router to trpc package

* Merge branch 'master' into feat/sw-2864-move-hotels-router-to-trpc-package


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-06-26 09:02:59 +00:00
parent 002d093af4
commit bbcabfa0ba
135 changed files with 1403 additions and 1387 deletions

View File

@@ -2,122 +2,6 @@ import { z } from "zod"
import { imageSchema } from "@scandic-hotels/trpc/routers/hotels/schemas/image"
import { countriesMap } from "@/constants/countries"
import { getFriendsMembership } from "@/utils/user"
const scandicFriendsTier = z.enum(["L1", "L2", "L3", "L4", "L5", "L6", "L7"])
const sasEurobonusTier = z.enum(["EBB", "EBS", "EBG", "EBD", "EBP"])
const commonMembershipSchema = z.object({
membershipNumber: z.string(),
tierExpires: z.string().nullish().default(null),
memberSince: z.string().nullish(),
})
// This prevents validation errors if the API returns an unhandled membership type
const otherMembershipSchema = z
.object({
// This ensures that `type` won't widen into "string", losing the literal types, when used in a union
type: z.string().refine((val): val is string & {} => true),
})
.merge(commonMembershipSchema)
export const sasMembershipSchema = z
.object({
type: z.literal("SAS_EB"),
tier: sasEurobonusTier,
nextTier: sasEurobonusTier.nullish(),
spendablePoints: z.number().nullish(),
boostedByScandic: z.boolean().nullish(),
boostedTier: sasEurobonusTier.nullish(),
boostedTierExpires: z.string().nullish().default(null),
})
.merge(commonMembershipSchema)
.transform((response) => {
return {
...response,
tierExpires:
// SAS API returns 1900-01-01 for non-expiring tiers
response.tierExpires === "1900-01-01" ? null : response.tierExpires,
}
})
export const friendsMembershipSchema = z
.object({
type: z.literal("SCANDIC_NATIVE"),
tier: scandicFriendsTier,
nextTier: scandicFriendsTier.nullish(),
pointsToNextTier: z.number().nullish(),
nightsToTopTier: z.number().nullish(),
})
.merge(commonMembershipSchema)
export const membershipSchema = z.union([
friendsMembershipSchema,
sasMembershipSchema,
otherMembershipSchema,
])
const pointExpirationSchema = z.object({
points: z.number().int(),
expires: z.string(),
})
export const userLoyaltySchema = z.object({
memberships: z.array(membershipSchema),
points: z.object({
spendable: z.number().int(),
earned: z.number().int(),
spent: z.number().int(),
}),
tier: scandicFriendsTier,
tierExpires: z.string(),
tierBoostedBy: z.string().nullish(),
pointExpirations: z.array(pointExpirationSchema),
})
export const getUserSchema = z
.object({
data: z.object({
attributes: z.object({
dateOfBirth: z.string().optional().default("1900-01-01"),
email: z.string().email(),
firstName: z.string(),
language: z
.string()
// Preserve Profile v1 formatting for now so it matches ApiLang enum
.transform((s) => s.charAt(0).toUpperCase() + s.slice(1))
.optional(),
lastName: z.string(),
phoneNumber: z.string().optional(),
profileId: z.string(),
membershipNumber: z.string(),
address: z
.object({
city: z.string().optional(),
country: z.string().optional(),
countryCode: z.nativeEnum(countriesMap).optional(),
streetAddress: z.string().optional(),
zipCode: z.string().optional(),
})
.optional()
.nullable(),
loyalty: userLoyaltySchema.optional(),
}),
type: z.string(),
}),
})
.transform((apiResponse) => {
return {
...apiResponse.data.attributes,
membership: apiResponse.data.attributes.loyalty
? getFriendsMembership(apiResponse.data.attributes.loyalty)
: null,
name: `${apiResponse.data.attributes.firstName} ${apiResponse.data.attributes.lastName}`,
}
})
// Schema is the same for upcoming and previous stays endpoints
export const getStaysSchema = z.object({
data: z.array(
@@ -234,35 +118,6 @@ type GetFriendTransactionsData = z.infer<typeof getFriendTransactionsSchema>
export type FriendTransaction = GetFriendTransactionsData["data"][number]
export const creditCardSchema = z
.object({
attribute: z.object({
cardName: z.string().optional(),
alias: z.string(),
truncatedNumber: z.string().transform((s) => s.slice(-4)),
expirationDate: z.string(),
cardType: z
.string()
.transform((s) => s.charAt(0).toLowerCase() + s.slice(1)),
}),
id: z.string(),
type: z.string(),
})
.transform((apiResponse) => {
return {
id: apiResponse.id,
type: apiResponse.attribute.cardType,
truncatedNumber: apiResponse.attribute.truncatedNumber,
alias: apiResponse.attribute.alias,
expirationDate: apiResponse.attribute.expirationDate,
cardType: apiResponse.attribute.cardType,
}
})
export const creditCardsSchema = z.object({
data: z.array(creditCardSchema),
})
export const initiateSaveCardSchema = z.object({
data: z.object({
attribute: z.object({

View File

@@ -6,10 +6,12 @@ import {
protectedProcedure,
safeProtectedProcedure,
} from "@scandic-hotels/trpc/procedures"
import { getFriendsMembership } from "@scandic-hotels/trpc/routers/user/helpers"
import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils"
import { toApiLang } from "@scandic-hotels/trpc/utils"
import { isValidSession } from "@/utils/session"
import { getFriendsMembership, getMembershipCards } from "@/utils/user"
import { getMembershipCards } from "@/utils/user"
import {
friendTransactionsInput,
@@ -22,13 +24,14 @@ import {
getCreditCards,
getPreviousStays,
getUpcomingStays,
getVerifiedUser,
parsedUser,
updateStaysBookingUrl,
} from "./utils"
import type { LoginType } from "@scandic-hotels/trpc/types/loginType"
import type {
LoginType,
// LoginType,
TrackingSDKUserData,
} from "@/types/components/tracking"
import { Transactions } from "@/types/enums/transactions"

View File

@@ -2,9 +2,12 @@ import { myStay } from "@scandic-hotels/common/constants/routes/myStay"
import { dt } from "@scandic-hotels/common/dt"
import { createCounter } from "@scandic-hotels/common/telemetry"
import * as api from "@scandic-hotels/trpc/api"
import { countries } from "@scandic-hotels/trpc/constants/countries"
import { getFriendsMembership } from "@scandic-hotels/trpc/routers/user/helpers"
import { creditCardsSchema } from "@scandic-hotels/trpc/routers/user/output"
import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils"
import { toApiLang } from "@scandic-hotels/trpc/utils"
import { countries } from "@/constants/countries"
import { myBookingPath } from "@/constants/myBooking"
import { env } from "@/env/server"
@@ -13,93 +16,13 @@ import { encrypt } from "@/utils/encryption"
import * as maskValue from "@/utils/maskValue"
import { isValidSession } from "@/utils/session"
import { getCurrentWebUrl } from "@/utils/url"
import { getFriendsMembership } from "@/utils/user"
import {
creditCardsSchema,
type FriendTransaction,
getStaysSchema,
getUserSchema,
type Stay,
} from "./output"
import { type FriendTransaction, getStaysSchema, type Stay } from "./output"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { User } from "@scandic-hotels/trpc/types/user"
import type { Session } from "next-auth"
import type { User } from "@/types/user"
export const getVerifiedUser = cache(
async ({
session,
includeExtendedPartnerData,
}: {
session: Session
includeExtendedPartnerData?: boolean
}) => {
const getVerifiedUserCounter = createCounter("user", "getVerifiedUser")
const metricsGetVerifiedUser = getVerifiedUserCounter.init()
metricsGetVerifiedUser.start()
const now = Date.now()
if (session.token.expires_at && session.token.expires_at < now) {
metricsGetVerifiedUser.dataError(`Token expired`)
return { error: true, cause: "token_expired" } as const
}
const apiResponse = await api.get(
api.endpoints.v2.Profile.profile,
{
headers: {
Authorization: `Bearer ${session.token.access_token}`,
},
},
includeExtendedPartnerData
? { includes: "extendedPartnerInformation" }
: {}
)
if (!apiResponse.ok) {
await metricsGetVerifiedUser.httpError(apiResponse)
if (apiResponse.status === 401) {
return { error: true, cause: "unauthorized" } as const
} else if (apiResponse.status === 403) {
return { error: true, cause: "forbidden" } as const
} else if (apiResponse.status === 404) {
return { error: true, cause: "notfound" } as const
}
return {
error: true,
cause: "unknown",
status: apiResponse.status,
} as const
}
const apiJson = await apiResponse.json()
if (!apiJson.data?.attributes) {
metricsGetVerifiedUser.dataError(
`Missing data attributes in API response`,
{
data: apiJson,
}
)
return null
}
const verifiedData = getUserSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsGetVerifiedUser.validationError(verifiedData.error)
return null
}
metricsGetVerifiedUser.success()
return verifiedData
}
)
export async function getMembershipNumber(
session: Session | null
): Promise<string | undefined> {