Merge branch 'develop' into feat/SW-266-seo-loyalty-pages
This commit is contained in:
@@ -44,6 +44,7 @@ export function createContext() {
|
||||
|
||||
const cookie = cookies()
|
||||
const webviewTokenCookie = cookie.get("webviewToken")
|
||||
const loginType = h.get("loginType")
|
||||
|
||||
return createContextInner({
|
||||
auth: async () => {
|
||||
@@ -53,7 +54,12 @@ export function createContext() {
|
||||
return null
|
||||
}
|
||||
|
||||
return session || ({ token: { access_token: webToken } } as Session)
|
||||
return (
|
||||
session ||
|
||||
({
|
||||
token: { access_token: webToken, loginType },
|
||||
} as Session)
|
||||
)
|
||||
},
|
||||
lang: h.get("x-lang") as Lang,
|
||||
pathname: h.get("x-pathname")!,
|
||||
|
||||
@@ -1,94 +1,98 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { fromUppercaseToLangEnum } from "@/utils/languages"
|
||||
import { toLang } from "@/server/utils"
|
||||
|
||||
const RatingsSchema = z.object({
|
||||
tripAdvisor: z.object({
|
||||
numberOfReviews: z.number(),
|
||||
rating: z.number(),
|
||||
ratingImageUrl: z.string(),
|
||||
webUrl: z.string(),
|
||||
awards: z.array(
|
||||
z.object({
|
||||
displayName: z.string(),
|
||||
images: z.object({
|
||||
small: z.string(),
|
||||
medium: z.string(),
|
||||
large: z.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
reviews: z.object({
|
||||
widgetHtmlTagId: z.string(),
|
||||
widgetScriptEmbedUrlIframe: z.string(),
|
||||
widgetScriptEmbedUrlJavaScript: z.string(),
|
||||
const ratingsSchema = z
|
||||
.object({
|
||||
tripAdvisor: z.object({
|
||||
numberOfReviews: z.number(),
|
||||
rating: z.number(),
|
||||
ratingImageUrl: z.string(),
|
||||
webUrl: z.string(),
|
||||
awards: z.array(
|
||||
z.object({
|
||||
displayName: z.string(),
|
||||
images: z.object({
|
||||
small: z.string(),
|
||||
medium: z.string(),
|
||||
large: z.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
reviews: z
|
||||
.object({
|
||||
widgetHtmlTagId: z.string(),
|
||||
widgetScriptEmbedUrlIframe: z.string(),
|
||||
widgetScriptEmbedUrlJavaScript: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
.optional()
|
||||
|
||||
const AddressSchema = z.object({
|
||||
const addressSchema = z.object({
|
||||
streetAddress: z.string(),
|
||||
city: z.string(),
|
||||
zipCode: z.string(),
|
||||
country: z.string(),
|
||||
})
|
||||
|
||||
const ContactInformationSchema = z.object({
|
||||
const contactInformationSchema = z.object({
|
||||
phoneNumber: z.string(),
|
||||
faxNumber: z.string(),
|
||||
faxNumber: z.string().optional(),
|
||||
email: z.string(),
|
||||
websiteUrl: z.string(),
|
||||
})
|
||||
|
||||
const CheckinSchema = z.object({
|
||||
const checkinSchema = z.object({
|
||||
checkInTime: z.string(),
|
||||
checkOutTime: z.string(),
|
||||
onlineCheckOutAvailableFrom: z.string().nullable().optional(),
|
||||
onlineCheckout: z.boolean(),
|
||||
})
|
||||
|
||||
const EcoLabelsSchema = z.object({
|
||||
const ecoLabelsSchema = z.object({
|
||||
euEcoLabel: z.boolean(),
|
||||
greenGlobeLabel: z.boolean(),
|
||||
nordicEcoLabel: z.boolean(),
|
||||
svanenEcoLabelCertificateNumber: z.string().optional(),
|
||||
})
|
||||
|
||||
const HotelFacilityDetailSchema = z.object({
|
||||
const hotelFacilityDetailSchema = z.object({
|
||||
heading: z.string(),
|
||||
description: z.string(),
|
||||
})
|
||||
|
||||
const HotelFacilitySchema = z.object({
|
||||
breakfast: HotelFacilityDetailSchema,
|
||||
checkout: HotelFacilityDetailSchema,
|
||||
gym: HotelFacilityDetailSchema,
|
||||
internet: HotelFacilityDetailSchema,
|
||||
laundry: HotelFacilityDetailSchema,
|
||||
luggage: HotelFacilityDetailSchema,
|
||||
shop: HotelFacilityDetailSchema,
|
||||
telephone: HotelFacilityDetailSchema,
|
||||
const hotelFacilitySchema = z.object({
|
||||
breakfast: hotelFacilityDetailSchema,
|
||||
checkout: hotelFacilityDetailSchema,
|
||||
gym: hotelFacilityDetailSchema,
|
||||
internet: hotelFacilityDetailSchema,
|
||||
laundry: hotelFacilityDetailSchema,
|
||||
luggage: hotelFacilityDetailSchema,
|
||||
shop: hotelFacilityDetailSchema,
|
||||
telephone: hotelFacilityDetailSchema,
|
||||
})
|
||||
|
||||
const HotelInformationDetailSchema = z.object({
|
||||
const hotelInformationDetailSchema = z.object({
|
||||
heading: z.string(),
|
||||
description: z.string(),
|
||||
link: z.string().optional(),
|
||||
})
|
||||
|
||||
const HotelInformationSchema = z.object({
|
||||
accessibility: HotelInformationDetailSchema,
|
||||
safety: HotelInformationDetailSchema,
|
||||
sustainability: HotelInformationDetailSchema,
|
||||
const hotelInformationSchema = z.object({
|
||||
accessibility: hotelInformationDetailSchema,
|
||||
safety: hotelInformationDetailSchema,
|
||||
sustainability: hotelInformationDetailSchema,
|
||||
})
|
||||
|
||||
const InteriorSchema = z.object({
|
||||
const interiorSchema = z.object({
|
||||
numberOfBeds: z.number(),
|
||||
numberOfCribs: z.number(),
|
||||
numberOfFloors: z.number(),
|
||||
numberOfRooms: z.object({
|
||||
connected: z.number(),
|
||||
forEllergics: z.number(),
|
||||
forAllergics: z.number().optional(),
|
||||
forDisabled: z.number(),
|
||||
nonSmoking: z.number(),
|
||||
pet: z.number(),
|
||||
@@ -97,37 +101,37 @@ const InteriorSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
const ReceptionHoursSchema = z.object({
|
||||
const receptionHoursSchema = z.object({
|
||||
alwaysOpen: z.boolean(),
|
||||
isClosed: z.boolean(),
|
||||
openingTime: z.string().optional(),
|
||||
closingTime: z.string().optional(),
|
||||
})
|
||||
|
||||
const LocationSchema = z.object({
|
||||
const locationSchema = z.object({
|
||||
distanceToCentre: z.number(),
|
||||
latitude: z.number(),
|
||||
longitude: z.number(),
|
||||
})
|
||||
|
||||
const ImageMetaDataSchema = z.object({
|
||||
const imageMetaDataSchema = z.object({
|
||||
title: z.string(),
|
||||
altText: z.string(),
|
||||
altText_En: z.string(),
|
||||
copyRight: z.string(),
|
||||
})
|
||||
|
||||
const ImageSizesSchema = z.object({
|
||||
const imageSizesSchema = z.object({
|
||||
tiny: z.string(),
|
||||
small: z.string(),
|
||||
medium: z.string(),
|
||||
large: z.string(),
|
||||
})
|
||||
|
||||
const HotelContentSchema = z.object({
|
||||
const hotelContentSchema = z.object({
|
||||
images: z.object({
|
||||
metaData: ImageMetaDataSchema,
|
||||
imageSizes: ImageSizesSchema,
|
||||
metaData: imageMetaDataSchema,
|
||||
imageSizes: imageSizesSchema,
|
||||
}),
|
||||
texts: z.object({
|
||||
facilityInformation: z.string(),
|
||||
@@ -145,24 +149,24 @@ const HotelContentSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
const DetailedFacilitySchema = z.object({
|
||||
const detailedFacilitySchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
code: z.string().optional(),
|
||||
applyToAllHotels: z.boolean(),
|
||||
public: z.boolean(),
|
||||
icon: z.number(),
|
||||
icon: z.string(),
|
||||
iconName: z.string().optional(),
|
||||
sortOrder: z.number(),
|
||||
})
|
||||
|
||||
const HealthFacilitySchema = z.object({
|
||||
const healthFacilitySchema = z.object({
|
||||
type: z.string(),
|
||||
content: z.object({
|
||||
images: z.array(
|
||||
z.object({
|
||||
metaData: ImageMetaDataSchema,
|
||||
imageSizes: ImageSizesSchema,
|
||||
metaData: imageMetaDataSchema,
|
||||
imageSizes: imageSizesSchema,
|
||||
})
|
||||
),
|
||||
texts: z.object({
|
||||
@@ -181,15 +185,15 @@ const HealthFacilitySchema = z.object({
|
||||
ordinary: z.object({
|
||||
alwaysOpen: z.boolean(),
|
||||
isClosed: z.boolean(),
|
||||
openingTime: z.string(),
|
||||
closingTime: z.string(),
|
||||
openingTime: z.string().optional(),
|
||||
closingTime: z.string().optional(),
|
||||
sortOrder: z.number().optional(),
|
||||
}),
|
||||
weekends: z.object({
|
||||
alwaysOpen: z.boolean(),
|
||||
isClosed: z.boolean(),
|
||||
openingTime: z.string(),
|
||||
closingTime: z.string(),
|
||||
openingTime: z.string().optional(),
|
||||
closingTime: z.string().optional(),
|
||||
sortOrder: z.number().optional(),
|
||||
}),
|
||||
}),
|
||||
@@ -203,7 +207,7 @@ const HealthFacilitySchema = z.object({
|
||||
),
|
||||
})
|
||||
|
||||
const RewardNightSchema = z.object({
|
||||
const rewardNightSchema = z.object({
|
||||
points: z.number(),
|
||||
campaign: z.object({
|
||||
start: z.string(),
|
||||
@@ -212,30 +216,30 @@ const RewardNightSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
const PointsOfInterestSchema = z.object({
|
||||
const pointsOfInterestSchema = z.object({
|
||||
name: z.string(),
|
||||
distance: z.number(),
|
||||
category: z.object({
|
||||
name: z.string(),
|
||||
group: z.string(),
|
||||
}),
|
||||
location: LocationSchema,
|
||||
location: locationSchema,
|
||||
isHighlighted: z.boolean(),
|
||||
})
|
||||
|
||||
const ParkingPricingSchema = z.object({
|
||||
const parkingPricingSchema = z.object({
|
||||
freeParking: z.boolean(),
|
||||
paymentType: z.string(),
|
||||
localCurrency: z.object({
|
||||
currency: z.string(),
|
||||
range: z.object({
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
max: z.number().optional(),
|
||||
}),
|
||||
ordinary: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number(),
|
||||
amount: z.number().optional(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
@@ -243,38 +247,40 @@ const ParkingPricingSchema = z.object({
|
||||
weekend: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
requestedCurrency: z.object({
|
||||
currency: z.string(),
|
||||
range: z.object({
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
}),
|
||||
ordinary: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
),
|
||||
weekend: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number(),
|
||||
amount: z.number().optional(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
requestedCurrency: z
|
||||
.object({
|
||||
currency: z.string(),
|
||||
range: z.object({
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
}),
|
||||
ordinary: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
),
|
||||
weekend: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const ParkingSchema = z.object({
|
||||
const parkingSchema = z.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
address: z.string(),
|
||||
@@ -282,37 +288,37 @@ const ParkingSchema = z.object({
|
||||
numberOfChargingSpaces: z.number(),
|
||||
distanceToHotel: z.number(),
|
||||
canMakeReservation: z.boolean(),
|
||||
pricing: ParkingPricingSchema,
|
||||
pricing: parkingPricingSchema,
|
||||
})
|
||||
|
||||
const SpecialNeedSchema = z.object({
|
||||
const specialNeedSchema = z.object({
|
||||
name: z.string(),
|
||||
details: z.string(),
|
||||
})
|
||||
|
||||
const SpecialNeedGroupSchema = z.object({
|
||||
const specialNeedGroupSchema = z.object({
|
||||
name: z.string(),
|
||||
specialNeeds: z.array(SpecialNeedSchema),
|
||||
specialNeeds: z.array(specialNeedSchema),
|
||||
})
|
||||
|
||||
const SocialMediaSchema = z.object({
|
||||
const socialMediaSchema = z.object({
|
||||
instagram: z.string().optional(),
|
||||
facebook: z.string().optional(),
|
||||
})
|
||||
|
||||
const MetaSpecialAlertSchema = z.object({
|
||||
const metaSpecialAlertSchema = z.object({
|
||||
type: z.string(),
|
||||
description: z.string(),
|
||||
description: z.string().optional(),
|
||||
displayInBookingFlow: z.boolean(),
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
})
|
||||
|
||||
const MetaSchema = z.object({
|
||||
specialAlerts: z.array(MetaSpecialAlertSchema),
|
||||
const metaSchema = z.object({
|
||||
specialAlerts: z.array(metaSpecialAlertSchema),
|
||||
})
|
||||
|
||||
const RelationshipsSchema = z.object({
|
||||
const relationshipsSchema = z.object({
|
||||
restaurants: z.object({
|
||||
links: z.object({
|
||||
related: z.string(),
|
||||
@@ -335,11 +341,11 @@ const RelationshipsSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
const RoomContentSchema = z.object({
|
||||
const roomContentSchema = z.object({
|
||||
images: z.array(
|
||||
z.object({
|
||||
metaData: ImageMetaDataSchema,
|
||||
imageSizes: ImageSizesSchema,
|
||||
metaData: imageMetaDataSchema,
|
||||
imageSizes: imageSizesSchema,
|
||||
})
|
||||
),
|
||||
texts: z.object({
|
||||
@@ -350,7 +356,7 @@ const RoomContentSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
const RoomTypesSchema = z.object({
|
||||
const roomTypesSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
code: z.string(),
|
||||
@@ -384,20 +390,20 @@ const RoomTypesSchema = z.object({
|
||||
isLackingExtraBeds: z.boolean(),
|
||||
})
|
||||
|
||||
const RoomFacilitiesSchema = z.object({
|
||||
const roomFacilitiesSchema = z.object({
|
||||
availableInAllRooms: z.boolean(),
|
||||
name: z.string(),
|
||||
isUniqueSellingPoint: z.boolean(),
|
||||
sortOrder: z.number(),
|
||||
})
|
||||
|
||||
export const RoomSchema = z.object({
|
||||
export const roomSchema = z.object({
|
||||
attributes: z.object({
|
||||
name: z.string(),
|
||||
sortOrder: z.number(),
|
||||
content: RoomContentSchema,
|
||||
roomTypes: z.array(RoomTypesSchema),
|
||||
roomFacilities: z.array(RoomFacilitiesSchema),
|
||||
content: roomContentSchema,
|
||||
roomTypes: z.array(roomTypesSchema),
|
||||
roomFacilities: z.array(roomFacilitiesSchema),
|
||||
occupancy: z.object({
|
||||
total: z.number(),
|
||||
adults: z.number(),
|
||||
@@ -417,18 +423,13 @@ export const getHotelDataSchema = z.object({
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
type: z.string(), // No enum here but the standard return appears to be "hotels".
|
||||
language: z
|
||||
.string()
|
||||
.refine((val) => fromUppercaseToLangEnum(val) !== undefined, {
|
||||
message: "Invalid language",
|
||||
})
|
||||
.transform((val) => {
|
||||
const lang = fromUppercaseToLangEnum(val)
|
||||
if (!lang) {
|
||||
throw new Error("Invalid language")
|
||||
}
|
||||
return lang
|
||||
}),
|
||||
language: z.string().transform((val) => {
|
||||
const lang = toLang(val)
|
||||
if (!lang) {
|
||||
throw new Error("Invalid language")
|
||||
}
|
||||
return lang
|
||||
}),
|
||||
attributes: z.object({
|
||||
name: z.string(),
|
||||
operaId: z.string(),
|
||||
@@ -436,36 +437,35 @@ export const getHotelDataSchema = z.object({
|
||||
isPublished: z.boolean(),
|
||||
cityId: z.string(),
|
||||
cityName: z.string(),
|
||||
ratings: RatingsSchema,
|
||||
address: AddressSchema,
|
||||
contactInformation: ContactInformationSchema,
|
||||
ratings: ratingsSchema,
|
||||
address: addressSchema,
|
||||
contactInformation: contactInformationSchema,
|
||||
hotelFacts: z.object({
|
||||
checkin: CheckinSchema,
|
||||
ecoLabels: EcoLabelsSchema,
|
||||
hotelFacilityDetail: HotelFacilitySchema,
|
||||
hotelInformation: HotelInformationSchema,
|
||||
interior: InteriorSchema,
|
||||
receptionHours: ReceptionHoursSchema,
|
||||
checkin: checkinSchema,
|
||||
ecoLabels: ecoLabelsSchema,
|
||||
hotelFacilityDetail: hotelFacilitySchema,
|
||||
hotelInformation: hotelInformationSchema,
|
||||
interior: interiorSchema,
|
||||
receptionHours: receptionHoursSchema,
|
||||
yearBuilt: z.string(),
|
||||
}),
|
||||
location: LocationSchema,
|
||||
hotelContent: HotelContentSchema,
|
||||
detailedFacilities: z.array(DetailedFacilitySchema),
|
||||
healthFacilities: z.array(HealthFacilitySchema),
|
||||
rewardNight: RewardNightSchema,
|
||||
pointsOfInterest: z.array(PointsOfInterestSchema),
|
||||
parking: z.array(ParkingSchema),
|
||||
specialNeedGroups: z.array(SpecialNeedGroupSchema),
|
||||
socialMedia: SocialMediaSchema,
|
||||
meta: MetaSchema,
|
||||
location: locationSchema,
|
||||
hotelContent: hotelContentSchema,
|
||||
detailedFacilities: z.array(detailedFacilitySchema),
|
||||
healthFacilities: z.array(healthFacilitySchema),
|
||||
rewardNight: rewardNightSchema,
|
||||
pointsOfInterest: z.array(pointsOfInterestSchema),
|
||||
parking: z.array(parkingSchema),
|
||||
specialNeedGroups: z.array(specialNeedGroupSchema),
|
||||
socialMedia: socialMediaSchema,
|
||||
meta: metaSchema.optional(),
|
||||
isActive: z.boolean(),
|
||||
}),
|
||||
relationships: RelationshipsSchema,
|
||||
relationships: relationshipsSchema,
|
||||
}),
|
||||
// NOTE: We can pass an "include" param to the hotel API to retrieve
|
||||
// additional data for an individual hotel.
|
||||
// Example "included" data can be found in our tempHotelData file.
|
||||
included: z.array(RoomSchema).optional(),
|
||||
included: z.array(roomSchema).optional(),
|
||||
})
|
||||
|
||||
const rate = z.object({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as api from "@/lib/api"
|
||||
import { badRequestError } from "@/server/errors/trpc"
|
||||
import { publicProcedure, router } from "@/server/trpc"
|
||||
import { publicProcedure, router, serviceProcedure } from "@/server/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import {
|
||||
getFiltersInputSchema,
|
||||
@@ -11,60 +12,58 @@ import {
|
||||
getFiltersSchema,
|
||||
getHotelDataSchema,
|
||||
getRatesSchema,
|
||||
RoomSchema,
|
||||
roomSchema,
|
||||
} from "./output"
|
||||
import tempFilterData from "./tempFilterData.json"
|
||||
import tempHotelData from "./tempHotelData.json"
|
||||
import tempRatesData from "./tempRatesData.json"
|
||||
import { toApiLang } from "./utils"
|
||||
|
||||
export const hotelQueryRouter = router({
|
||||
getHotel: publicProcedure
|
||||
getHotel: serviceProcedure
|
||||
.input(getHotelInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { hotelId, language, include } = input
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
const apiLang = toApiLang(language)
|
||||
params.set("hotelId", hotelId.toString())
|
||||
params.set("language", apiLang)
|
||||
|
||||
if (include) {
|
||||
params.set("include", include.join(","))
|
||||
}
|
||||
|
||||
// TODO: Enable once we have authorized API access.
|
||||
// const apiResponse = await api.get(
|
||||
// api.endpoints.v1.hotel,
|
||||
// {}, // Include token.
|
||||
// params
|
||||
// )
|
||||
//
|
||||
// if (!apiResponse.ok) {
|
||||
// console.info(`API Response Failed - Getting Hotel`)
|
||||
// console.error(apiResponse)
|
||||
// return null
|
||||
// }
|
||||
// const apiJson = await apiResponse.json()
|
||||
|
||||
// NOTE: We can pass an "include" param to the hotel API to retrieve
|
||||
// additional data for an individual hotel.
|
||||
// Example "included" data can be found in our tempHotelData file.
|
||||
const { included, ...apiJsonWithoutIncluded } = tempHotelData
|
||||
const validatedHotelData = getHotelDataSchema.safeParse(
|
||||
apiJsonWithoutIncluded
|
||||
const apiResponse = await api.get(
|
||||
`${api.endpoints.v1.hotels}/${hotelId}`,
|
||||
{
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
console.info(`API Response Failed - Getting Hotel`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedHotelData = getHotelDataSchema.safeParse(apiJson)
|
||||
|
||||
if (!validatedHotelData.success) {
|
||||
console.error(`Get Individual Hotel Data - Verified Data Error`)
|
||||
console.error(validatedHotelData.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
const included = validatedHotelData.data.included || []
|
||||
|
||||
const roomCategories = included
|
||||
? included
|
||||
.filter((item) => item.type === "roomcategories")
|
||||
.map((roomCategory) => {
|
||||
const validatedRoom = RoomSchema.safeParse(roomCategory)
|
||||
const validatedRoom = roomSchema.safeParse(roomCategory)
|
||||
if (!validatedRoom.success) {
|
||||
console.error(`Get Room Category Data - Verified Data Error`)
|
||||
console.error(validatedRoom.error)
|
||||
@@ -75,7 +74,7 @@ export const hotelQueryRouter = router({
|
||||
: []
|
||||
|
||||
return {
|
||||
attributes: validatedHotelData.data.data.attributes,
|
||||
hotel: validatedHotelData.data.data.attributes,
|
||||
roomCategories: roomCategories,
|
||||
}
|
||||
}),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
const langMap: { [key in Lang]: string } = {
|
||||
[Lang.en]: "En",
|
||||
[Lang.sv]: "Sv",
|
||||
[Lang.no]: "No",
|
||||
[Lang.fi]: "Fi",
|
||||
[Lang.da]: "Da",
|
||||
[Lang.de]: "De",
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert Lang enum to uppercase
|
||||
* Needed for the Hotel endpoint.
|
||||
*/
|
||||
export const toApiLang = (lang: Lang): string => {
|
||||
const result = langMap[lang]
|
||||
if (!result) {
|
||||
throw new Error("Invalid language")
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -18,3 +18,20 @@ export const soonestUpcomingStaysInput = z
|
||||
limit: z.number().int().positive(),
|
||||
})
|
||||
.default({ limit: 3 })
|
||||
|
||||
export const initiateSaveCardInput = z.object({
|
||||
language: z.string(),
|
||||
mobileToken: z.boolean(),
|
||||
redirectUrl: z.string(),
|
||||
})
|
||||
|
||||
export const saveCardInput = z.object({
|
||||
transactionId: z.string(),
|
||||
merchantId: z.string().optional(),
|
||||
})
|
||||
export const friendTransactionsInput = z
|
||||
.object({
|
||||
limit: z.number().int().positive(),
|
||||
page: z.number().int().positive(),
|
||||
})
|
||||
.default({ limit: 5, page: 1 })
|
||||
|
||||
@@ -180,6 +180,8 @@ export const getCreditCardsSchema = z.object({
|
||||
expirationDate: z.string(),
|
||||
cardType: z.string(),
|
||||
}),
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
@@ -193,3 +195,14 @@ export const getMembershipCardsSchema = z.array(
|
||||
membershipType: z.string(),
|
||||
})
|
||||
)
|
||||
|
||||
export const initiateSaveCardSchema = z.object({
|
||||
data: z.object({
|
||||
attribute: z.object({
|
||||
transactionId: z.string(),
|
||||
link: z.string(),
|
||||
mobileToken: z.string().optional(),
|
||||
}),
|
||||
type: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { internalServerError } from "@/server/errors/next"
|
||||
import {
|
||||
badRequestError,
|
||||
forbiddenError,
|
||||
unauthorizedError,
|
||||
} from "@/server/errors/trpc"
|
||||
import {
|
||||
protectedProcedure,
|
||||
router,
|
||||
@@ -12,13 +18,20 @@ import * as maskValue from "@/utils/maskValue"
|
||||
import { getMembership, getMembershipCards } from "@/utils/user"
|
||||
|
||||
import encryptValue from "../utils/encryptValue"
|
||||
import { getUserInputSchema, staysInput } from "./input"
|
||||
import {
|
||||
friendTransactionsInput,
|
||||
getUserInputSchema,
|
||||
initiateSaveCardInput,
|
||||
saveCardInput,
|
||||
staysInput,
|
||||
} from "./input"
|
||||
import {
|
||||
getCreditCardsSchema,
|
||||
getFriendTransactionsSchema,
|
||||
getMembershipCardsSchema,
|
||||
getStaysSchema,
|
||||
getUserSchema,
|
||||
initiateSaveCardSchema,
|
||||
Stay,
|
||||
} from "./output"
|
||||
import { benefits, extendedUser, nextLevelPerks } from "./temp"
|
||||
@@ -441,53 +454,68 @@ export const userQueryRouter = router({
|
||||
}),
|
||||
}),
|
||||
transaction: router({
|
||||
friendTransactions: protectedProcedure.query(async (opts) => {
|
||||
const apiResponse = await api.get(api.endpoints.v1.friendTransactions, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${opts.ctx.session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
friendTransactions: protectedProcedure
|
||||
.input(friendTransactionsInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { limit, page } = input
|
||||
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()
|
||||
// }
|
||||
console.error(`API Response Failed - Getting Friend Transactions`)
|
||||
console.error(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
if (!apiResponse.ok) {
|
||||
// switch (apiResponse.status) {
|
||||
// case 400:
|
||||
// throw badRequestError()
|
||||
// case 401:
|
||||
// throw unauthorizedError()
|
||||
// case 403:
|
||||
// throw forbiddenError()
|
||||
// default:
|
||||
// throw internalServerError()
|
||||
// }
|
||||
console.error(`API Response Failed - Getting Friend Transactions`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(`Failed to validate Friend Transactions Data`)
|
||||
console.error(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(`Failed to validate Friend Transactions Data`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
data: verifiedData.data.data.map(({ attributes }) => {
|
||||
return {
|
||||
awardPoints: attributes.awardPoints,
|
||||
checkinDate: attributes.checkinDate,
|
||||
checkoutDate: attributes.checkoutDate,
|
||||
city: attributes.hotelInformation?.city,
|
||||
confirmationNumber: attributes.confirmationNumber,
|
||||
hotelName: attributes.hotelInformation?.name,
|
||||
nights: attributes.nights,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
const pageData = verifiedData.data.data.slice(
|
||||
limit * (page - 1),
|
||||
limit * page
|
||||
)
|
||||
|
||||
return {
|
||||
data: {
|
||||
transactions: pageData.map(({ attributes }) => {
|
||||
return {
|
||||
awardPoints: attributes.awardPoints,
|
||||
checkinDate: attributes.checkinDate,
|
||||
checkoutDate: attributes.checkoutDate,
|
||||
city: attributes.hotelInformation?.city,
|
||||
confirmationNumber: attributes.confirmationNumber,
|
||||
hotelName: attributes.hotelInformation?.name,
|
||||
nights: attributes.nights,
|
||||
}
|
||||
}),
|
||||
},
|
||||
meta: {
|
||||
totalPages: Math.ceil(verifiedData.data.data.length / limit),
|
||||
},
|
||||
}
|
||||
}),
|
||||
}),
|
||||
|
||||
creditCards: protectedProcedure.query(async function ({ ctx }) {
|
||||
@@ -517,6 +545,69 @@ export const userQueryRouter = router({
|
||||
return verifiedData.data.data
|
||||
}),
|
||||
|
||||
initiateSaveCard: protectedProcedure
|
||||
.input(initiateSaveCardInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
body: {
|
||||
language: input.language,
|
||||
mobileToken: input.mobileToken,
|
||||
redirectUrl: input.redirectUrl,
|
||||
},
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
switch (apiResponse.status) {
|
||||
case 400:
|
||||
throw badRequestError(apiResponse)
|
||||
case 401:
|
||||
throw unauthorizedError(apiResponse)
|
||||
case 403:
|
||||
throw forbiddenError(apiResponse)
|
||||
default:
|
||||
throw internalServerError(apiResponse)
|
||||
}
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = initiateSaveCardSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(`Failed to initiate save card data`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return verifiedData.data.data
|
||||
}),
|
||||
|
||||
saveCard: protectedProcedure.input(saveCardInput).mutation(async function ({
|
||||
ctx,
|
||||
input,
|
||||
}) {
|
||||
const apiResponse = await api.post(
|
||||
`${api.endpoints.v1.creditCards}/${input.transactionId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
body: {},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
console.error(`API Response Failed - Save card`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
|
||||
membershipCards: protectedProcedure.query(async function ({ ctx }) {
|
||||
const apiResponse = await api.get(api.endpoints.v1.profile, {
|
||||
cache: "no-store",
|
||||
|
||||
35
server/tokenManager.ts
Normal file
35
server/tokenManager.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { ServiceTokenResponse } from "@/types/tokens"
|
||||
|
||||
const SERVICE_TOKEN_REVALIDATE_SECONDS = 3599 // 59 minutes and 59 seconds.
|
||||
|
||||
export async function fetchServiceToken(): Promise<ServiceTokenResponse> {
|
||||
try {
|
||||
const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
client_id: env.CURITY_CLIENT_ID_SERVICE,
|
||||
client_secret: env.CURITY_CLIENT_SECRET_SERVICE,
|
||||
scope: ["hotel"].join(","),
|
||||
}),
|
||||
next: {
|
||||
revalidate: SERVICE_TOKEN_REVALIDATE_SECONDS,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to obtain service token")
|
||||
}
|
||||
|
||||
return response.json()
|
||||
} catch (error) {
|
||||
console.error("Error fetching service token:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@ import { env } from "@/env/server"
|
||||
|
||||
import {
|
||||
badRequestError,
|
||||
internalServerError,
|
||||
sessionExpiredError,
|
||||
unauthorizedError,
|
||||
} from "./errors/trpc"
|
||||
import { fetchServiceToken } from "./tokenManager"
|
||||
import { transformer } from "./transformer"
|
||||
import { langInput } from "./utils"
|
||||
|
||||
@@ -99,3 +101,15 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const serviceProcedure = t.procedure.use(async (opts) => {
|
||||
const { access_token } = await fetchServiceToken()
|
||||
if (!access_token) {
|
||||
throw internalServerError("Failed to obtain service token")
|
||||
}
|
||||
return opts.next({
|
||||
ctx: {
|
||||
serviceToken: access_token,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,3 +5,31 @@ import { Lang } from "@/constants/languages"
|
||||
export const langInput = z.object({
|
||||
lang: z.nativeEnum(Lang),
|
||||
})
|
||||
|
||||
/**
|
||||
* Helper function to convert Lang enum to API lang enum.
|
||||
*/
|
||||
export const toApiLang = (lang: Lang): string => {
|
||||
const result = toApiLangMap[lang]
|
||||
if (!result) {
|
||||
throw new Error("Invalid language")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const toApiLangMap: { [key in Lang]: string } = {
|
||||
[Lang.en]: "En",
|
||||
[Lang.sv]: "Sv",
|
||||
[Lang.no]: "No",
|
||||
[Lang.fi]: "Fi",
|
||||
[Lang.da]: "Da",
|
||||
[Lang.de]: "De",
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert lang string to Lang enum.
|
||||
*/
|
||||
export function toLang(lang: string): Lang | undefined {
|
||||
const lowerCaseLang = lang.toLowerCase()
|
||||
return Object.values(Lang).find((l) => l === lowerCaseLang)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user