Merge branch 'develop' into feature/tracking

This commit is contained in:
Linus Flood
2024-10-24 12:39:34 +02:00
221 changed files with 5789 additions and 1491 deletions

View File

@@ -63,6 +63,10 @@ export const createBookingInput = z.object({
})
// Query
export const getBookingStatusInput = z.object({
const confirmationNumberInput = z.object({
confirmationNumber: z.string(),
})
export const bookingConfirmationInput = confirmationNumberInput
export const getBookingStatusInput = confirmationNumberInput

View File

@@ -35,96 +35,95 @@ async function getMembershipNumber(
}
export const bookingMutationRouter = router({
booking: router({
create: serviceProcedure
.input(createBookingInput)
.mutation(async function ({ ctx, input }) {
const { checkInDate, checkOutDate, hotelId } = input
create: serviceProcedure.input(createBookingInput).mutation(async function ({
ctx,
input,
}) {
const { checkInDate, checkOutDate, hotelId } = input
// TODO: add support for user token OR service token in procedure
// then we can fetch membership number if user token exists
const loggingAttributes = {
// membershipNumber: await getMembershipNumber(ctx.session),
checkInDate,
checkOutDate,
hotelId,
}
// TODO: add support for user token OR service token in procedure
// then we can fetch membership number if user token exists
const loggingAttributes = {
// membershipNumber: await getMembershipNumber(ctx.session),
checkInDate,
checkOutDate,
hotelId,
}
createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate })
createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate })
console.info(
"api.booking.booking.create start",
JSON.stringify({
query: loggingAttributes,
})
)
const headers = {
Authorization: `Bearer ${ctx.serviceToken}`,
}
console.info(
"api.booking.create start",
JSON.stringify({
query: loggingAttributes,
})
)
const headers = {
Authorization: `Bearer ${ctx.serviceToken}`,
}
const apiResponse = await api.post(api.endpoints.v1.booking, {
headers,
body: input,
const apiResponse = await api.post(api.endpoints.v1.booking, {
headers,
body: input,
})
if (!apiResponse.ok) {
const text = await apiResponse.text()
createBookingFailCounter.add(1, {
hotelId,
checkInDate,
checkOutDate,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
}),
})
console.error(
"api.booking.create error",
JSON.stringify({
query: loggingAttributes,
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
error: text,
},
})
)
return null
}
if (!apiResponse.ok) {
const text = await apiResponse.text()
createBookingFailCounter.add(1, {
hotelId,
checkInDate,
checkOutDate,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
}),
})
console.error(
"api.booking.booking.create error",
JSON.stringify({
query: loggingAttributes,
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
error: text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
createBookingFailCounter.add(1, {
hotelId,
checkInDate,
checkOutDate,
error_type: "validation_error",
})
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
createBookingFailCounter.add(1, {
hotelId,
checkInDate,
checkOutDate,
error_type: "validation_error",
})
console.error(
"api.booking.booking.create validation error",
JSON.stringify({
query: loggingAttributes,
error: verifiedData.error,
})
)
return null
}
createBookingSuccessCounter.add(1, {
hotelId,
checkInDate,
checkOutDate,
console.error(
"api.booking.create validation error",
JSON.stringify({
query: loggingAttributes,
error: verifiedData.error,
})
)
return null
}
console.info(
"api.booking.booking.create success",
JSON.stringify({
query: loggingAttributes,
})
)
return verifiedData.data
}),
createBookingSuccessCounter.add(1, {
hotelId,
checkInDate,
checkOutDate,
})
console.info(
"api.booking.create success",
JSON.stringify({
query: loggingAttributes,
})
)
return verifiedData.data
}),
})

View File

@@ -1,5 +1,8 @@
import { z } from "zod"
import { BedTypeEnum } from "@/constants/booking"
// MUTATION
export const createBookingSchema = z
.object({
data: z.object({
@@ -32,4 +35,46 @@ export const createBookingSchema = z
paymentUrl: d.data.attributes.paymentUrl,
}))
type CreateBookingData = z.infer<typeof createBookingSchema>
// QUERY
const childrenAgesSchema = z.object({
age: z.number(),
bedType: z.nativeEnum(BedTypeEnum),
})
const guestSchema = z.object({
firstName: z.string(),
lastName: z.string(),
})
const packagesSchema = z.object({
accessibility: z.boolean(),
allergyFriendly: z.boolean(),
breakfast: z.boolean(),
petFriendly: z.boolean(),
})
export const bookingConfirmationSchema = z
.object({
data: z.object({
attributes: z.object({
adults: z.number(),
checkInDate: z.date({ coerce: true }),
checkOutDate: z.date({ coerce: true }),
createDateTime: z.date({ coerce: true }),
childrenAges: z.array(childrenAgesSchema),
computedReservationStatus: z.string(),
confirmationNumber: z.string(),
currencyCode: z.string(),
guest: guestSchema,
hasPayRouting: z.boolean(),
hotelId: z.string(),
packages: packagesSchema,
rateCode: z.string(),
reservationStatus: z.string(),
totalPrice: z.number(),
}),
id: z.string(),
type: z.literal("booking"),
}),
})
.transform(({ data }) => data.attributes)

View File

@@ -4,10 +4,20 @@ import * as api from "@/lib/api"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { router, serviceProcedure } from "@/server/trpc"
import { getBookingStatusInput } from "./input"
import { createBookingSchema } from "./output"
import { bookingConfirmationInput, getBookingStatusInput } from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
const meter = metrics.getMeter("trpc.booking")
const getBookingConfirmationCounter = meter.createCounter(
"trpc.booking.confirmation"
)
const getBookingConfirmationSuccessCounter = meter.createCounter(
"trpc.booking.confirmation-success"
)
const getBookingConfirmationFailCounter = meter.createCounter(
"trpc.booking.confirmation-fail"
)
const getBookingStatusCounter = meter.createCounter("trpc.booking.status")
const getBookingStatusSuccessCounter = meter.createCounter(
"trpc.booking.status-success"
@@ -17,6 +27,113 @@ const getBookingStatusFailCounter = meter.createCounter(
)
export const bookingQueryRouter = router({
confirmation: serviceProcedure
.input(bookingConfirmationInput)
.query(async function ({ ctx, input: { confirmationNumber } }) {
getBookingConfirmationCounter.add(1, { confirmationNumber })
const apiResponse = await api.get(
`${api.endpoints.v1.booking}/${confirmationNumber}`,
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
}
)
if (!apiResponse.ok) {
const responseMessage = await apiResponse.text()
getBookingConfirmationFailCounter.add(1, {
confirmationNumber,
error_type: "http_error",
error: responseMessage,
})
console.error(
"api.booking.confirmation error",
JSON.stringify({
query: { confirmationNumber },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text: responseMessage,
},
})
)
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const booking = bookingConfirmationSchema.safeParse(apiJson)
if (!booking.success) {
getBookingConfirmationFailCounter.add(1, {
confirmationNumber,
error_type: "validation_error",
error: JSON.stringify(booking.error),
})
console.error(
"api.booking.confirmation validation error",
JSON.stringify({
query: { confirmationNumber },
error: booking.error,
})
)
throw badRequestError()
}
getBookingConfirmationSuccessCounter.add(1, { confirmationNumber })
console.info(
"api.booking.confirmation success",
JSON.stringify({
query: { confirmationNumber },
})
)
return {
...booking.data,
temp: {
breakfastFrom: "06:30",
breakfastTo: "11:00",
cancelPolicy: "Free rebooking",
fromDate: "2024-10-21 14:00",
packages: [
{
name: "Breakfast buffet",
price: "150 SEK",
},
{
name: "Member discount",
price: "-297 SEK",
},
{
name: "Points used / remaining",
price: "0 / 1044",
},
],
payment: "2024-08-09 1:47",
room: {
price: "2 589 SEK",
type: "Cozy Cabin",
vat: "684,79 SEK",
},
toDate: "2024-10-22 11:00",
total: "2 739 SEK",
totalInEuro: "265 EUR",
},
guest: {
email: "sarah.obrian@gmail.com",
firstName: "Sarah",
lastName: "O'Brian",
memberbershipNumber: "19822",
phoneNumber: "+46702446688",
},
hotel: {
email: "bookings@scandichotels.com",
name: "Downtown Camper by Scandic",
phoneNumber: "+4689001350",
},
}
}),
status: serviceProcedure.input(getBookingStatusInput).query(async function ({
ctx,
input,

View File

@@ -14,7 +14,8 @@ import { removeMultipleSlashes } from "@/utils/url"
import { systemSchema } from "../schemas/system"
import { Image } from "@/types/image"
import { AlertTypeEnum } from "@/types/enums/alert"
import type { Image } from "@/types/image"
// Help me write this zod schema based on the type ContactConfig
export const validateContactConfigSchema = z.object({
@@ -39,10 +40,12 @@ export const validateContactConfigSchema = z.object({
phone: z.object({
number: z.string().nullable(),
name: z.string().nullable(),
footnote: z.string().nullable(),
}),
phone_loyalty: z.object({
number: z.string().nullable(),
name: z.string().nullable(),
footnote: z.string().nullable(),
}),
visiting_address: z.object({
zip: z.string().nullable(),
@@ -366,16 +369,16 @@ export const validateFooterConfigSchema = z
all_footer: z.object({
items: z.array(
z.object({
main_links: validateLinks,
main_links: validateLinks.default([]),
app_downloads: z.object({
title: z.string(),
links: validateLinksWithType,
links: validateLinksWithType.default([]),
}),
secondary_links: validateSecondaryLinks,
secondary_links: validateSecondaryLinks.default([]),
social_media: z.object({
links: validateLinksWithType,
links: validateLinksWithType.default([]),
}),
tertiary_links: validateLinks,
tertiary_links: validateLinks.default([]),
})
),
}),
@@ -415,25 +418,31 @@ export const validateFooterRefConfigSchema = z.object({
items: z
.array(
z.object({
main_links: z.array(
z.object({
pageConnection: pageConnectionRefs,
})
),
secondary_links: z.array(
z.object({
links: z.array(
z.object({
pageConnection: pageConnectionRefs,
})
),
})
),
tertiary_links: z.array(
z.object({
pageConnection: pageConnectionRefs,
})
),
main_links: z
.array(
z.object({
pageConnection: pageConnectionRefs,
})
)
.nullable(),
secondary_links: z
.array(
z.object({
links: z.array(
z.object({
pageConnection: pageConnectionRefs,
})
),
})
)
.nullable(),
tertiary_links: z
.array(
z.object({
pageConnection: pageConnectionRefs,
})
)
.nullable(),
system: systemSchema,
})
)
@@ -538,6 +547,7 @@ export const headerRefsSchema = z
})
const linkUnionSchema = z.discriminatedUnion("__typename", [
pageLinks.accountPageSchema,
pageLinks.contentPageSchema,
pageLinks.hotelPageSchema,
pageLinks.loyaltyPageSchema,
@@ -660,3 +670,166 @@ export const headerSchema = z
},
}
})
export const alertSchema = z
.object({
type: z.nativeEnum(AlertTypeEnum),
text: z.string(),
heading: z.string(),
phone_contact: z.object({
display_text: z.string(),
phone_number: z.string().nullable(),
footnote: z.string().nullable(),
}),
has_link: z.boolean(),
link: linkAndTitleSchema,
has_sidepeek_button: z.boolean(),
sidepeek_button: z.object({
cta_text: z.string(),
}),
sidepeek_content: z.object({
heading: z.string(),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z
.discriminatedUnion("__typename", [
pageLinks.accountPageSchema,
pageLinks.contentPageSchema,
pageLinks.hotelPageSchema,
pageLinks.loyaltyPageSchema,
])
.transform((data) => {
const link = pageLinks.transform(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
}),
})
.transform(
({
type,
heading,
text,
phone_contact,
has_link,
link,
has_sidepeek_button,
sidepeek_button,
sidepeek_content,
}) => {
const hasLink = has_link && link.link
return {
type,
text,
heading,
phoneContact:
phone_contact.display_text && phone_contact.phone_number
? {
displayText: phone_contact.display_text,
phoneNumber: phone_contact.phone_number,
footnote: phone_contact.footnote,
}
: null,
hasSidepeekButton: !!has_sidepeek_button,
link: hasLink
? {
url: link.link.url,
title: link.title,
}
: null,
sidepeekButton:
!hasLink && has_sidepeek_button ? sidepeek_button : null,
sidepeekContent:
!hasLink && has_sidepeek_button ? sidepeek_content : null,
}
}
)
export const siteConfigSchema = z
.object({
all_site_config: z.object({
items: z
.array(
z.object({
sitewide_alert: z.object({
booking_widget_disabled: z.boolean(),
alertConnection: z.object({
edges: z
.array(
z.object({
node: alertSchema,
})
)
.max(1),
}),
}),
})
)
.max(1),
}),
})
.transform((data) => {
if (!data.all_site_config.items.length) {
return {
sitewideAlert: null,
bookingWidgetDisabled: false,
}
}
const { sitewide_alert } = data.all_site_config.items[0]
return {
sitewideAlert: sitewide_alert.alertConnection.edges[0]?.node || null,
bookingWidgetDisabled: sitewide_alert.booking_widget_disabled,
}
})
const sidepeekContentRefSchema = z.object({
content: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
pageLinks.accountPageRefSchema,
pageLinks.contentPageRefSchema,
pageLinks.hotelPageRefSchema,
pageLinks.loyaltyPageRefSchema,
]),
})
),
}),
}),
})
const alertConnectionRefSchema = z.object({
edges: z.array(
z.object({
node: z.object({
link: linkRefsSchema,
sidepeek_content: sidepeekContentRefSchema,
}),
})
),
})
export const siteConfigRefSchema = z.object({
all_site_config: z.object({
items: z.array(
z.object({
sitewide_alert: z.object({
alertConnection: alertConnectionRefSchema,
}),
system: systemSchema,
})
),
}),
})

View File

@@ -1,5 +1,4 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import { GetContactConfig } from "@/lib/graphql/Query/ContactConfig.graphql"
import {
GetCurrentFooter,
@@ -11,6 +10,10 @@ import {
} from "@/lib/graphql/Query/Current/Header.graphql"
import { GetFooter, GetFooterRef } from "@/lib/graphql/Query/Footer.graphql"
import { GetHeader, GetHeaderRef } from "@/lib/graphql/Query/Header.graphql"
import {
GetSiteConfig,
GetSiteConfigRef,
} from "@/lib/graphql/Query/SiteConfig.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackBaseProcedure, router } from "@/server/trpc"
@@ -31,13 +34,50 @@ import {
type GetCurrentHeaderData,
headerRefsSchema,
headerSchema,
siteConfigRefSchema,
siteConfigSchema,
validateContactConfigSchema,
validateCurrentFooterConfigSchema,
validateCurrentHeaderConfigSchema,
validateFooterConfigSchema,
validateFooterRefConfigSchema,
} from "./output"
import { getConnections, getFooterConnections } from "./utils"
import {
getContactConfigCounter,
getContactConfigFailCounter,
getContactConfigSuccessCounter,
getCurrentFooterCounter,
getCurrentFooterFailCounter,
getCurrentFooterRefCounter,
getCurrentFooterSuccessCounter,
getCurrentHeaderCounter,
getCurrentHeaderFailCounter,
getCurrentHeaderRefCounter,
getCurrentHeaderSuccessCounter,
getFooterCounter,
getFooterFailCounter,
getFooterRefCounter,
getFooterRefFailCounter,
getFooterRefSuccessCounter,
getFooterSuccessCounter,
getHeaderCounter,
getHeaderFailCounter,
getHeaderRefsCounter,
getHeaderRefsFailCounter,
getHeaderRefsSuccessCounter,
getHeaderSuccessCounter,
getSiteConfigCounter,
getSiteConfigFailCounter,
getSiteConfigRefCounter,
getSiteConfigRefFailCounter,
getSiteConfigRefSuccessCounter,
getSiteConfigSuccessCounter,
} from "./telemetry"
import {
getAlertPhoneContactData,
getConnections,
getFooterConnections,
} from "./utils"
import type {
FooterDataRaw,
@@ -47,157 +87,77 @@ import type {
GetHeader as GetHeaderData,
GetHeaderRefs,
} from "@/types/trpc/routers/contentstack/header"
import type {
GetSiteConfigData,
GetSiteConfigRefData,
} from "@/types/trpc/routers/contentstack/siteConfig"
const meter = metrics.getMeter("trpc.contentstack.base")
// OpenTelemetry metrics: ContactConfig
const getContactConfigCounter = meter.createCounter(
"trpc.contentstack.contactConfig.get"
)
const getContactConfigSuccessCounter = meter.createCounter(
"trpc.contentstack.contactConfig.get-success"
)
const getContactConfigFailCounter = meter.createCounter(
"trpc.contentstack.contactConfig.get-fail"
)
// OpenTelemetry metrics: CurrentHeader
const getCurrentHeaderRefCounter = meter.createCounter(
"trpc.contentstack.currentHeader.ref.get"
)
const getCurrentHeaderRefSuccessCounter = meter.createCounter(
"trpc.contentstack.currentHeader.ref.get-success"
)
const getCurrentHeaderRefFailCounter = meter.createCounter(
"trpc.contentstack.currentHeader.ref.get-fail"
)
const getCurrentHeaderCounter = meter.createCounter(
"trpc.contentstack.currentHeader.get"
)
const getCurrentHeaderSuccessCounter = meter.createCounter(
"trpc.contentstack.currentHeader.get-success"
)
const getCurrentHeaderFailCounter = meter.createCounter(
"trpc.contentstack.currentHeader.get-fail"
)
async function getContactConfig(lang: Lang) {
getContactConfigCounter.add(1, { lang })
console.info(
"contentstack.contactConfig start",
JSON.stringify({ query: { lang } })
)
const response = await request<ContactConfigData>(
GetContactConfig,
{
locale: lang,
},
{
cache: "force-cache",
next: {
tags: [`${lang}:contact`],
},
}
)
// OpenTelemetry metrics: Header
const getHeaderRefsCounter = meter.createCounter(
"trpc.contentstack.header.ref.get"
)
const getHeaderRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.header.ref.get-success"
)
const getHeaderRefsFailCounter = meter.createCounter(
"trpc.contentstack.header.ref.get-fail"
)
const getHeaderCounter = meter.createCounter("trpc.contentstack.header.get")
const getHeaderSuccessCounter = meter.createCounter(
"trpc.contentstack.header.get-success"
)
const getHeaderFailCounter = meter.createCounter(
"trpc.contentstack.header.get-fail"
)
if (!response.data) {
const notFoundError = notFound(response)
// OpenTelemetry metrics: CurrentHeader
const getCurrentFooterRefCounter = meter.createCounter(
"trpc.contentstack.currentFooter.ref.get"
)
const getCurrentFooterRefSuccessCounter = meter.createCounter(
"trpc.contentstack.currentFooter.ref.get-success"
)
const getCurrentFooterRefFailCounter = meter.createCounter(
"trpc.contentstack.currentFooter.ref.get-fail"
)
const getCurrentFooterCounter = meter.createCounter(
"trpc.contentstack.currentFooter.get"
)
const getCurrentFooterSuccessCounter = meter.createCounter(
"trpc.contentstack.currentFooter.get-success"
)
const getCurrentFooterFailCounter = meter.createCounter(
"trpc.contentstack.currentFooter.get-fail"
)
getContactConfigFailCounter.add(1, {
lang,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
// OpenTelemetry metrics: Footer
const getFooterRefCounter = meter.createCounter(
"trpc.contentstack.footer.ref.get"
)
const getFooterRefSuccessCounter = meter.createCounter(
"trpc.contentstack.footer.ref.get-success"
)
const getFooterRefFailCounter = meter.createCounter(
"trpc.contentstack.footer.ref.get-fail"
)
const getFooterCounter = meter.createCounter("trpc.contentstack.footer.get")
const getFooterSuccessCounter = meter.createCounter(
"trpc.contentstack.footer.get-success"
)
const getFooterFailCounter = meter.createCounter(
"trpc.contentstack.footer.get-fail"
)
console.error(
"contentstack.config not found error",
JSON.stringify({ query: { lang }, error: { code: notFoundError.code } })
)
throw notFoundError
}
const validatedContactConfigConfig = validateContactConfigSchema.safeParse(
response.data
)
if (!validatedContactConfigConfig.success) {
getContactConfigFailCounter.add(1, {
lang,
error_type: "validation_error",
error: JSON.stringify(validatedContactConfigConfig.error),
})
console.error(
"contentstack.contactConfig validation error",
JSON.stringify({
query: { lang },
error: validatedContactConfigConfig.error,
})
)
return null
}
getContactConfigSuccessCounter.add(1, { lang })
console.info(
"contentstack.contactConfig success",
JSON.stringify({ query: { lang } })
)
return validatedContactConfigConfig.data.all_contact_config.items[0]
}
export const baseQueryRouter = router({
contact: contentstackBaseProcedure.query(async ({ ctx }) => {
const { lang } = ctx
getContactConfigCounter.add(1, { lang })
console.info(
"contentstack.contactConfig start",
JSON.stringify({ query: { lang } })
)
const response = await request<ContactConfigData>(
GetContactConfig,
{
locale: lang,
},
{
cache: "force-cache",
next: {
tags: [`${lang}:contact`],
},
}
)
if (!response.data) {
const notFoundError = notFound(response)
getContactConfigFailCounter.add(1, {
lang,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.config not found error",
JSON.stringify({ query: { lang }, error: { code: notFoundError.code } })
)
throw notFoundError
}
const validatedContactConfigConfig = validateContactConfigSchema.safeParse(
response.data
)
if (!validatedContactConfigConfig.success) {
getContactConfigFailCounter.add(1, {
lang,
error_type: "validation_error",
error: JSON.stringify(validatedContactConfigConfig.error),
})
console.error(
"contentstack.contactConfig validation error",
JSON.stringify({
query: { lang },
error: validatedContactConfigConfig.error,
})
)
return null
}
getContactConfigSuccessCounter.add(1, { lang })
console.info(
"contentstack.contactConfig success",
JSON.stringify({ query: { lang } })
)
return validatedContactConfigConfig.data.all_contact_config.items[0]
return await getContactConfig(ctx.lang)
}),
header: contentstackBaseProcedure.query(async ({ ctx }) => {
const { lang } = ctx
@@ -652,4 +612,149 @@ export const baseQueryRouter = router({
return validatedFooterConfig.data
}),
siteConfig: contentstackBaseProcedure.query(async ({ ctx }) => {
const { lang } = ctx
getSiteConfigRefCounter.add(1, { lang })
console.info(
"contentstack.siteConfig.ref start",
JSON.stringify({ query: { lang } })
)
const responseRef = await request<GetSiteConfigRefData>(
GetSiteConfigRef,
{
locale: lang,
},
{
cache: "force-cache",
next: {
tags: [generateRefsResponseTag(lang, "siteConfig")],
},
}
)
if (!responseRef.data) {
const notFoundError = notFound(responseRef)
getSiteConfigRefFailCounter.add(1, {
lang,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.siteConfig.refs not found error",
JSON.stringify({
query: {
lang,
},
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedSiteConfigRef = siteConfigRefSchema.safeParse(
responseRef.data
)
if (!validatedSiteConfigRef.success) {
getSiteConfigRefFailCounter.add(1, {
lang,
error_type: "validation_error",
error: JSON.stringify(validatedSiteConfigRef.error),
})
console.error(
"contentstack.siteConfig.refs validation error",
JSON.stringify({
query: {
lang,
},
error: validatedSiteConfigRef.error,
})
)
return null
}
getSiteConfigRefSuccessCounter.add(1, { lang })
console.info(
"contentstack.siteConfig.refs success",
JSON.stringify({ query: { lang } })
)
getSiteConfigCounter.add(1, { lang })
console.info(
"contentstack.siteConfig start",
JSON.stringify({ query: { lang } })
)
const [siteConfigResponse, contactConfig] = await Promise.all([
request<GetSiteConfigData>(
GetSiteConfig,
{
locale: lang,
},
{
cache: "force-cache",
next: {
tags: [`${lang}:siteConfig`],
},
}
),
getContactConfig(lang),
])
if (!siteConfigResponse.data) {
const notFoundError = notFound(siteConfigResponse)
getSiteConfigFailCounter.add(1, {
lang,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.siteConfig not found error",
JSON.stringify({ query: { lang }, error: { code: notFoundError.code } })
)
throw notFoundError
}
const validatedSiteConfig = siteConfigSchema.safeParse(
siteConfigResponse.data
)
if (!validatedSiteConfig.success) {
getSiteConfigFailCounter.add(1, {
lang,
error_type: "validation_error",
error: JSON.stringify(validatedSiteConfig.error),
})
console.error(
"contentstack.siteConfig validation error",
JSON.stringify({
query: { lang },
error: validatedSiteConfig.error,
})
)
return null
}
getSiteConfigSuccessCounter.add(1, { lang })
console.info(
"contentstack.siteConfig success",
JSON.stringify({ query: { lang } })
)
const { sitewideAlert } = validatedSiteConfig.data
return {
...validatedSiteConfig.data,
sitewideAlert: sitewideAlert
? {
...sitewideAlert,
phoneContact: contactConfig
? getAlertPhoneContactData(sitewideAlert, contactConfig)
: null,
}
: null,
}
}),
})

View File

@@ -0,0 +1,112 @@
import { metrics } from "@opentelemetry/api"
const meter = metrics.getMeter("trpc.contentstack.base")
// OpenTelemetry metrics: ContactConfig
export const getContactConfigCounter = meter.createCounter(
"trpc.contentstack.contactConfig.get"
)
export const getContactConfigSuccessCounter = meter.createCounter(
"trpc.contentstack.contactConfig.get-success"
)
export const getContactConfigFailCounter = meter.createCounter(
"trpc.contentstack.contactConfig.get-fail"
)
// OpenTelemetry metrics: CurrentHeader
export const getCurrentHeaderRefCounter = meter.createCounter(
"trpc.contentstack.currentHeader.ref.get"
)
export const getCurrentHeaderRefSuccessCounter = meter.createCounter(
"trpc.contentstack.currentHeader.ref.get-success"
)
export const getCurrentHeaderRefFailCounter = meter.createCounter(
"trpc.contentstack.currentHeader.ref.get-fail"
)
export const getCurrentHeaderCounter = meter.createCounter(
"trpc.contentstack.currentHeader.get"
)
export const getCurrentHeaderSuccessCounter = meter.createCounter(
"trpc.contentstack.currentHeader.get-success"
)
export const getCurrentHeaderFailCounter = meter.createCounter(
"trpc.contentstack.currentHeader.get-fail"
)
// OpenTelemetry metrics: Header
export const getHeaderRefsCounter = meter.createCounter(
"trpc.contentstack.header.ref.get"
)
export const getHeaderRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.header.ref.get-success"
)
export const getHeaderRefsFailCounter = meter.createCounter(
"trpc.contentstack.header.ref.get-fail"
)
export const getHeaderCounter = meter.createCounter(
"trpc.contentstack.header.get"
)
export const getHeaderSuccessCounter = meter.createCounter(
"trpc.contentstack.header.get-success"
)
export const getHeaderFailCounter = meter.createCounter(
"trpc.contentstack.header.get-fail"
)
// OpenTelemetry metrics: CurrentFooter
export const getCurrentFooterRefCounter = meter.createCounter(
"trpc.contentstack.currentFooter.ref.get"
)
export const getCurrentFooterRefSuccessCounter = meter.createCounter(
"trpc.contentstack.currentFooter.ref.get-success"
)
export const getCurrentFooterRefFailCounter = meter.createCounter(
"trpc.contentstack.currentFooter.ref.get-fail"
)
export const getCurrentFooterCounter = meter.createCounter(
"trpc.contentstack.currentFooter.get"
)
export const getCurrentFooterSuccessCounter = meter.createCounter(
"trpc.contentstack.currentFooter.get-success"
)
export const getCurrentFooterFailCounter = meter.createCounter(
"trpc.contentstack.currentFooter.get-fail"
)
// OpenTelemetry metrics: Footer
export const getFooterRefCounter = meter.createCounter(
"trpc.contentstack.footer.ref.get"
)
export const getFooterRefSuccessCounter = meter.createCounter(
"trpc.contentstack.footer.ref.get-success"
)
export const getFooterRefFailCounter = meter.createCounter(
"trpc.contentstack.footer.ref.get-fail"
)
export const getFooterCounter = meter.createCounter(
"trpc.contentstack.footer.get"
)
export const getFooterSuccessCounter = meter.createCounter(
"trpc.contentstack.footer.get-success"
)
export const getFooterFailCounter = meter.createCounter(
"trpc.contentstack.footer.get-fail"
)
// OpenTelemetry metrics: SiteConfig
export const getSiteConfigRefCounter = meter.createCounter(
"trpc.contentstack.SiteConfig.ref.get"
)
export const getSiteConfigRefSuccessCounter = meter.createCounter(
"trpc.contentstack.SiteConfig.ref.get-success"
)
export const getSiteConfigRefFailCounter = meter.createCounter(
"trpc.contentstack.SiteConfig.ref.get-fail"
)
export const getSiteConfigCounter = meter.createCounter(
"trpc.contentstack.SiteConfig.get"
)
export const getSiteConfigSuccessCounter = meter.createCounter(
"trpc.contentstack.SiteConfig.get-success"
)
export const getSiteConfigFailCounter = meter.createCounter(
"trpc.contentstack.SiteConfig.get-fail"
)

View File

@@ -1,11 +1,12 @@
import type {
FooterLinkItem,
FooterRefDataRaw,
} from "@/types/components/footer/footer"
import { System } from "@/types/requests/system"
import { Edges } from "@/types/requests/utils/edges"
import { NodeRefs } from "@/types/requests/utils/refs"
import { getValueFromContactConfig } from "@/utils/contactConfig"
import type { FooterRefDataRaw } from "@/types/components/footer/footer"
import type { System } from "@/types/requests/system"
import type { Edges } from "@/types/requests/utils/edges"
import type { NodeRefs } from "@/types/requests/utils/refs"
import type { HeaderRefs } from "@/types/trpc/routers/contentstack/header"
import type { Alert } from "@/types/trpc/routers/contentstack/siteConfig"
import type { ContactConfig } from "./output"
export function getConnections({ header }: HeaderRefs) {
const connections: System["system"][] = [header.system]
@@ -68,3 +69,21 @@ export function getFooterConnections(refs: FooterRefDataRaw) {
return connections
}
export function getAlertPhoneContactData(
alert: Alert,
contactConfig: ContactConfig
) {
if (alert.phoneContact) {
const { displayText, phoneNumber, footnote } = alert.phoneContact
return {
displayText,
phoneNumber: getValueFromContactConfig(phoneNumber, contactConfig),
footnote: footnote
? getValueFromContactConfig(footnote, contactConfig)
: null,
}
}
return null
}

View File

@@ -39,6 +39,18 @@ import {
joinLoyaltyContactRefsSchema,
joinLoyaltyContactSchema,
} from "../schemas/sidebar/joinLoyaltyContact"
import {
quickLinksRefschema,
quickLinksSchema,
} from "../schemas/sidebar/quickLinks"
import {
scriptedCardRefschema,
scriptedCardsSchema,
} from "../schemas/sidebar/scriptedCard"
import {
teaserCardRefschema,
teaserCardsSchema,
} from "../schemas/sidebar/teaserCard"
import { systemSchema } from "../schemas/system"
import { ContentPageEnum } from "@/types/enums/contentPage"
@@ -122,10 +134,31 @@ export const contentPageJoinLoyaltyContact = z
})
.merge(joinLoyaltyContactSchema)
export const contentPageSidebarScriptedCard = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.ScriptedCard),
})
.merge(scriptedCardsSchema)
export const contentPageSidebarTeaserCard = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.TeaserCard),
})
.merge(teaserCardsSchema)
export const contentPageSidebarQuicklinks = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.QuickLinks),
})
.merge(quickLinksSchema)
export const sidebarSchema = z.discriminatedUnion("__typename", [
contentPageSidebarContent,
contentPageSidebarDynamicContent,
contentPageJoinLoyaltyContact,
contentPageSidebarScriptedCard,
contentPageSidebarTeaserCard,
contentPageSidebarQuicklinks,
])
const navigationLinksSchema = z
@@ -241,9 +274,30 @@ const contentPageSidebarJoinLoyaltyContactRef = z
})
.merge(joinLoyaltyContactRefsSchema)
const contentPageSidebarScriptedCardRef = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.ScriptedCard),
})
.merge(scriptedCardRefschema)
const contentPageSidebarTeaserCardRef = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.TeaserCard),
})
.merge(teaserCardRefschema)
const contentPageSidebarQuickLinksRef = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.QuickLinks),
})
.merge(quickLinksRefschema)
const contentPageSidebarRefsItem = z.discriminatedUnion("__typename", [
contentPageSidebarContentRef,
contentPageSidebarJoinLoyaltyContactRef,
contentPageSidebarScriptedCardRef,
contentPageSidebarTeaserCardRef,
contentPageSidebarQuickLinksRef,
])
const contentPageHeaderRefs = z.object({

View File

@@ -1,7 +1,10 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import { GetContentPageRefs } from "@/lib/graphql/Query/ContentPage/ContentPage.graphql"
import {
GetContentPageBlocksRefs,
GetContentPageRefs,
} from "@/lib/graphql/Query/ContentPage/ContentPage.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
@@ -41,18 +44,30 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) {
query: { lang, uid },
})
)
const refsResponse = await request<GetContentPageRefsSchema>(
GetContentPageRefs,
{ locale: lang, uid },
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
const [mainRefsResponse, blockRefsResponse] = await Promise.all([
request<GetContentPageRefsSchema>(
GetContentPageRefs,
{ locale: lang, uid },
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
),
request<GetContentPageRefsSchema>(
GetContentPageBlocksRefs,
{ locale: lang, uid },
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
),
])
if (!mainRefsResponse.data) {
const notFoundError = notFound(mainRefsResponse)
getContentPageRefsFailCounter.add(1, {
lang,
uid,
@@ -73,8 +88,14 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) {
)
throw notFoundError
}
return refsResponse.data
const responseData = {
...mainRefsResponse.data,
content_page: {
...mainRefsResponse.data.content_page,
blocks: blockRefsResponse.data.content_page.blocks,
},
}
return responseData
}
export function validateContentPageRefs(
@@ -171,17 +192,30 @@ export function getConnections({ content_page }: ContentPageRefs) {
connections.push(...block.content)
}
break
case ContentPageEnum.ContentStack.sidebar.JoinLoyaltyContact:
if (block.join_loyalty_contact?.button) {
connections.push(block.join_loyalty_contact.button)
}
break
case ContentPageEnum.ContentStack.sidebar.ScriptedCard:
if (block.scripted_card?.length) {
connections.push(...block.scripted_card)
}
break
case ContentPageEnum.ContentStack.sidebar.TeaserCard:
if (block.teaser_card?.length) {
connections.push(...block.teaser_card)
}
break
case ContentPageEnum.ContentStack.sidebar.QuickLinks:
if (block.shortcuts.shortcuts.length) {
connections.push(...block.shortcuts.shortcuts)
}
break
default:
break
}
})
}
return connections
}

