feat(LOY-232): DTMC API Integration * feat(LOY-232): DTMC API Integration * feat(LOY-232): use employment data in team member card * refactor(LOY-232): remove static data, return employment details in parsed response & fix tests * refactor(LOY-232): improve DTMC API Linking error control flow + make res type safe * fix(LOY-232): remove unused utils * fix(LOY-232): error vars Approved-by: Christian Andolf Approved-by: Erik Tiekstra
377 lines
9.6 KiB
TypeScript
377 lines
9.6 KiB
TypeScript
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 maskValue from "@scandic-hotels/common/utils/maskValue"
|
|
|
|
import { env } from "../../../env/server"
|
|
import * as api from "../../api"
|
|
import { countries } from "../../constants/countries"
|
|
import { cache } from "../../DUPLICATED/cache"
|
|
import { getFriendsMembership } from "../../routers/user/helpers"
|
|
import { creditCardsSchema } from "../../routers/user/output"
|
|
import { toApiLang } from "../../utils"
|
|
import { encrypt } from "../../utils/encryption"
|
|
import { isValidSession } from "../../utils/session"
|
|
import { getUserSchema } from "./output"
|
|
import { type FriendTransaction, getStaysSchema, type Stay } from "./output"
|
|
|
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
|
import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRoute"
|
|
import type { Session } from "next-auth"
|
|
|
|
import type { User } from "../../types/user"
|
|
|
|
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 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 getPreviousStays(
|
|
accessToken: string,
|
|
limit: number = 10,
|
|
language: Lang,
|
|
cursor?: string
|
|
) {
|
|
const getPreviousStaysCounter = createCounter("user", "getPreviousStays")
|
|
const metricsGetPreviousStays = getPreviousStaysCounter.init({
|
|
limit,
|
|
cursor,
|
|
language,
|
|
})
|
|
|
|
metricsGetPreviousStays.start()
|
|
|
|
const params: Record<string, string> = {
|
|
limit: String(limit),
|
|
language: toApiLang(language),
|
|
}
|
|
|
|
if (cursor) {
|
|
params.offset = cursor
|
|
}
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Booking.Stays.past,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
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,
|
|
language: Lang,
|
|
cursor?: string
|
|
) {
|
|
const getUpcomingStaysCounter = createCounter("user", "getUpcomingStays")
|
|
const metricsGetUpcomingStays = getUpcomingStaysCounter.init({
|
|
limit,
|
|
cursor,
|
|
language,
|
|
})
|
|
|
|
metricsGetUpcomingStays.start()
|
|
|
|
const params: Record<string, string> = {
|
|
limit: String(limit),
|
|
language: toApiLang(language),
|
|
}
|
|
|
|
if (cursor) {
|
|
params.offset = cursor
|
|
}
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Booking.Stays.future,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
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,
|
|
employmentDetails: data.employmentDetails,
|
|
firstName: data.firstName,
|
|
language: data.language,
|
|
lastName: data.lastName,
|
|
loyalty: data.loyalty,
|
|
membershipNumber: data.membershipNumber,
|
|
membership: data.loyalty ? getFriendsMembership(data.loyalty) : null,
|
|
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 = 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
|
|
}
|
|
|
|
export const myBookingPath: LangRoute = {
|
|
da: "/hotelreservation/min-booking",
|
|
de: "/hotelreservation/my-booking",
|
|
en: "/hotelreservation/my-booking",
|
|
fi: "/varaa-hotelli/varauksesi",
|
|
no: "/hotelreservation/my-booking",
|
|
sv: "/hotelreservation/din-bokning",
|
|
}
|