Setting the file. One moment.
Seed Pricing Plans · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page 22.10
Post Detail
This file
Number 22.102
Position 102 of 145
Type JavaScript
Size 12 KB
Lines 251 references/pricing-plans/seed/ seed-pricing-plans.cjs
JavaScript · 251 lines · 12 KB
14 // const plans = await seed.createPlans(ctx, [
15 // { name: "Studio Membership", description: "Unlimited group classes.", price: "20.00",
16 // type: "recurring", billingCycle: { period: "MONTH", count: 1 },
17 // perks: ["Unlimited group classes", "10% off workshops"],
18 // coveredServiceIds: bookingsServiceIds }, // optional — only when this plan covers bookings
19 // ]);
20 // // Then, per plan that covers bookings services (STEP 2; skip entirely with no bookings):
21 // await seed.attachBookingsCoverage(ctx, plans[0].id, plans[0].coveredServiceIds);
22 //
23 // If any call fails with a shape the caller didn't expect, fall back to the documentation skill available in your environment
24 // (search + read the live Wix API reference) — never guess. Source recipe (authoritative):
25 // wix-headless/references/inline-recipes/setup-pricing-plans.md.
26
27 const { randomUUID } = require ( "crypto" );
28
29 const API = "https://www.wixapis.com" ;
30 const BOOKINGS_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97" ; // per recipe: providerAppId for coverage
31 const PRICING_PLANS_APP_ID = "1522827f-c56c-a5c9-2ac9-00f9e6ae12d3" ; // installPricingPlansApp installs this before seeding
32 const PP_NAMESPACE = "@wix/pricing-plans" ; // per recipe: literal, with @ and slash, in every 2a–2c call
33
34 async function req ( ctx , path , { method = "POST" , body } = {}) {
35 const res = await fetch ( API + path, {
36 method,
37 headers: {
38 Authorization: `Bearer ${ ctx . token }` ,
39 "wix-site-id" : ctx.siteId,
40 "Content-Type" : "application/json" ,
41 },
42 body: body ? JSON . stringify (body) : undefined ,
43 });
44 const json = await res. json (). catch (() => ({}));
45 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
46 return json;
47 }
48
49 const sleep = ( ms ) => new Promise (( r ) => setTimeout (r, ms));
50
51 // [ "text" | { description } ] -> Wix perks[]; each gets a fresh client-supplied GUID (required).
52 function buildPerks ( perks = []) {
53 return perks. map (( p ) => ({
54 id: randomUUID (),
55 description: typeof p === "string" ? p : p.description,
56 }));
57 }
58
59 // billingTerms selects the plan type (recurring | one-time | free). Amounts are decimal STRINGS.
60 // recurring: billingCycle {period,count} (default MONTH/1) + startType ON_PURCHASE + endType UNTIL_CANCELLED.
61 // one-time: endType CYCLES_COMPLETED + cyclesCompletedDetails {billingCycleCount:1} (bill once, then ends);
62 // an unlimited one-time instead sets billingCycle:null + endType UNTIL_CANCELLED.
63 function buildBillingTerms ( plan ) {
64 const type = plan.type || "recurring" ;
65 if (type === "one-time" ) {
66 if (plan.billingCycle === null ) {
67 return { billingCycle: null , startType: "ON_PURCHASE" , endType: "UNTIL_CANCELLED" };
68 }
69 const cycle = plan.billingCycle || { period: "MONTH" , count: 1 };
70 return {
71 billingCycle: cycle,
72 startType: "ON_PURCHASE" ,
73 endType: "CYCLES_COMPLETED" ,
74 cyclesCompletedDetails: { billingCycleCount: 1 },
75 };
76 }
77 // recurring (also the shape a free plan rides on)
78 const cycle = plan.billingCycle || { period: "MONTH" , count: 1 };
79 return { billingCycle: cycle, startType: "ON_PURCHASE" , endType: "UNTIL_CANCELLED" };
80 }
81
82 // one pricingVariant with one pricingStrategy (schema allows ≤20 but is "currently limited to 1").
83 function buildPricingVariant ( plan ) {
84 const free = (plan.type || "recurring" ) === "free" ;
85 const amount = free ? "0" : String (plan.price);
86 return {
87 id: randomUUID (), // required, client-supplied GUID — never model-typed
88 name: plan.variantName || "Standard" ,
89 billingTerms: buildBillingTerms (plan),
90 pricingStrategies: [{ flatRate: { amount } }],
91 };
92 }
93
94 // ---- exported operations ----
95
96 /**
97 * Create the plan(s). Plans V3 has NO bulk-create — one create call per plan.
98 * @param plans [{ name, description?, price, // price = decimal string ("20.00"); ignored for free
99 * type?: "recurring"|"one-time"|"free", // default "recurring"
100 * billingCycle?: { period: "DAY"|"WEEK"|"MONTH"|"YEAR", count } | null, // default { MONTH, 1 }
101 * perks?: ["..."] | [{ description }], // display-only bullets (no functional effect)
102 * visibility?, buyable?, // default "PUBLIC" / true (public + orderable)
103 * coveredServiceIds? }] // metadata for attachBookingsCoverage; not sent here
104 * @returns [{ id, name, coveredServiceIds }] (id = plan.id — orders by it AND is the coverage externalId)
105 */
106 async function createPlans ( ctx , plans ) {
107 const out = [];
108 for ( const plan of plans) {
109 const free = (plan.type || "recurring" ) === "free" ;
110 const body = {
111 plan: {
112 name: plan.name,
113 description: plan.description,
114 status: "ACTIVE" , // REQUIRED — omitting returns 400 "status value is required"
115 visibility: plan.visibility || "PUBLIC" ,
116 buyable: plan.buyable ?? true ,
117 buyerCanCancel: plan.buyerCanCancel ?? true ,
118 pricingVariants: [ buildPricingVariant (plan)],
119 perks: buildPerks (plan.perks),
120 // free: cap lifetime reuse (older maxPurchasesPerBuyer is deprecated — prefer purchaseLimits)
121 ... (free ? { purchaseLimits: { type: "PER_MEMBER_LIFETIME" , count: 1 } } : {}),
122 },
123 };
124 const r = await req (ctx, "/pricing-plans/v3/plans" , { body });
125 out. push ({ id: r.plan?.id, name: plan.name, coveredServiceIds: plan.coveredServiceIds });
126 }
127 return out;
128 }
129
130 // ---- STEP 2: bookings coverage via Benefit Programs API (SKIP when no bookings in the run) ----
131 // Strictly ordered: plan.id -> programDefinition.id -> itemSetId -> items. Do NOT parallelize.
132
133 // 2a: READ the auto-created program definition (the Plans app creates it; you never create it).
134 // Provisioning is ~immediate but async — retry ONCE after a short backoff on 404/empty, don't loop.
135 async function getProgramDefinition ( ctx , planId ) {
136 const path = `/benefit-programs/v1/program-definitions/by-namespace-and-external-id?externalId=${ planId }&namespace=${ encodeURIComponent ( PP_NAMESPACE ) }` ;
137 try {
138 const r = await req (ctx, path, { method: "GET" });
139 if (r.programDefinition?.id) return r.programDefinition.id;
140 } catch {
141 // fall through to the single insurance retry
142 }
143 await sleep ( 1000 );
144 const r = await req (ctx, path, { method: "GET" });
145 return r.programDefinition?.id;
146 }
147
148 // 2b: create ONE pool definition with EXACTLY ONE benefit naming Bookings as the provider.
149 // price "0" = unlimited (default; no creditConfiguration). Limited pack: pass creditAmount ->
150 // price "1" + details.creditConfiguration (SIBLING of benefits[], NOT inside a benefit).
151 async function createPoolDefinition ( ctx , programDefinitionId , { creditAmount } = {}) {
152 const limited = creditAmount != null ;
153 const details = {
154 ... (limited ? { creditConfiguration: { amount: String (creditAmount) } } : {}),
155 benefits: [
156 {
157 benefitKey: randomUUID (), // freshly generated random UUID you supply
158 displayName: "Bookings sessions" ,
159 providerAppId: BOOKINGS_APP_ID ,
160 price: limited ? "1" : "0" ,
161 },
162 ],
163 };
164 const r = await req (ctx, "/benefit-programs/v1/pool-definitions" , {
165 body: {
166 poolDefinition: {
167 namespace: PP_NAMESPACE ,
168 displayName: "Bookings benefit" ,
169 programDefinitionIds: [programDefinitionId],
170 details,
171 },
172 cascade: "IMMEDIATELY" ,
173 },
174 });
175 // itemSetId lives at poolDefinition.details.benefits[i].itemSetId — one benefit here, so [0].
176 return r.poolDefinition?.details?.benefits?.[ 0 ]?.itemSetId;
177 }
178
179 // 2c: bulk-create the benefit items — ONE item per covered service (externalId = bookings service id).
180 // Up to 100 items per call; category is an empty string; namespace/providerAppId repeat 2b's values.
181 async function createBenefitItems ( ctx , itemSetId , serviceIds ) {
182 return req (ctx, "/benefit-programs/v1/bulk/items/create" , {
183 body: {
184 items: serviceIds. map (( externalId ) => ({
185 namespace: PP_NAMESPACE ,
186 category: "" ,
187 providerAppId: BOOKINGS_APP_ID ,
188 itemSetId,
189 externalId,
190 })),
191 returnEntity: true ,
192 },
193 });
194 }
195
196 /**
197 * Wire a plan to COVER bookings services (STEP 2, per plan). Runs 2a -> 2b -> 2c in order.
198 * @param planId plan.id from createPlans
199 * @param serviceIds bookings service ids (seeded.bookings.serviceIds[]) the plan should cover
200 * @param creditAmount ? session-count for a limited pack; omit for the default unlimited ("0") case
201 * @returns { itemSetId, serviceIds } — keep as seed-time linkage (bookingsCoverage[planId])
202 */
203 async function attachBookingsCoverage ( ctx , planId , serviceIds , { creditAmount } = {}) {
204 const programDefinitionId = await getProgramDefinition (ctx, planId);
205 const itemSetId = await createPoolDefinition (ctx, programDefinitionId, { creditAmount });
206 await createBenefitItems (ctx, itemSetId, serviceIds);
207 return { itemSetId, serviceIds };
208 }
209
210 /**
211 * DEFAULT one-call path — create plan(s) and, per plan, wire bookings coverage when provided.
212 * One call; created plan ids stay in memory so coverage never needs hand-threading of plan.id.
213 * Order: createPlans (all plans) -> per covering plan attachBookingsCoverage (2a->2b->2c, in order).
214 * The program definition is provisioned asynchronously ~1s after the plan; the retry-once-after-
215 * backoff on its 404 lives in getProgramDefinition and is preserved via attachBookingsCoverage.
216 * @param plan { plans: [{ ...createPlans fields (name, description?, price, type?, billingCycle?,
217 * perks?, visibility?, buyable?),
218 * coveredServiceIds?, // bookings service ids this plan covers
219 * bookingsCoverage?: { serviceIds?, creditAmount? } }] } // richer coverage; creditAmount = limited pack
220 * @returns { plans: [{id,name,coveredServiceIds}], coverageAttached: [{planId,itemSetId,serviceIds}], benefitsCreated }
221 */
222 // Install the Wix Pricing Plans app before seeding — base44 sites aren't guaranteed to have it (no
223 // separate Setup step here, unlike the wix-headless recipe). Idempotent: re-installing returns 200.
224 async function installPricingPlansApp ( ctx ) {
225 return req (ctx, "/apps-installer-service/v1/app-instance/install" , { body: {
226 tenant: { tenantType: "SITE" , id: ctx.siteId },
227 appInstance: { appDefId: PRICING_PLANS_APP_ID , enabled: true },
228 } });
229 }
230
231 async function setupPricingPlans ( ctx , { plans = [] } = {}) {
232 await installPricingPlansApp (ctx);
233 const created = await createPlans (ctx, plans); // ids kept in memory
234 const coverageAttached = [];
235 for ( let i = 0 ; i < created. length ; i ++ ) {
236 const cov = plans[i].bookingsCoverage || {};
237 const serviceIds = cov.serviceIds || created[i].coveredServiceIds;
238 if ( ! serviceIds?. length ) continue ; // plans-only plan — skip STEP 2
239 const r = await attachBookingsCoverage (ctx, created[i].id, serviceIds, { creditAmount: cov.creditAmount });
240 coverageAttached. push ({ planId: created[i].id, ... r });
241 }
242 const benefitsCreated = coverageAttached. reduce (( n , c ) => n + c.serviceIds. length , 0 );
243 return { plans: created, coverageAttached, benefitsCreated };
244 }
245
246 module . exports = {
247 setupPricingPlans, installPricingPlansApp,
248 createPlans,
249 attachBookingsCoverage,
250 getProgramDefinition, createPoolDefinition, createBenefitItems,
251 };