Setting the file. One moment.
Seed Rentals · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page 22.10
Post Detail
references/bookings/seed/ seed-rentals.cjs
JavaScript · 267 lines · 12 KB
14 // await seed.setupRentals(ctx, {
15 // resourceTypeName: "Meeting rooms",
16 // resources: ["Room A", "Room B"], // capacity = MORE RESOURCES
17 // rentals: [
18 // { name: "Meeting room, hourly", description: "…", price: 25, unit: "HOUR", min: 60, max: 480 },
19 // { name: "Meeting room, daily", description: "…", price: 180, unit: "DAY", min: 1, max: 5 },
20 // ],
21 // });
22 // // Calling the steps by hand instead? ORDER IS LOAD-BEARING (resource type → resources → service),
23 // // and each rental spec must carry `resourceTypeId` AND `resourceIds` (the ids from createResources).
24 //
25 // What a bookable rental needs, and setupRentals sets every one:
26 // 1. `appId` on the service at create time (it is immutable, so set it here rather than later).
27 // 2. Resources created before the service, and their ids listed in `serviceResources[].resourceIds`.
28 // 3. No category — a rental is surfaced by its `appId`, not by a Bookings category.
29 //
30 // Images are reused from seed-bookings.cjs — require both.
31 // Source recipe: wix-headless/references/inline-recipes/setup-rentals.md.
32
33 const API = "https://www.wixapis.com" ;
34
35 /** The Wix Rentals app — adds resource types and duration ranges on top of Bookings. */
36 const RENTALS_APP_ID = "ff5d6eb1-65e4-4f9a-8b14-64d34c12cc2e" ;
37
38 async function req ( ctx , path , { method = "POST" , body } = {}) {
39 const res = await fetch ( API + path, {
40 method,
41 headers: {
42 Authorization: `Bearer ${ ctx . token }` ,
43 "wix-site-id" : ctx.siteId,
44 "Content-Type" : "application/json" ,
45 },
46 body: body ? JSON . stringify (body) : undefined ,
47 });
48 const json = await res. json (). catch (() => ({}));
49 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
50 return json;
51 }
52
53 /**
54 * Install Wix Rentals. Idempotent. Rentals provisions the Bookings infrastructure it runs on, so a
55 * rental's availability comes from a Rentals-only install — Wix Bookings does not need to be added.
56 */
57 async function installRentalsApp ( ctx ) {
58 try {
59 await req (ctx, "/apps-installer-service/v1/app-instance/install" , {
60 body: {
61 tenant: { tenantType: "SITE" , id: ctx.siteId },
62 appInstance: { appDefId: RENTALS_APP_ID , enabled: true },
63 },
64 });
65 } catch {
66 /* already installed is fine */
67 }
68 }
69
70 /**
71 * A resource TYPE — the kind of thing being rented ("Meeting rooms", "Vans", "Cameras").
72 * A rental service points at one of these; the individual rentable items live inside it.
73 * @returns {Promise<{ id: string, name: string }>}
74 */
75 async function createResourceType ( ctx , name ) {
76 const res = await req (ctx, "/bookings/v2/resources/resource-types" , { body: { resourceType: { name } } });
77 const type = res?.resourceType ?? {};
78 return { id: type.id ?? type._id, name: type.name ?? name };
79 }
80
81 /** Existing resource types, so a re-run reuses one instead of making a duplicate. */
82 async function queryResourceTypes ( ctx ) {
83 const res = await req (ctx, "/bookings/v2/resources/resource-types/query" , { body: { query: {} } });
84 return (res?.resourceTypes ?? []). map (( t ) => ({ id: t.id ?? t._id, name: t.name }));
85 }
86
87 /** Get a resource type by name, creating it only if it isn't there. Additive. */
88 async function ensureResourceType ( ctx , name ) {
89 const existing = ( await queryResourceTypes (ctx)). find (( t ) => t.name === name);
90 return existing ?? createResourceType (ctx, name);
91 }
92
93 /**
94 * The individual rentable items inside a type — Room A, Room B, Van 1.
95 *
96 * ⚠️ PARALLEL CAPACITY COMES FROM MORE RESOURCES, not from a capacity number: two rooms that
97 * can be booked at the same time are two resources.
98 *
99 * ⚠️ Seeded 24/7 on purpose — no `workingHoursSchedules`. A resource with working hours makes a
100 * multi-day rental span several bookable windows, which Wix then models as a multi-service
101 * group booking instead of one booking. Pass `workingHours` only when the brief names opening
102 * hours and you accept that.
103 * @returns {Promise<{ id: string, name: string }[]>}
104 */
105 async function createResources ( ctx , resourceTypeId , names , { workingHours } = {}) {
106 const out = [];
107 for ( const name of names) {
108 const res = await req (ctx, "/bookings/v2/resources" , {
109 body: {
110 resource: {
111 name,
112 // Link the resource to its type with a top-level `typeId` (a bare GUID).
113 typeId: resourceTypeId,
114 ... (workingHours ? { workingHoursSchedules: workingHours } : {}),
115 },
116 },
117 });
118 const r = res?.resource ?? {};
119 out. push ({ id: r.id ?? r._id, name: r.name ?? name });
120 }
121 return out;
122 }
123
124 /** The Wix Rentals default booking form. Provisioned by the Rentals app; the id is the same on every site. */
125 const RENTALS_FORM_ID = "3a2ea2ce-91f4-4617-ab24-629933c0c31a" ;
126
127 /**
128 * Build one rental service payload.
129 *
130 * Field values that MAKE it a rental (all required together):
131 * type: "APPOINTMENT" — rentals are always appointment-typed
132 * appId: RENTALS_APP_ID — immutable after create
133 * durationRange — replaces sessionDurations; the two are mutually exclusive
134 * serviceResources + — which resource type AND which resources supply availability
135 * primaryResourceType
136 *
137 * Hourly bounds are MINUTES (30–1440); daily bounds are DAYS (1–8). One service = one unit
138 * type: to rent the same room by the hour AND by the day, create two services.
139 * @param {object} s spec + `resourceTypeId` and `resourceIds` (the ids from createResources).
140 */
141 function buildRental ( s ) {
142 const unit = s.unit === "DAY" ? "DAY" : "HOUR" ;
143 const durationRange =
144 unit === "DAY"
145 ? { unitType: "DAY" , dayOptions: { minDurationInDays: s.min ?? 1 , maxDurationInDays: s.max ?? 5 } }
146 : { unitType: "HOUR" , hourOptions: { minDurationInMinutes: s.min ?? 60 , maxDurationInMinutes: s.max ?? 480 } };
147
148 return {
149 type: "APPOINTMENT" ,
150 appId: RENTALS_APP_ID ,
151 name: s.name,
152 description: s.description ?? "" ,
153 ... (s.tagLine ? { tagLine: s.tagLine } : {}),
154 hidden: false ,
155 defaultCapacity: 1 ,
156 // Use the Wix Rentals default booking form so checkout collects contact details.
157 form: { id: RENTALS_FORM_ID },
158 // Make the rental bookable online.
159 onlineBooking: { enabled: true , requireManualApproval: false , allowMultipleRequests: false },
160 // A rental is priced as a RATE per unit of time — the number below is per hour or per day,
161 // and Wix multiplies it by the length the customer picks.
162 payment: {
163 rateType: "FIXED" ,
164 fixed: { price: { value: String (s.price ?? 0 ), currency: s.currency ?? "USD" } },
165 options: { online: true , inPerson: false },
166 },
167 schedule: { availabilityConstraints: { durationRange } },
168 // Name the resource type AND its concrete resource ids — availability comes from the listed ids.
169 serviceResources: [{ resourceType: { id: s.resourceTypeId }, resourceIds: { values: s.resourceIds ?? [] } }],
170 // The resource type as a bare GUID string, and one of the types named in serviceResources above.
171 primaryResourceType: s.resourceTypeId,
172 locations: [{ type: "BUSINESS" }],
173 };
174 }
175
176 /**
177 * Create rental services. Additive — an existing service with the same name is left alone.
178 * @returns {Promise<{ id: string, name: string, unit: string, revision: string }[]>}
179 */
180 async function createRentals ( ctx , rentals ) {
181 const existing = await req (ctx, "/bookings/v2/services/query" , {
182 body: { query: { filter: { appId: RENTALS_APP_ID } } },
183 }). catch (() => ({ services: [] }));
184 const byName = new Map ((existing.services ?? []). map (( s ) => [s.name, s]));
185
186 const out = [];
187 for ( const spec of rentals) {
188 const already = byName. get (spec.name);
189 if (already) {
190 out. push ({
191 id: already.id ?? already._id,
192 name: already.name,
193 unit: already.schedule?.availabilityConstraints?.durationRange?.unitType ?? null ,
194 revision: already.revision,
195 created: false ,
196 });
197 continue ;
198 }
199 const res = await req (ctx, "/bookings/v2/services" , { body: { service: buildRental (spec) } });
200 const svc = res?.service ?? {};
201 out. push ({
202 id: svc.id ?? svc._id,
203 name: svc.name ?? spec.name,
204 unit: svc.schedule?.availabilityConstraints?.durationRange?.unitType ?? null ,
205 revision: svc.revision,
206 created: true ,
207 });
208 }
209 return out;
210 }
211
212 /**
213 * Read the rentals back and confirm each one is actually a rental. A service created without
214 * `appId` or without `durationRange` still returns 200 — it is simply a plain Bookings service
215 * from then on, permanently, and this is the only check that catches it.
216 * @returns {Promise<{ ok: boolean, problems: string[] }>}
217 */
218 async function verifyRentals ( ctx , expectedNames ) {
219 const res = await req (ctx, "/bookings/v2/services/query" , {
220 body: { query: { filter: { appId: RENTALS_APP_ID } } },
221 });
222 const live = new Map ((res?.services ?? []). map (( s ) => [s.name, s]));
223 const problems = [];
224 for ( const name of expectedNames) {
225 const svc = live. get (name);
226 if ( ! svc) {
227 problems. push ( `${ name }: not returned by an appId-filtered query — it was not created as a rental` );
228 continue ;
229 }
230 if ( ! svc.schedule?.availabilityConstraints?.durationRange) {
231 problems. push ( `${ name }: no durationRange — created as a plain service, and appId is immutable` );
232 }
233 if ( ! svc.primaryResourceType) problems. push ( `${ name }: no primaryResourceType — availability will be empty` );
234 if ( ! svc.serviceResources?. some (( sr ) => sr.resourceIds?.values?. length ))
235 problems. push ( `${ name }: serviceResources carry no resourceIds — availability will be empty` );
236 }
237 return { ok: problems. length === 0 , problems };
238 }
239
240 /**
241 * The whole rentals seed, in the one order that works.
242 * @param {object} ctx
243 * @param {{ resourceTypeName: string, resources: string[], rentals: object[] }} plan
244 */
245 async function setupRentals ( ctx , { resourceTypeName , resources = [], rentals = [] }) {
246 await installRentalsApp (ctx);
247 const type = await ensureResourceType (ctx, resourceTypeName);
248 const created = await createResources (ctx, type.id, resources);
249 // Every rental service must list the concrete resource ids, or its availability is empty (see buildRental).
250 const resourceIds = created. map (( c ) => c.id);
251 const services = await createRentals (ctx, rentals. map (( r ) => ({ ... r, resourceTypeId: type.id, resourceIds })));
252 const check = await verifyRentals (ctx, rentals. map (( r ) => r.name));
253 return { resourceType: type, resources: created, services, ... check };
254 }
255
256 module . exports = {
257 RENTALS_APP_ID,
258 installRentalsApp,
259 createResourceType,
260 queryResourceTypes,
261 ensureResourceType,
262 createResources,
263 buildRental,
264 createRentals,
265 verifyRentals,
266 setupRentals,
267 };