Merged in feat/sw-2863-move-contentstack-router-to-trpc-package (pull request #2389)

feat(SW-2863): Move contentstack router to trpc package

* Add exports to packages and lint rule to prevent relative imports

* Add env to trpc package

* Add eslint to trpc package

* Apply lint rules

* Use direct imports from trpc package

* Add lint-staged config to trpc

* Move lang enum to common

* Restructure trpc package folder structure

* WIP first step

* update internal imports in trpc

* Fix most errors in scandic-web

Just 100 left...

* Move Props type out of trpc

* Fix CategorizedFilters types

* Move more schemas in hotel router

* Fix deps

* fix getNonContentstackUrls

* Fix import error

* Fix entry error handling

* Fix generateMetadata metrics

* Fix alertType enum

* Fix duplicated types

* lint:fix

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package

* Fix broken imports

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-06-26 07:53:01 +00:00
parent 0263ab8c87
commit 002d093af4
921 changed files with 3112 additions and 3008 deletions

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
import { ChildBedTypeEnum } from "../../../../enums/childBedTypeEnum"
export const childrenSchema = z.object({
age: z.number(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
export const occupancySchema = z.object({
adults: z.number(),
children: z.array(childrenSchema).default([]),
})

View File

@@ -0,0 +1,18 @@
import { z } from "zod"
import {
productTypeCorporateChequeSchema,
productTypePointsSchema,
productTypePriceSchema,
productTypeVoucherSchema,
} from "../productTypePrice"
export const productTypeSchema = z
.object({
bonusCheque: productTypeCorporateChequeSchema.optional(),
public: productTypePriceSchema.optional(),
member: productTypePriceSchema.optional(),
redemptions: z.array(productTypePointsSchema).optional(),
voucher: productTypeVoucherSchema.optional(),
})
.optional()

View File

@@ -0,0 +1,14 @@
import { z } from "zod"
export const citySchema = z.object({
attributes: z.object({
cityIdentifier: z.string().default(""),
ianaTimeZoneId: z.string().default(""),
isPublished: z.boolean().default(false),
keywords: z.array(z.string()).default([]),
name: z.string(),
timeZoneId: z.string().default(""),
}),
id: z.string(),
type: z.literal("cities"),
})

View File

@@ -0,0 +1,84 @@
import { z } from "zod"
import {
nullableArrayObjectValidator,
nullableArrayStringValidator,
} from "@scandic-hotels/common/utils/zod/arrayValidator"
import { nullableNumberValidator } from "@scandic-hotels/common/utils/zod/numberValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { addressSchema } from "./hotel/address"
import { contactInformationSchema } from "./hotel/contactInformation"
import { hotelContentSchema } from "./hotel/content"
import { detailedFacilitiesSchema } from "./hotel/detailedFacility"
import { hotelFactsSchema } from "./hotel/facts"
import { healthFacilitiesSchema } from "./hotel/healthFacilities"
import { includeSchema } from "./hotel/include"
import { locationSchema } from "./hotel/location"
import { merchantInformationSchema } from "./hotel/merchantInformation"
import { parkingSchema } from "./hotel/parking"
import { pointOfInterestsSchema } from "./hotel/poi"
import { ratingsSchema } from "./hotel/rating"
import { rewardNightSchema } from "./hotel/rewardNight"
import { socialMediaSchema } from "./hotel/socialMedia"
import { specialAlertsSchema } from "./hotel/specialAlerts"
import { imageSchema } from "./image"
export const attributesSchema = z.object({
address: addressSchema,
cityId: nullableStringValidator,
cityName: nullableStringValidator,
contactInformation: contactInformationSchema,
countryCode: nullableStringValidator,
detailedFacilities: detailedFacilitiesSchema,
galleryImages: z
.array(imageSchema)
.nullish()
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
healthFacilities: healthFacilitiesSchema,
hotelContent: hotelContentSchema,
hotelFacts: hotelFactsSchema,
hotelType: nullableStringValidator,
isActive: z.boolean(),
isPublished: z.boolean(),
keywords: nullableArrayStringValidator,
location: locationSchema,
merchantInformationData: merchantInformationSchema,
name: nullableStringValidator,
operaId: nullableStringValidator,
parking: nullableArrayObjectValidator(parkingSchema),
pointsOfInterest: pointOfInterestsSchema,
ratings: ratingsSchema,
rewardNight: rewardNightSchema,
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
vat: nullableNumberValidator,
})
export const includedSchema = z
.array(includeSchema)
.default([])
.transform((data) =>
data.filter((item) => {
if (item) {
if ("isPublished" in item && item.isPublished === false) {
return false
}
return true
}
return false
})
)
const relationshipSchema = z.object({
links: z.object({
related: z.string(),
}),
})
export const relationshipsSchema = z.object({
meetingRooms: relationshipSchema,
nearbyHotels: relationshipSchema,
restaurants: relationshipSchema,
roomCategories: relationshipSchema,
})

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
export const addressSchema = z.object({
city: nullableStringValidator,
country: nullableStringValidator,
streetAddress: nullableStringValidator,
zipCode: nullableStringValidator,
})

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
import {
nullableStringEmailValidator,
nullableStringValidator,
} from "@scandic-hotels/common/utils/zod/stringValidator"
export const contactInformationSchema = z.object({
email: nullableStringEmailValidator,
faxNumber: nullableStringValidator,
phoneNumber: nullableStringValidator,
websiteUrl: nullableStringValidator,
})

View File

@@ -0,0 +1,24 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { imageSchema } from "../image"
const descriptionSchema = z
.object({
medium: nullableStringValidator,
short: nullableStringValidator,
})
.nullish()
const textsSchema = z.object({
descriptions: descriptionSchema,
facilityInformation: nullableStringValidator,
meetingDescription: descriptionSchema,
surroundingInformation: nullableStringValidator,
})
export const hotelContentSchema = z.object({
images: imageSchema,
texts: textsSchema,
})

View File

@@ -0,0 +1,37 @@
import slugify from "slugify"
import { z } from "zod"
import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { FacilityEnum } from "../../../../enums/facilities"
const rawDetailedFacilitySchema = z.object({
filter: nullableStringValidator,
icon: nullableStringValidator,
id: z.nativeEnum(FacilityEnum),
name: nullableStringValidator,
public: z.boolean(),
sortOrder: z.number(),
})
function transformDetailedFacility(
data: z.output<typeof rawDetailedFacilitySchema>
) {
return {
...data,
slug: slugify(data.name, { lower: true, strict: true }),
}
}
export const detailedFacilitySchema = rawDetailedFacilitySchema.transform(
transformDetailedFacility
)
export const detailedFacilitiesSchema = nullableArrayObjectValidator(
rawDetailedFacilitySchema
).transform((facilities) =>
facilities
.sort((a, b) => b.sortOrder - a.sortOrder)
.map(transformDetailedFacility)
)

View File

@@ -0,0 +1,47 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
export const checkinSchema = z.object({
checkInTime: nullableStringValidator,
checkOutTime: nullableStringValidator,
onlineCheckout: z.boolean(),
onlineCheckOutAvailableFrom: nullableStringValidator,
})
const ecoLabelsSchema = z.object({
euEcoLabel: z.boolean(),
greenGlobeLabel: z.boolean(),
nordicEcoLabel: z.boolean(),
svanenEcoLabelCertificateNumber: nullableStringValidator,
})
const interiorSchema = z.object({
numberOfBeds: z.number(),
numberOfCribs: z.number(),
numberOfFloors: z.number(),
numberOfRooms: z.object({
connected: z.number(),
forAllergics: z.number(),
forDisabled: z.number(),
nonSmoking: z.number(),
pet: z.number(),
withExtraBeds: z.number(),
total: z.number(),
}),
})
const receptionHoursSchema = z.object({
alwaysOpen: z.boolean(),
closingTime: nullableStringValidator,
isClosed: z.boolean(),
openingTime: nullableStringValidator,
})
export const hotelFactsSchema = z.object({
checkin: checkinSchema,
ecoLabels: ecoLabelsSchema,
interior: interiorSchema,
receptionHours: receptionHoursSchema,
yearBuilt: nullableStringValidator,
})

View File

@@ -0,0 +1,62 @@
import { z } from "zod"
import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator"
import { nullableNumberValidator } from "@scandic-hotels/common/utils/zod/numberValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { imageSchema } from "../image"
const healthFacilitiesOpenHoursSchema = z.object({
alwaysOpen: z.boolean(),
closingTime: nullableStringValidator,
isClosed: z.boolean(),
openingTime: nullableStringValidator,
sortOrder: nullableNumberValidator,
})
const descriptionSchema = z
.object({
medium: nullableStringValidator,
short: nullableStringValidator,
})
.nullish()
const detailsSchema = z.object({
name: nullableStringValidator,
type: nullableStringValidator,
value: nullableStringValidator,
})
const textsSchema = z.object({
descriptions: descriptionSchema,
facilityInformation: nullableStringValidator,
meetingDescription: descriptionSchema,
surroundingInformation: nullableStringValidator,
})
export const healthFacilitySchema = z.object({
content: z.object({
images: z
.array(imageSchema)
.nullish()
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
texts: textsSchema,
}),
details: nullableArrayObjectValidator(detailsSchema),
openingDetails: z.object({
manualOpeningHours: nullableStringValidator,
openingHours: z.object({
ordinary: healthFacilitiesOpenHoursSchema,
weekends: healthFacilitiesOpenHoursSchema,
}),
useManualOpeningHours: z
.boolean()
.nullish()
.transform((b) => !!b),
openingHoursType: nullableStringValidator,
}),
type: nullableStringValidator,
})
export const healthFacilitiesSchema =
nullableArrayObjectValidator(healthFacilitySchema)

View File

@@ -0,0 +1,44 @@
import { z } from "zod"
import { citySchema } from "../city"
import {
additionalDataSchema,
transformAdditionalData,
} from "./include/additionalData"
import { nearbyHotelsSchema } from "./include/nearbyHotels"
import { restaurantsSchema } from "./include/restaurants"
import {
roomCategoriesSchema,
transformRoomCategories,
} from "./include/roomCategories"
export const includeSchema = z
.union([
z.null(),
z.undefined(),
z.discriminatedUnion("type", [
additionalDataSchema,
citySchema,
nearbyHotelsSchema,
restaurantsSchema,
roomCategoriesSchema,
]),
])
.transform((data) => {
switch (data?.type) {
case "additionalData":
return transformAdditionalData(data)
case "cities":
case "hotels":
case "restaurants":
return {
...data.attributes,
id: data.id,
type: data.type,
}
case "roomcategories":
return transformRoomCategories(data)
default:
return null
}
})

View File

@@ -0,0 +1,54 @@
import { z } from "zod"
import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { displayWebPageSchema } from "./additionalData/displayWebPage"
import { facilitySchema } from "./additionalData/facility"
import { gallerySchema } from "./additionalData/gallery"
import { restaurantsOverviewPageSchema } from "./additionalData/restaurantsOverviewPage"
import { specialNeedGroupSchema } from "./additionalData/specialNeedGroups"
export const extraPageSchema = z.object({
elevatorPitch: nullableStringValidator,
mainBody: nullableStringValidator,
nameInUrl: nullableStringValidator,
})
export const additionalDataAttributesSchema = z.object({
accessibility: facilitySchema.nullish(),
conferencesAndMeetings: facilitySchema.nullish(),
displayWebPage: displayWebPageSchema,
gallery: gallerySchema.nullish(),
healthAndFitness: extraPageSchema,
healthAndWellness: facilitySchema.nullish(),
hotelParking: extraPageSchema,
hotelRoomElevatorPitchText: nullableStringValidator,
hotelSpecialNeeds: extraPageSchema,
id: nullableStringValidator,
meetingRooms: extraPageSchema.merge(
z.object({
meetingOnlineLink: z.string().nullish(),
})
),
name: nullableStringValidator,
parkingImages: facilitySchema.nullish(),
restaurantImages: facilitySchema.nullish(),
restaurantsOverviewPage: restaurantsOverviewPageSchema,
specialNeedGroups: nullableArrayObjectValidator(specialNeedGroupSchema),
})
export const additionalDataSchema = z.object({
attributes: additionalDataAttributesSchema,
type: z.literal("additionalData"),
})
export function transformAdditionalData(
data: z.output<typeof additionalDataSchema>
) {
return {
...data.attributes,
id: data.attributes.id,
type: data.type,
}
}

View File

@@ -0,0 +1,19 @@
import { z } from "zod"
export const displayWebPageSchema = z
.object({
healthGym: z.boolean().default(false),
meetingRoom: z.boolean().default(false),
parking: z.boolean().default(false),
specialNeeds: z.boolean().default(false),
})
.nullish()
.transform(
(object) =>
object ?? {
healthGym: false,
meetingRoom: false,
parking: false,
specialNeeds: false,
}
)

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { imageSchema } from "../../../image"
export const facilitySchema = z.object({
headingText: nullableStringValidator,
heroImages: z
.array(imageSchema)
.nullish()
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
})

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
import { imageSchema } from "../../../image"
const imagesSchema = z
.array(imageSchema)
.nullish()
.transform((arr) => (arr ? arr.filter(Boolean) : []))
export const gallerySchema = z.object({
heroImages: imagesSchema,
smallerImages: imagesSchema,
})

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
export const restaurantsOverviewPageSchema = z.object({
restaurantsContentDescriptionMedium: nullableStringValidator,
restaurantsContentDescriptionShort: nullableStringValidator,
restaurantsOverviewPageLink: nullableStringValidator,
restaurantsOverviewPageLinkText: nullableStringValidator,
})

View File

@@ -0,0 +1,14 @@
import { z } from "zod"
import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
const specialNeedSchema = z.object({
details: nullableStringValidator,
name: nullableStringValidator,
})
export const specialNeedGroupSchema = z.object({
name: nullableStringValidator,
specialNeeds: nullableArrayObjectValidator(specialNeedSchema),
})

View File

@@ -0,0 +1,23 @@
import { z } from "zod"
import { attributesSchema } from "../../hotel"
export const nearbyHotelsSchema = z.object({
attributes: z.lazy(() =>
attributesSchema.pick({
address: true,
cityId: true,
cityName: true,
detailedFacilities: true,
hotelContent: true,
isActive: true,
isPublished: true,
location: true,
name: true,
operaId: true,
ratings: true,
})
),
id: z.string(),
type: z.literal("hotels"),
})

View File

@@ -0,0 +1,95 @@
import { z } from "zod"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import {
nullableIntValidator,
nullableNumberValidator,
} from "@scandic-hotels/common/utils/zod/numberValidator"
import {
nullableStringUrlValidator,
nullableStringValidator,
} from "@scandic-hotels/common/utils/zod/stringValidator"
import { imageSchema } from "../../image"
import { specialAlertsSchema } from "../specialAlerts"
const descriptionSchema = z.object({
medium: nullableStringValidator,
short: nullableStringValidator,
})
const textSchema = z.object({
descriptions: descriptionSchema,
facilityInformation: nullableStringValidator,
meetingDescription: descriptionSchema.optional(),
surroundingInformation: nullableStringValidator,
})
const contentSchema = z.object({
images: z.array(imageSchema).default([]),
texts: textSchema,
})
const restaurantPriceSchema = z.object({
amount: nullableNumberValidator,
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.SEK),
})
const externalBreakfastSchema = z.object({
isAvailable: z.boolean().default(false),
localPriceForExternalGuests: restaurantPriceSchema.optional(),
requestedPriceForExternalGuests: restaurantPriceSchema.optional(),
})
const menuItemSchema = z.object({
name: nullableStringValidator,
url: nullableStringUrlValidator,
})
export const openingHoursDetailsSchema = z.object({
alwaysOpen: z.boolean().default(false),
closingTime: nullableStringValidator,
isClosed: z.boolean().default(false),
openingTime: nullableStringValidator,
sortOrder: nullableIntValidator,
})
export const openingHoursSchema = z.object({
friday: openingHoursDetailsSchema.optional(),
isActive: z.boolean().default(false),
monday: openingHoursDetailsSchema.optional(),
name: nullableStringValidator,
saturday: openingHoursDetailsSchema.optional(),
sunday: openingHoursDetailsSchema.optional(),
thursday: openingHoursDetailsSchema.optional(),
tuesday: openingHoursDetailsSchema.optional(),
wednesday: openingHoursDetailsSchema.optional(),
})
const openingDetailsSchema = z.object({
alternateOpeningHours: openingHoursSchema.optional(),
openingHours: openingHoursSchema,
ordinary: openingHoursSchema.optional(),
weekends: openingHoursSchema.optional(),
})
export const restaurantsSchema = z.object({
attributes: z.object({
bookTableUrl: nullableStringValidator,
content: contentSchema,
email: z.string().email().optional(),
externalBreakfast: externalBreakfastSchema,
isPublished: z.boolean().default(false),
menus: z.array(menuItemSchema).default([]),
name: z.string().default(""),
openingDetails: z.array(openingDetailsSchema).default([]),
phoneNumber: z.string().optional(),
restaurantPage: z.boolean().default(false),
elevatorPitch: z.string().optional(),
nameInUrl: z.string().optional(),
mainBody: z.string().optional(),
specialAlerts: specialAlertsSchema,
}),
id: z.string(),
type: z.literal("restaurants"),
})

View File

@@ -0,0 +1,107 @@
import { z } from "zod"
import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { imageSchema } from "../../image"
const minMaxSchema = z.object({
max: z.number(),
min: z.number(),
})
const bedTypeSchema = z.object({
description: nullableStringValidator,
type: nullableStringValidator,
widthRange: minMaxSchema,
})
const occupancySchema = z.object({
adults: z.number(),
children: z.number(),
total: z.number(),
})
const roomContentSchema = z.object({
images: z
.array(imageSchema)
.nullish()
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
texts: z.object({
descriptions: z.object({
medium: nullableStringValidator,
short: nullableStringValidator,
}),
}),
})
const roomTypesSchema = z.object({
code: nullableStringValidator,
description: nullableStringValidator,
fixedExtraBed: bedTypeSchema.optional(),
isLackingCribs: z.boolean(),
isLackingExtraBeds: z.boolean(),
mainBed: bedTypeSchema,
name: nullableStringValidator,
occupancy: occupancySchema,
roomCount: z.number(),
roomSize: minMaxSchema,
})
const roomFacilitiesSchema = z.object({
availableInAllRooms: z.boolean(),
icon: z.string().optional(),
isUniqueSellingPoint: z.boolean(),
name: z.string(),
sortOrder: z.number(),
})
export const roomCategoriesSchema = z.object({
attributes: z.object({
content: roomContentSchema,
name: nullableStringValidator,
occupancy: minMaxSchema,
roomFacilities: nullableArrayObjectValidator(roomFacilitiesSchema),
roomSize: minMaxSchema,
roomTypes: nullableArrayObjectValidator(roomTypesSchema),
sortOrder: z.number(),
}),
id: z.string(),
type: z.literal("roomcategories"),
})
export function transformRoomCategories(
data: z.output<typeof roomCategoriesSchema>
) {
return {
descriptions: data.attributes.content.texts.descriptions,
id: data.id,
images: data.attributes.content.images,
name: data.attributes.name,
occupancy: data.attributes.occupancy,
roomFacilities: data.attributes.roomFacilities,
roomSize: data.attributes.roomSize,
roomTypes: data.attributes.roomTypes.map((roomType) => {
if (!roomType || roomType.fixedExtraBed?.type.toLowerCase() === "none") {
return {
...roomType,
fixedExtraBed: undefined,
}
}
return roomType
}),
sortOrder: data.attributes.sortOrder,
type: data.type,
totalOccupancy:
data.attributes.occupancy.min === data.attributes.occupancy.max
? {
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.max}`,
}
: {
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.min}-${data.attributes.occupancy.max}`,
},
}
}

View File

@@ -0,0 +1,7 @@
import { z } from "zod"
export const locationSchema = z.object({
distanceToCentre: z.number(),
latitude: z.number(),
longitude: z.number(),
})

View File

@@ -0,0 +1,23 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import type { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
export const merchantInformationSchema = z.object({
alternatePaymentOptions: z
.record(z.string(), z.boolean())
.transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
cards: z.record(z.string(), z.boolean()).transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
webMerchantId: nullableStringValidator,
})

View File

@@ -0,0 +1,45 @@
import { z } from "zod"
import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator"
import { nullableNumberValidator } from "@scandic-hotels/common/utils/zod/numberValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
const periodSchema = z.object({
amount: nullableNumberValidator,
endTime: nullableStringValidator,
period: nullableStringValidator,
startTime: nullableStringValidator,
})
const currencySchema = z
.object({
currency: nullableStringValidator,
ordinary: nullableArrayObjectValidator(periodSchema),
range: z
.object({
min: nullableNumberValidator,
max: nullableNumberValidator,
})
.nullish(),
weekend: nullableArrayObjectValidator(periodSchema),
})
.nullish()
const pricingSchema = z.object({
freeParking: z.boolean(),
localCurrency: currencySchema,
paymentType: nullableStringValidator,
requestedCurrency: currencySchema,
})
export const parkingSchema = z.object({
address: nullableStringValidator,
canMakeReservation: z.boolean(),
distanceToHotel: nullableNumberValidator,
externalParkingUrl: nullableStringValidator,
name: nullableStringValidator,
numberOfChargingSpaces: nullableNumberValidator,
numberOfParkingSpots: nullableNumberValidator,
pricing: pricingSchema,
type: nullableStringValidator,
})

View File

@@ -0,0 +1,63 @@
import { z } from "zod"
import { nullableNumberValidator } from "@scandic-hotels/common/utils/zod/numberValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { PointOfInterestGroupEnum } from "../../../../enums/pointOfInterest"
import { locationSchema } from "./location"
export const pointOfInterestSchema = z
.object({
category: z.object({
name: nullableStringValidator,
}),
distance: nullableNumberValidator,
location: locationSchema,
name: nullableStringValidator,
})
.transform((poi) => ({
categoryName: poi.category.name,
coordinates: {
lat: poi.location.latitude,
lng: poi.location.longitude,
},
distance: poi.distance,
group: getPoiGroupByCategoryName(poi.category.name),
name: poi.name,
}))
export const pointOfInterestsSchema = z
.array(pointOfInterestSchema)
.nullish()
.transform((arr) => (arr ? arr.filter(Boolean) : []))
.transform((pois) =>
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
)
function getPoiGroupByCategoryName(category: string | undefined) {
if (!category) return PointOfInterestGroupEnum.LOCATION
switch (category) {
case "Airport":
case "Bus terminal":
case "Transportations":
return PointOfInterestGroupEnum.PUBLIC_TRANSPORT
case "Amusement park":
case "Museum":
case "Sports":
case "Theatre":
case "Tourist":
case "Zoo":
return PointOfInterestGroupEnum.ATTRACTIONS
case "Nearby companies":
case "Fair":
return PointOfInterestGroupEnum.BUSINESS
case "Parking / Garage":
return PointOfInterestGroupEnum.PARKING
case "Shopping":
case "Restaurant":
return PointOfInterestGroupEnum.SHOPPING_DINING
case "Hospital":
default:
return PointOfInterestGroupEnum.LOCATION
}
}

View File

@@ -0,0 +1,57 @@
import { z } from "zod"
import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator"
import {
nullableStringUrlValidator,
nullableStringValidator,
} from "@scandic-hotels/common/utils/zod/stringValidator"
const awardSchema = z.object({
displayName: nullableStringValidator,
images: z
.object({
large: nullableStringValidator,
medium: nullableStringValidator,
small: nullableStringValidator,
})
.nullish()
.transform((obj) =>
obj
? obj
: {
small: "",
medium: "",
large: "",
}
),
})
const reviewsSchema = z
.object({
widgetHtmlTagId: nullableStringValidator,
widgetScriptEmbedUrlIframe: nullableStringValidator,
widgetScriptEmbedUrlJavaScript: nullableStringValidator,
})
.nullish()
.transform((obj) =>
obj
? obj
: {
widgetHtmlTagId: "",
widgetScriptEmbedUrlIframe: "",
widgetScriptEmbedUrlJavaScript: "",
}
)
export const ratingsSchema = z
.object({
tripAdvisor: z.object({
awards: nullableArrayObjectValidator(awardSchema),
numberOfReviews: z.number(),
rating: z.number(),
ratingImageUrl: nullableStringUrlValidator,
reviews: reviewsSchema,
webUrl: nullableStringUrlValidator,
}),
})
.optional()

View File

@@ -0,0 +1,12 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
export const rewardNightSchema = z.object({
campaign: z.object({
end: nullableStringValidator,
points: z.number(),
start: nullableStringValidator,
}),
points: z.number(),
})

View File

@@ -0,0 +1,8 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
export const socialMediaSchema = z.object({
facebook: nullableStringValidator,
instagram: nullableStringValidator,
})

View File

@@ -0,0 +1,42 @@
import { z } from "zod"
import { dt } from "@scandic-hotels/common/dt"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { AlertTypeEnum } from "../../../../types/alertType"
const specialAlertSchema = z.object({
description: nullableStringValidator,
displayInBookingFlow: z.boolean().default(false),
endDate: nullableStringValidator,
startDate: nullableStringValidator,
title: nullableStringValidator,
type: nullableStringValidator,
})
export const specialAlertsSchema = z
.array(specialAlertSchema)
.nullish()
.transform((arr) => (arr ? arr.filter(Boolean) : []))
.transform((data) => {
const now = dt().utc().format("YYYY-MM-DD")
const filteredAlerts = data.filter((alert) => {
const hasText = alert.description || alert.title
const hasDates = alert.startDate && alert.endDate
if (!hasDates) {
return hasText
}
const shouldShowNow = alert.startDate <= now && alert.endDate >= now
return shouldShowNow && hasText
})
return filteredAlerts.map((alert, idx) => ({
heading: alert.title || null,
id: `alert-${alert.type}-${idx}`,
name: alert.type,
text: alert.description || null,
type: AlertTypeEnum.Info,
displayInBookingFlow: alert.displayInBookingFlow,
endDate: alert.endDate,
startDate: alert.startDate,
}))
})

View File

@@ -0,0 +1,45 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
export const imageSizesSchema = z.object({
large: nullableStringValidator,
medium: nullableStringValidator,
small: nullableStringValidator,
tiny: nullableStringValidator,
})
export const imageMetaDataSchema = z.object({
altText: nullableStringValidator,
altText_En: nullableStringValidator,
copyRight: nullableStringValidator,
title: nullableStringValidator,
})
const DEFAULT_IMAGE_OBJ = {
metaData: {
altText: "Default image",
altText_En: "Default image",
copyRight: "Default image",
title: "Default image",
},
imageSizes: {
tiny: "https://placehold.co/1280x720",
small: "https://placehold.co/1280x720",
medium: "https://placehold.co/1280x720",
large: "https://placehold.co/1280x720",
},
}
export const imageSchema = z
.object({
imageSizes: imageSizesSchema,
metaData: imageMetaDataSchema,
})
.nullish()
.transform((val) => {
if (!val) {
return DEFAULT_IMAGE_OBJ
}
return val
})

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
export const locationCitySchema = z.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
countryName: z.string().optional().default(""),
isPublished: z.boolean(),
}),
id: z.string().optional().default(""),
type: z.literal("cities"),
})

