Merge branch 'develop' into feature/tracking

This commit is contained in:
Linus Flood
2024-10-10 08:10:11 +02:00
43 changed files with 749 additions and 478 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api"
import * as api from "@/lib/api"
import { getVerifiedUser } from "@/server/routers/user/query"
import { bookingServiceProcedure, router } from "@/server/trpc"
import { router, serviceProcedure } from "@/server/trpc"
import { getMembership } from "@/utils/user"
@@ -36,7 +36,7 @@ async function getMembershipNumber(
export const bookingMutationRouter = router({
booking: router({
create: bookingServiceProcedure
create: serviceProcedure
.input(createBookingInput)
.mutation(async function ({ ctx, input }) {
const { checkInDate, checkOutDate, hotelId } = input
+59 -58
View File
@@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api"
import * as api from "@/lib/api"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { bookingServiceProcedure, router } from "@/server/trpc"
import { router, serviceProcedure } from "@/server/trpc"
import { getBookingStatusInput } from "./input"
import { createBookingSchema } from "./output"
@@ -17,69 +17,70 @@ const getBookingStatusFailCounter = meter.createCounter(
)
export const bookingQueryRouter = router({
status: bookingServiceProcedure
.input(getBookingStatusInput)
.query(async function ({ ctx, input }) {
const { confirmationNumber } = input
getBookingStatusCounter.add(1, { confirmationNumber })
status: serviceProcedure.input(getBookingStatusInput).query(async function ({
ctx,
input,
}) {
const { confirmationNumber } = input
getBookingStatusCounter.add(1, { confirmationNumber })
const apiResponse = await api.get(
`${api.endpoints.v1.booking}/${confirmationNumber}/status`,
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
}
)
if (!apiResponse.ok) {
const responseMessage = await apiResponse.text()
getBookingStatusFailCounter.add(1, {
confirmationNumber,
error_type: "http_error",
error: responseMessage,
})
console.error(
"api.booking.status error",
JSON.stringify({
query: { confirmationNumber },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text: responseMessage,
},
})
)
throw serverErrorByStatus(apiResponse.status, apiResponse)
const apiResponse = await api.get(
`${api.endpoints.v1.booking}/${confirmationNumber}/status`,
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
}
)
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
getBookingStatusFailCounter.add(1, {
confirmationNumber,
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
console.error(
"api.booking.status validation error",
JSON.stringify({
query: { confirmationNumber },
error: verifiedData.error,
})
)
throw badRequestError()
}
getBookingStatusSuccessCounter.add(1, { confirmationNumber })
console.info(
"api.booking.status success",
if (!apiResponse.ok) {
const responseMessage = await apiResponse.text()
getBookingStatusFailCounter.add(1, {
confirmationNumber,
error_type: "http_error",
error: responseMessage,
})
console.error(
"api.booking.status error",
JSON.stringify({
query: { confirmationNumber },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text: responseMessage,
},
})
)
return verifiedData.data
}),
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
getBookingStatusFailCounter.add(1, {
confirmationNumber,
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
console.error(
"api.booking.status validation error",
JSON.stringify({
query: { confirmationNumber },
error: verifiedData.error,
})
)
throw badRequestError()
}
getBookingStatusSuccessCounter.add(1, { confirmationNumber })
console.info(
"api.booking.status success",
JSON.stringify({
query: { confirmationNumber },
})
)
return verifiedData.data
}),
})
+54 -50
View File
@@ -1,14 +1,14 @@
import { metrics } from "@opentelemetry/api"
import { unstable_cache } from "next/cache"
import { Lang } from "@/constants/languages"
import * as api from "@/lib/api"
import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql"
import { request } from "@/lib/graphql/request"
import { Context } from "@/server/context"
import { notFound } from "@/server/errors/trpc"
import {
contentStackBaseWithProfileServiceProcedure,
contentStackBaseWithProtectedProcedure,
contentStackBaseWithServiceProcedure,
router,
} from "@/server/trpc"
@@ -62,69 +62,71 @@ const getAllRewardFailCounter = meter.createCounter(
"trpc.contentstack.reward.all-fail"
)
const ONE_HOUR = 60 * 60
function getUniqueRewardIds(rewardIds: string[]) {
const uniqueRewardIds = new Set(rewardIds)
return Array.from(uniqueRewardIds)
}
async function getAllApiRewards(ctx: Context & { serviceToken: string }) {
const apiResponse = await api.get(api.endpoints.v1.tierRewards, {
cache: undefined, // override defaultOptions
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
// One hour. Since the service token is refreshed every hour, this is the longest cache we can have.
next: { revalidate: 60 * 60 },
})
if (!apiResponse.ok) {
const text = await apiResponse.text()
getCurrentRewardFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
const getAllCachedApiRewards = unstable_cache(
async function (token) {
const apiResponse = await api.get(api.endpoints.v1.tierRewards, {
headers: {
Authorization: `Bearer ${token}`,
},
})
console.error(
"api.rewards.tierRewards error ",
JSON.stringify({
error: {
if (!apiResponse.ok) {
const text = await apiResponse.text()
getCurrentRewardFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
}),
})
)
}
console.error(
"api.rewards.tierRewards error ",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
}
const data = await apiResponse.json()
const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
const data = await apiResponse.json()
const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
if (!validatedApiTierRewards.success) {
getAllRewardFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(validatedApiTierRewards.error),
})
console.error(validatedApiTierRewards.error)
console.error(
"api.rewards validation error",
JSON.stringify({
error: validatedApiTierRewards.error,
if (!validatedApiTierRewards.success) {
getAllRewardFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(validatedApiTierRewards.error),
})
)
return null
}
console.error(validatedApiTierRewards.error)
console.error(
"api.rewards validation error",
JSON.stringify({
error: validatedApiTierRewards.error,
})
)
return null
}
return validatedApiTierRewards.data
}
return validatedApiTierRewards.data
},
["getAllApiRewards"],
{ revalidate: ONE_HOUR }
)
async function getCmsRewards(locale: Lang, rewardIds: string[]) {
const tags = rewardIds.map((id) =>
generateLoyaltyConfigTag(locale, "reward", id)
)
const cmsRewardsResponse = await request<CmsRewardsResponse>(
GetRewards,
{
@@ -260,13 +262,15 @@ export const rewardQueryRouter = router({
nextCursor,
}
}),
byLevel: contentStackBaseWithProfileServiceProcedure
byLevel: contentStackBaseWithServiceProcedure
.input(rewardsByLevelInput)
.query(async function ({ input, ctx }) {
getByLevelRewardCounter.add(1)
const { level_id } = input
const allUpcomingApiRewards = await getAllApiRewards(ctx)
const allUpcomingApiRewards = await getAllCachedApiRewards(
ctx.serviceToken
)
if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) {
getByLevelRewardFailCounter.add(1)
@@ -310,11 +314,11 @@ export const rewardQueryRouter = router({
getByLevelRewardSuccessCounter.add(1)
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
}),
all: contentStackBaseWithProfileServiceProcedure
all: contentStackBaseWithServiceProcedure
.input(rewardsAllInput)
.query(async function ({ input, ctx }) {
getAllRewardCounter.add(1)
const allApiRewards = await getAllApiRewards(ctx)
const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken)
if (!allApiRewards) {
return []
+12 -31
View File
@@ -1,5 +1,4 @@
import { metrics } from "@opentelemetry/api"
import { unstable_cache } from "next/cache"
import * as api from "@/lib/api"
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
@@ -11,10 +10,10 @@ import {
} from "@/server/errors/trpc"
import { extractHotelImages } from "@/server/routers/utils/hotels"
import {
contentStackUidWithHotelServiceProcedure,
hotelServiceProcedure,
contentStackUidWithServiceProcedure,
publicProcedure,
router,
serviceProcedure,
} from "@/server/trpc"
import { toApiLang } from "@/server/utils"
@@ -38,7 +37,6 @@ import {
getCitiesByCountry,
getCountries,
getLocations,
locationsAffix,
TWENTYFOUR_HOURS,
} from "./utils"
@@ -99,7 +97,7 @@ async function getContentstackData(
}
export const hotelQueryRouter = router({
get: contentStackUidWithHotelServiceProcedure
get: contentStackUidWithServiceProcedure
.input(getHotelInputSchema)
.query(async ({ ctx, input }) => {
const { lang, uid } = ctx
@@ -264,7 +262,7 @@ export const hotelQueryRouter = router({
}
}),
availability: router({
hotels: hotelServiceProcedure
hotels: serviceProcedure
.input(getHotelsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const {
@@ -388,7 +386,7 @@ export const hotelQueryRouter = router({
.flatMap((hotels) => hotels.attributes),
}
}),
rooms: hotelServiceProcedure
rooms: serviceProcedure
.input(getRoomsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const {
@@ -543,7 +541,7 @@ export const hotelQueryRouter = router({
}),
}),
hotelData: router({
get: hotelServiceProcedure
get: serviceProcedure
.input(getlHotelDataInputSchema)
.query(async ({ ctx, input }) => {
const { hotelId, language, include } = input
@@ -641,7 +639,7 @@ export const hotelQueryRouter = router({
}),
}),
locations: router({
get: hotelServiceProcedure.query(async function ({ ctx }) {
get: serviceProcedure.query(async function ({ ctx }) {
const searchParams = new URLSearchParams()
searchParams.set("language", toApiLang(ctx.lang))
@@ -657,36 +655,19 @@ export const hotelQueryRouter = router({
},
}
const getCachedCountries = unstable_cache(
getCountries,
[`${ctx.lang}:${locationsAffix}:countries`],
{ revalidate: TWENTYFOUR_HOURS }
)
const countries = await getCachedCountries(options, searchParams)
const getCachedCitiesByCountry = unstable_cache(
getCitiesByCountry,
[`${ctx.lang}:${locationsAffix}:cities-by-country`],
{ revalidate: TWENTYFOUR_HOURS }
)
const countries = await getCountries(options, searchParams, ctx.lang)
let citiesByCountry = null
if (countries) {
citiesByCountry = await getCachedCitiesByCountry(
citiesByCountry = await getCitiesByCountry(
countries,
options,
searchParams
searchParams,
ctx.lang
)
}
const getCachedLocations = unstable_cache(
getLocations,
[`${ctx.lang}:${locationsAffix}`],
{ revalidate: TWENTYFOUR_HOURS }
)
const locations = await getCachedLocations(
const locations = await getLocations(
ctx.lang,
options,
searchParams,
+174 -126
View File
@@ -17,6 +17,7 @@ import {
PointOfInterestCategoryNameEnum,
PointOfInterestGroupEnum,
} from "@/types/hotel"
import { HotelLocation } from "@/types/trpc/routers/hotel/locations"
import type { Lang } from "@/constants/languages"
import type { Endpoint } from "@/lib/api/endpoints"
@@ -54,89 +55,119 @@ export const locationsAffix = "locations"
export const TWENTYFOUR_HOURS = 60 * 60 * 24
export async function getCity(
cityUrl: string,
options: RequestOptionsWithOutBody
options: RequestOptionsWithOutBody,
lang: Lang,
relationshipCity: HotelLocation["relationships"]["city"]
) {
const url = new URL(cityUrl)
const cityResponse = await api.get(
url.pathname as Endpoint,
options,
url.searchParams
)
return unstable_cache(
async function (locationCityUrl: string) {
const url = new URL(locationCityUrl)
const cityResponse = await api.get(
url.pathname as Endpoint,
options,
url.searchParams
)
if (!cityResponse.ok) {
return null
}
if (!cityResponse.ok) {
return null
}
const cityJson = await cityResponse.json()
const city = apiCitySchema.safeParse(cityJson)
if (!city.success) {
console.info(`Validation of city failed`)
console.info(`cityUrl: ${cityUrl}`)
console.error(city.error)
return null
}
const cityJson = await cityResponse.json()
const city = apiCitySchema.safeParse(cityJson)
if (!city.success) {
console.info(`Validation of city failed`)
console.info(`cityUrl: ${locationCityUrl}`)
console.error(city.error)
return null
}
return city.data
return city.data
},
[cityUrl, `${lang}:${relationshipCity}`],
{ revalidate: TWENTYFOUR_HOURS }
)(cityUrl)
}
export async function getCountries(
options: RequestOptionsWithOutBody,
params: URLSearchParams
params: URLSearchParams,
lang: Lang
) {
const countryResponse = await api.get(
api.endpoints.v1.countries,
options,
params
)
if (!countryResponse.ok) {
return null
}
const countriesJson = await countryResponse.json()
const countries = apiCountriesSchema.safeParse(countriesJson)
if (!countries.success) {
console.info(`Validation for countries failed`)
console.error(countries.error)
return null
}
return countries.data
}
export async function getCitiesByCountry(
countries: Countries,
options: RequestOptionsWithOutBody,
params: URLSearchParams
) {
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
await Promise.all(
countries.data.map(async (country) => {
return unstable_cache(
async function (searchParams) {
const countryResponse = await api.get(
`${api.endpoints.v1.citiesCountry}/${country.name}`,
api.endpoints.v1.countries,
options,
params
searchParams
)
if (!countryResponse.ok) {
return null
}
const countryJson = await countryResponse.json()
const citiesByCountry = apiCitiesByCountrySchema.safeParse(countryJson)
if (!citiesByCountry.success) {
console.info(`Failed to validate Cities by Country payload`)
console.error(citiesByCountry.error)
const countriesJson = await countryResponse.json()
const countries = apiCountriesSchema.safeParse(countriesJson)
if (!countries.success) {
console.info(`Validation for countries failed`)
console.error(countries.error)
return null
}
citiesGroupedByCountry[country.name] = citiesByCountry.data.data
return true
})
)
return countries.data
},
[`${lang}:${locationsAffix}:countries`, params.toString()],
{ revalidate: TWENTYFOUR_HOURS }
)(params)
}
return citiesGroupedByCountry
export async function getCitiesByCountry(
countries: Countries,
options: RequestOptionsWithOutBody,
params: URLSearchParams,
lang: Lang
) {
return unstable_cache(
async function (
searchParams: URLSearchParams,
searchedCountries: Countries
) {
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
await Promise.all(
searchedCountries.data.map(async (country) => {
const countryResponse = await api.get(
`${api.endpoints.v1.citiesCountry}/${country.name}`,
options,
searchParams
)
if (!countryResponse.ok) {
return null
}
const countryJson = await countryResponse.json()
const citiesByCountry =
apiCitiesByCountrySchema.safeParse(countryJson)
if (!citiesByCountry.success) {
console.info(`Failed to validate Cities by Country payload`)
console.error(citiesByCountry.error)
return null
}
citiesGroupedByCountry[country.name] = citiesByCountry.data.data
return true
})
)
return citiesGroupedByCountry
},
[
`${lang}:${locationsAffix}:cities-by-country`,
params.toString(),
JSON.stringify(countries),
],
{ revalidate: TWENTYFOUR_HOURS }
)(params, countries)
}
export async function getLocations(
@@ -145,72 +176,89 @@ export async function getLocations(
params: URLSearchParams,
citiesByCountry: CitiesGroupedByCountry | null
) {
const apiResponse = await api.get(api.endpoints.v1.locations, options, params)
return unstable_cache(
async function (
searchParams: URLSearchParams,
groupedCitiesByCountry: CitiesGroupedByCountry | null
) {
const apiResponse = await api.get(
api.endpoints.v1.locations,
options,
searchParams
)
if (!apiResponse.ok) {
if (apiResponse.status === 401) {
return { error: true, cause: "unauthorized" } as const
} else if (apiResponse.status === 403) {
return { error: true, cause: "forbidden" } as const
}
return null
}
const apiJson = await apiResponse.json()
const verifiedLocations = apiLocationsSchema.safeParse(apiJson)
if (!verifiedLocations.success) {
console.info(`Locations Verification Failed`)
console.error(verifiedLocations.error)
return null
}
return await Promise.all(
verifiedLocations.data.data.map(async (location) => {
if (location.type === "cities") {
if (citiesByCountry) {
const country = Object.keys(citiesByCountry).find((country) => {
if (
citiesByCountry[country].find((loc) => loc.name === location.name)
) {
return true
}
return false
})
if (country) {
return {
...location,
country,
}
} else {
console.info(
`Location cannot be found in any of the countries cities`
)
console.info(location)
}
}
} else if (location.type === "hotels") {
if (location.relationships.city?.url) {
const getCachedCity = unstable_cache(
getCity,
[`${lang}:${location.relationships.city}`],
{ revalidate: TWENTYFOUR_HOURS }
)
const city = await getCachedCity(
location.relationships.city.url,
options
)
if (city) {
return deepmerge(location, {
relationships: {
city,
},
})
}
if (!apiResponse.ok) {
if (apiResponse.status === 401) {
return { error: true, cause: "unauthorized" } as const
} else if (apiResponse.status === 403) {
return { error: true, cause: "forbidden" } as const
}
return null
}
return location
})
)
const apiJson = await apiResponse.json()
const verifiedLocations = apiLocationsSchema.safeParse(apiJson)
if (!verifiedLocations.success) {
console.info(`Locations Verification Failed`)
console.error(verifiedLocations.error)
return null
}
return await Promise.all(
verifiedLocations.data.data.map(async (location) => {
if (location.type === "cities") {
if (groupedCitiesByCountry) {
const country = Object.keys(groupedCitiesByCountry).find(
(country) => {
if (
groupedCitiesByCountry[country].find(
(loc) => loc.name === location.name
)
) {
return true
}
return false
}
)
if (country) {
return {
...location,
country,
}
} else {
console.info(
`Location cannot be found in any of the countries cities`
)
console.info(location)
}
}
} else if (location.type === "hotels") {
if (location.relationships.city?.url) {
const city = await getCity(
location.relationships.city.url,
options,
lang,
location.relationships.city
)
if (city) {
return deepmerge(location, {
relationships: {
city,
},
})
}
}
}
return location
})
)
},
[
`${lang}:${locationsAffix}`,
params.toString(),
JSON.stringify(citiesByCountry),
],
{ revalidate: TWENTYFOUR_HOURS }
)(params, citiesByCountry)
}
-1
View File
@@ -1,5 +1,4 @@
import { metrics } from "@opentelemetry/api"
import { SafeParseSuccess } from "zod"
import * as api from "@/lib/api"
import {