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:
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user