View File

@@ -0,0 +1,35 @@
import { z } from "zod"
export const locationHotelSchema = z.object({
attributes: z.object({
distanceToCentre: z.number().optional(),
images: z
.object({
large: z.string().optional(),
medium: z.string().optional(),
small: z.string().optional(),
tiny: z.string().optional(),
})
.optional(),
isActive: z.boolean(),
isPublished: z.boolean(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
operaId: z.coerce.string().optional(),
}),
id: z.string().optional().default(""),
relationships: z
.object({
city: z
.object({
links: z
.object({
related: z.string().optional(),
})
.optional(),
})
.optional(),
})
.optional(),
type: z.literal("hotels"),
})

View File

@@ -0,0 +1,68 @@
import { z } from "zod"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { BreakfastPackageEnum } from "../../../enums/breakfast"
import { PackageTypeEnum } from "../../../enums/packages"
import { RoomPackageCodeEnum } from "../../../enums/roomFilter"
import { imageSizesSchema } from "./image"
// TODO: Remove optional and default when the API change has been deployed
export const packagePriceSchema = z
.object({
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown),
price: z.number(),
totalPrice: z.number(),
})
.optional()
.default({
currency: CurrencyEnum.Unknown,
price: 0,
totalPrice: 0,
})
const inventorySchema = z.object({
date: z.string(),
total: z.number(),
available: z.number(),
})
export const ancillaryContentSchema = z.object({
status: z.string(),
id: z.string(),
variants: z.object({
ancillary: z.object({ id: z.string(), price: packagePriceSchema }),
ancillaryLoyalty: z
.object({ points: z.number(), code: z.string() })
.optional(),
}),
title: z.string(),
descriptions: z.object({ html: z.string() }),
images: z.array(z.object({ imageSizes: imageSizesSchema })),
requiresDeliveryTime: z.boolean(),
})
export const packageSchema = z.object({
code: z.nativeEnum({ ...RoomPackageCodeEnum, ...BreakfastPackageEnum }),
description: z.string(),
inventories: z.array(inventorySchema),
itemCode: z.string().default(""),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
})
export const breakfastPackageSchema = z.object({
code: z.string(),
description: z.string(),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
packageType: z.enum([
PackageTypeEnum.BreakfastAdult,
PackageTypeEnum.BreakfastChildren,
]),
})
export const ancillaryPackageSchema = z.object({
categoryName: z.string(),
ancillaryContent: z.array(ancillaryContentSchema),
})

