Merged in chore/remove-enrichHotel-call (pull request #3244)

Chore/remove enrichHotel call

* chore: remove enrichHotel api call

* include cityId when errors happen

* remove unused funciton and cleanup code

* tests no longer expect us to have called a function that is not used

* remove commented code


Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-12-01 07:49:59 +00:00
parent 77ac3628c1
commit f6a807758e
10 changed files with 77 additions and 176 deletions

View File

@@ -5,7 +5,7 @@ export const autoCompleteLocationSchema = z.object({
name: z.string(),
type: z.enum(["cities", "hotels", "countries"]),
searchTokens: z.array(z.string()),
destination: z.string(),
destination: z.string().optional(),
url: z.string().optional(),
cityIdentifier: z.string().optional(),
})

View File

@@ -38,7 +38,7 @@ export function filterAutoCompleteLocations<T extends AutoCompleteLocation>(
const searchable = locations.map((x) => ({
...x,
nameTokens: extractTokens(x.name),
destinationTokens: extractTokens(x.destination),
destinationTokens: extractTokens(x.destination ?? ""),
}))
fuseConfig.setCollection(searchable)

View File

@@ -27,8 +27,6 @@ import { addressSchema } from "./schemas/hotel/address"
import { detailedFacilitiesSchema } from "./schemas/hotel/detailedFacility"
import { locationSchema } from "./schemas/hotel/location"
import { imageSchema } from "./schemas/image"
import { locationCitySchema } from "./schemas/location/city"
import { locationHotelSchema } from "./schemas/location/hotel"
import { relationshipsSchema } from "./schemas/relationships"
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
@@ -429,66 +427,6 @@ export const citiesSchema = z
return null
})
export const locationsSchema = z.object({
data: z
.array(
z
.discriminatedUnion("type", [locationCitySchema, locationHotelSchema])
.transform((location) => {
if (location.type === "cities") {
return {
...location.attributes,
country: location.attributes.countryName || "",
id: location.id,
type: location.type,
}
}
return {
...location.attributes,
id: location.id,
relationships: {
city: {
cityIdentifier: "",
ianaTimeZoneId: "",
id: "",
isPublished: false,
keywords: [],
name: "",
timeZoneId: "",
type: "cities",
url: location?.relationships?.city?.links?.related ?? "",
},
},
type: location.type,
operaId: location.attributes.operaId ?? "",
}
})
)
.transform((data) =>
data
.filter((node) => !!node && node.isPublished)
.filter((node) => {
if (node.type === "hotels") {
if (!node.operaId) {
return false
}
} else {
if (!node.cityIdentifier) {
return false
}
}
return true
})
.sort((a, b) => {
if (a.type === b.type) {
return a.name.localeCompare(b.name)
} else {
return a.type === "cities" ? -1 : 1
}
})
),
})
export type BreakfastPackages = z.output<typeof breakfastPackagesSchema>
export const breakfastPackagesSchema = z
.object({

View File

@@ -42,14 +42,18 @@ export async function getHotelIdsByCityId({
if (!apiResponse.ok) {
await metricsGetHotelIdsByCityId.httpError(apiResponse)
throw new Error("Unable to fetch hotelIds by cityId")
throw new Error(`Unable to fetch hotelIds by cityId`, {
cause: { cityId },
})
}
const apiJson = await apiResponse.json()
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
if (!validatedHotelIds.success) {
metricsGetHotelIdsByCityId.validationError(validatedHotelIds.error)
throw new Error("Unable to parse data for hotelIds by cityId")
throw new Error(`Unable to parse data for hotelIds by cityId`, {
cause: { cityId, errors: validatedHotelIds.error },
})
}
return validatedHotelIds.data

View File

@@ -4,7 +4,7 @@ import { Lang } from "@scandic-hotels/common/constants/language"
import { getCacheClient } from "@scandic-hotels/common/dataCache"
import * as api from "../../../api"
import { getCity } from "./getCity"
// import { getCity } from "./getCity"
import { getLocationsByCountries } from "./getLocationsByCountries"
import type { CitiesGroupedByCountry } from "../../../types/locations"
@@ -35,15 +35,16 @@ vi.mock("./getCity", () => {
vi.mock("@scandic-hotels/common/logger/createLogger", () => {
return {
createLogger: () => ({
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}
})
const mockedGetCacheClient = getCacheClient as unknown as Mock
const mockedApiGet = api.get as unknown as Mock
const mockedGetCity = getCity as unknown as Mock
// const mockedGetCity = getCity as unknown as Mock
describe("getLocationsByCountries", () => {
const mockedCacheClient = {
@@ -125,20 +126,6 @@ describe("getLocationsByCountries", () => {
json: async () => apiPayload,
})
// getCity returns enriched city object for hotel relationship
const mockedCity: Awaited<ReturnType<typeof getCity>> = {
cityIdentifier: "remote-ci-1",
ianaTimeZoneId: "Europe/Stockholm",
id: "remote-city-id",
isPublished: true,
keywords: [],
name: "RemoteCity",
timeZoneId: "Europe/Stockholm",
type: "cities",
}
mockedGetCity.mockResolvedValueOnce(mockedCity)
const citiesByCountry = {
CountryX: [{ name: "CityAA" }],
} as unknown as CitiesGroupedByCountry
@@ -164,16 +151,13 @@ describe("getLocationsByCountries", () => {
expect(cityNode!.country).toBe("CountryX") // country assigned based on citiesByCountry
expect(hotelNode).toBeDefined()
expect(mockedGetCity).toHaveBeenCalledWith({
cityUrl: "https://api/cities/city1",
serviceToken: "token",
})
// hotel relationships.city should be the object returned by getCity (merged)
expect(hotelNode?.relationships).toBeDefined()
expect(hotelNode?.relationships.city).toEqual(
expect.objectContaining({
id: mockedCity.id,
name: mockedCity.name,
id: "city1",
name: "City1",
})
)
})
@@ -192,19 +176,6 @@ describe("getLocationsByCountries", () => {
json: async () => apiPayload,
})
// getCity returns enriched city object for hotel relationship
const mockedCity: Awaited<ReturnType<typeof getCity>> = {
cityIdentifier: "remote-ci-1",
ianaTimeZoneId: "Europe/Stockholm",
id: "remote-city-id",
isPublished: true,
keywords: [],
name: "RemoteCity",
timeZoneId: "Europe/Stockholm",
type: "cities",
}
mockedGetCity.mockResolvedValue(mockedCity)
const citiesByCountry = {
CountryX: [{ name: "CityAA" }],
} as unknown as CitiesGroupedByCountry
@@ -230,17 +201,13 @@ describe("getLocationsByCountries", () => {
.filter((n) => n.type === "hotels")
.find((n) => n.name === "Hotel1")
expect(hotel1).toBeDefined()
expect(mockedGetCity).toHaveBeenCalledWith({
cityUrl: "https://api/cities/city1",
serviceToken: "token",
})
// hotel relationships.city should be the object returned by getCity (merged)
expect(hotel1?.relationships).toBeDefined()
expect(hotel1?.relationships.city).toEqual(
expect.objectContaining({
id: mockedCity.id,
name: mockedCity.name,
id: "city1",
name: "City1",
})
)
@@ -248,20 +215,15 @@ describe("getLocationsByCountries", () => {
.filter((n) => n.type === "hotels")
.find((n) => n.name === "Hotel2")
expect(hotel2).toBeDefined()
expect(mockedGetCity).toHaveBeenCalledWith({
cityUrl: "https://api/cities/city2",
serviceToken: "token",
})
// hotel relationships.city should be the object returned by getCity (merged)
expect(hotel2?.relationships).toBeDefined()
expect(hotel2?.relationships.city).toEqual(
expect.objectContaining({
id: mockedCity.id,
name: mockedCity.name,
id: "city2",
name: "City2",
})
)
expect(mockedGetCity).toHaveBeenCalledTimes(2)
})
it("filters out unpublished cities", async () => {

View File

@@ -1,16 +1,13 @@
import deepmerge from "deepmerge"
import { z } from "zod"
import { getCacheClient } from "@scandic-hotels/common/dataCache"
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import { chunk } from "@scandic-hotels/common/utils/chunk"
import * as api from "../../../api"
import { serverErrorByStatus } from "../../../errors"
import { toApiLang } from "../../../utils"
import { locationCitySchema } from "../schemas/location/city"
import { locationHotelSchema } from "../schemas/location/hotel"
import { getCity } from "./getCity"
import type { Country } from "@scandic-hotels/common/constants/country"
import type { Lang } from "@scandic-hotels/common/constants/language"
@@ -21,6 +18,14 @@ type CitiesNamesByCountry = Record<
Country | (string & {}),
Array<{ name: string }>
> | null
type Hotel = Extract<
z.infer<typeof locationsSchema>["data"][number],
{ type: "hotels" }
>
type City = Extract<
z.infer<typeof locationsSchema>["data"][number],
{ type: "cities" }
>
export async function getLocationsByCountries({
lang,
@@ -73,21 +78,11 @@ export async function getLocationsByCountries({
const data = cleanData(verifiedLocations.data.data)
const cities = data
.filter((x) => x.type === "cities")
.map((x) => enrichCity(x, citiesByCountry))
.map((city) => addCountryDataToCity(city, citiesByCountry))
const chunkedHotels = chunk(
data.filter((x) => x.type === "hotels"),
10
)
const hotels = (
await Promise.all(
chunkedHotels.flatMap(async (chunk) => {
return await Promise.all(
chunk.flatMap(async (hotel) => enrichHotel(hotel, serviceToken))
)
})
)
).flat()
const hotels = data
.filter((x) => x.type === "hotels")
.map((hotel) => addCityDataToHotel(hotel, cities))
let locations: z.infer<typeof locationsSchema>["data"] = [
...cities,
@@ -100,48 +95,32 @@ export async function getLocationsByCountries({
)
}
async function enrichHotel(
hotel: Extract<
z.infer<typeof locationsSchema>["data"][number],
{ type: "hotels" }
>,
serviceToken: string
): Promise<
Extract<z.infer<typeof locationsSchema>["data"][number], { type: "hotels" }>
> {
if (hotel.type !== "hotels") {
return hotel
}
if (!hotel.relationships.city?.url) {
return hotel
}
const city = await getCity({
cityUrl: hotel.relationships.city.url,
serviceToken,
})
function addCityDataToHotel(hotel: Hotel, cities: City[]) {
const city = cities.find((c) => c.id === hotel.relationships.city.id)
if (!city) {
hotelUtilsLogger.warn(
`City with id ${hotel.relationships.city.id} not found for hotel ${hotel.id}`
)
return hotel
}
return deepmerge(hotel, {
return {
...hotel,
relationships: {
city,
city: {
...city,
cityIdentifier: city.cityIdentifier,
url: hotel.relationships.city.url,
keywords: city.keyWords ?? [],
},
},
})
} satisfies Hotel
}
function enrichCity(
city: Extract<
z.infer<typeof locationsSchema>["data"][number],
{ type: "cities" }
>,
function addCountryDataToCity(
city: City,
citiesByCountry: CitiesNamesByCountry | null
): Extract<
z.infer<typeof locationsSchema>["data"][number],
{ type: "cities" }
> {
): City {
if (!citiesByCountry) {
return city
}
@@ -206,13 +185,13 @@ export const locationsSchema = z.object({
id: location.id,
relationships: {
city: {
cityIdentifier: "",
ianaTimeZoneId: "",
id: "",
cityIdentifier: undefined as string | undefined,
id: extractCityId(
location.relationships?.city?.links?.related ?? ""
),
isPublished: false,
keywords: [],
name: "",
timeZoneId: "",
keywords: [] as string[],
name: undefined as string | undefined,
type: "cities",
url: location?.relationships?.city?.links?.related ?? "",
},
@@ -223,3 +202,18 @@ export const locationsSchema = z.object({
})
),
})
function extractCityId(cityUrl: string): string | null {
try {
const url = new URL(cityUrl)
if (!url.pathname.toLowerCase().includes("/cities/")) {
return null
}
const id = url.pathname.split("/").at(-1)
return id ?? null
} catch {
return null
}
}

View File

@@ -4,8 +4,8 @@ import type { z } from "zod"
import type {
citiesByCountrySchema,
countriesSchema,
locationsSchema,
} from "../routers/hotels/output"
import type { locationsSchema } from "../routers/hotels/services/getLocationsByCountries"
export interface LocationSchema extends z.output<typeof locationsSchema> {}