Setting the file. One moment.
Seed Events · Wix Headless Templates · wix/skills · Skills Docs
ContentsBack to the top of the page — line 213
This file
Number 16.338
Position 338 of 828
Type JavaScript
Size 15 KB
Lines 324 events/seed/ seed-events.mjs
JavaScript · 324 lines · 15 KB
// "startDate", "endDate" (future ISO-8601 UTC), "timeZoneId",
15 // "location" ({name,type:"VENUE",address} | {name,type:"ONLINE"} | {locationTbd:true,name}),
16 // "ticketTiers"?: [{ "name" (≤30 chars), "price" (decimal STRING), "description"?, "initialLimit"?, "feeType"? }],
17 // "category"? (name), "imageMediaId"? | "imageUrl"? | "imagePrompt"?, "rsvpResponseType"? }] }
18 //
19 // Seeding is ADDITIVE — never deletes or overwrites existing content. Unexpected shapes →
20 // read the live API reference; every call below
21 // carries a docs: line with its reference page.
22 import { setSiteCurrency } from "../../shared/seed/site.mjs" ;
23 import { readFileSync } from "node:fs" ;
24 import { resolveItemImages } from "../../shared/seed/images.mjs" ;
25 import { seedSiteId } from "../../shared/seed/site-context.mjs" ;
26 import { wixToken } from "../../shared/seed/wix-cli.mjs" ;
27 import { isMain } from "../../shared/seed/main.mjs" ;
28
29 const API = "https://www.wixapis.com" ;
30 const EVENTS_APP_ID = "140603ad-af8d-84a5-2c80-a0f60cb47351" ;
31
32 export function makeCtx ({ cwd = process. cwd () } = {}) {
33 // The content site: the config's site, or the parent on a migration preview (site-context.mjs stops
34 // a seed there unless --allow-parent is passed after the user confirmed).
35 const siteId = seedSiteId ({ cwd, argv: process.argv });
36 const token = wixToken (siteId, cwd);
37 return { token, siteId };
38 }
39
40 async function req ( ctx , path , { method = "POST" , body } = {}) {
41 const res = await fetch ( API + path, {
42 method,
43 headers: {
44 Authorization: `Bearer ${ ctx . token }` ,
45 "wix-site-id" : ctx.siteId,
46 "Content-Type" : "application/json" ,
47 },
48 body: body ? JSON . stringify (body) : undefined ,
49 });
50 const json = await res. json (). catch (() => ({}));
51 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
52 return json;
53 }
54
55 // {type:"TICKETING"|"RSVP", …} -> the registration block. TICKETING carries a tickets{}
56 // config; RSVP carries rsvp{responseType} and seeds NO form fields (name+email is built-in).
57 // initialType is IMMUTABLE after create — set from the plan, never plan to convert.
58 function buildRegistration ( ev ) {
59 if (ev.type === "TICKETING" ) {
60 return {
61 initialType: "TICKETING" ,
62 tickets: {
63 ticketLimitPerOrder: ev.ticketLimitPerOrder ?? 8 ,
64 currency: ev.currency ?? "USD" , // setupEvents threads the site currency; USD is the last resort
65 reservationDurationInMinutes: ev.reservationDurationInMinutes ?? 20 ,
66 },
67 };
68 }
69 return {
70 initialType: "RSVP" ,
71 rsvp: {
72 responseType: ev.rsvpResponseType ?? "YES_ONLY" , // "YES_ONLY" | "YES_AND_NO"
73 ... (Number. isInteger (ev.rsvpLimit) && ev.rsvpLimit > 0 ? { limit: ev.rsvpLimit } : {}),
74 ... (ev.waitlist === true ? { waitlistEnabled: true } : {}),
75 // A future start date schedules the opening: the event publishes with status SCHEDULED_RSVP and
76 // the page says when registration opens; before it, an RSVP is refused. The API requires the
77 // window's END with its start (run 152: "both start and end dates"); it defaults to the event's
78 // start, the moment registration stops making sense.
79 ... (ev.registrationOpensAt ? { startDate: ev.registrationOpensAt, endDate: ev.registrationClosesAt ?? ev.startDate } : {}),
80 },
81 };
82 }
83
84 // {guests: N} on an RSVP event -> the registration form's guest control, added through the Events
85 // Forms API AFTER the event exists (Create Event ignores form.controls; probed live 2026-09-28). The
86 // control lands as a count input (options "0".."N") plus a names list capped at N — the shape the
87 // site's form reader (events-core toRsvpForm) reads its max from — and takes effect at once.
88 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/form/add-control.md
89 export async function addGuestControl ( ctx , eventId , guests ) {
90 const n = Number (guests);
91 if ( ! Number. isInteger (n) || n < 1 ) return false ;
92 await req (ctx, `/events/v1/events/${ eventId }/form/control` , { body: {
93 additionalGuests: { maxGuests: n, namesMandatory: false , labels: { single: "Bringing a guest?" , multiple: "How many guests?" } },
94 } });
95 return true ;
96 }
97
98 // ---- operations ----------------------------------------------------------------------------------
99
100 // Idempotent: re-installing an already-installed app returns 200.
101 // docs: https://dev.wix.com/docs/api-reference/articles/work-with-wix-apis/platform/about-apps-created-by-wix.md
102 export async function installEventsApp ( ctx ) {
103 try {
104 await req (ctx, "/apps-installer-service/v1/app-instance/install" , { body: {
105 tenant: { tenantType: "SITE" , id: ctx.siteId },
106 appInstance: { appDefId: EVENTS_APP_ID , enabled: true },
107 } });
108 } catch {
109 /* already installed is fine */
110 }
111 }
112
113 // The ticket-definitions API REQUIRES currency and uses whatever you pass verbatim (it does
114 // NOT fall back to the site currency) — resolve it once, or a non-USD site gets mis-priced.
115 // docs: https://dev.wix.com/docs/api-reference/business-management/site-properties/properties/get-site-properties.md
116 export async function getSiteCurrency ( ctx ) {
117 try {
118 const r = await req (ctx, "/site-properties/v4/properties" , { method: "GET" });
119 return r?.properties?.paymentCurrency || "USD" ;
120 } catch {
121 return "USD" ;
122 }
123 }
124
125 /**
126 * STEP 1 — create ONE event as a draft (no bulk endpoint; loop for multiple). Dates MUST be
127 * in the future — a past event isn't registerable and won't show in the live listing.
128 * Returns { id, slug } — id feeds the tier/publish steps; slug is the URL identifier.
129 * docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/create-event.md
130 */
131 export async function createEvent ( ctx , ev ) {
132 const body = {
133 draft: true ,
134 event: {
135 title: ev.title,
136 shortDescription: ev.shortDescription,
137 location: ev.location,
138 dateAndTimeSettings: {
139 startDate: ev.startDate,
140 endDate: ev.endDate,
141 timeZoneId: ev.timeZoneId,
142 showTimeZone: ev.showTimeZone ?? true ,
143 },
144 registration: buildRegistration (ev),
145 },
146 fields: [ "DETAILS" , "TEXTS" , "REGISTRATION" , "URLS" ],
147 };
148 const r = await req (ctx, "/events/v3/events" , { body });
149 return { id: r.event?.id, slug: r.event?.slug };
150 }
151
152 /**
153 * STEP 2 — ticket tiers for a TICKETING event (skip for RSVP). Must run BEFORE publish —
154 * publishing a ticketed event with no tiers ships an event with nothing to buy, and there is
155 * no un-publish. price is a decimal STRING ("65.00", never a number); name ≤ 30 chars; omit
156 * initialLimit for unlimited. `feeType` is FEE_INCLUDED (the Wix fee comes out of the price),
157 * FEE_ADDED_AT_CHECKOUT (the buyer pays it on top) or NO_FEE (free tickets and sites that don't
158 * require a fee). Tiers are independent — fired as one parallel batch.
159 * docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/ticket-definitions-v3/create-ticket-definition.md
160 */
161 export async function createTicketTiers ( ctx , eventId , tiers ) {
162 return Promise . all (tiers. map ( async ( t ) => {
163 const body = ( feeType ) => ({
164 ticketDefinition: {
165 eventId,
166 name: t.name,
167 description: t.description,
168 ... (t.initialLimit != null ? { initialLimit: t.initialLimit } : {}),
169 pricingMethod: { fixedPrice: { value: String (t.price), currency: t.currency ?? "USD" } },
170 feeType,
171 },
172 fields: [ "SALES_DETAILS" ],
173 });
174 // INVALID_FEE_TYPE ("feeType=FEE_INCLUDED feeRequired=false") means this site doesn't take a
175 // fee on this tier — right after the Events app is installed the fee settings can also lag a
176 // few seconds (seen on a fresh site, 2026-09-27). So the retries change the fee type instead
177 // of re-sending the same one: the plan's (default FEE_INCLUDED), then NO_FEE, then
178 // FEE_ADDED_AT_CHECKOUT after a five-second pause for the install race. Anything else, or a
179 // third failure, throws as before.
180 const feeTypes = [t.feeType ?? "FEE_INCLUDED" , "NO_FEE" , "FEE_ADDED_AT_CHECKOUT" ];
181 for ( let attempt = 0 ; ; attempt ++ ) {
182 try {
183 const r = await req (ctx, "/events/v3/ticket-definitions" , { body: body (feeTypes[attempt]) });
184 return { id: r.ticketDefinition?.id, feeType: feeTypes[attempt] };
185 } catch (e) {
186 if ( ! /INVALID_FEE_TYPE/ . test (e.message) || attempt >= 2 ) throw e;
187 if (attempt === 1 ) await new Promise (( res ) => setTimeout (res, 5000 ));
188 }
189 }
190 }));
191 }
192
193 // STEP 3 — publish (one-way; for TICKETING only after its tiers exist).
194 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/publish-draft-event.md
195 export async function publishEvent ( ctx , eventId ) {
196 return req (ctx, `/events/v3/events/${ eventId }/publish` , { body: {} });
197 }
198
199 // STEP 4 (optional) — group events by a format/track. Categories are the v1 API (NOT v3).
200 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/categories/create-category.md
201 export async function createEventCategories ( ctx , names ) {
202 const out = [];
203 for ( const name of names) {
204 const r = await req (ctx, "/events/v1/categories" , { body: { category: { name } } });
205 out. push ({ id: r.category?.id, name });
206 }
207 return out;
208 }
209
210 // Assign events to a category. Path is /{categoryId}/events (NOT /assign); body key is
211 // `eventId` — an ARRAY despite the singular name.
212 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/categories/assign-events.md
213 export async function assignEventsToCategory ( ctx , categoryId , eventIds ) {
214 return req (ctx, `/events/v1/categories/${ categoryId }/events` , { body: { eventId: eventIds } });
215 }
216
217 // Events binds mainImage by Wix Media file ID — an external url must be imported first
218 // (a raw url as the id stores 200 but renders nothing); a plan `imagePrompt` is generated
219 // (Wix AI, 1 credit) then imported. Both live in the shared util (parallel, resilient,
220 // never blocks the seed).
221 export { importImage } from "../../shared/seed/images.mjs" ;
222
223 // mainImage is an Image OBJECT; height/width are REQUIRED or it won't render. Events V3 uses
224 // NO revision — partial PATCH keyed by event.id. Works before OR after publish.
225 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/update-event.md
226 export async function setEventMainImage ( ctx , it ) {
227 return req (ctx, `/events/v3/events/${ it . eventId }` , {
228 method: "PATCH" ,
229 body: {
230 event: {
231 id: it.eventId,
232 mainImage: { id: it.id, url: it.url, height: it.height ?? 1024 , width: it.width ?? 1024 , altText: it.altText },
233 },
234 fields: [ "DETAILS" ], // mainImage reads back only under DETAILS
235 },
236 });
237 }
238
239 /**
240 * ONE-CALL seed: install → site currency → per event create DRAFT → tiers → publish →
241 * categories → images, ids threaded in memory. The default path.
242 */
243 export async function setupEvents ( ctx , { events = [], currency } = {}) {
244 // Before any event exists: a ticket tier's currency is fixed at creation.
245 if (currency) await setSiteCurrency (ctx, currency);
246 await installEventsApp (ctx);
247 const siteCurrency = currency ?? await getSiteCurrency (ctx);
248
249 const created = [];
250 for ( const ev of events) {
251 const e = await createEvent (ctx, { ... ev, currency: ev.currency ?? siteCurrency });
252 if ( ! e.id) throw new Error ( `Event "${ ev . title }" was not created — no id returned.` );
253 const tiers = ev.type === "TICKETING" && ev.ticketTiers?. length
254 ? await createTicketTiers (ctx, e.id, ev.ticketTiers. map (( t ) => ({ ... t, currency: t.currency ?? siteCurrency })))
255 : [];
256 // The guest control goes on the draft's form before publish; a refusal leaves the event without it
257 // (the owner adds it in the dashboard) and the result says so.
258 let guestControl = false ;
259 if (ev.type !== "TICKETING" && ev.guests) {
260 try { guestControl = await addGuestControl (ctx, e.id, ev.guests); }
261 catch (err) { console. error ( `guest control skipped for "${ ev . title }": ${ err . message }` ); }
262 }
263 await publishEvent (ctx, e.id);
264 created. push ({ ... e, category: ev.category, imageMediaId: ev.imageMediaId, imageUrl: ev.imageUrl, imagePrompt: ev.imagePrompt, ticketCount: tiers. length , feeTypes: tiers. map (( t ) => t.feeType), ... (ev.guests ? { guestControl } : {}) });
265 }
266
267 const names = [ ...new Set (created. map (( e ) => e.category). filter (Boolean))];
268 const categories = names. length ? await createEventCategories (ctx, names) : [];
269 for ( const c of categories) {
270 const eventIds = created. filter (( e ) => e.category === c.name). map (( e ) => e.id);
271 if (eventIds. length ) await assignEventsToCategory (ctx, c.id, eventIds);
272 }
273
274 // Pass 2 — images: resolve (import by url / generate by prompt) in one parallel wave, then
275 // attach. Failures leave the event text-only; the seed's exit never depends on images.
276 const files = await resolveItemImages (ctx, created. map (( e ) => ({
277 mediaId: e.imageMediaId,
278 url: e.imageUrl,
279 path: e.imagePath,
280 prompt: e.imagePrompt,
281 displayName: `${ e . slug || "event"}.png` ,
282 })));
283 let imagesAttached = 0 ;
284 for ( let i = 0 ; i < created. length ; i ++ ) {
285 const e = created[i];
286 if ( ! files[i]) continue ;
287 try {
288 await setEventMainImage (ctx, { eventId: e.id, id: files[i].id, url: files[i].url, height: 1024 , width: 1024 , altText: e.slug });
289 imagesAttached ++ ;
290 } catch {
291 /* never block on image failure — the event stays text-only */
292 }
293 }
294
295 return {
296 events: created. map (( e ) => ({ id: e.id, slug: e.slug, ticketCount: e.ticketCount, feeTypes: e.feeTypes, category: e.category ?? null })),
297 categories,
298 imagesAttached,
299 // Completing a PAID purchase needs a premium plan + a configured payment method in the
300 // dashboard — not a seeding failure; surface it to the owner. Free/RSVP need neither.
301 notes: created. some (( e ) => e.ticketCount > 0 )
302 ? [ "Paid tickets require a premium plan + a configured payment method in the dashboard to complete a purchase." ]
303 : [],
304 };
305 }
306
307 // ---- CLI entry ----------------------------------------------------------------------------------
308
309 const invokedDirectly = isMain ( import . meta .url);
310 if (invokedDirectly) {
311 const planPath = process.argv[ 2 ];
312 if ( ! planPath) {
313 console. error ( "usage: node seed-events.mjs <plan.json> (run from the project root)" );
314 process. exit ( 1 );
315 }
316 const plan = JSON . parse ( readFileSync (planPath, "utf8" ));
317 const ctx = makeCtx ();
318 setupEvents (ctx, plan)
319 . then (( result ) => console. log ( JSON . stringify (result, null , 2 )))
320 . catch (( e ) => {
321 console. error (e.message);
322 process. exit ( 1 );
323 });
324 }