364 lines
8.9 KiB
TypeScript
364 lines
8.9 KiB
TypeScript
import { countries } from "@/constants/countries"
|
|
import { myBookingPath } from "@/constants/myBooking"
|
|
import { myStay } from "@/constants/routes/myStay"
|
|
import { env } from "@/env/server"
|
|
import * as api from "@/lib/api"
|
|
import { dt } from "@/lib/dt"
|
|
import { encrypt } from "@/server/routers/utils/encryption"
|
|
import { createCounter } from "@/server/telemetry"
|
|
|
|
import { cache } from "@/utils/cache"
|
|
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 { Session } from "next-auth"
|
|
|
|
import type { User } from "@/types/user"
|
|
import type { Lang } from "@/constants/languages"
|
|
|
|
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> {
|
|
if (!isValidSession(session)) return undefined
|
|
|
|
const verifiedUser = await getVerifiedUser({ session })
|
|
if (!verifiedUser || "error" in verifiedUser) {
|
|
return undefined
|
|
}
|
|
|
|
return verifiedUser.data.membershipNumber
|
|
}
|
|
|
|
export async function getPreviousStays(
|
|
accessToken: string,
|
|
limit: number = 10,
|
|
cursor?: string
|
|
) {
|
|
const getPreviousStaysCounter = createCounter("user", "getPreviousStays")
|
|
const metricsGetPreviousStays = getPreviousStaysCounter.init({
|
|
limit,
|
|
cursor,
|
|
})
|
|
|
|
metricsGetPreviousStays.start()
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Booking.Stays.past,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
},
|
|
{
|
|
limit,
|
|
cursor,
|
|
}
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
await metricsGetPreviousStays.httpError(apiResponse)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
|
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
|
if (!verifiedData.success) {
|
|
metricsGetPreviousStays.validationError(verifiedData.error)
|
|
return null
|
|
}
|
|
|
|
metricsGetPreviousStays.success()
|
|
|
|
return verifiedData.data
|
|
}
|
|
|
|
export async function getUpcomingStays(
|
|
accessToken: string,
|
|
limit: number = 10,
|
|
cursor?: string
|
|
) {
|
|
const getUpcomingStaysCounter = createCounter("user", "getUpcomingStays")
|
|
const metricsGetUpcomingStays = getUpcomingStaysCounter.init({
|
|
limit,
|
|
cursor,
|
|
})
|
|
|
|
metricsGetUpcomingStays.start()
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Booking.Stays.future,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
},
|
|
{
|
|
limit,
|
|
cursor,
|
|
}
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
await metricsGetUpcomingStays.httpError(apiResponse)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
|
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
|
if (!verifiedData.success) {
|
|
metricsGetUpcomingStays.validationError(verifiedData.error)
|
|
return null
|
|
}
|
|
|
|
metricsGetUpcomingStays.success()
|
|
|
|
return verifiedData.data
|
|
}
|
|
|
|
export function parsedUser(data: User, isMFA: boolean) {
|
|
const country = countries.find((c) => c.code === data.address?.countryCode)
|
|
|
|
const user = {
|
|
address: {
|
|
city: data.address?.city,
|
|
country: country?.name ?? "",
|
|
countryCode: data.address?.countryCode,
|
|
streetAddress: data.address?.streetAddress,
|
|
zipCode: data.address?.zipCode,
|
|
},
|
|
dateOfBirth: data.dateOfBirth,
|
|
email: data.email,
|
|
firstName: data.firstName,
|
|
language: data.language,
|
|
lastName: data.lastName,
|
|
membershipNumber: data.membershipNumber,
|
|
membership: getFriendsMembership(data.loyalty),
|
|
loyalty: data.loyalty,
|
|
name: `${data.firstName} ${data.lastName}`,
|
|
phoneNumber: data.phoneNumber,
|
|
profileId: data.profileId,
|
|
}
|
|
|
|
if (!isMFA) {
|
|
if (user.address.city) {
|
|
user.address.city = maskValue.text(user.address.city)
|
|
}
|
|
if (user.address.streetAddress) {
|
|
user.address.streetAddress = maskValue.text(user.address.streetAddress)
|
|
}
|
|
|
|
user.address.zipCode = data.address?.zipCode
|
|
? maskValue.text(data.address.zipCode)
|
|
: ""
|
|
|
|
user.dateOfBirth = maskValue.all(user.dateOfBirth)
|
|
|
|
user.email = maskValue.email(user.email)
|
|
|
|
user.phoneNumber = user.phoneNumber ? maskValue.phone(user.phoneNumber) : ""
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
export const getCreditCards = cache(
|
|
async ({
|
|
session,
|
|
onlyNonExpired,
|
|
}: {
|
|
session: Session
|
|
onlyNonExpired?: boolean
|
|
}) => {
|
|
const getCreditCardsCounter = createCounter("user", "getCreditCards")
|
|
const metricsGetCreditCards = getCreditCardsCounter.init({
|
|
onlyNonExpired,
|
|
})
|
|
|
|
metricsGetCreditCards.start()
|
|
|
|
const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, {
|
|
headers: {
|
|
Authorization: `Bearer ${session.token.access_token}`,
|
|
},
|
|
})
|
|
|
|
if (!apiResponse.ok) {
|
|
await metricsGetCreditCards.httpError(apiResponse)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const verifiedData = creditCardsSchema.safeParse(apiJson)
|
|
if (!verifiedData.success) {
|
|
metricsGetCreditCards.validationError(verifiedData.error)
|
|
return null
|
|
}
|
|
|
|
const result = verifiedData.data.data.filter((card) => {
|
|
if (onlyNonExpired) {
|
|
try {
|
|
const expirationDate = dt(card.expirationDate).startOf("day")
|
|
const currentDate = dt().startOf("day")
|
|
return expirationDate > currentDate
|
|
} catch (_) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
metricsGetCreditCards.success()
|
|
|
|
return result
|
|
}
|
|
)
|
|
|
|
export async function updateStaysBookingUrl(
|
|
data: Stay[],
|
|
session: Session,
|
|
lang: Lang
|
|
): Promise<Stay[]>
|
|
|
|
export async function updateStaysBookingUrl(
|
|
data: FriendTransaction[],
|
|
session: Session,
|
|
lang: Lang
|
|
): Promise<FriendTransaction[]>
|
|
|
|
export async function updateStaysBookingUrl(
|
|
data: Stay[] | FriendTransaction[],
|
|
session: Session,
|
|
lang: Lang
|
|
) {
|
|
const user = await getVerifiedUser({
|
|
session,
|
|
})
|
|
|
|
if (user && !("error" in user)) {
|
|
return data.map((d) => {
|
|
const originalString =
|
|
d.attributes.confirmationNumber.toString() + "," + user.data.lastName
|
|
const encryptedBookingValue = encrypt(originalString)
|
|
|
|
// Get base URL with fallback for ephemeral environments (like deploy previews).
|
|
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
|
|
|
// Construct Booking URL.
|
|
const bookingUrl = env.HIDE_FOR_NEXT_RELEASE
|
|
? new URL(
|
|
getCurrentWebUrl({
|
|
path: myBookingPath[lang],
|
|
lang,
|
|
baseUrl,
|
|
})
|
|
)
|
|
: new URL(myStay[lang], baseUrl)
|
|
|
|
// Add search parameters.
|
|
if (encryptedBookingValue) {
|
|
bookingUrl.searchParams.set("RefId", encryptedBookingValue)
|
|
} else {
|
|
bookingUrl.searchParams.set("lastName", user.data.lastName)
|
|
bookingUrl.searchParams.set(
|
|
"bookingId",
|
|
d.attributes.confirmationNumber.toString()
|
|
)
|
|
}
|
|
|
|
return {
|
|
...d,
|
|
attributes: {
|
|
...d.attributes,
|
|
bookingUrl: bookingUrl.toString(),
|
|
},
|
|
}
|
|
})
|
|
}
|
|
|
|
return data
|
|
}
|