View File

@@ -0,0 +1,77 @@
import { z } from "zod"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { nullableNumberValidator } from "@scandic-hotels/common/utils/zod/numberValidator"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { RateTypeEnum } from "../../../enums/rateType"
export const corporateChequeSchema = z
.object({
additionalPricePerStay: nullableNumberValidator,
currency: z.nativeEnum(CurrencyEnum).nullish(),
numberOfBonusCheques: nullableNumberValidator,
})
.transform((data) => ({
additionalPricePerStay: data.additionalPricePerStay,
currency: data.currency,
numberOfCheques: data.numberOfBonusCheques,
}))
export const redemptionSchema = z.object({
additionalPricePerStay: nullableNumberValidator,
currency: z.nativeEnum(CurrencyEnum).nullish(),
pointsPerNight: nullableNumberValidator,
pointsPerStay: nullableNumberValidator,
})
export const priceSchema = z.object({
currency: z.nativeEnum(CurrencyEnum),
omnibusPricePerNight: nullableNumberValidator,
pricePerNight: nullableNumberValidator,
pricePerStay: nullableNumberValidator,
regularPricePerNight: nullableNumberValidator,
regularPricePerStay: nullableNumberValidator,
})
const partialPriceSchema = z.object({
rateCode: nullableStringValidator,
rateType: z.nativeEnum(RateTypeEnum).catch((err) => {
const issue = err.error.issues[0]
// This is necessary to handle cases were a
// new `rateType` is added in the API that has
// not yet been handled in web
if (issue.code === "invalid_enum_value") {
return issue.received.toString() as RateTypeEnum
}
return RateTypeEnum.Regular
}),
})
export const productTypeCorporateChequeSchema = z
.object({
localPrice: corporateChequeSchema,
requestedPrice: corporateChequeSchema.nullish(),
})
.merge(partialPriceSchema)
export const productTypePriceSchema = z
.object({
localPrice: priceSchema,
requestedPrice: priceSchema.nullish(),
})
.merge(partialPriceSchema)
export const productTypePointsSchema = z
.object({
localPrice: redemptionSchema,
requestedPrice: redemptionSchema.nullish(),
hasEnoughPoints: z.boolean().optional().default(false),
})
.merge(partialPriceSchema)
export const productTypeVoucherSchema = z
.object({
numberOfVouchers: nullableNumberValidator,
})
.merge(partialPriceSchema)

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
export const relationshipsSchema = z.object({
links: z.array(
z.object({
type: z.string(),
url: z.string().url(),
})
),
})

