Merge master

This commit is contained in:
Linus Flood
2024-11-28 13:37:45 +01:00
225 changed files with 4488 additions and 3192 deletions

View File

@@ -83,6 +83,10 @@ export const createBookingInput = z.object({
payment: paymentSchema,
})
export const priceChangeInput = z.object({
confirmationNumber: z.string(),
})
// Query
const confirmationNumberInput = z.object({
confirmationNumber: z.string(),

View File

@@ -6,7 +6,7 @@ import { router, safeProtectedServiceProcedure } from "@/server/trpc"
import { getMembership } from "@/utils/user"
import { createBookingInput } from "./input"
import { createBookingInput, priceChangeInput } from "./input"
import { createBookingSchema } from "./output"
import type { Session } from "next-auth"
@@ -20,6 +20,14 @@ const createBookingFailCounter = meter.createCounter(
"trpc.bookings.create-fail"
)
const priceChangeCounter = meter.createCounter("trpc.bookings.price-change")
const priceChangeSuccessCounter = meter.createCounter(
"trpc.bookings.price-change-success"
)
const priceChangeFailCounter = meter.createCounter(
"trpc.bookings.price-change-fail"
)
async function getMembershipNumber(
session: Session | null
): Promise<string | undefined> {
@@ -122,6 +130,71 @@ export const bookingMutationRouter = router({
query: loggingAttributes,
})
)
return verifiedData.data
}),
priceChange: safeProtectedServiceProcedure
.input(priceChangeInput)
.mutation(async function ({ ctx, input }) {
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
const { confirmationNumber } = input
priceChangeCounter.add(1, { confirmationNumber })
const headers = {
Authorization: `Bearer ${accessToken}`,
}
const apiResponse = await api.put(
api.endpoints.v1.Booking.priceChange(confirmationNumber),
{
headers,
body: input,
}
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
priceChangeFailCounter.add(1, {
confirmationNumber,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
}),
})
console.error(
"api.booking.priceChange error",
JSON.stringify({
query: { confirmationNumber },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
error: text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
priceChangeFailCounter.add(1, {
confirmationNumber,
error_type: "validation_error",
})
console.error(
"api.booking.priceChange validation error",
JSON.stringify({
query: { confirmationNumber },
error: verifiedData.error,
})
)
return null
}
priceChangeSuccessCounter.add(1, { confirmationNumber })
return verifiedData.data
}),
})

View File

