@revolugo/common 6.14.4 → 6.14.5-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -8
- package/src/constants/hotel-room-offer.ts +11 -0
- package/src/constants/index.ts +2 -1
- package/src/constants/locales.ts +5 -5
- package/src/constants/tax.ts +9 -0
- package/src/icons/index.ts +0 -1
- package/src/schemas/cancellation-policies.ts +18 -0
- package/src/schemas/currency.ts +7 -0
- package/src/schemas/hotel-offer-request.ts +43 -0
- package/src/schemas/hotel-offer.ts +58 -0
- package/src/schemas/hotel-room-offer.ts +99 -0
- package/src/schemas/hotel-room.ts +106 -0
- package/src/schemas/hotel.ts +360 -0
- package/src/schemas/index.ts +10 -0
- package/src/schemas/list-polling-meta.ts +43 -0
- package/src/schemas/tag.ts +13 -0
- package/src/schemas/taxes.ts +34 -0
- package/src/types/elements/hotel.ts +2 -0
- package/src/types/severities.ts +1 -0
- package/src/utils/case-transformers.ts +5 -5
- package/src/utils/get-sanitized-room-count.ts +29 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/is-object.ts +2 -0
- package/src/utils/keys-case-transformer.ts +118 -0
- package/src/utils/transform-schema-keys.ts +143 -0
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revolugo/common",
|
|
3
|
-
"version": "6.14.
|
|
3
|
+
"version": "6.14.5-beta.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Revolugo common",
|
|
6
6
|
"author": "Revolugo",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.18.1 <25"
|
|
10
|
+
},
|
|
11
|
+
"volta": {
|
|
12
|
+
"extends": "../../package.json"
|
|
13
|
+
},
|
|
8
14
|
"files": [
|
|
9
15
|
"src"
|
|
10
16
|
],
|
|
@@ -17,6 +23,7 @@
|
|
|
17
23
|
"./icons": "./src/icons/index.ts",
|
|
18
24
|
"./map": "./src/map/index.ts",
|
|
19
25
|
"./models": "./src/models/index.ts",
|
|
26
|
+
"./schemas": "./src/schemas/index.ts",
|
|
20
27
|
"./types": "./src/types/index.ts",
|
|
21
28
|
"./utils": "./src/utils/index.ts"
|
|
22
29
|
},
|
|
@@ -25,13 +32,9 @@
|
|
|
25
32
|
"dayjs": "1.11.19",
|
|
26
33
|
"ky": "1.14.0",
|
|
27
34
|
"slugify": "1.6.6",
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"node": ">=20.18.1 <25"
|
|
32
|
-
},
|
|
33
|
-
"volta": {
|
|
34
|
-
"extends": "../../package.json"
|
|
35
|
+
"type-fest": "5.3.0",
|
|
36
|
+
"uuid": "13.0.0",
|
|
37
|
+
"zod": "3.25.76"
|
|
35
38
|
},
|
|
36
39
|
"scripts": {
|
|
37
40
|
"test": "vitest"
|
package/src/constants/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export * from '../countries/constants.ts'
|
|
2
2
|
export * from '../currencies/constants.ts'
|
|
3
|
-
|
|
4
3
|
export * from './environment.ts'
|
|
5
4
|
export * from './hotel.ts'
|
|
6
5
|
export * from './hotel-offers.ts'
|
|
6
|
+
export * from './hotel-room-offer.ts'
|
|
7
7
|
export * from './locales.ts'
|
|
8
8
|
export * from './measurement.ts'
|
|
9
9
|
export * from './poller.ts'
|
|
10
|
+
export * from './tax.ts'
|
|
10
11
|
export * from './time.ts'
|
|
11
12
|
export * from './stay-taxes-info.ts'
|
|
12
13
|
export * from './venue.ts'
|
package/src/constants/locales.ts
CHANGED
|
@@ -33,11 +33,11 @@ export function langFromString(langStr: string): Lang | undefined {
|
|
|
33
33
|
export const LANG_TO_LOCALE: Record<Lang, Locale> = {
|
|
34
34
|
[Lang.EN]: Locale.en_US,
|
|
35
35
|
[Lang.FR]: Locale.fr_FR,
|
|
36
|
-
[Lang.DE]: Locale.
|
|
37
|
-
[Lang.ES]: Locale.
|
|
38
|
-
[Lang.IT]: Locale.
|
|
39
|
-
[Lang.NL]: Locale.
|
|
40
|
-
[Lang.PT]: Locale.
|
|
36
|
+
[Lang.DE]: Locale.de_DE,
|
|
37
|
+
[Lang.ES]: Locale.es_ES,
|
|
38
|
+
[Lang.IT]: Locale.it_IT,
|
|
39
|
+
[Lang.NL]: Locale.nl_NL,
|
|
40
|
+
[Lang.PT]: Locale.pt_PT,
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/* @__PURE__ */
|
package/src/icons/index.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export const CANCELLATION_POLICY_SCHEMA = z.object({
|
|
5
|
+
date_from: z.string().openapi({
|
|
6
|
+
description:
|
|
7
|
+
'The start date and time of the cancellation policy, given in the hotel timezone.',
|
|
8
|
+
}),
|
|
9
|
+
date_to: z.string().openapi({
|
|
10
|
+
description:
|
|
11
|
+
'The end date and time of the cancellation policy, given in the hotel timezone.',
|
|
12
|
+
}),
|
|
13
|
+
penalty_percentage: z.number().min(0).max(100).openapi({
|
|
14
|
+
description:
|
|
15
|
+
'The penalty percentage that is due in case of cancellation during the **date_from** to **date_to** period range.',
|
|
16
|
+
}),
|
|
17
|
+
})
|
|
18
|
+
/* eslint-enable camelcase */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { CountryIso2Code } from '../constants/index.ts'
|
|
4
|
+
|
|
5
|
+
export const ADULT_COUNT_SCHEMA = z.number().int().min(1).max(200).openapi({
|
|
6
|
+
description:
|
|
7
|
+
'The total number of adults who will be staying in the property.',
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const CHECK_IN_DATE_SCHEMA = z
|
|
11
|
+
.string()
|
|
12
|
+
.regex(/^(19|20)\d\d[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$/u)
|
|
13
|
+
.openapi({ description: 'Date of check-in formatted as YYYY-MM-DD.' })
|
|
14
|
+
|
|
15
|
+
export const CHECK_OUT_DATE_SCHEMA = z
|
|
16
|
+
.string()
|
|
17
|
+
.regex(/^(19|20)\d\d[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$/u)
|
|
18
|
+
.openapi({ description: 'Date of check-out formatted as YYYY-MM-DD.' })
|
|
19
|
+
|
|
20
|
+
export const CHILDREN_SCHEMA = z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(/^(([0-9]{1})|(1[0-7]){1})(,(([0-9]{1})|(1[0-7]){1}))*$|^$/u)
|
|
23
|
+
.openapi({
|
|
24
|
+
description:
|
|
25
|
+
'A comma-separated list of child ages (0 up to 17). e.g.: "3,7" represents 2 children respectively 3 and 7 years old.',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const COUNTRY_ISO2_CODE_SCHEMA = z
|
|
29
|
+
.nativeEnum(CountryIso2Code)
|
|
30
|
+
.refine(check => Object.values(CountryIso2Code).includes(check), {
|
|
31
|
+
message: 'Invalid ISO Alpha-2 country code.',
|
|
32
|
+
})
|
|
33
|
+
.openapi({
|
|
34
|
+
description: 'ISO Alpha-2 country code.',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const SOURCE_MARKET_SCHEMA = COUNTRY_ISO2_CODE_SCHEMA.openapi(
|
|
38
|
+
'sourceMarket',
|
|
39
|
+
{
|
|
40
|
+
description:
|
|
41
|
+
'For sourcing availability within certain markets, a source market option may be used to get more accurate prices. You may use any valid ISO Alpha-2 country code, e.g. JP.',
|
|
42
|
+
},
|
|
43
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { CURRENCY_SCHEMA } from './currency.ts'
|
|
5
|
+
import {
|
|
6
|
+
ADULT_COUNT_SCHEMA,
|
|
7
|
+
CHECK_IN_DATE_SCHEMA,
|
|
8
|
+
CHECK_OUT_DATE_SCHEMA,
|
|
9
|
+
CHILDREN_SCHEMA,
|
|
10
|
+
} from './hotel-offer-request.ts'
|
|
11
|
+
import { HOTEL_ROOM_OFFER_SCHEMA } from './hotel-room-offer.ts'
|
|
12
|
+
import { HOTEL_IMAGES, HOTEL_SCHEMA, VENUES_SCHEMA } from './hotel.ts'
|
|
13
|
+
import { LIST_POLLING_META_SCHEMA } from './list-polling-meta.ts'
|
|
14
|
+
import { TAGS_SCHEMA } from './tag.ts'
|
|
15
|
+
|
|
16
|
+
export const HOTEL_OFFER_SCHEMA = HOTEL_SCHEMA.extend({
|
|
17
|
+
hotel_images: HOTEL_IMAGES.openapi({
|
|
18
|
+
description: `⚠️ Cached images of the hotel. May be empty, use the endpoint /hotels/{id}/images to get the latest images.
|
|
19
|
+
|
|
20
|
+
List of hotel images in various sizes featuring an indicator for the primary (hero) image
|
|
21
|
+
`,
|
|
22
|
+
}),
|
|
23
|
+
hotel_room_offers: z.array(HOTEL_ROOM_OFFER_SCHEMA),
|
|
24
|
+
tags: TAGS_SCHEMA,
|
|
25
|
+
venues: VENUES_SCHEMA,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const HOTEL_OFFERS_SCHEMA = z.array(HOTEL_OFFER_SCHEMA)
|
|
29
|
+
|
|
30
|
+
export const HOTEL_OFFERS_RESPONSE_SCHEMA = z.object({
|
|
31
|
+
data: z.object({
|
|
32
|
+
adult_count: ADULT_COUNT_SCHEMA,
|
|
33
|
+
check_in_date: CHECK_IN_DATE_SCHEMA,
|
|
34
|
+
check_out_date: CHECK_OUT_DATE_SCHEMA,
|
|
35
|
+
children: CHILDREN_SCHEMA.optional().nullish(),
|
|
36
|
+
currency: CURRENCY_SCHEMA,
|
|
37
|
+
hotel_offers: HOTEL_OFFERS_SCHEMA.openapi({
|
|
38
|
+
description: 'List of Hotel Offers',
|
|
39
|
+
}),
|
|
40
|
+
price_histogram: z
|
|
41
|
+
.array(z.number())
|
|
42
|
+
.openapi({
|
|
43
|
+
description:
|
|
44
|
+
'**Hotel Offers** price histogram dataset based on the price of the cheapest **Hotel Room Offer** included on each **Hotel Offer** returned. It represents the number of available **Hotel Offers** grouped by price sorted ascendingly. Each item of the list represents a price step based on returned **price_min**, **price_max** and requested **price_histogram_step_count**',
|
|
45
|
+
})
|
|
46
|
+
.nullish(),
|
|
47
|
+
price_max: z.number().nullish().openapi({
|
|
48
|
+
description: 'Maximum price of available returned **Hotel Offers**',
|
|
49
|
+
}),
|
|
50
|
+
price_min: z.number().nullish().openapi({
|
|
51
|
+
description: 'Minimum price of available returned **Hotel Offers**',
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
event: z.any().optional(),
|
|
55
|
+
meta: LIST_POLLING_META_SCHEMA.extend({
|
|
56
|
+
total_count: z.number(),
|
|
57
|
+
}),
|
|
58
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BreakfastOption,
|
|
6
|
+
HotelRoomOfferType,
|
|
7
|
+
PackageType,
|
|
8
|
+
} from '../constants/index.ts'
|
|
9
|
+
|
|
10
|
+
import { CANCELLATION_POLICY_SCHEMA } from './cancellation-policies.ts'
|
|
11
|
+
import { CURRENCY_SCHEMA } from './currency.ts'
|
|
12
|
+
import { SOURCE_MARKET_SCHEMA } from './hotel-offer-request.ts'
|
|
13
|
+
import { HOTEL_ROOMS_SCHEMA } from './hotel-room.ts'
|
|
14
|
+
import { TAGS_SCHEMA } from './tag.ts'
|
|
15
|
+
import { TAXES_SCHEMA } from './taxes.ts'
|
|
16
|
+
|
|
17
|
+
export const BREAKFAST_OPTION_DESCRIPTION = `This parameter describes the breakfast option for the given **Hotel Room Offer**:
|
|
18
|
+
- **breakfast_option = "${BreakfastOption.Included}"**:
|
|
19
|
+
1. When **HotelRoomOffer.type = "${HotelRoomOfferType.Package}"**:
|
|
20
|
+
the returned **Hotel Room Offer** includes breakfast for the requested **guest count (adult_count + children over 3)**, you cannot choose otherwise, and the returned **price** already includes it,
|
|
21
|
+
2. When **HotelRoomOffer.type = "${HotelRoomOfferType.HotelRoom}"**:
|
|
22
|
+
the returned **Hotel Room Offer** includes breakfast for the **Hotel Room Offer** maximum occupancy, you cannot choose otherwise, and the returned **price** already includes it,
|
|
23
|
+
-**breakfast_option = "${BreakfastOption.Optional}"**: the returned **Hotel Room Offer** does not include by default the breakfast and so does the returned **price**, but you'll be able to let your customers choose to add it to their booking. In that case, a **breakfast_price_per_guest_per_night** expressed in the requested **currency** will be available in the returned data, and you'll be to perform one of the two following actions: \n - Call **[Create Hotel Room Offer endpoint](/v1/documentation#operation/postV1Hotel_room_offers)** and get a fresh **Hotel Room Offer** with updated price \n - Compute and display the total price of the **Hotel Room Offer** related to the guest_count and night count requested including extra breafasts.,
|
|
24
|
+
- **breakfast_option = "${BreakfastOption.NotIncluded}"**: the returned **Hotel Room Offer** does not include breakfast and you cannot choose otherwise through API. Guest may still be able to add extra breakfast option(s) at the time of check-in directly at the hotel's reception.`
|
|
25
|
+
|
|
26
|
+
export const PACKAGE_TYPES_DESCRIPTION = `An **Hotel Room Offer** of type **${HotelRoomOfferType.Package}** can be of **4 types**, described by **package_type** parameter: \n\n - **${PackageType.Cheapest}** : The cheapest combination of hotel rooms that can accommodate the requested guest count. Note that it may not match the requested room count (e.g., 4 guests and 2 rooms requested may return an hotel room package including only 1 room with an occupancy of 4). \n\n -**${PackageType.MatchingRoomCount}** : the cheapest hotel room package that can accommodate the given guest count and that matches exactly the room count given. \n\n -**${PackageType.BestMatch}** : The cheapest hotel room package that matches the given room and guest count with a balanced distribution of guests across the rooms (e.g.; 8 guests and 3 rooms requested may return an **Hotel Room Offer** package including 2 rooms with an occupancy of 3 on each one and 1 room with an occupancy of 2). \n\n -**${PackageType.Regular}** : any other available package.`
|
|
27
|
+
|
|
28
|
+
export const HOTEL_ROOM_OFFER_SCHEMA = z
|
|
29
|
+
.object({
|
|
30
|
+
breakfast_count: z.number().min(0).optional().nullish().openapi({
|
|
31
|
+
description:
|
|
32
|
+
'Quantity of breakfast per night included in the given **Hotel Room Offer**',
|
|
33
|
+
}),
|
|
34
|
+
breakfast_option: z.nativeEnum(BreakfastOption).openapi({
|
|
35
|
+
description: BREAKFAST_OPTION_DESCRIPTION,
|
|
36
|
+
}),
|
|
37
|
+
breakfast_price_per_guest_per_night: z
|
|
38
|
+
.number()
|
|
39
|
+
.optional()
|
|
40
|
+
.nullish()
|
|
41
|
+
.openapi({
|
|
42
|
+
description:
|
|
43
|
+
'Price of breakfast per guest per night for the given **Hotel Room Offer**, expressed in the requested **currency**, when applicable.\n\n <div style="background-color: #ffffef; padding: 20px; border: 1px solid lightgrey; border-radius: 5px;"><b style="color: red; margin-top: 10px;">🛑 DEPRECATED.</b>\n\n <b style="color: orange;">Field renamed to "breakfast_unit_price"</div>',
|
|
44
|
+
}),
|
|
45
|
+
breakfast_unit_price: z.number().optional().nullish().openapi({
|
|
46
|
+
description:
|
|
47
|
+
'Price of breakfast per guest per night for the given **Hotel Room Offer**, expressed in the requested **currency**, when applicable.',
|
|
48
|
+
}),
|
|
49
|
+
cancellation_policies: z
|
|
50
|
+
.array(CANCELLATION_POLICY_SCHEMA)
|
|
51
|
+
.openapi('cancellationPoliciesApi', {
|
|
52
|
+
description:
|
|
53
|
+
'The list of cancellation policies applied to the given **Hotel Room Offer**.',
|
|
54
|
+
}),
|
|
55
|
+
count: z
|
|
56
|
+
.number()
|
|
57
|
+
.optional()
|
|
58
|
+
.nullish()
|
|
59
|
+
.openapi({
|
|
60
|
+
description: `When **type = ${HotelRoomOfferType.HotelRoom}**: this parameters represents the available quantity for the given **Hotel Room Offer**.
|
|
61
|
+
When **type = ${HotelRoomOfferType.Package}**: count = 1 always.`,
|
|
62
|
+
}),
|
|
63
|
+
currency: CURRENCY_SCHEMA,
|
|
64
|
+
hotel_id: z
|
|
65
|
+
.string()
|
|
66
|
+
.openapi({ description: 'id of the associated Hotel.' }),
|
|
67
|
+
hotel_rooms: HOTEL_ROOMS_SCHEMA.openapi({
|
|
68
|
+
description: 'List of Hotel Rooms included in the Hotel Room Offer.',
|
|
69
|
+
}),
|
|
70
|
+
id: z.string().openapi({ description: 'Hotel Room Offer id.' }).optional(),
|
|
71
|
+
package_type: z
|
|
72
|
+
.nativeEnum(PackageType)
|
|
73
|
+
.openapi({
|
|
74
|
+
description: PACKAGE_TYPES_DESCRIPTION,
|
|
75
|
+
})
|
|
76
|
+
.nullish()
|
|
77
|
+
.optional(),
|
|
78
|
+
price: z.number().openapi({
|
|
79
|
+
description:
|
|
80
|
+
'Price with taxes NOT INCLUDED of the given **Hotel Room Offer** including breakfast(s) when applicable, expressed in the requested **currency**.',
|
|
81
|
+
}),
|
|
82
|
+
source_market: SOURCE_MARKET_SCHEMA,
|
|
83
|
+
tags: TAGS_SCHEMA,
|
|
84
|
+
tax_included_price: z.number().openapi({
|
|
85
|
+
description: `Price of the given **Hotel Room Offer** including breakfast(s) when applicable, and including all taxes from returned **taxes** list parameter expressed in the requested **currency**.\n\nThis data is not returned for a **Hotel Room Offer** of type **${HotelRoomOfferType.HotelRoom}**, you'll need to compute and display the actual tax included price of the final **Hotel Room** package corresponding to the guest count and night count requested, or make a call to the **[Create Hotel Room Offer](/v1/documentation#operation/postV1Hotel_room_offers)** endpoint in order to get a valid and bookable **Hotel Room Offer** where **type = ${HotelRoomOfferType.Package}** based on a packaged list of **Hotel Room Offers** of type **${HotelRoomOfferType.HotelRoom}**.`,
|
|
86
|
+
}),
|
|
87
|
+
taxes: TAXES_SCHEMA,
|
|
88
|
+
type: z
|
|
89
|
+
.nativeEnum(HotelRoomOfferType)
|
|
90
|
+
.openapi({
|
|
91
|
+
description: `Hotel Room Offer type.\n\n **Hotel Room Offers** with **type = "${HotelRoomOfferType.Package}"** are **Hotel Room Offers** that are already bookable and you'll be able to follow the next step of the **Booking Flow** calling **[Create Booking Policies endpoint](/v1/documentation#operation/postV1Booking_policies)**. \n\n Otherwise, you'll be able to create a new **Hotel Room Offer** with **type = "${HotelRoomOfferType.Package}"** from multiple **Hotel Room Offers** with **type = "${HotelRoomOfferType.HotelRoom}"**. See **[Create Hotel Room Offer endpoint](/v1/documentation#operation/postV1Hotel_room_offers)** for details.`,
|
|
92
|
+
})
|
|
93
|
+
.nullish()
|
|
94
|
+
.optional(),
|
|
95
|
+
})
|
|
96
|
+
.openapi('hotelRoomOfferApi', {
|
|
97
|
+
description: 'Description of the Hotel Room Offer.',
|
|
98
|
+
})
|
|
99
|
+
/* eslint-enable camelcase */
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { HOTEL_IMAGE } from './hotel.ts'
|
|
5
|
+
|
|
6
|
+
export const BED_SCHEMA = z
|
|
7
|
+
.object({
|
|
8
|
+
count: z.number().openapi({
|
|
9
|
+
description: 'Number of beds of the given type in the room.',
|
|
10
|
+
}),
|
|
11
|
+
name: z.string().openapi({ description: 'Bed name.' }),
|
|
12
|
+
occupancy: z.number().openapi({ description: 'Bed occupancy.' }),
|
|
13
|
+
})
|
|
14
|
+
.openapi('bedApi')
|
|
15
|
+
|
|
16
|
+
export const BEDS_SCHEMA = z
|
|
17
|
+
.array(z.array(BED_SCHEMA))
|
|
18
|
+
.openapi({
|
|
19
|
+
description: `Beds list.
|
|
20
|
+
Each nested array of beds represents a single combination of possible beds.
|
|
21
|
+
e.g.: The following object represents **1 double bed or 1 sofa bed and 1 double bed or 1 single bed**:
|
|
22
|
+
\`\`\`
|
|
23
|
+
[
|
|
24
|
+
[
|
|
25
|
+
{ count: 1, name: 'double', occupancy: 2 },
|
|
26
|
+
{ count: 1, name: 'sofa', occupancy: 1 }
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
{ count: 1, name: 'double', occupancy: 2 },
|
|
30
|
+
{ count: 1, name: 'single', occupancy: 1 }
|
|
31
|
+
]
|
|
32
|
+
]
|
|
33
|
+
\`\`\`
|
|
34
|
+
`,
|
|
35
|
+
})
|
|
36
|
+
.openapi('bedsApi')
|
|
37
|
+
|
|
38
|
+
export const HOTEL_ROOM_SCHEMA = z
|
|
39
|
+
.object({
|
|
40
|
+
amenities: z
|
|
41
|
+
.array(z.string().openapi('amenity'))
|
|
42
|
+
.openapi({
|
|
43
|
+
description:
|
|
44
|
+
'List of amenities in the room. May be subject to changes at the Hotel.',
|
|
45
|
+
})
|
|
46
|
+
.nullish(),
|
|
47
|
+
beds: BEDS_SCHEMA,
|
|
48
|
+
beds_pretty: z
|
|
49
|
+
.string()
|
|
50
|
+
.openapi({ description: 'Prettified and localized list of beds' }),
|
|
51
|
+
count: z.number().openapi({
|
|
52
|
+
description: 'Hotel Room count included in the Hotel Room Offer.',
|
|
53
|
+
}),
|
|
54
|
+
description: z.string().openapi({ description: 'Hotel Room description.' }),
|
|
55
|
+
highres_images: z
|
|
56
|
+
.boolean()
|
|
57
|
+
.openapi({ description: 'Whether high resolution images are available.' })
|
|
58
|
+
.optional()
|
|
59
|
+
.nullish(),
|
|
60
|
+
id: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.openapi({ description: 'Hotel Room id, when applicable.' }),
|
|
64
|
+
image_indexes: z.array(z.number()).optional().nullish().openapi({
|
|
65
|
+
description:
|
|
66
|
+
'List of indexes corresponding to image names for the given Hotel Room among the related hotel images.',
|
|
67
|
+
}),
|
|
68
|
+
images: z
|
|
69
|
+
.array(HOTEL_IMAGE)
|
|
70
|
+
.openapi({
|
|
71
|
+
description: 'Hotel Room images.',
|
|
72
|
+
})
|
|
73
|
+
.optional()
|
|
74
|
+
.nullish(),
|
|
75
|
+
lowres_images: z
|
|
76
|
+
.boolean()
|
|
77
|
+
.openapi({ description: 'Whether low resolution images are available.' })
|
|
78
|
+
.optional()
|
|
79
|
+
.nullish(),
|
|
80
|
+
occupancy: z
|
|
81
|
+
.number()
|
|
82
|
+
.openapi({ description: 'Total occupancy of a single hotel room.' }),
|
|
83
|
+
rich_description: z.string().optional().nullish().openapi({
|
|
84
|
+
description: 'Rich Hotel Room description. May contain HTML tags markup.',
|
|
85
|
+
}),
|
|
86
|
+
room_square_feet: z.number().optional().nullish().openapi({
|
|
87
|
+
description:
|
|
88
|
+
'Room surface in square feet. May be subject to changes at the Hotel.',
|
|
89
|
+
}),
|
|
90
|
+
room_square_meters: z.number().optional().nullish().openapi({
|
|
91
|
+
description:
|
|
92
|
+
'Room surface in square meters. May be subject to changes at the Hotel.',
|
|
93
|
+
}),
|
|
94
|
+
thumb_images: z
|
|
95
|
+
.boolean()
|
|
96
|
+
.openapi({
|
|
97
|
+
description:
|
|
98
|
+
'Whether thumb resolution images are available (in order to display them as image carousel navigation for instance).',
|
|
99
|
+
})
|
|
100
|
+
.optional()
|
|
101
|
+
.nullish(),
|
|
102
|
+
})
|
|
103
|
+
.openapi({ description: 'Hotel Room details.' })
|
|
104
|
+
|
|
105
|
+
export const HOTEL_ROOMS_SCHEMA = z.array(HOTEL_ROOM_SCHEMA)
|
|
106
|
+
/* eslint-enable camelcase */
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { CountryIso2Code } from '../constants/index.ts'
|
|
5
|
+
|
|
6
|
+
import { CURRENCY_SCHEMA } from './currency.ts'
|
|
7
|
+
|
|
8
|
+
export const AMENITIES_SCHEMA = z.object({
|
|
9
|
+
air_conditioning: z
|
|
10
|
+
.boolean()
|
|
11
|
+
.optional()
|
|
12
|
+
.openapi({ description: 'Air conditioning.' }),
|
|
13
|
+
airport_transportation: z
|
|
14
|
+
.boolean()
|
|
15
|
+
.optional()
|
|
16
|
+
.openapi({ description: 'Airport transportation service.' }),
|
|
17
|
+
business_center: z
|
|
18
|
+
.boolean()
|
|
19
|
+
.optional()
|
|
20
|
+
.openapi({ description: 'Business center.' }),
|
|
21
|
+
car_rent_desk: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.optional()
|
|
24
|
+
.openapi({ description: 'Car rental desk service.' }),
|
|
25
|
+
children_allowed: z
|
|
26
|
+
.boolean()
|
|
27
|
+
.optional()
|
|
28
|
+
.openapi({ description: 'Children welcomed.' }),
|
|
29
|
+
clothing_iron: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.openapi({ description: 'Clothing iron.' }),
|
|
33
|
+
coffee_tea_maker: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.openapi({ description: 'Coffea/tea maker.' }),
|
|
37
|
+
combination: z.boolean().optional().openapi({ description: 'Combination.' }),
|
|
38
|
+
continental_breakfast: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.openapi({ description: 'Continental Breakfast.' }),
|
|
42
|
+
data_ports: z
|
|
43
|
+
.boolean()
|
|
44
|
+
.optional()
|
|
45
|
+
.openapi({ description: 'Data ports in room.' }),
|
|
46
|
+
dry_cleaning: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.optional()
|
|
49
|
+
.openapi({ description: 'Dry cleaning.' }),
|
|
50
|
+
electronic_room_keys: z
|
|
51
|
+
.boolean()
|
|
52
|
+
.optional()
|
|
53
|
+
.openapi({ description: 'Electornic room keys.' }),
|
|
54
|
+
exterior_room_entrance: z
|
|
55
|
+
.boolean()
|
|
56
|
+
.optional()
|
|
57
|
+
.openapi({ description: 'Exterior room entrance.' }),
|
|
58
|
+
family_rooms: z
|
|
59
|
+
.boolean()
|
|
60
|
+
.optional()
|
|
61
|
+
.openapi({ description: 'Family rooms.' }),
|
|
62
|
+
fitness_facility: z
|
|
63
|
+
.boolean()
|
|
64
|
+
.optional()
|
|
65
|
+
.openapi({ description: 'Fitness facility.' }),
|
|
66
|
+
game_room: z.boolean().optional().openapi({ description: 'Game room.' }),
|
|
67
|
+
golf_course: z.boolean().optional().openapi({ description: 'Golf course.' }),
|
|
68
|
+
hair_dryer: z.boolean().optional().openapi({ description: 'Hair dryer.' }),
|
|
69
|
+
handicap_accessible: z
|
|
70
|
+
.boolean()
|
|
71
|
+
.optional()
|
|
72
|
+
.openapi({ description: 'Handicap Accessible.' }),
|
|
73
|
+
in_house_bar: z
|
|
74
|
+
.boolean()
|
|
75
|
+
.optional()
|
|
76
|
+
.openapi({ description: 'In house bar.' }),
|
|
77
|
+
in_house_dining: z
|
|
78
|
+
.boolean()
|
|
79
|
+
.optional()
|
|
80
|
+
.openapi({ description: 'In house dining.' }),
|
|
81
|
+
in_room_movies: z
|
|
82
|
+
.boolean()
|
|
83
|
+
.optional()
|
|
84
|
+
.openapi({ description: 'In room movies.' }),
|
|
85
|
+
indoor_pool: z.boolean().optional().openapi({ description: 'Indoor pool.' }),
|
|
86
|
+
interior_room_entrance: z
|
|
87
|
+
.boolean()
|
|
88
|
+
.optional()
|
|
89
|
+
.openapi({ description: 'Interior room entrance.' }),
|
|
90
|
+
kitchen: z.boolean().optional().openapi({ description: 'Kitchen.' }),
|
|
91
|
+
map: z.boolean().optional().openapi({ description: 'Map.' }),
|
|
92
|
+
meeting_rooms: z
|
|
93
|
+
.boolean()
|
|
94
|
+
.optional()
|
|
95
|
+
.openapi({ description: 'Meeting rooms.' }),
|
|
96
|
+
mini_bar_in_room: z
|
|
97
|
+
.boolean()
|
|
98
|
+
.optional()
|
|
99
|
+
.openapi({ description: 'mini bar in room.' }),
|
|
100
|
+
non_smoking_rooms: z
|
|
101
|
+
.boolean()
|
|
102
|
+
.optional()
|
|
103
|
+
.openapi({ description: 'Non smoking rooms.' }),
|
|
104
|
+
outdoor_pool: z
|
|
105
|
+
.boolean()
|
|
106
|
+
.optional()
|
|
107
|
+
.openapi({ description: 'Outdoor pool.' }),
|
|
108
|
+
parking_garage: z
|
|
109
|
+
.boolean()
|
|
110
|
+
.optional()
|
|
111
|
+
.openapi({ description: 'Parking garage.' }),
|
|
112
|
+
pets_allowed: z
|
|
113
|
+
.boolean()
|
|
114
|
+
.optional()
|
|
115
|
+
.openapi({ description: 'Pets allowed.' }),
|
|
116
|
+
restricted_access: z
|
|
117
|
+
.boolean()
|
|
118
|
+
.optional()
|
|
119
|
+
.openapi({ description: 'Restricted access.' }),
|
|
120
|
+
room_service: z
|
|
121
|
+
.boolean()
|
|
122
|
+
.optional()
|
|
123
|
+
.openapi({ description: 'Room service.' }),
|
|
124
|
+
safe: z.boolean().optional().openapi({ description: 'Safe in room.' }),
|
|
125
|
+
sauna: z.boolean().optional().openapi({ description: 'Sauna.' }),
|
|
126
|
+
t_v_in_room: z.boolean().optional().openapi({ description: 'TV in room.' }),
|
|
127
|
+
tennis_court: z
|
|
128
|
+
.boolean()
|
|
129
|
+
.optional()
|
|
130
|
+
.openapi({ description: 'Tennis court.' }),
|
|
131
|
+
twenty_four_hour_security: z
|
|
132
|
+
.boolean()
|
|
133
|
+
.optional()
|
|
134
|
+
.openapi({ description: ' 24/7 security.' }),
|
|
135
|
+
valet_parking: z
|
|
136
|
+
.boolean()
|
|
137
|
+
.optional()
|
|
138
|
+
.openapi({ description: 'Valet parking.' }),
|
|
139
|
+
video_check_out: z
|
|
140
|
+
.boolean()
|
|
141
|
+
.optional()
|
|
142
|
+
.openapi({ description: 'Video check out.' }),
|
|
143
|
+
voice_mail: z.boolean().optional().openapi({ description: 'Voice mail.' }),
|
|
144
|
+
wake_up_service: z
|
|
145
|
+
.boolean()
|
|
146
|
+
.optional()
|
|
147
|
+
.openapi({ description: 'Wake up service.' }),
|
|
148
|
+
whirpool: z.boolean().optional().openapi({ description: 'Whirpool.' }),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
export const HOTEL_IMAGE = z.object({
|
|
152
|
+
caption: z.string().nullish(),
|
|
153
|
+
is_hero_image: z.boolean(),
|
|
154
|
+
l: z.string(),
|
|
155
|
+
m: z.string(),
|
|
156
|
+
s: z.string(),
|
|
157
|
+
xl: z.string(),
|
|
158
|
+
xs: z.string(),
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
export const HOTEL_IMAGES = z.array(HOTEL_IMAGE).openapi({
|
|
162
|
+
description:
|
|
163
|
+
'List of hotel images in various sizes featuring an indicator for the primary (hero) image',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
export const HOTEL_REVIEW_RATING_SCHEMA = z
|
|
167
|
+
.object({
|
|
168
|
+
category: z
|
|
169
|
+
.string()
|
|
170
|
+
.openapi({
|
|
171
|
+
description: 'Category of the collected reviews for the Hotel.',
|
|
172
|
+
})
|
|
173
|
+
.nullish()
|
|
174
|
+
.optional(),
|
|
175
|
+
rating: z.number().openapi({
|
|
176
|
+
description: 'Rating of the collected review for the Hotel.',
|
|
177
|
+
}),
|
|
178
|
+
})
|
|
179
|
+
.openapi({
|
|
180
|
+
description: 'Review rating with category collected for the Hotel.',
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
export const HOTEL_REVIEW_RATINGS_SCHEMA = z
|
|
184
|
+
.array(HOTEL_REVIEW_RATING_SCHEMA)
|
|
185
|
+
.openapi({
|
|
186
|
+
description:
|
|
187
|
+
'List of meta reviews (category and rating) that are summary of verified reviews collected across the web on the Hotel to help choose the best option.',
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
export const IMAGES_SCHEMA = z
|
|
191
|
+
.object({
|
|
192
|
+
count: z
|
|
193
|
+
.number()
|
|
194
|
+
.openapi({ description: 'Number of images.' })
|
|
195
|
+
.optional()
|
|
196
|
+
.nullish(),
|
|
197
|
+
highres: z
|
|
198
|
+
.boolean()
|
|
199
|
+
.openapi({ description: 'Whether images exist in highres format.' })
|
|
200
|
+
.optional()
|
|
201
|
+
.nullish(),
|
|
202
|
+
|
|
203
|
+
lowres: z
|
|
204
|
+
.boolean()
|
|
205
|
+
.openapi({ description: 'Whether images exist in lowres format.' })
|
|
206
|
+
.optional()
|
|
207
|
+
.nullish(),
|
|
208
|
+
prefix: z
|
|
209
|
+
.string()
|
|
210
|
+
.openapi({ description: 'Base URL for the images.' })
|
|
211
|
+
.optional()
|
|
212
|
+
.nullish(),
|
|
213
|
+
suffix: z
|
|
214
|
+
.string()
|
|
215
|
+
.openapi({
|
|
216
|
+
description:
|
|
217
|
+
'This parameter usually represents the extension of the image (e.g.: .jpg, .png)',
|
|
218
|
+
})
|
|
219
|
+
.optional()
|
|
220
|
+
.nullish(),
|
|
221
|
+
thumb: z
|
|
222
|
+
.boolean()
|
|
223
|
+
.openapi({
|
|
224
|
+
description:
|
|
225
|
+
'Whether images exist in thumb format (for thumbnails preview).',
|
|
226
|
+
})
|
|
227
|
+
.optional()
|
|
228
|
+
.nullish(),
|
|
229
|
+
})
|
|
230
|
+
.openapi({
|
|
231
|
+
description:
|
|
232
|
+
'🛑 DEPRECATED - Hotel images details.\n\nIn order to retrieve a specific image you need to construct the complete URL from the images parameters: **[images.prefix][highres|lowres|thumb]/[index]/[images.suffix]**. If **images.count = n**, then index is in [0...n-1] range.\n\ne.g.: https://s3.eu-west-3.amazonaws.com/revolugo/hotels/yhKY/images/highres/0.jpg',
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
export const VENUES_SCHEMA = z
|
|
236
|
+
.array(
|
|
237
|
+
z.object({
|
|
238
|
+
description: z.string().optional().nullish(),
|
|
239
|
+
name: z.string(),
|
|
240
|
+
travel_times: z
|
|
241
|
+
.object({
|
|
242
|
+
driving: z.number().optional(),
|
|
243
|
+
transit: z.number().optional(),
|
|
244
|
+
walking: z.number().optional(),
|
|
245
|
+
})
|
|
246
|
+
.optional()
|
|
247
|
+
.nullish(),
|
|
248
|
+
}),
|
|
249
|
+
)
|
|
250
|
+
.optional()
|
|
251
|
+
export const HOTEL_SCHEMA = z
|
|
252
|
+
.object({
|
|
253
|
+
address: z
|
|
254
|
+
.string()
|
|
255
|
+
.openapi({ description: 'Hotel address.' })
|
|
256
|
+
.optional()
|
|
257
|
+
.nullish(),
|
|
258
|
+
address2: z
|
|
259
|
+
.string()
|
|
260
|
+
.openapi({ description: 'Second part of hotel address.' })
|
|
261
|
+
.optional()
|
|
262
|
+
.nullish(),
|
|
263
|
+
amenities: AMENITIES_SCHEMA.nullish(),
|
|
264
|
+
check_in_time: z
|
|
265
|
+
.string()
|
|
266
|
+
.openapi({ description: 'Check in time of the hotel.' })
|
|
267
|
+
.optional()
|
|
268
|
+
.nullish(),
|
|
269
|
+
check_out_time: z
|
|
270
|
+
.string()
|
|
271
|
+
.openapi({ description: 'Check out time of the hotel.' })
|
|
272
|
+
.optional()
|
|
273
|
+
.nullish(),
|
|
274
|
+
city: z.string().openapi({ description: 'City' }).optional().nullish(),
|
|
275
|
+
country: z
|
|
276
|
+
.string()
|
|
277
|
+
.openapi({ description: 'Country' })
|
|
278
|
+
.optional()
|
|
279
|
+
.nullish(),
|
|
280
|
+
country_code: z
|
|
281
|
+
.string()
|
|
282
|
+
.refine(check =>
|
|
283
|
+
Object.values(CountryIso2Code).includes(check as CountryIso2Code),
|
|
284
|
+
)
|
|
285
|
+
.openapi({ description: 'Hotel country code in ISO2.' })
|
|
286
|
+
.optional()
|
|
287
|
+
.nullish(),
|
|
288
|
+
currency: CURRENCY_SCHEMA.openapi({ description: 'Hotel currency.' })
|
|
289
|
+
.optional()
|
|
290
|
+
.nullish(),
|
|
291
|
+
description: z
|
|
292
|
+
.string()
|
|
293
|
+
.openapi({ description: 'Hotel description.' })
|
|
294
|
+
.optional()
|
|
295
|
+
.nullish(),
|
|
296
|
+
distance: z
|
|
297
|
+
.number()
|
|
298
|
+
.optional()
|
|
299
|
+
.openapi({
|
|
300
|
+
description: 'Distance from a requested location, expressed in meters',
|
|
301
|
+
})
|
|
302
|
+
.optional()
|
|
303
|
+
.nullish(),
|
|
304
|
+
email: z
|
|
305
|
+
.string()
|
|
306
|
+
.openapi({ description: 'Hotel email.' })
|
|
307
|
+
.optional()
|
|
308
|
+
.nullish(),
|
|
309
|
+
fax: z
|
|
310
|
+
.string()
|
|
311
|
+
.openapi({ description: 'Hotel fax number.' })
|
|
312
|
+
.optional()
|
|
313
|
+
.nullish(),
|
|
314
|
+
hotel_images: HOTEL_IMAGES.nullish(),
|
|
315
|
+
hotel_review_ratings: HOTEL_REVIEW_RATINGS_SCHEMA.optional().nullish(),
|
|
316
|
+
id: z.string().openapi({ description: 'Hotel id.' }),
|
|
317
|
+
images: IMAGES_SCHEMA.nullish(),
|
|
318
|
+
latitude: z.number().openapi({ description: 'Hotel latitude.' }),
|
|
319
|
+
longitude: z.number().openapi({ description: 'Hotel longitude.' }),
|
|
320
|
+
name: z.string().openapi({ description: 'Hotel name.' }),
|
|
321
|
+
phone: z
|
|
322
|
+
.string()
|
|
323
|
+
.openapi({ description: 'Hotel phone number.' })
|
|
324
|
+
.optional()
|
|
325
|
+
.nullish(),
|
|
326
|
+
policy: z
|
|
327
|
+
.string()
|
|
328
|
+
.openapi({ description: 'Internal policy of the hotel.' })
|
|
329
|
+
.optional()
|
|
330
|
+
.nullish(),
|
|
331
|
+
postal_code: z
|
|
332
|
+
.string()
|
|
333
|
+
.openapi({ description: 'Hotel address postal code.' })
|
|
334
|
+
.optional()
|
|
335
|
+
.nullish(),
|
|
336
|
+
rating: z
|
|
337
|
+
.number()
|
|
338
|
+
.optional()
|
|
339
|
+
.nullish()
|
|
340
|
+
.openapi({ description: 'Hotel Star rating.' }),
|
|
341
|
+
state: z
|
|
342
|
+
.string()
|
|
343
|
+
.openapi({ description: 'Hotel address state.' })
|
|
344
|
+
.optional()
|
|
345
|
+
.nullish(),
|
|
346
|
+
ta_id: z
|
|
347
|
+
.string()
|
|
348
|
+
.optional()
|
|
349
|
+
.openapi({ description: 'TripAdvisor property id. When applicable.' })
|
|
350
|
+
.optional()
|
|
351
|
+
.nullish(),
|
|
352
|
+
timezone: z.string().openapi({ description: 'Hotel timezone.' }),
|
|
353
|
+
venues: VENUES_SCHEMA,
|
|
354
|
+
website: z
|
|
355
|
+
.string()
|
|
356
|
+
.openapi({ description: 'Hotel website url.' })
|
|
357
|
+
.optional()
|
|
358
|
+
.nullish(),
|
|
359
|
+
})
|
|
360
|
+
.openapi('hotelApi')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './cancellation-policies.ts'
|
|
2
|
+
export * from './currency.ts'
|
|
3
|
+
export * from './hotel-offer-request.ts'
|
|
4
|
+
export * from './hotel-offer.ts'
|
|
5
|
+
export * from './hotel-room-offer.ts'
|
|
6
|
+
export * from './hotel-room.ts'
|
|
7
|
+
export * from './hotel.ts'
|
|
8
|
+
export * from './list-polling-meta.ts'
|
|
9
|
+
export * from './tag.ts'
|
|
10
|
+
export * from './taxes.ts'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { PollerStatus } from '@revolugo/common/constants'
|
|
5
|
+
|
|
6
|
+
export const ENDING_BEFORE_SCHEMA = z.string().optional().nullable().openapi({
|
|
7
|
+
description:
|
|
8
|
+
'A cursor to use in pagination. `ending_before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with `obj_bar`, your subsequent call can include `ending_before=obj_bar` in order to fetch the previous page of the list.',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const STARTING_AFTER_SCHEMA = z.string().optional().nullable().openapi({
|
|
12
|
+
description:
|
|
13
|
+
'A cursor to use in pagination. `starting_after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with `obj_foo`, your subsequent call can include `starting_after=obj_foo` in order to fetch the next page of the list.',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const STATUS_SCHEMA = z
|
|
17
|
+
.nativeEnum(PollerStatus)
|
|
18
|
+
.openapi({ description: 'Status of the response data.' })
|
|
19
|
+
.openapi('pollerStatus')
|
|
20
|
+
|
|
21
|
+
export const LIMIT_SCHEMA = z
|
|
22
|
+
.number()
|
|
23
|
+
.optional()
|
|
24
|
+
.openapi({ description: 'A limit on the number of object to be returned.' })
|
|
25
|
+
.openapi('limit')
|
|
26
|
+
|
|
27
|
+
export const LIST_META_SCHEMA = z
|
|
28
|
+
.object({
|
|
29
|
+
ending_before: ENDING_BEFORE_SCHEMA,
|
|
30
|
+
limit: LIMIT_SCHEMA,
|
|
31
|
+
starting_after: STARTING_AFTER_SCHEMA,
|
|
32
|
+
total_count: z.number().nullish(),
|
|
33
|
+
})
|
|
34
|
+
.openapi('metaApiResponse')
|
|
35
|
+
.openapi({
|
|
36
|
+
description:
|
|
37
|
+
'Meta information about the response list, such as pagination cursors or status.',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const LIST_POLLING_META_SCHEMA = LIST_META_SCHEMA.extend({
|
|
41
|
+
status: STATUS_SCHEMA,
|
|
42
|
+
}).openapi('metaApiPollingResponse')
|
|
43
|
+
/* eslint-enable camelcase */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export const TAG_SCHEMA = z.object({
|
|
5
|
+
bg: z.string().optional().nullish(),
|
|
6
|
+
color: z.string().optional().nullish(),
|
|
7
|
+
description: z.string().optional().nullish(),
|
|
8
|
+
fa_icon: z.string().optional().nullish(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const TAGS_SCHEMA = z.array(TAG_SCHEMA).optional().default([])
|
|
13
|
+
/* eslint-enable camelcase */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { TaxFrequency, TaxMode } from '../constants/index.ts'
|
|
5
|
+
|
|
6
|
+
export const TAX_SCHEMA = z
|
|
7
|
+
.object({
|
|
8
|
+
amount: z.number().optional().nullish().openapi({
|
|
9
|
+
description: 'Tax amount expressed in the requested currency.',
|
|
10
|
+
}),
|
|
11
|
+
code: z.string().optional().openapi({ description: 'Tax code.' }),
|
|
12
|
+
description: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.nullish()
|
|
16
|
+
.openapi({ description: 'Tax description.' }),
|
|
17
|
+
percentage: z
|
|
18
|
+
.number()
|
|
19
|
+
.optional()
|
|
20
|
+
.nullish()
|
|
21
|
+
.openapi({ description: 'Tax percentage on the total amount.' }),
|
|
22
|
+
tax_frequency: z.nativeEnum(TaxFrequency).openapi({
|
|
23
|
+
description:
|
|
24
|
+
'Tax frequency. Specifies if the tax applies per stay or per night',
|
|
25
|
+
}),
|
|
26
|
+
tax_mode: z.nativeEnum(TaxMode).openapi({
|
|
27
|
+
description:
|
|
28
|
+
'Tax mode. Specifies if the tax applies per occupant, per booking or per room',
|
|
29
|
+
}),
|
|
30
|
+
})
|
|
31
|
+
.openapi('taxApi')
|
|
32
|
+
|
|
33
|
+
export const TAXES_SCHEMA = z.array(TAX_SCHEMA).optional()
|
|
34
|
+
/* eslint-enable camelcase */
|
package/src/types/severities.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from 'change-case'
|
|
8
8
|
import slugify from 'slugify'
|
|
9
9
|
|
|
10
|
-
function slugCase(input: string) {
|
|
10
|
+
function slugCase(input: string): string {
|
|
11
11
|
return slugify(kebabCase(input), {
|
|
12
12
|
lower: true,
|
|
13
13
|
strict: true,
|
|
@@ -23,7 +23,7 @@ export enum CaseTransformer {
|
|
|
23
23
|
Snake = 'snakeCase',
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
export const CASE_TRANSFORMERS_MAPPING = {
|
|
27
27
|
[CaseTransformer.Camel]: camelCase,
|
|
28
28
|
[CaseTransformer.Capital]: capitalCase,
|
|
29
29
|
[CaseTransformer.Param]: kebabCase,
|
|
@@ -38,11 +38,11 @@ export function changeCase<T extends string | string[]>(
|
|
|
38
38
|
): T extends string ? string : string[] {
|
|
39
39
|
if (Array.isArray(input)) {
|
|
40
40
|
return input.map(item =>
|
|
41
|
-
|
|
41
|
+
CASE_TRANSFORMERS_MAPPING[toCase](item),
|
|
42
42
|
) as T extends string ? string : string[]
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
return
|
|
45
|
+
return CASE_TRANSFORMERS_MAPPING[toCase](input) as T extends string
|
|
46
46
|
? string
|
|
47
47
|
: string[]
|
|
48
48
|
}
|
|
@@ -85,7 +85,7 @@ export function keysChangeCase<T>(
|
|
|
85
85
|
const transformedKey =
|
|
86
86
|
options?.exclude && matches(options.exclude, key)
|
|
87
87
|
? key
|
|
88
|
-
:
|
|
88
|
+
: CASE_TRANSFORMERS_MAPPING[toCase](key)
|
|
89
89
|
|
|
90
90
|
result[transformedKey] = options.deep
|
|
91
91
|
? keysChangeCase(obj[key], toCase, options)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const MAX_ADULTS_PER_ROOM = 2
|
|
2
|
+
|
|
3
|
+
export function getSanitizedRoomCount({
|
|
4
|
+
adultCount,
|
|
5
|
+
maxAdultsPerRoom = MAX_ADULTS_PER_ROOM,
|
|
6
|
+
roomCount,
|
|
7
|
+
}: {
|
|
8
|
+
adultCount: number
|
|
9
|
+
roomCount: number
|
|
10
|
+
maxAdultsPerRoom?: number
|
|
11
|
+
}): number {
|
|
12
|
+
// Ensure at least enough rooms for each adult and at most MAX_ADULTS_PER_ROOM per room
|
|
13
|
+
if (!Number.isFinite(roomCount) || roomCount < 1) {
|
|
14
|
+
return adultCount
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const minRooms = Math.ceil(adultCount / maxAdultsPerRoom)
|
|
18
|
+
const maxRooms = adultCount
|
|
19
|
+
|
|
20
|
+
if (roomCount <= minRooms) {
|
|
21
|
+
return minRooms
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (roomCount >= maxRooms) {
|
|
25
|
+
return maxRooms
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return roomCount
|
|
29
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './add-classes.ts'
|
|
2
2
|
export * from './amount-from-percentage.ts'
|
|
3
3
|
export * from './case-transformers.ts'
|
|
4
|
+
export * from './keys-case-transformer.ts'
|
|
4
5
|
export * from './chunk.ts'
|
|
5
6
|
export * from './colors.ts'
|
|
6
7
|
export * from './compact-object.ts'
|
|
@@ -20,11 +21,13 @@ export * from './get-guest-count.ts'
|
|
|
20
21
|
export * from './get-random-element-from-array.ts'
|
|
21
22
|
export * from './get-random-hex-color.ts'
|
|
22
23
|
export * from './get-random-int.ts'
|
|
24
|
+
export * from './get-sanitized-room-count.ts'
|
|
23
25
|
export * from './group-by.ts'
|
|
24
26
|
export * from './images.ts'
|
|
25
27
|
export * from './is-empty.ts'
|
|
26
28
|
export * from './is-equal.ts'
|
|
27
29
|
export * from './is-nil.ts'
|
|
30
|
+
export * from './is-object.ts'
|
|
28
31
|
export * from './key-by.ts'
|
|
29
32
|
export * from './lang-default-fallbacks.ts'
|
|
30
33
|
export * from './map-keys.ts'
|
|
@@ -45,6 +48,7 @@ export * from './sort-by.ts'
|
|
|
45
48
|
export * from './sum-by.ts'
|
|
46
49
|
export * from './sum.ts'
|
|
47
50
|
export * from './to-boolean.ts'
|
|
51
|
+
export * from './transform-schema-keys.ts'
|
|
48
52
|
export * from './uniq.ts'
|
|
49
53
|
export * from './uniq-by.ts'
|
|
50
54
|
export * from './uniq-with.ts'
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { type CaseTransformer, changeCase } from './case-transformers.ts'
|
|
2
|
+
import { isObject } from './is-object.ts'
|
|
3
|
+
import { mapKeys } from './map-keys.ts'
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CamelCasedProperties,
|
|
7
|
+
CamelCasedPropertiesDeep,
|
|
8
|
+
KebabCasedProperties,
|
|
9
|
+
KebabCasedPropertiesDeep,
|
|
10
|
+
PascalCasedProperties,
|
|
11
|
+
PascalCasedPropertiesDeep,
|
|
12
|
+
SnakeCasedProperties,
|
|
13
|
+
SnakeCasedPropertiesDeep,
|
|
14
|
+
} from 'type-fest'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps CaseTransformer enum to the corresponding type-fest property transformation (shallow).
|
|
18
|
+
* Capital and Slug cases don't have type-fest equivalents, so they preserve the original type.
|
|
19
|
+
*/
|
|
20
|
+
type TransformProperties<
|
|
21
|
+
T,
|
|
22
|
+
C extends CaseTransformer,
|
|
23
|
+
> = C extends CaseTransformer.Camel
|
|
24
|
+
? CamelCasedProperties<T>
|
|
25
|
+
: C extends CaseTransformer.Snake
|
|
26
|
+
? SnakeCasedProperties<T>
|
|
27
|
+
: C extends CaseTransformer.Pascal
|
|
28
|
+
? PascalCasedProperties<T>
|
|
29
|
+
: C extends CaseTransformer.Param
|
|
30
|
+
? KebabCasedProperties<T>
|
|
31
|
+
: T
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Maps CaseTransformer enum to the corresponding type-fest deep property transformation.
|
|
35
|
+
* Capital and Slug cases don't have type-fest equivalents, so they preserve the original type.
|
|
36
|
+
*/
|
|
37
|
+
type TransformPropertiesDeep<
|
|
38
|
+
T,
|
|
39
|
+
C extends CaseTransformer,
|
|
40
|
+
> = C extends CaseTransformer.Camel
|
|
41
|
+
? CamelCasedPropertiesDeep<T>
|
|
42
|
+
: C extends CaseTransformer.Snake
|
|
43
|
+
? SnakeCasedPropertiesDeep<T>
|
|
44
|
+
: C extends CaseTransformer.Pascal
|
|
45
|
+
? PascalCasedPropertiesDeep<T>
|
|
46
|
+
: C extends CaseTransformer.Param
|
|
47
|
+
? KebabCasedPropertiesDeep<T>
|
|
48
|
+
: T
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Transforms object keys based on the specified case transformer.
|
|
52
|
+
* - Arrays: recursively applies transformation to each element
|
|
53
|
+
* - Objects: applies property transformation (shallow by default, deep if Deep=true)
|
|
54
|
+
* - Primitives: returns as-is
|
|
55
|
+
*/
|
|
56
|
+
export type KeysCaseTransformed<
|
|
57
|
+
T,
|
|
58
|
+
C extends CaseTransformer,
|
|
59
|
+
Deep extends boolean = false,
|
|
60
|
+
> = Deep extends true
|
|
61
|
+
? T extends readonly (infer U)[]
|
|
62
|
+
? KeysCaseTransformed<U, C, true>[]
|
|
63
|
+
: T extends object
|
|
64
|
+
? TransformPropertiesDeep<T, C>
|
|
65
|
+
: T
|
|
66
|
+
: T extends readonly (infer U)[]
|
|
67
|
+
? KeysCaseTransformed<U, C>[]
|
|
68
|
+
: T extends object
|
|
69
|
+
? TransformProperties<T, C>
|
|
70
|
+
: T
|
|
71
|
+
|
|
72
|
+
export interface KeysCaseTransformerOptions {
|
|
73
|
+
deep?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Overload: deep transformation
|
|
77
|
+
export function keysCaseTransformer<T, C extends CaseTransformer>(
|
|
78
|
+
obj: T,
|
|
79
|
+
toCase: C,
|
|
80
|
+
options: { deep: true },
|
|
81
|
+
): KeysCaseTransformed<T, C, true>
|
|
82
|
+
|
|
83
|
+
// Overload: shallow transformation (default)
|
|
84
|
+
export function keysCaseTransformer<T, C extends CaseTransformer>(
|
|
85
|
+
obj: T,
|
|
86
|
+
toCase: C,
|
|
87
|
+
options?: { deep?: false },
|
|
88
|
+
): KeysCaseTransformed<T, C>
|
|
89
|
+
|
|
90
|
+
// Implementation
|
|
91
|
+
export function keysCaseTransformer<T, C extends CaseTransformer>(
|
|
92
|
+
obj: T,
|
|
93
|
+
toCase: C,
|
|
94
|
+
options?: KeysCaseTransformerOptions,
|
|
95
|
+
): KeysCaseTransformed<T, C, boolean> {
|
|
96
|
+
if (Array.isArray(obj)) {
|
|
97
|
+
return obj.map(item =>
|
|
98
|
+
keysCaseTransformer(item, toCase, options as { deep: true }),
|
|
99
|
+
) as KeysCaseTransformed<T, C, boolean>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isObject(obj)) {
|
|
103
|
+
const transformed = mapKeys(obj, key => changeCase(key, toCase))
|
|
104
|
+
if (options?.deep) {
|
|
105
|
+
// Recursively transform nested values
|
|
106
|
+
return Object.fromEntries(
|
|
107
|
+
Object.entries(transformed).map(([k, v]) => [
|
|
108
|
+
k,
|
|
109
|
+
keysCaseTransformer(v, toCase, options as { deep: true }),
|
|
110
|
+
]),
|
|
111
|
+
) as KeysCaseTransformed<T, C, boolean>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return transformed as KeysCaseTransformed<T, C, boolean>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return obj as KeysCaseTransformed<T, C, boolean>
|
|
118
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { type ZodTypeAny, z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CASE_TRANSFORMERS_MAPPING,
|
|
5
|
+
type CaseTransformer,
|
|
6
|
+
} from './case-transformers.ts'
|
|
7
|
+
|
|
8
|
+
/* eslint-disable no-underscore-dangle, max-statements */
|
|
9
|
+
|
|
10
|
+
function transformSchemaKeysWithTransformer(
|
|
11
|
+
schema: ZodTypeAny,
|
|
12
|
+
transformer: (str: string) => string,
|
|
13
|
+
): ZodTypeAny {
|
|
14
|
+
let result: ZodTypeAny = schema
|
|
15
|
+
|
|
16
|
+
// Handle ZodObject
|
|
17
|
+
if (schema instanceof z.ZodObject) {
|
|
18
|
+
const shape = schema._def.shape()
|
|
19
|
+
const transformedShape: Record<string, ZodTypeAny> = {}
|
|
20
|
+
|
|
21
|
+
for (const key in shape) {
|
|
22
|
+
if (Object.hasOwn(shape, key)) {
|
|
23
|
+
const transformedKey = transformer(key)
|
|
24
|
+
transformedShape[transformedKey] = transformSchemaKeysWithTransformer(
|
|
25
|
+
shape[key],
|
|
26
|
+
transformer,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
result = z.object(transformedShape) as ZodTypeAny
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle ZodArray
|
|
35
|
+
else if (schema instanceof z.ZodArray) {
|
|
36
|
+
const { type: elementSchema } = schema._def
|
|
37
|
+
result = z.array(
|
|
38
|
+
transformSchemaKeysWithTransformer(elementSchema, transformer),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Handle ZodOptional
|
|
43
|
+
else if (schema instanceof z.ZodOptional) {
|
|
44
|
+
const { innerType: innerSchema } = schema._def
|
|
45
|
+
result = transformSchemaKeysWithTransformer(
|
|
46
|
+
innerSchema,
|
|
47
|
+
transformer,
|
|
48
|
+
).optional()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle ZodNullable
|
|
52
|
+
else if (schema instanceof z.ZodNullable) {
|
|
53
|
+
const { innerType: innerSchema } = schema._def
|
|
54
|
+
result = transformSchemaKeysWithTransformer(
|
|
55
|
+
innerSchema,
|
|
56
|
+
transformer,
|
|
57
|
+
).nullable()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle ZodDefault
|
|
61
|
+
else if (schema instanceof z.ZodDefault) {
|
|
62
|
+
const { defaultValue, innerType: innerSchema } = schema._def
|
|
63
|
+
result = transformSchemaKeysWithTransformer(
|
|
64
|
+
innerSchema,
|
|
65
|
+
transformer,
|
|
66
|
+
).default(defaultValue())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle ZodEffects (includes .refine(), .transform(), etc.)
|
|
70
|
+
else if (schema instanceof z.ZodEffects) {
|
|
71
|
+
const { schema: innerSchema } = schema._def
|
|
72
|
+
const transformedInner = transformSchemaKeysWithTransformer(
|
|
73
|
+
innerSchema,
|
|
74
|
+
transformer,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// We need to reconstruct the effects
|
|
78
|
+
// This is a simplified version - effects are not transformed, just passed through
|
|
79
|
+
result = transformedInner
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle ZodUnion
|
|
83
|
+
else if (schema instanceof z.ZodUnion) {
|
|
84
|
+
const { options } = schema._def
|
|
85
|
+
const transformedOptions = options.map((option: ZodTypeAny) =>
|
|
86
|
+
transformSchemaKeysWithTransformer(option, transformer),
|
|
87
|
+
)
|
|
88
|
+
result = z.union(
|
|
89
|
+
transformedOptions as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]],
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle ZodIntersection
|
|
94
|
+
else if (schema instanceof z.ZodIntersection) {
|
|
95
|
+
const { left, right } = schema._def
|
|
96
|
+
result = z.intersection(
|
|
97
|
+
transformSchemaKeysWithTransformer(left, transformer),
|
|
98
|
+
transformSchemaKeysWithTransformer(right, transformer),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Propagate openapi metadata
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
const { openapi } = schema._def as any
|
|
105
|
+
if (openapi && result !== schema) {
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
|
+
;(result._def as any).openapi = openapi
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Transforms all keys in a Zod schema from one case to another (e.g., snake_case to camelCase).
|
|
115
|
+
* This function recursively transforms all nested schemas as well.
|
|
116
|
+
*
|
|
117
|
+
* @param schema - The Zod schema to transform
|
|
118
|
+
* @param toCase - The target case (e.g., CaseTransformer.Camel)
|
|
119
|
+
* @returns A new Zod schema with transformed keys
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const snakeSchema = z.object({
|
|
124
|
+
* first_name: z.string(),
|
|
125
|
+
* last_name: z.string(),
|
|
126
|
+
* })
|
|
127
|
+
*
|
|
128
|
+
* const camelSchema = transformSchemaKeys(snakeSchema, CaseTransformer.Camel)
|
|
129
|
+
* // Results in: { firstName: z.string(), lastName: z.string() }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function transformSchemaKeys<T extends ZodTypeAny>(
|
|
133
|
+
schema: T,
|
|
134
|
+
toCase: CaseTransformer,
|
|
135
|
+
): T {
|
|
136
|
+
const transformer = CASE_TRANSFORMERS_MAPPING[toCase]
|
|
137
|
+
|
|
138
|
+
if (!transformer) {
|
|
139
|
+
throw new Error(`Unsupported case transformer: ${toCase}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return transformSchemaKeysWithTransformer(schema, transformer) as T
|
|
143
|
+
}
|