This implements the actual call to the API to create a booking. That’s the only thing it does, it doesn’t handle the response in any way. This PR is just to get it there and the new booking sub team will handle it further, with payment etc. Approved-by: Michael Zetterberg Approved-by: Fredrik Thorsson Approved-by: Simon.Emanuelsson
903 lines
27 KiB
TypeScript
903 lines
27 KiB
TypeScript
import { metrics } from "@opentelemetry/api"
|
|
|
|
import { Lang } from "@/constants/languages"
|
|
import { env } from "@/env/server"
|
|
import * as api from "@/lib/api"
|
|
import {
|
|
protectedProcedure,
|
|
router,
|
|
safeProtectedProcedure,
|
|
} from "@/server/trpc"
|
|
|
|
import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
|
|
import * as maskValue from "@/utils/maskValue"
|
|
import { getMembership, getMembershipCards } from "@/utils/user"
|
|
|
|
import encryptValue from "../utils/encryptValue"
|
|
import { friendTransactionsInput, staysInput } from "./input"
|
|
import {
|
|
creditCardsSchema,
|
|
FriendTransaction,
|
|
getFriendTransactionsSchema,
|
|
getMembershipCardsSchema,
|
|
getStaysSchema,
|
|
getUserSchema,
|
|
Stay,
|
|
} from "./output"
|
|
import { benefits, extendedUser, nextLevelPerks } from "./temp"
|
|
|
|
import type { Session } from "next-auth"
|
|
|
|
import { RewardTransactionTypes } from "@/types/components/myPages/myPage/enums"
|
|
import type {
|
|
LoginType,
|
|
TrackingSDKUserData,
|
|
} from "@/types/components/tracking"
|
|
import type { MembershipLevel } from "@/constants/membershipLevels"
|
|
|
|
// OpenTelemetry metrics: User
|
|
const meter = metrics.getMeter("trpc.user")
|
|
const getVerifiedUserCounter = meter.createCounter("trpc.user.get")
|
|
const getVerifiedUserSuccessCounter = meter.createCounter(
|
|
"trpc.user.get-success"
|
|
)
|
|
const getVerifiedUserFailCounter = meter.createCounter("trpc.user.get-fail")
|
|
const getProfileCounter = meter.createCounter("trpc.user.profile")
|
|
const getProfileSuccessCounter = meter.createCounter(
|
|
"trpc.user.profile-success"
|
|
)
|
|
const getProfileFailCounter = meter.createCounter("trpc.user.profile-fail")
|
|
|
|
// OpenTelemetry metrics: Stays
|
|
const getPreviousStaysCounter = meter.createCounter("trpc.user.stays.previous")
|
|
const getPreviousStaysSuccessCounter = meter.createCounter(
|
|
"trpc.user.stays.previous-success"
|
|
)
|
|
const getPreviousStaysFailCounter = meter.createCounter(
|
|
"trpc.user.stays.previous-fail"
|
|
)
|
|
const getUpcomingStaysCounter = meter.createCounter("trpc.user.stays.upcoming")
|
|
const getUpcomingStaysSuccessCounter = meter.createCounter(
|
|
"trpc.user.stays.upcoming-success"
|
|
)
|
|
const getUpcomingStaysFailCounter = meter.createCounter(
|
|
"trpc.user.stays.upcoming-fail"
|
|
)
|
|
|
|
// OpenTelemetry metrics: Transactions
|
|
const getFriendTransactionsCounter = meter.createCounter(
|
|
"trpc.user.transactions.friendTransactions"
|
|
)
|
|
const getFriendTransactionsSuccessCounter = meter.createCounter(
|
|
"trpc.user.transactions.friendTransactions-success"
|
|
)
|
|
const getFriendTransactionsFailCounter = meter.createCounter(
|
|
"trpc.user.transactions.friendTransactions-fail"
|
|
)
|
|
|
|
// OpenTelemetry metrics: Credit Cards
|
|
const getCreditCardsCounter = meter.createCounter("trpc.user.creditCards")
|
|
const getCreditCardsSuccessCounter = meter.createCounter(
|
|
"trpc.user.creditCards-success"
|
|
)
|
|
const getCreditCardsFailCounter = meter.createCounter(
|
|
"trpc.user.creditCards-fail"
|
|
)
|
|
|
|
export async function getVerifiedUser({ session }: { session: Session }) {
|
|
const now = Date.now()
|
|
|
|
if (session.token.expires_at && session.token.expires_at < now) {
|
|
return { error: true, cause: "token_expired" } as const
|
|
}
|
|
getVerifiedUserCounter.add(1)
|
|
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
|
|
const apiResponse = await api.get(api.endpoints.v1.profile, {
|
|
cache: "no-store",
|
|
headers: {
|
|
Authorization: `Bearer ${session.token.access_token}`,
|
|
},
|
|
})
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
getVerifiedUserFailCounter.add(1, {
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.user.profile getVerifiedUser error",
|
|
JSON.stringify({
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
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) {
|
|
getVerifiedUserFailCounter.add(1, {
|
|
error_type: "data_error",
|
|
})
|
|
console.error(
|
|
"api.user.profile getVerifiedUser data error",
|
|
JSON.stringify({
|
|
apiResponse: apiJson,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const verifiedData = getUserSchema.safeParse(apiJson.data.attributes)
|
|
if (!verifiedData.success) {
|
|
getVerifiedUserFailCounter.add(1, {
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(verifiedData.error),
|
|
})
|
|
console.error(
|
|
"api.user.profile validation error",
|
|
JSON.stringify({
|
|
errors: verifiedData.error,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
getVerifiedUserSuccessCounter.add(1)
|
|
console.info("api.user.profile getVerifiedUser success", JSON.stringify({}))
|
|
return verifiedData
|
|
}
|
|
|
|
function fakingRequest<T>(payload: T): Promise<T> {
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
resolve(payload)
|
|
}, 1500)
|
|
})
|
|
}
|
|
|
|
async function updateStaysBookingUrl(
|
|
data: Stay[],
|
|
token: string,
|
|
lang: Lang
|
|
): Promise<Stay[]>
|
|
|
|
async function updateStaysBookingUrl(
|
|
data: FriendTransaction[],
|
|
token: string,
|
|
lang: Lang
|
|
): Promise<FriendTransaction[]>
|
|
|
|
async function updateStaysBookingUrl(
|
|
data: Stay[] | FriendTransaction[],
|
|
token: string,
|
|
lang: Lang
|
|
) {
|
|
// Tenporary API call needed till we have user name in ctx session data
|
|
getProfileCounter.add(1)
|
|
console.info("api.user.profile updatebookingurl start", JSON.stringify({}))
|
|
const apiResponse = await api.get(api.endpoints.v1.profile, {
|
|
cache: "no-store",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
|
|
// Temporary Url, domain and lang support for current web
|
|
let localeDomain = env.PUBLIC_URL
|
|
let fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
|
|
switch (lang) {
|
|
case Lang.sv:
|
|
localeDomain = localeDomain?.replace(".com", ".se")
|
|
fullBookingUrl = localeDomain + "/hotelreservation/din-bokning"
|
|
break
|
|
case Lang.no:
|
|
localeDomain = localeDomain?.replace(".com", ".no")
|
|
fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
|
|
break
|
|
case Lang.da:
|
|
localeDomain = localeDomain?.replace(".com", ".dk")
|
|
fullBookingUrl = localeDomain + "/hotelreservation/min-booking"
|
|
break
|
|
case Lang.fi:
|
|
localeDomain = localeDomain?.replace(".com", ".fi")
|
|
fullBookingUrl = localeDomain + "/varaa-hotelli/varauksesi"
|
|
break
|
|
case Lang.de:
|
|
localeDomain = localeDomain?.replace(".com", ".de")
|
|
fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
|
|
if (apiResponse.ok) {
|
|
getProfileSuccessCounter.add(1)
|
|
console.info(
|
|
"api.user.profile updatebookingurl success",
|
|
JSON.stringify({})
|
|
)
|
|
const apiJson = await apiResponse.json()
|
|
if (apiJson.data?.attributes) {
|
|
return data.map((d) => {
|
|
const originalString =
|
|
d.attributes.confirmationNumber.toString() +
|
|
"," +
|
|
apiJson.data.attributes.lastName
|
|
const encryptedBookingValue = encryptValue(originalString)
|
|
const bookingUrl = !!encryptedBookingValue
|
|
? fullBookingUrl + "?RefId=" + encryptedBookingValue
|
|
: fullBookingUrl +
|
|
"?lastName=" +
|
|
apiJson.data.attributes.lastName +
|
|
"&bookingId=" +
|
|
d.attributes.confirmationNumber
|
|
return {
|
|
...d,
|
|
attributes: {
|
|
...d.attributes,
|
|
bookingUrl: bookingUrl,
|
|
},
|
|
}
|
|
})
|
|
}
|
|
}
|
|
getProfileFailCounter.add(1, { error: JSON.stringify(apiResponse) })
|
|
console.info(
|
|
"api.user.profile updatebookingurl error",
|
|
JSON.stringify({ error: apiResponse })
|
|
)
|
|
|
|
return data
|
|
}
|
|
|
|
export const userQueryRouter = router({
|
|
get: protectedProcedure
|
|
.use(async function (opts) {
|
|
return opts.next({
|
|
ctx: {
|
|
...opts.ctx,
|
|
isMFA:
|
|
opts.ctx.session.token.mfa_scope &&
|
|
opts.ctx.session.token.mfa_expires_at > Date.now(),
|
|
},
|
|
})
|
|
})
|
|
.query(async function getUser({ ctx }) {
|
|
const data = await getVerifiedUser({ session: ctx.session })
|
|
|
|
if (!data) {
|
|
return null
|
|
}
|
|
|
|
if ("error" in data) {
|
|
return data
|
|
}
|
|
|
|
const verifiedData = data
|
|
|
|
const country = countries.find(
|
|
(c) => c.code === verifiedData.data.address.countryCode
|
|
)
|
|
|
|
const user = {
|
|
...extendedUser,
|
|
address: {
|
|
city: verifiedData.data.address.city,
|
|
country: country?.name ?? "",
|
|
countryCode: verifiedData.data.address.countryCode,
|
|
streetAddress: verifiedData.data.address.streetAddress,
|
|
zipCode: verifiedData.data.address.zipCode,
|
|
},
|
|
dateOfBirth: verifiedData.data.dateOfBirth,
|
|
email: verifiedData.data.email,
|
|
firstName: verifiedData.data.firstName,
|
|
language: verifiedData.data.language,
|
|
lastName: verifiedData.data.lastName,
|
|
memberships: verifiedData.data.memberships,
|
|
name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`,
|
|
phoneNumber: verifiedData.data.phoneNumber,
|
|
profileId: verifiedData.data.profileId,
|
|
}
|
|
|
|
if (!ctx.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 = verifiedData.data.address?.zipCode
|
|
? maskValue.text(verifiedData.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
|
|
}),
|
|
name: safeProtectedProcedure.query(async function ({ ctx }) {
|
|
if (!ctx.session) {
|
|
return null
|
|
}
|
|
const verifiedData = await getVerifiedUser({ session: ctx.session })
|
|
|
|
if (!verifiedData || "error" in verifiedData) {
|
|
return null
|
|
}
|
|
return {
|
|
firstName: verifiedData.data.firstName,
|
|
lastName: verifiedData.data.lastName,
|
|
}
|
|
}),
|
|
membershipLevel: protectedProcedure.query(async function ({ ctx }) {
|
|
const verifiedData = await getVerifiedUser({ session: ctx.session })
|
|
if (!verifiedData || "error" in verifiedData) {
|
|
return null
|
|
}
|
|
|
|
const membershipLevel = getMembership(verifiedData.data.memberships)
|
|
return membershipLevel
|
|
}),
|
|
safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) {
|
|
if (!ctx.session) {
|
|
return null
|
|
}
|
|
const verifiedData = await getVerifiedUser({ session: ctx.session })
|
|
|
|
if (!verifiedData || "error" in verifiedData) {
|
|
return null
|
|
}
|
|
|
|
const membershipLevel = getMembership(verifiedData.data.memberships)
|
|
return membershipLevel
|
|
}),
|
|
tracking: safeProtectedProcedure.query(async function ({ ctx }) {
|
|
const notLoggedInUserTrackingData: TrackingSDKUserData = {
|
|
loginStatus: "Non-logged in",
|
|
}
|
|
|
|
if (!ctx.session) {
|
|
return notLoggedInUserTrackingData
|
|
}
|
|
const verifiedUserData = await getVerifiedUser({ session: ctx.session })
|
|
|
|
if (!verifiedUserData || "error" in verifiedUserData) {
|
|
return notLoggedInUserTrackingData
|
|
}
|
|
|
|
const params = new URLSearchParams()
|
|
params.set("limit", "1")
|
|
getPreviousStaysCounter.add(1, { query: JSON.stringify({ params }) })
|
|
console.info(
|
|
"api.booking.stays.past start",
|
|
JSON.stringify({ query: { params } })
|
|
)
|
|
const previousStaysResponse = await api.get(
|
|
api.endpoints.v1.previousStays,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!previousStaysResponse.ok) {
|
|
getPreviousStaysFailCounter.add(1, {
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: previousStaysResponse.status,
|
|
statusText: previousStaysResponse.statusText,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.booking.stays.past error",
|
|
JSON.stringify({
|
|
error: {
|
|
status: previousStaysResponse.status,
|
|
statusText: previousStaysResponse.statusText,
|
|
},
|
|
})
|
|
)
|
|
return notLoggedInUserTrackingData
|
|
}
|
|
|
|
const previousStaysApiJson = await previousStaysResponse.json()
|
|
const verifiedPreviousStaysData =
|
|
getStaysSchema.safeParse(previousStaysApiJson)
|
|
if (!verifiedPreviousStaysData.success) {
|
|
getPreviousStaysFailCounter.add(1, {
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(verifiedPreviousStaysData.error),
|
|
})
|
|
console.error(
|
|
"api.booking.stays.past validation error, ",
|
|
JSON.stringify({ error: verifiedPreviousStaysData.error })
|
|
)
|
|
return notLoggedInUserTrackingData
|
|
}
|
|
getPreviousStaysSuccessCounter.add(1)
|
|
console.info("api.booking.stays.past success", JSON.stringify({}))
|
|
|
|
const membership = getMembership(verifiedUserData.data.memberships)
|
|
|
|
const loggedInUserTrackingData: TrackingSDKUserData = {
|
|
loginStatus: "logged in",
|
|
loginType: ctx.session.token.loginType as LoginType,
|
|
memberId: membership?.membershipNumber,
|
|
memberLevel: membership?.membershipLevel as MembershipLevel,
|
|
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
|
|
totalPointsAvailableToSpend: membership?.currentPoints,
|
|
loginAction: "login success",
|
|
}
|
|
return loggedInUserTrackingData
|
|
}),
|
|
benefits: router({
|
|
current: protectedProcedure.query(async function (opts) {
|
|
// TODO: Make request to get user data from Scandic API
|
|
return await fakingRequest<typeof benefits>(benefits)
|
|
}),
|
|
nextLevel: protectedProcedure.query(async function (opts) {
|
|
// TODO: Make request to get user data from Scandic API
|
|
return await fakingRequest<typeof nextLevelPerks>(nextLevelPerks)
|
|
}),
|
|
}),
|
|
|
|
stays: router({
|
|
previous: protectedProcedure
|
|
.input(staysInput)
|
|
.query(async ({ ctx, input }) => {
|
|
const { limit, cursor } = input
|
|
const params: Record<string, string> = { limit }
|
|
if (cursor) {
|
|
params.offset = cursor
|
|
}
|
|
getPreviousStaysCounter.add(1, { query: JSON.stringify({ params }) })
|
|
console.info(
|
|
"api.booking.stays.past start",
|
|
JSON.stringify({ query: { params } })
|
|
)
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.previousStays,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
getPreviousStaysFailCounter.add(1, {
|
|
query: JSON.stringify({ params }),
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.booking.stays.past error ",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
|
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
|
if (!verifiedData.success) {
|
|
getPreviousStaysFailCounter.add(1, {
|
|
query: JSON.stringify({ params }),
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(verifiedData.error),
|
|
})
|
|
console.error(
|
|
"api.booking.stays.past validation error ",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: verifiedData.error,
|
|
})
|
|
)
|
|
|
|
return null
|
|
}
|
|
getPreviousStaysSuccessCounter.add(1, {
|
|
query: JSON.stringify({ params }),
|
|
})
|
|
console.info(
|
|
"api.booking.stays.past success",
|
|
JSON.stringify({ query: { params } })
|
|
)
|
|
const nextCursor =
|
|
verifiedData.data.links &&
|
|
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
|
? verifiedData.data.links.offset
|
|
: undefined
|
|
|
|
const updatedData = await updateStaysBookingUrl(
|
|
verifiedData.data.data,
|
|
ctx.session.token.access_token,
|
|
ctx.lang
|
|
)
|
|
|
|
return {
|
|
data: updatedData,
|
|
nextCursor,
|
|
}
|
|
}),
|
|
|
|
upcoming: protectedProcedure
|
|
.input(staysInput)
|
|
.query(async ({ ctx, input }) => {
|
|
const { limit, cursor } = input
|
|
|
|
const params: Record<string, string> = { limit }
|
|
if (cursor) {
|
|
params.offset = cursor
|
|
}
|
|
getUpcomingStaysCounter.add(1, {
|
|
query: JSON.stringify({ params }),
|
|
})
|
|
console.info(
|
|
"api.booking.stays.future start",
|
|
JSON.stringify({ query: { params } })
|
|
)
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.upcomingStays,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
getUpcomingStaysFailCounter.add(1, {
|
|
query: JSON.stringify({ params }),
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.booking.stays.future error ",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error_type: "http_error",
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
|
if (!verifiedData.success) {
|
|
getUpcomingStaysFailCounter.add(1, {
|
|
query: JSON.stringify({ params }),
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(verifiedData.error),
|
|
})
|
|
console.error(
|
|
"api.booking.stays.future validation error ",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: verifiedData.error,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
getUpcomingStaysSuccessCounter.add(1, {
|
|
query: JSON.stringify({ params }),
|
|
})
|
|
console.info("api.booking.stays.future success", {
|
|
query: JSON.stringify({ params }),
|
|
})
|
|
const nextCursor =
|
|
verifiedData.data.links &&
|
|
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
|
? verifiedData.data.links.offset
|
|
: undefined
|
|
|
|
const updatedData = await updateStaysBookingUrl(
|
|
verifiedData.data.data,
|
|
ctx.session.token.access_token,
|
|
ctx.lang
|
|
)
|
|
|
|
return {
|
|
data: updatedData,
|
|
nextCursor,
|
|
}
|
|
}),
|
|
}),
|
|
transaction: router({
|
|
friendTransactions: protectedProcedure
|
|
.input(friendTransactionsInput)
|
|
.query(async ({ ctx, input }) => {
|
|
const { limit, page } = input
|
|
getFriendTransactionsCounter.add(1)
|
|
console.info(
|
|
"api.transaction.friendTransactions start",
|
|
JSON.stringify({})
|
|
)
|
|
const apiResponse = await api.get(api.endpoints.v1.friendTransactions, {
|
|
cache: undefined, // override defaultOptions
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
|
},
|
|
next: { revalidate: 30 * 60 * 1000 },
|
|
})
|
|
|
|
if (!apiResponse.ok) {
|
|
// switch (apiResponse.status) {
|
|
// case 400:
|
|
// throw badRequestError()
|
|
// case 401:
|
|
// throw unauthorizedError()
|
|
// case 403:
|
|
// throw forbiddenError()
|
|
// default:
|
|
// throw internalServerError()
|
|
// }
|
|
const text = await apiResponse.text()
|
|
getFriendTransactionsFailCounter.add(1, {
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.transaction.friendTransactions error ",
|
|
JSON.stringify({
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
|
if (!verifiedData.success) {
|
|
getFriendTransactionsFailCounter.add(1, {
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(verifiedData.error),
|
|
})
|
|
console.error(
|
|
"api.transaction.friendTransactions validation error ",
|
|
JSON.stringify({ error: verifiedData.error })
|
|
)
|
|
return null
|
|
}
|
|
|
|
getFriendTransactionsSuccessCounter.add(1)
|
|
console.info(
|
|
"api.transaction.friendTransactions success",
|
|
JSON.stringify({})
|
|
)
|
|
const updatedData = await updateStaysBookingUrl(
|
|
verifiedData.data.data,
|
|
ctx.session.token.access_token,
|
|
ctx.lang
|
|
)
|
|
|
|
const pageData = updatedData
|
|
.filter((t) => t.type !== RewardTransactionTypes.expired)
|
|
.sort((a, b) => {
|
|
// 'BALFWD' are transactions from Opera migration that happended in May 2021
|
|
if (a.attributes.confirmationNumber === "BALFWD") return 1
|
|
if (b.attributes.confirmationNumber === "BALFWD") return -1
|
|
|
|
const dateA = new Date(
|
|
a.attributes.checkinDate
|
|
? a.attributes.checkinDate
|
|
: a.attributes.transactionDate
|
|
)
|
|
const dateB = new Date(
|
|
b.attributes.checkinDate
|
|
? b.attributes.checkinDate
|
|
: b.attributes.transactionDate
|
|
)
|
|
|
|
return dateA > dateB ? -1 : 1
|
|
})
|
|
|
|
const slicedData = pageData.slice(limit * (page - 1), limit * page)
|
|
|
|
return {
|
|
data: {
|
|
transactions: slicedData.map(({ type, attributes }) => {
|
|
return {
|
|
type,
|
|
awardPoints: attributes.awardPoints,
|
|
checkinDate: attributes.checkinDate,
|
|
checkoutDate: attributes.checkoutDate,
|
|
city: attributes.hotelInformation?.city,
|
|
confirmationNumber: attributes.confirmationNumber,
|
|
hotelName: attributes.hotelInformation?.name,
|
|
nights: attributes.nights,
|
|
pointsCalculated: attributes.pointsCalculated,
|
|
hotelId: attributes.hotelOperaId,
|
|
transactionDate: attributes.transactionDate,
|
|
bookingUrl: attributes.bookingUrl,
|
|
}
|
|
}),
|
|
},
|
|
meta: {
|
|
totalPages: Math.ceil(pageData.length / limit),
|
|
},
|
|
}
|
|
}),
|
|
}),
|
|
|
|
creditCards: protectedProcedure.query(async function ({ ctx }) {
|
|
getCreditCardsCounter.add(1)
|
|
console.info("api.profile.creditCards start", JSON.stringify({}))
|
|
const apiResponse = await api.get(api.endpoints.v1.creditCards, {
|
|
cache: "no-store",
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
|
},
|
|
})
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
getCreditCardsFailCounter.add(1, {
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.profile.creditCards error ",
|
|
JSON.stringify({
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const verifiedData = creditCardsSchema.safeParse(apiJson)
|
|
if (!verifiedData.success) {
|
|
getCreditCardsFailCounter.add(1, {
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(verifiedData.error),
|
|
})
|
|
console.error(
|
|
"api.profile.creditCards validation error ",
|
|
JSON.stringify({ error: verifiedData.error })
|
|
)
|
|
return null
|
|
}
|
|
getCreditCardsSuccessCounter.add(1)
|
|
console.info("api.profile.creditCards success", JSON.stringify({}))
|
|
return verifiedData.data.data
|
|
}),
|
|
|
|
membershipCards: protectedProcedure.query(async function ({ ctx }) {
|
|
getProfileCounter.add(1)
|
|
console.info("api.profile start", JSON.stringify({}))
|
|
const apiResponse = await api.get(api.endpoints.v1.profile, {
|
|
cache: "no-store",
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
|
},
|
|
})
|
|
|
|
if (!apiResponse.ok) {
|
|
// switch (apiResponse.status) {
|
|
// case 400:
|
|
// throw badRequestError()
|
|
// case 401:
|
|
// throw unauthorizedError()
|
|
// case 403:
|
|
// throw forbiddenError()
|
|
// default:
|
|
// throw internalServerError()
|
|
// }
|
|
const text = await apiResponse.text()
|
|
getProfileFailCounter.add(1, {
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.profile error",
|
|
JSON.stringify({
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
|
|
const verifiedData = getMembershipCardsSchema.safeParse(
|
|
apiJson.data.attributes.memberships
|
|
)
|
|
|
|
if (!verifiedData.success) {
|
|
getProfileFailCounter.add(1, {
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(verifiedData),
|
|
})
|
|
console.error(
|
|
"api.profile validation error",
|
|
JSON.stringify({ error: verifiedData })
|
|
)
|
|
return null
|
|
}
|
|
getProfileSuccessCounter.add(1)
|
|
console.info("api.profile success", JSON.stringify({}))
|
|
const cards = getMembershipCards(verifiedData.data)
|
|
|
|
return cards
|
|
}),
|
|
})
|