@@ -21,8 +21,8 @@ export const createBookingSchema = z
errorMessage: z.string().nullable().optional(),
priceChangedMetadata: z
.object({
roomPrice: z.number().nullable().optional(),
totalPrice: z.number().nullable().optional(),
roomPrice: z.number(),
totalPrice: z.number(),
})
.nullable()
.optional(),
@@ -54,19 +54,20 @@ export const createBookingSchema = z
// QUERY
const extraBedTypesSchema = z.object({
quantity: z.number(),
bedType: z.nativeEnum(ChildBedTypeEnum),
quantity: z.number().int(),
})
const guestSchema = z.object({
email: z.string().email().nullable().default(""),
firstName: z.string(),
lastName: z.string(),
firstName: z.string().nullable().default(""),
lastName: z.string().nullable().default(""),
membershipNumber: z.string().nullable().default(""),
phoneNumber: phoneValidator().nullable().default(""),
})
const packageSchema = z.object({
code: z.string().default(""),
code: z.string().nullable().default(""),
currency: z.nativeEnum(CurrencyEnum),
quantity: z.number().int(),
totalPrice: z.number(),
@@ -74,35 +75,37 @@ const packageSchema = z.object({
unitPrice: z.number(),
})
const rateDefinitionSchema = z.object({
breakfastIncluded: z.boolean().default(false),
cancellationRule: z.string().nullable().default(""),
cancellationText: z.string().nullable().default(""),
generalTerms: z.array(z.string()).default([]),
isMemberRate: z.boolean().default(false),
mustBeGuaranteed: z.boolean().default(false),
rateCode: z.string().nullable().default(""),
title: z.string().nullable().default(""),
})
export const bookingConfirmationSchema = z
.object({
data: z.object({
attributes: z.object({
adults: z.number(),
adults: z.number().int(),
checkInDate: z.date({ coerce: true }),
checkOutDate: z.date({ coerce: true }),
createDateTime: z.date({ coerce: true }),
childrenAges: z.array(z.number()),
childrenAges: z.array(z.number().int()).default([]),
extraBedTypes: z.array(extraBedTypesSchema).default([]),
computedReservationStatus: z.string(),
confirmationNumber: z.string(),
computedReservationStatus: z.string().nullable().default(""),
confirmationNumber: z.string().nullable().default(""),
currencyCode: z.nativeEnum(CurrencyEnum),
guest: guestSchema,
hotelId: z.string(),
packages: z.array(packageSchema),
rateDefinition: z.object({
rateCode: z.string(),
title: z.string().nullable(),
breakfastIncluded: z.boolean(),
isMemberRate: z.boolean(),
generalTerms: z.array(z.string()).optional(),
cancellationRule: z.string().optional(),
cancellationText: z.string().optional(),
mustBeGuaranteed: z.boolean(),
}),
reservationStatus: z.string(),
roomPrice: z.number().int(),
roomTypeCode: z.string(),
packages: z.array(packageSchema).default([]),
rateDefinition: rateDefinitionSchema,
reservationStatus: z.string().nullable().default(""),
roomPrice: z.number(),
roomTypeCode: z.string().nullable().default(""),
totalPrice: z.number(),
totalPriceExVat: z.number(),
vatAmount: z.number(),

View File

@@ -422,6 +422,7 @@ export const baseQueryRouter = router({
locale: input.lang,
},
{
cache: "force-cache",
next: {
tags: [generateTag(input.lang, currentFooterUID)],
},

View File

@@ -70,6 +70,7 @@ export const bookingwidgetQueryRouter = router({
locale: lang,
},
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid, bookingwidgetAffix)],
},

View File

@@ -1,7 +1,8 @@
import { z } from "zod"
import { hotelAttributesSchema } from "../../hotels/output"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { getDescription, getImages, getTitle } from "./utils"
import { getDescription, getImage, getTitle } from "./utils"
import type { Metadata } from "next"
@@ -71,16 +72,21 @@ export const rawMetadataSchema = z.object({
.nullable(),
hero_image: tempImageVaultAssetSchema.nullable(),
blocks: metaDataBlocksSchema,
hotel_page_id: z.string().optional().nullable(),
hotelData: hotelAttributesSchema
.pick({ name: true, address: true, hotelContent: true, gallery: true })
.optional()
.nullable(),
})
export const metadataSchema = rawMetadataSchema.transform((data) => {
export const metadataSchema = rawMetadataSchema.transform(async (data) => {
const noIndex = !!data.web?.seo_metadata?.noindex
const metadata: Metadata = {
title: getTitle(data),
title: await getTitle(data),
description: getDescription(data),
openGraph: {
images: getImages(data),
images: getImage(data),
},
}

View File

@@ -4,13 +4,15 @@ import { cache } from "react"
import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql"
import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql"
import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql"
import { GetHotelPageMetadata } from "@/lib/graphql/Query/HotelPage/Metadata.graphql"
import { GetLoyaltyPageMetadata } from "@/lib/graphql/Query/LoyaltyPage/Metadata.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
import { getHotelData } from "../../hotels/query"
import { metadataSchema } from "./output"
import { affix } from "./utils"
@@ -86,10 +88,10 @@ const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
return response.data
})
function getTransformedMetadata(data: unknown) {
async function getTransformedMetadata(data: unknown) {
transformMetadataCounter.add(1)
console.info("contentstack.metadata transform start")
const validatedMetadata = metadataSchema.safeParse(data)
const validatedMetadata = await metadataSchema.safeParseAsync(data)
if (!validatedMetadata.success) {
transformMetadataFailCounter.add(1, {
@@ -112,7 +114,7 @@ function getTransformedMetadata(data: unknown) {
}
export const metadataQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
const variables = {
lang: ctx.lang,
uid: ctx.uid,
@@ -139,6 +141,22 @@ export const metadataQueryRouter = router({
loyalty_page: RawMetadataSchema
}>(GetLoyaltyPageMetadata, variables)
return getTransformedMetadata(loyaltyPageResponse.loyalty_page)
case PageTypeEnum.hotelPage:
const hotelPageResponse = await fetchMetadata<{
hotel_page: RawMetadataSchema
}>(GetHotelPageMetadata, variables)
const hotelPageData = hotelPageResponse.hotel_page
const hotelData = hotelPageData.hotel_page_id
? await getHotelData(
{ hotelId: hotelPageData.hotel_page_id, language: ctx.lang },
ctx.serviceToken
)
: null
return getTransformedMetadata({
...hotelPageData,
hotelData: hotelData?.data.attributes,
})
default:
return null
}

View File

@@ -1,3 +1,5 @@
import { getIntl } from "@/i18n"
import { RTETypeEnum } from "@/types/rte/enums"
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
@@ -58,11 +60,21 @@ function truncateTextAfterLastPeriod(
return `${maxLengthText}...`
}
export function getTitle(data: RawMetadataSchema) {
export async function getTitle(data: RawMetadataSchema) {
const intl = await getIntl()
const metadata = data.web?.seo_metadata
if (metadata?.title) {
return metadata.title
}
if (data.hotelData) {
return intl.formatMessage(
{ id: "Stay at HOTEL_NAME | Hotel in DESTINATION" },
{
hotelName: data.hotelData.name,
destination: data.hotelData.address.city,
}
)
}
if (data.web?.breadcrumbs?.title) {
return data.web.breadcrumbs.title
}
@@ -80,6 +92,9 @@ export function getDescription(data: RawMetadataSchema) {
if (metadata?.description) {
return metadata.description
}
if (data.hotelData) {
return data.hotelData.hotelContent.texts.descriptions.short
}
if (data.preamble) {
return truncateTextAfterLastPeriod(data.preamble)
}
@@ -102,28 +117,35 @@ export function getDescription(data: RawMetadataSchema) {
return ""
}
export function getImages(data: RawMetadataSchema) {
export function getImage(data: RawMetadataSchema) {
const metadataImage = data.web?.seo_metadata?.seo_image
const heroImage = data.hero_image
const hotelImage =
data.hotelData?.gallery?.heroImages?.[0] ||
data.hotelData?.gallery?.smallerImages?.[0]
// Currently we don't have the possibility to get smaller images from ImageVault (2024-11-15)
if (metadataImage) {
return [
{
url: metadataImage.url,
width: metadataImage.dimensions.width,
height: metadataImage.dimensions.height,
},
]
return {
url: metadataImage.url,
alt: metadataImage.meta.alt || undefined,
width: metadataImage.dimensions.width,
height: metadataImage.dimensions.height,
}
}
if (hotelImage) {
return {
url: hotelImage.imageSizes.small,
alt: hotelImage.metaData.altText || undefined,
}
}
if (heroImage) {
return [
{
url: heroImage.url,
width: heroImage.dimensions.width,
height: heroImage.dimensions.height,
},
]
return {
url: heroImage.url,
alt: heroImage.meta.alt || undefined,
width: heroImage.dimensions.width,
height: heroImage.dimensions.height,
}
}
return []
return undefined
}

View File

@@ -131,13 +131,15 @@ export const rewardQueryRouter = router({
.map((reward) => reward?.rewardId)
.filter((id): id is string => Boolean(id))
const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds)
const [contentStackRewards, loyaltyLevelsConfig] = await Promise.all([
getCmsRewards(ctx.lang, rewardIds),
getLoyaltyLevel(ctx, input.level_id),
])
if (!contentStackRewards) {
return null
}
const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id)
const levelsWithRewards = apiRewards
.map((reward) => {
const contentStackReward = contentStackRewards.find((r) => {

View File

@@ -25,7 +25,7 @@ export const tableSchema = z.object({
data: z.array(z.object({}).catchall(z.string())),
skipReset: z.boolean(),
tableActionEnabled: z.boolean(),
headerRowAdded: z.boolean(),
headerRowAdded: z.boolean().optional().default(false),
}),
}),
})

View File

@@ -76,4 +76,7 @@ export const getRoomPackagesInputSchema = z.object({
})
export const getCityCoordinatesInputSchema = z.object({
city: z.string(),
hotel: z.object({
address: z.string(),
}),
})

View File

@@ -1,6 +1,6 @@
import { z } from "zod"
import { ChildBedTypeEnum } from "@/constants/booking"
import { ChildBedTypeEnum, PaymentMethodEnum } from "@/constants/booking"
import { dt } from "@/lib/dt"
import { toLang } from "@/server/utils"
@@ -369,6 +369,7 @@ const merchantInformationSchema = z.object({
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
alternatePaymentOptions: z
.record(z.string(), z.boolean())
@@ -376,6 +377,7 @@ const merchantInformationSchema = z.object({
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
})
@@ -422,6 +424,47 @@ const hotelFactsSchema = z.object({
yearBuilt: z.string(),
})
export const hotelAttributesSchema = z.object({
accessibilityElevatorPitchText: z.string().optional(),
address: addressSchema,
cityId: z.string(),
cityName: z.string(),
conferencesAndMeetings: facilitySchema.optional(),
contactInformation: contactInformationSchema,
detailedFacilities: z
.array(detailedFacilitySchema)
.transform((facilities) =>
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
),
gallery: gallerySchema.optional(),
galleryImages: z.array(imageSchema).optional(),
healthAndWellness: facilitySchema.optional(),
healthFacilities: z.array(healthFacilitySchema),
hotelContent: hotelContentSchema,
hotelFacts: hotelFactsSchema,
hotelRoomElevatorPitchText: z.string().optional(),
hotelType: z.string().optional(),
isActive: z.boolean(),
isPublished: z.boolean(),
keywords: z.array(z.string()),
location: locationSchema,
merchantInformationData: merchantInformationSchema,
name: z.string(),
operaId: z.string(),
parking: z.array(parkingSchema),
pointsOfInterest: z
.array(pointOfInterestSchema)
.transform((pois) =>
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
),
ratings: ratingsSchema,
rewardNight: rewardNightSchema,
restaurantImages: facilitySchema.optional(),
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
specialNeedGroups: z.array(specialNeedGroupSchema),
})
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
export const getHotelDataSchema = z.object({
data: z.object({
@@ -434,46 +477,7 @@ export const getHotelDataSchema = z.object({
}
return lang
}),
attributes: z.object({
accessibilityElevatorPitchText: z.string().optional(),
address: addressSchema,
cityId: z.string(),
cityName: z.string(),
conferencesAndMeetings: facilitySchema.optional(),
contactInformation: contactInformationSchema,
detailedFacilities: z
.array(detailedFacilitySchema)
.transform((facilities) =>
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
),
gallery: gallerySchema.optional(),
galleryImages: z.array(imageSchema).optional(),
healthAndWellness: facilitySchema.optional(),
healthFacilities: z.array(healthFacilitySchema),
hotelContent: hotelContentSchema,
hotelFacts: hotelFactsSchema,
hotelRoomElevatorPitchText: z.string().optional(),
hotelType: z.string().optional(),
isActive: z.boolean(),
isPublished: z.boolean(),
keywords: z.array(z.string()),
location: locationSchema,
merchantInformationData: merchantInformationSchema,
name: z.string(),
operaId: z.string(),
parking: z.array(parkingSchema),
pointsOfInterest: z
.array(pointOfInterestSchema)
.transform((pois) =>
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
),
ratings: ratingsSchema,
rewardNight: rewardNightSchema,
restaurantImages: facilitySchema.optional(),
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
specialNeedGroups: z.array(specialNeedGroupSchema),
}),
attributes: hotelAttributesSchema,
relationships: relationshipsSchema,
}),
// NOTE: We can pass an "include" param to the hotel API to retrieve

View File

@@ -1,18 +1,9 @@
import { metrics } from "@opentelemetry/api"
import { cache } from "react"
import { Lang } from "@/constants/languages"
import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
import { request } from "@/lib/graphql/request"
import { badRequestError } from "@/server/errors/trpc"
import {
badRequestError,
notFound,
serverErrorByStatus,
} from "@/server/errors/trpc"
import {
contentStackUidWithServiceProcedure,
publicProcedure,
router,
safeProtectedServiceProcedure,
@@ -20,13 +11,8 @@ import {
} from "@/server/trpc"
import { toApiLang } from "@/server/utils"
import { hotelPageSchema } from "../contentstack/hotelPage/output"
import {
fetchHotelPageRefs,
generatePageTags,
getHotelPageCounter,
validateHotelPageRefs,
} from "../contentstack/hotelPage/utils"
import { cache } from "@/utils/cache"
import { getVerifiedUser, parsedUser } from "../user/query"
import {
getBreakfastPackageInputSchema,
@@ -55,14 +41,10 @@ import {
TWENTYFOUR_HOURS,
} from "./utils"
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { Facility } from "@/types/hotel"
import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
const meter = metrics.getMeter("trpc.hotels")
const getHotelCounter = meter.createCounter("trpc.hotel.get")
@@ -115,55 +97,6 @@ const breakfastPackagesFailCounter = meter.createCounter(
"trpc.package.breakfast-fail"
)
async function getContentstackData(lang: Lang, uid?: string | null) {
if (!uid) {
return null
}
const contentPageRefsData = await fetchHotelPageRefs(lang, uid)
const contentPageRefs = validateHotelPageRefs(contentPageRefsData, lang, uid)
if (!contentPageRefs) {
return null
}
const tags = generatePageTags(contentPageRefs, lang)
getHotelPageCounter.add(1, { lang, uid })
console.info(
"contentstack.hotelPage start",
JSON.stringify({
query: { lang, uid },
})
)
const response = await request<GetHotelPageData>(
GetHotelPage,
{
locale: lang,
uid,
},
{
cache: "force-cache",
next: {
tags,
},
}
)
if (!response.data) {
throw notFound(response)
}
const hotelPageData = hotelPageSchema.safeParse(response.data)
if (!hotelPageData.success) {
console.error(
`Failed to validate Hotel Page - (uid: ${uid}, lang: ${lang})`
)
console.error(hotelPageData.error)
return null
}
return hotelPageData.data.hotel_page
}
export const getHotelData = cache(
async (input: HotelDataInput, serviceToken: string) => {
const { hotelId, language, isCardOnlyPayment } = input
@@ -277,90 +210,6 @@ export const getHotelData = cache(
)
export const hotelQueryRouter = router({
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
const { lang, uid } = ctx
const contentstackData = await getContentstackData(lang, uid)
const hotelId = contentstackData?.hotel_page_id
if (!hotelId) {
throw notFound(`Hotel not found for uid: ${uid}`)
}
const hotelData = await getHotelData(
{
hotelId,
language: ctx.lang,
},
ctx.serviceToken
)
if (!hotelData) {
throw notFound()
}
const included = hotelData.included || []
const hotelAttributes = hotelData.data.attributes
const images = hotelAttributes.gallery?.smallerImages
const hotelAlerts = hotelAttributes.specialAlerts
const roomCategories = included
? included.filter((item) => item.type === "roomcategories")
: []
const activities = contentstackData?.content
? contentstackData?.content[0]
: null
const facilities: Facility[] = [
{
...hotelData.data.attributes.restaurantImages,
id: FacilityCardTypeEnum.restaurant,
headingText:
hotelData?.data.attributes.restaurantImages?.headingText ?? "",
heroImages:
hotelData?.data.attributes.restaurantImages?.heroImages ?? [],
},
{
...hotelData.data.attributes.conferencesAndMeetings,
id: FacilityCardTypeEnum.conference,
headingText:
hotelData?.data.attributes.conferencesAndMeetings?.headingText ?? "",
heroImages:
hotelData?.data.attributes.conferencesAndMeetings?.heroImages ?? [],
},
{
...hotelData.data.attributes.healthAndWellness,
id: FacilityCardTypeEnum.wellness,
headingText:
hotelData?.data.attributes.healthAndWellness?.headingText ?? "",
heroImages:
hotelData?.data.attributes.healthAndWellness?.heroImages ?? [],
},
]
return {
hotelId,
hotelName: hotelAttributes.name,
hotelDescriptions: hotelAttributes.hotelContent.texts,
hotelLocation: hotelAttributes.location,
hotelAddress: hotelAttributes.address,
hotelRatings: hotelAttributes.ratings,
hotelDetailedFacilities: hotelAttributes.detailedFacilities,
hotelImages: images,
pointsOfInterest: hotelAttributes.pointsOfInterest,
roomCategories,
activitiesCard: activities?.upcoming_activities_card,
facilities,
alerts: hotelAlerts,
faq: contentstackData?.faq,
healthFacilities: hotelAttributes.healthFacilities,
contact: hotelAttributes.contactInformation,
socials: hotelAttributes.socialMedia,
ecoLabels: hotelAttributes.hotelFacts.ecoLabels,
}
}),
availability: router({
hotels: serviceProcedure
.input(getHotelsAvailabilityInputSchema)
@@ -1087,14 +936,37 @@ export const hotelQueryRouter = router({
.input(getCityCoordinatesInputSchema)
.query(async function ({ input }) {
const apiKey = process.env.GOOGLE_STATIC_MAP_KEY
const { city } = input
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(city)}&key=${apiKey}`
const { city, hotel } = input
const response = await fetch(url)
const data = await response.json()
const { lat, lng } = data.results[0].geometry.location
async function fetchCoordinates(address: string) {
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
const response = await fetch(url)
const data = await response.json()
return { lat, lng }
if (data.status !== "OK") {
console.error(`Geocode error: ${data.status}`)
return null
}
const location = data.results[0]?.geometry?.location
if (!location) {
console.error("No location found in geocode response")
return null
}
return location
}
let location = await fetchCoordinates(city)
if (!location) {
location = await fetchCoordinates(`${city}, ${hotel.address}`)
}
if (!location) {
throw new Error("Unable to fetch coordinates")
}
return location
}),
}),
})

View File

@@ -55,3 +55,11 @@ export const signupInput = signUpSchema
streetAddress: "",
},
}))
export const getSavedPaymentCardsInput = z.object({
supportedCards: z.array(z.string()),
})
export type GetSavedPaymentCardsInput = z.input<
typeof getSavedPaymentCardsInput
>

View File

@@ -1,8 +1,6 @@
import { z } from "zod"
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
import { passwordValidator } from "@/utils/passwordValidator"
import { phoneValidator } from "@/utils/phoneValidator"
import { getMembership } from "@/utils/user"
export const membershipSchema = z.object({

View File

@@ -13,7 +13,11 @@ import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
import * as maskValue from "@/utils/maskValue"
import { getMembership, getMembershipCards } from "@/utils/user"
import { friendTransactionsInput, staysInput } from "./input"
import {
friendTransactionsInput,
getSavedPaymentCardsInput,
staysInput,
} from "./input"
import {
creditCardsSchema,
getFriendTransactionsSchema,
@@ -154,6 +158,7 @@ export const getVerifiedUser = cache(
"api.user.profile validation error",
JSON.stringify({
errors: verifiedData.error,
apiResponse: apiJson,
})
)
return null
@@ -751,13 +756,26 @@ export const userQueryRouter = router({
creditCards: protectedProcedure.query(async function ({ ctx }) {
return await getCreditCards({ session: ctx.session })
}),
safeCreditCards: safeProtectedProcedure.query(async function ({ ctx }) {
if (!ctx.session) {
return null
}
safePaymentCards: safeProtectedProcedure
.input(getSavedPaymentCardsInput)
.query(async function ({ ctx, input }) {
if (!ctx.session) {
return null
}
return await getCreditCards({ session: ctx.session, onlyNonExpired: true })
}),
const savedCards = await getCreditCards({
session: ctx.session,
onlyNonExpired: true,
})
if (!savedCards) {
return null
}
return savedCards.filter((card) =>
input.supportedCards.includes(card.type)
)
}),
membershipCards: protectedProcedure.query(async function ({ ctx }) {
getProfileCounter.add(1)

View File

@@ -71,11 +71,12 @@ async function fetchServiceToken(scopes: string[]) {
export async function getServiceToken() {
let scopes: string[] = []
if (env.HIDE_FOR_NEXT_RELEASE) {
scopes = ["profile"]
} else {
if (env.ENABLE_BOOKING_FLOW) {
scopes = ["profile", "hotel", "booking", "package"]
} else {
scopes = ["profile"]
}
const tag = generateServiceTokenTag(scopes)
const getCachedJwt = unstable_cache(
async (scopes) => {