Setting the file. One moment.
Wix Bookings Rentals · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page 22.10
Post Detail
22.28 references/bookings/app/rest/ wix-bookings-rentals.js
JavaScript · 250 lines · 12 KB
13 * 2. the customer picks the LENGTH, so availability is two calls: start times, then the valid
14 * ends for the chosen start
15 * 3. price is a rate per unit, so the total for a chosen length comes from the server
16 *
17 * What makes a service a rental (from the docs' own mapping — nothing is inferred):
18 * type always "APPOINTMENT"
19 * appId always RENTALS_APP_ID
20 * schedule.availabilityConstraints.durationRange replaces `sessionDurations`
21 * — MUTUALLY EXCLUSIVE, one or the other
22 * primaryResourceType a resource type id, not staff — availability comes from its resources
23 * payment.rateType always "FIXED", charged per hour or per day
24 *
25 * ⚠️ Because the two constraint fields are mutually exclusive, `isRental()` below is a reliable
26 * test on the DATA. Never decide "this is a rental" from the brief, the service name, or a
27 * category — a 60-minute yoga class and a meeting room rented for 1–8 hours are told apart by
28 * which constraint the service carries, and nothing else.
29 *
30 * docs: https://dev.wix.com/docs/api-reference/business-solutions/rentals/wix-rentals-and-the-bookings-apis.md
31 * docs: https://dev.wix.com/docs/api-reference/business-solutions/rentals/about-wix-rentals-availability.md
32 */
33
34 /** The Wix Rentals app. Used twice: to filter the catalog, and as the cart's catalogReference.appId. */
35 export const RENTALS_APP_ID = "ff5d6eb1-65e4-4f9a-8b14-64d34c12cc2e" ;
36
37 /** The Wix Bookings app — the other half of a mixed site. */
38 export const BOOKINGS_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97" ;
39
40 /**
41 * Is this service a rental? True when it carries a duration RANGE the customer picks within,
42 * rather than a fixed session length. This is the only correct test.
43 * @param {object} service
44 * @returns {boolean}
45 */
46 export function isRental ( service ) {
47 return Boolean (service?.schedule?.availabilityConstraints?.durationRange);
48 }
49
50 /**
51 * The rental's unit and bounds, or null for an ordinary service.
52 * Hourly bounds are MINUTES (30–1440), daily bounds are DAYS (1–8).
53 * @param {object} service
54 * @returns {{ unit: "HOUR"|"DAY", min: number, max: number, step: number }|null}
55 */
56 export function rentalDuration ( service ) {
57 const range = service?.schedule?.availabilityConstraints?.durationRange;
58 if ( ! range) return null ;
59 if (range.unitType === "DAY" ) {
60 return {
61 unit: "DAY" ,
62 min: range.dayOptions?.minDurationInDays ?? 1 ,
63 max: range.dayOptions?.maxDurationInDays ?? 1 ,
64 step: 1 ,
65 };
66 }
67 return {
68 unit: "HOUR" ,
69 min: range.hourOptions?.minDurationInMinutes ?? 30 ,
70 max: range.hourOptions?.maxDurationInMinutes ?? 1440 ,
71 step: 30 ,
72 };
73 }
74
75 /**
76 * Query rentals only. On a site that has both apps an unfiltered `queryServices` returns
77 * haircuts next to meeting rooms, so this is not optional — it is the same query with the
78 * rentals `appId` on the filter.
79 *
80 * Pass `appId: BOOKINGS_APP_ID` to get the ordinary services instead, on a mixed site.
81 * @param {{ limit?: number, offset?: number, appId?: string }} [options]
82 * @returns {Promise<{ services: object[], total: number, nextOffset: number|null }>}
83 */
84 export async function queryRentals ({ limit = 100 , offset = 0 , appId = RENTALS_APP_ID } = {}) {
85 const res = await wixApiRequest ( "/bookings/v2/services/query" , {
86 method: "POST" ,
87 body: { query: { filter: { hidden: false , appId }, paging: { limit, offset } } },
88 });
89 const services = res?.services ?? [];
90 const total = res?.pagingMetadata?.total ?? services. length ;
91 const loaded = offset + services. length ;
92 return { services, total, nextOffset: loaded < total ? loaded : null };
93 }
94
95 /**
96 * The resource type id(s) a rental's availability comes from — read off `serviceResources`.
97 * Every rental availability call must pass these as `includeResourceTypeIds`, or it returns zero slots.
98 * @param {object} service
99 * @returns {string[]}
100 */
101 export function rentalResourceTypeIds ( service ) {
102 return (service?.serviceResources ?? [])
103 . map (( sr ) => sr.resourceType?._id ?? sr.resourceType?.id)
104 . filter (Boolean);
105 }
106
107 /**
108 * Start times for a rental — step 1 of 2.
109 *
110 * A rental is an APPOINTMENT-typed service, so this is the same availability call an appointment
111 * uses; what comes back are the times a rental may START. The customer then picks how long, which is
112 * `listEndOptions` below. Pass the whole `service` so its resource types ride along — the
113 * availability engine reads them to find a resource-driven service's slots.
114 * @param {object} service A rental service from queryRentals.
115 * @param {{ fromLocalDate: string, toLocalDate: string, timeZone?: string, limit?: number, cursor?: string }} options
116 * @returns {Promise<{ slots: object[], nextCursor: string|null, timeZone: string|null }>}
117 */
118 export function listRentalStartSlots ( service , options = {}) {
119 const serviceId = service?._id || service?.id;
120 return listAvailableSlots (serviceId, { ... options, includeResourceTypeIds: rentalResourceTypeIds (service) });
121 }
122
123 /**
124 * The valid END times for a chosen start — step 2 of 2, HOURLY rentals only.
125 *
126 * Each entry is a TimeSlot sharing `localStartDate` with the request and varying only in
127 * `localEndDate`, sorted shortest first. The service's maximum duration always caps the list,
128 * so a `maxLocalEndDate` beyond it is ignored rather than rejected.
129 *
130 * ⚠️ Hourly only. A DAILY rental has no end options — its lengths are whole days, so walk
131 * consecutive days from the chosen start instead (`dailyEndOptions` below).
132 * https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slot-end-options.md
133 * Pass the chosen start slot's own `location`. It carries `locationType` (and an `id` on a
134 * multi-location site); `{ locationType: "BUSINESS" }` is enough on a single-location site.
135 * @param {string} serviceId
136 * @param {{ localStartDate: string, location: object, maxLocalEndDate?: string, timeZone?: string }} options
137 * @returns {Promise<{ endOptions: object[], timeZone: string|null }>}
138 */
139 export async function listEndOptions ( serviceId , { localStartDate , location , maxLocalEndDate , timeZone } = {}) {
140 if ( ! localStartDate) throw new Error ( "listEndOptions requires localStartDate (local 'YYYY-MM-DDThh:mm:ss')." );
141 if ( ! location) throw new Error ( "listEndOptions requires the chosen slot's location." );
142 const res = await wixApiRequest ( "/_api/service-availability/v2/time-slots/end-options" , {
143 method: "POST" ,
144 body: {
145 serviceId,
146 localStartDate,
147 location,
148 ... (maxLocalEndDate ? { maxLocalEndDate } : {}),
149 ... (timeZone ? { timeZone } : {}),
150 },
151 });
152 return { endOptions: res?.endOptions ?? [], timeZone: res?.timeZone ?? null };
153 }
154
155 /**
156 * The lengths a DAILY rental can run for from a chosen start day — the daily counterpart to
157 * `listEndOptions`. A day's slot list is the availability, so this walks forward from the start
158 * and stops at the first day that has none: a 3-day rental needs all 3 days free, and a gap
159 * means the longer options are not bookable however far out the calendar goes.
160 *
161 * Returns one entry per bookable length, `{ days, localEndDate }`, shortest first.
162 * `localEndDate` is midnight of the day AFTER the last rented day — the end boundary is
163 * exclusive, which is the same convention the catalog's availability window uses.
164 * @param {object} service A rental service (daily).
165 * @param {{ localStartDate: string, timeZone?: string }} options
166 * @returns {Promise<{ days: number, localEndDate: string }[]>}
167 */
168 export async function dailyEndOptions ( service , { localStartDate , timeZone } = {}) {
169 const duration = rentalDuration (service);
170 if ( ! duration || duration.unit !== "DAY" ) {
171 throw new Error ( "dailyEndOptions expects a DAY-unit rental — use listEndOptions for hourly." );
172 }
173 const serviceId = service._id || service.id;
174 const includeResourceTypeIds = rentalResourceTypeIds (service);
175 const startDay = localStartDate. slice ( 0 , 10 );
176 const dayAfter = ( isoDay , n ) => {
177 const d = new Date ( `${ isoDay }T00:00:00Z` );
178 d. setUTCDate (d. getUTCDate () + n);
179 return d. toISOString (). slice ( 0 , 10 );
180 };
181
182 const out = [];
183 for ( let days = 1 ; days <= duration.max; days ++ ) {
184 const dayToCheck = dayAfter (startDay, days - 1 );
185 const { slots } = await listAvailableSlots (serviceId, {
186 fromLocalDate: `${ dayToCheck }T00:00:00` ,
187 toLocalDate: `${ dayAfter ( dayToCheck , 1 ) }T00:00:00` ,
188 timeZone,
189 limit: 1 ,
190 includeResourceTypeIds,
191 });
192 if ( ! slots. length ) break ; // a gap — every longer option is unbookable too
193 if (days >= duration.min) out. push ({ days, localEndDate: `${ dayAfter ( startDay , days ) }T00:00:00` });
194 }
195 return out;
196 }
197
198 /**
199 * The price for a chosen length, from the server.
200 *
201 * ⚠️ Never compute this client-side. A rental is charged as a RATE — per hour or per day — and
202 * hourly is calculated at per-minute granularity, so a 90-minute rental of a $10/hour room is
203 * $15. Multiplying a displayed base price by a rounded number of hours quietly disagrees with
204 * what the customer is charged at checkout.
205 *
206 * ⚠️ Omitting the local dates or the time zone does NOT error — it returns a duration-blind
207 * price, which is the same trap in a quieter form.
208 * https://dev.wix.com/docs/api-reference/business-solutions/bookings/pricing/pricing-api/preview-price.md
209 * @param {string} serviceId
210 * @param {{ localStartDate: string, localEndDate: string, timeZone: string, numberOfParticipants?: number }} options
211 * @returns {Promise<{ total: string, currency: string|null, raw: object }>}
212 */
213 export async function previewRentalPrice ( serviceId , { localStartDate , localEndDate , timeZone , numberOfParticipants = 1 } = {}) {
214 if ( ! localStartDate || ! localEndDate || ! timeZone) {
215 throw new Error ( "previewRentalPrice requires localStartDate, localEndDate and timeZone — without them the price ignores the duration." );
216 }
217 const res = await wixApiRequest ( "/bookings/v2/pricing/v2/pricing/preview" , {
218 method: "POST" ,
219 body: {
220 bookingLineItems: [{ serviceId, localStartDate, localEndDate, timeZone, numberOfParticipants }],
221 },
222 });
223 const info = res?.priceInfo ?? {};
224 const total = info.totalPrice ?? info.total ?? {};
225 return {
226 total: total.formattedValue ?? (total.value != null ? String (total.value) : "" ),
227 currency: total.currency ?? null ,
228 raw: info,
229 };
230 }
231
232 /**
233 * A rental's rate, ready to render — "$10 / hour" or "$120 / day".
234 * `servicePriceLabel` in `lib/serviceFacts.js` renders the same number as a flat price, which
235 * reads as the whole cost of the rental rather than its rate.
236 * @param {object} service
237 * @returns {string}
238 */
239 export function rentalRateLabel ( service ) {
240 const duration = rentalDuration (service);
241 if ( ! duration) return "" ;
242 const price = service?.payment?.fixed?.price;
243 const money =
244 price?.formattedValue ||
245 (price?.currency && price?.value != null
246 ? new Intl. NumberFormat ( undefined , { style: "currency" , currency: price.currency }). format ( Number (price.value))
247 : "" );
248 if ( ! money) return "" ;
249 return `${ money } / ${ duration . unit === "DAY" ? "day" : "hour"}` ;
250 }