View File

@@ -0,0 +1,57 @@
import { z } from "zod"
import { RoomPackageCodeEnum } from "../../../../enums/roomFilter"
import { AvailabilityEnum } from "../../../../enums/selectHotel"
import {
corporateChequeProduct,
priceProduct,
productSchema,
redemptionProduct,
voucherProduct,
} from "./product"
export const roomConfigurationSchema = z.object({
breakfastIncludedInAllRatesMember: z.boolean().default(false),
breakfastIncludedInAllRates: z.boolean().default(false),
features: z
.array(
z.object({
inventory: z.number(),
code: z.enum([
RoomPackageCodeEnum.PET_ROOM,
RoomPackageCodeEnum.ALLERGY_ROOM,
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
]),
})
)
.default([]),
products: z.array(productSchema).default([]),
roomsLeft: z.number(),
roomType: z.string(),
roomTypeCode: z.string(),
status: z
.nativeEnum(AvailabilityEnum)
.nullish()
.default(AvailabilityEnum.NotAvailable),
// Red
campaign: z
.array(priceProduct)
.optional()
.transform((val) => (val ? val.filter(Boolean) : [])),
// Blue
code: z
.array(z.union([corporateChequeProduct, priceProduct, voucherProduct]))
.optional()
.transform((val) => (val ? val.filter(Boolean) : [])),
// Beige
regular: z
.array(priceProduct)
.optional()
.transform((val) => (val ? val.filter(Boolean) : [])),
// Burgundy
redemptions: z
.array(redemptionProduct)
.optional()
.transform((val) => (val ? val.filter(Boolean) : [])),
})