View File

@@ -31,7 +31,8 @@ export const hotelFaqSchema = z
.object({
questions: accordionItemsSchema,
})
.optional(),
.optional()
.nullable(),
})
.transform((data) => {
const array = []
@@ -51,7 +52,7 @@ export const hotelFaqRefsSchema = z
.optional()
.default(HotelPageEnum.ContentStack.blocks.Faq),
global_faqConnection: globalAccordionConnectionRefs.optional(),
specific_faq: specificAccordionConnectionRefs.optional(),
specific_faq: specificAccordionConnectionRefs.optional().nullable(),
})
.transform((data) => {
const array = []

View File

@@ -4,11 +4,7 @@ import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
import { BlocksEnums } from "@/types/enums/blocks"
export const shortcutsSchema = z.object({
typename: z
.literal(BlocksEnums.block.Shortcuts)
.optional()
.default(BlocksEnums.block.Shortcuts),
export const shortcutsBlockSchema = z.object({
shortcuts: z
.object({
subtitle: z.string().nullable(),
@@ -62,6 +58,15 @@ export const shortcutsSchema = z.object({
}),
})
export const shortcutsSchema = z
.object({
typename: z
.literal(BlocksEnums.block.Shortcuts)
.optional()
.default(BlocksEnums.block.Shortcuts),
})
.merge(shortcutsBlockSchema)
export const shortcutsRefsSchema = z.object({
shortcuts: z.object({
shortcuts: z

View File

@@ -11,6 +11,11 @@ const metaData = z.object({
Value: z.string().nullable(),
})
export const focalPointSchema = z.object({
x: z.number(),
y: z.number(),
})
/**
* Defines a media asset, original or conversion
*/
@@ -94,6 +99,7 @@ export const imageVaultAssetSchema = z.object({
* Name of the user that added the asset to ImageVault
*/
AddedBy: z.string(),
FocalPoint: focalPointSchema.optional(),
})
export const imageVaultAssetTransformedSchema = imageVaultAssetSchema.transform(
@@ -124,6 +130,7 @@ export const imageVaultAssetTransformedSchema = imageVaultAssetSchema.transform(
height: mediaConversion.Height,
aspectRatio,
},
focalPoint: rawData.FocalPoint || { x: 50, y: 50 },
}
}
)

View File

@@ -10,7 +10,6 @@ import {
import { ContentEnum } from "@/types/enums/content"
import { SidebarEnums } from "@/types/enums/sidebar"
import { System } from "@/types/requests/system"
export const contentSchema = z.object({
typename: z

View File

@@ -0,0 +1,16 @@
import { z } from "zod"
import { shortcutsBlockSchema, shortcutsRefsSchema } from "../blocks/shortcuts"
import { SidebarEnums } from "@/types/enums/sidebar"
export const quickLinksSchema = z
.object({
typename: z
.literal(SidebarEnums.blocks.QuickLinks)
.optional()
.default(SidebarEnums.blocks.QuickLinks),
})
.merge(shortcutsBlockSchema)
export const quickLinksRefschema = shortcutsRefsSchema

View File

@@ -0,0 +1,66 @@
import { z } from "zod"
import {
cardBlockRefsSchema,
cardBlockSchema,
transformCardBlock,
transformCardBlockRefs,
} from "../blocks/cardsGrid"
import { SidebarEnums } from "@/types/enums/sidebar"
export const scriptedCardsSchema = z.object({
typename: z
.literal(SidebarEnums.blocks.ScriptedCard)
.optional()
.default(SidebarEnums.blocks.ScriptedCard),
scripted_card: z
.object({
theme: z
.enum([
"one",
"two",
"three",
"primaryDim",
"primaryDark",
"primaryInverted",
"primaryStrong",
])
.nullable(),
scripted_cardConnection: z.object({
edges: z.array(
z.object({
node: cardBlockSchema,
})
),
}),
})
.transform((data) => {
return {
theme: data.theme,
...transformCardBlock(data.scripted_cardConnection.edges[0].node),
}
}),
})
export const scriptedCardRefschema = z.object({
scripted_card: z
.object({
scripted_cardConnection: z.object({
edges: z.array(
z.object({
node: cardBlockRefsSchema,
})
),
}),
})
.transform((data) => {
let card = null
if (data.scripted_cardConnection.edges.length) {
card = transformCardBlockRefs(
data.scripted_cardConnection.edges[0].node
)
}
return card
}),
})

View File

@@ -0,0 +1,54 @@
import { z } from "zod"
import {
teaserCardBlockRefsSchema,
teaserCardBlockSchema,
transformCardBlockRefs,
transformTeaserCardBlock,
} from "../blocks/cardsGrid"
import { SidebarEnums } from "@/types/enums/sidebar"
export const teaserCardsSchema = z.object({
typename: z
.literal(SidebarEnums.blocks.TeaserCard)
.optional()
.default(SidebarEnums.blocks.TeaserCard),
teaser_card: z
.object({
theme: z.enum(["featured", "default"]).nullable().default("default"),
teaser_cardConnection: z.object({
edges: z.array(
z.object({
node: teaserCardBlockSchema,
})
),
}),
})
.transform((data) => {
return {
...transformTeaserCardBlock(data.teaser_cardConnection.edges[0].node),
theme: data.theme,
}
}),
})
export const teaserCardRefschema = z.object({
teaser_card: z
.object({
teaser_cardConnection: z.object({
edges: z.array(
z.object({
node: teaserCardBlockRefsSchema,
})
),
}),
})
.transform((data) => {
let card = null
if (data.teaser_cardConnection.edges.length) {
card = transformCardBlockRefs(data.teaser_cardConnection.edges[0].node)
}
return card
}),
})

View File

@@ -1,11 +1,14 @@
import { z } from "zod"
import { dt } from "@/lib/dt"
import { toLang } from "@/server/utils"
import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image"
import { roomSchema } from "./schemas/room"
import { getPoiGroupByCategoryName } from "./utils"
import { AlertTypeEnum } from "@/types/enums/alert"
import { FacilityEnum } from "@/types/enums/facilities"
import { PointOfInterestCategoryNameEnum } from "@/types/hotel"
const ratingsSchema = z
@@ -142,7 +145,7 @@ const hotelContentSchema = z.object({
})
const detailedFacilitySchema = z.object({
id: z.number(),
id: z.nativeEnum(FacilityEnum),
name: z.string(),
public: z.boolean(),
sortOrder: z.number(),
@@ -159,6 +162,21 @@ export const facilitySchema = z.object({
),
})
export const gallerySchema = z.object({
heroImages: z.array(
z.object({
metaData: imageMetaDataSchema,
imageSizes: imageSizesSchema,
})
),
smallerImages: z.array(
z.object({
metaData: imageMetaDataSchema,
imageSizes: imageSizesSchema,
})
),
})
const healthFacilitySchema = z.object({
type: z.string(),
content: z.object({
@@ -320,6 +338,7 @@ const socialMediaSchema = z.object({
const metaSpecialAlertSchema = z.object({
type: z.string(),
title: z.string().optional(),
description: z.string().optional(),
displayInBookingFlow: z.boolean(),
startDate: z.string(),
@@ -327,7 +346,23 @@ const metaSpecialAlertSchema = z.object({
})
const metaSchema = z.object({
specialAlerts: z.array(metaSpecialAlertSchema),
specialAlerts: z
.array(metaSpecialAlertSchema)
.transform((data) => {
const now = dt().utc().format("YYYY-MM-DD")
const filteredAlerts = data.filter((alert) => {
const shouldShowNow = alert.startDate <= now && alert.endDate >= now
const hasText = alert.description || alert.title
return shouldShowNow && hasText
})
return filteredAlerts.map((alert, idx) => ({
id: `alert-${alert.type}-${idx}`,
type: AlertTypeEnum.Info,
heading: alert.title || null,
text: alert.description || null,
}))
})
.default([]),
})
const relationshipsSchema = z.object({
@@ -402,7 +437,11 @@ export const getHotelDataSchema = z.object({
}),
location: locationSchema,
hotelContent: hotelContentSchema,
detailedFacilities: z.array(detailedFacilitySchema),
detailedFacilities: z
.array(detailedFacilitySchema)
.transform((facilities) =>
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
),
healthFacilities: z.array(healthFacilitySchema),
merchantInformationData: merchantInformationSchema,
rewardNight: rewardNightSchema,
@@ -417,6 +456,7 @@ export const getHotelDataSchema = z.object({
conferencesAndMeetings: facilitySchema.optional(),
healthAndWellness: facilitySchema.optional(),
restaurantImages: facilitySchema.optional(),
gallery: gallerySchema.optional(),
}),
relationships: relationshipsSchema,
}),

View File

@@ -46,7 +46,7 @@ import {
TWENTYFOUR_HOURS,
} from "./utils"
import { FacilityEnum } from "@/types/components/hotelPage/facilities"
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { Facility } from "@/types/hotel"
@@ -219,6 +219,7 @@ export const hotelQueryRouter = router({
const hotelAttributes = validatedHotelData.data.data.attributes
const images = extractHotelImages(hotelAttributes)
const hotelAlerts = hotelAttributes.meta?.specialAlerts || []
const roomCategories = included
? included.filter((item) => item.type === "roomcategories")
@@ -231,15 +232,15 @@ export const hotelQueryRouter = router({
const facilities: Facility[] = [
{
...apiJson.data.attributes.restaurantImages,
id: FacilityEnum.restaurant,
id: FacilityCardTypeEnum.restaurant,
},
{
...apiJson.data.attributes.conferencesAndMeetings,
id: FacilityEnum.conference,
id: FacilityCardTypeEnum.conference,
},
{
...apiJson.data.attributes.healthAndWellness,
id: FacilityEnum.wellness,
id: FacilityCardTypeEnum.wellness,
},
]
@@ -262,6 +263,7 @@ export const hotelQueryRouter = router({
roomCategories,
activitiesCard: activities?.upcoming_activities_card,
facilities,
alerts: hotelAlerts,
faq: contentstackData?.faq,
}
}),
@@ -269,6 +271,8 @@ export const hotelQueryRouter = router({
hotels: serviceProcedure
.input(getHotelsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
const {
cityId,
roomStayStartDate,
@@ -288,6 +292,7 @@ export const hotelQueryRouter = router({
promotionCode,
reservationProfileType,
attachedProfileId,
language: apiLang,
}
hotelsAvailabilityCounter.add(1, {

View File

@@ -196,9 +196,11 @@ export const creditCardSchema = z
attribute: z.object({
cardName: z.string().optional(),
alias: z.string(),
truncatedNumber: z.string(),
truncatedNumber: z.string().transform((s) => s.slice(-4)),
expirationDate: z.string(),
cardType: z.string(),
cardType: z
.string()
.transform((s) => s.charAt(0).toLowerCase() + s.slice(1)),
}),
id: z.string(),
type: z.string(),
@@ -208,6 +210,9 @@ export const creditCardSchema = z
id: apiResponse.id,
type: apiResponse.attribute.cardType,
truncatedNumber: apiResponse.attribute.truncatedNumber,
alias: apiResponse.attribute.alias,
expirationDate: apiResponse.attribute.expirationDate,
cardType: apiResponse.attribute.cardType,
}
})

View File

@@ -206,6 +206,56 @@ function parsedUser(data: User, isMFA: boolean) {
return user
}
async function getCreditCards(session: Session) {
getCreditCardsCounter.add(1)
console.info("api.profile.creditCards start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.creditCards, {
headers: {
Authorization: `Bearer ${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
}
export const userQueryRouter = router({
get: protectedProcedure
.use(async function (opts) {
@@ -675,53 +725,14 @@ export const userQueryRouter = router({
}),
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, {
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 await getCreditCards(ctx.session)
}),
safeCreditCards: safeProtectedProcedure.query(async function ({ ctx }) {
if (!ctx.session) {
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
return await getCreditCards(ctx.session)
}),
membershipCards: protectedProcedure.query(async function ({ ctx }) {