View File

@@ -0,0 +1,132 @@
import { z } from "zod"
import { RateEnum } from "../../../../enums/rate"
import {
productTypeCorporateChequeSchema,
productTypePointsSchema,
productTypePriceSchema,
productTypeVoucherSchema,
} from "../productTypePrice"
import { rateDefinitionSchema } from "./rateDefinition"
const baseProductSchema = z.object({
// transform empty string to undefined
bookingCode: z
.string()
.optional()
.transform((val) => val),
// Used to set the rate that we use to chose titles etc.
rate: z.nativeEnum(RateEnum).default(RateEnum.save),
rateDefinition: rateDefinitionSchema.optional().transform((val) =>
val
? val
: {
breakfastIncluded: false,
cancellationRule: "",
cancellationText: "",
displayPriceRed: false,
isCampaignRate: false,
isMemberRate: false,
isPackageRate: false,
generalTerms: [],
mustBeGuaranteed: false,
rateCode: "",
rateType: "",
title: "",
}
),
rateDefinitionMember: rateDefinitionSchema.optional(),
})
function mapBaseProduct(baseProduct: typeof baseProductSchema._type) {
return {
bookingCode: baseProduct.bookingCode,
rate: baseProduct.rate,
rateDefinition: baseProduct.rateDefinition,
rateDefinitionMember: baseProduct.rateDefinitionMember,
}
}
export const corporateChequeProduct = z
.object({
productType: z
.object({
bonusCheque: productTypeCorporateChequeSchema,
})
.transform((data) => ({
corporateCheque: data.bonusCheque,
})),
})
.merge(baseProductSchema)
.transform((data) => ({
...data.productType,
...mapBaseProduct(data),
}))
export const priceProduct = z
.object({
productType: z.object({
member: productTypePriceSchema.nullish().default(null),
public: productTypePriceSchema.nullish().default(null),
}),
})
.merge(baseProductSchema)
.transform((data) => ({
...data.productType,
...mapBaseProduct(data),
}))
export const redemptionProduct = z
.object({
redemption: productTypePointsSchema,
})
.merge(baseProductSchema)
.transform((data) => ({
redemption: data.redemption,
...mapBaseProduct(data),
}))
export const redemptionsProduct = z
.object({
productType: z.object({
redemptions: z
.array(productTypePointsSchema.merge(baseProductSchema))
.transform((data) =>
data.map(
({
bookingCode,
rate,
rateDefinition,
rateDefinitionMember,
...redemption
}) => ({
bookingCode,
rate,
rateDefinition,
rateDefinitionMember,
redemption,
})
)
),
}),
})
.transform((data) => data.productType.redemptions)
export const voucherProduct = z
.object({
productType: z.object({
voucher: productTypeVoucherSchema,
}),
})
.merge(baseProductSchema)
.transform((data) => ({
...data.productType,
...mapBaseProduct(data),
}))
export const productSchema = z.union([
corporateChequeProduct,
redemptionsProduct,
voucherProduct,
priceProduct,
])

View File

@@ -0,0 +1,18 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
export const rateDefinitionSchema = z.object({
breakfastIncluded: z.boolean(),
cancellationRule: z.string(),
cancellationText: nullableStringValidator,
displayPriceRed: z.boolean().default(false),
generalTerms: z.array(z.string()),
isCampaignRate: z.boolean().default(false),
isMemberRate: z.boolean().default(false),
isPackageRate: z.boolean().default(false),
mustBeGuaranteed: z.boolean(),
rateCode: z.string(),
rateType: nullableStringValidator,
title: z.string(),
})