@bash-app/bash-common 30.386.0 → 30.391.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.
Files changed (36) hide show
  1. package/dist/__tests__/scenePulse.test.d.ts +2 -0
  2. package/dist/__tests__/scenePulse.test.d.ts.map +1 -0
  3. package/dist/__tests__/scenePulse.test.js +52 -0
  4. package/dist/__tests__/scenePulse.test.js.map +1 -0
  5. package/dist/definitions.d.ts +5 -0
  6. package/dist/definitions.d.ts.map +1 -1
  7. package/dist/definitions.js.map +1 -1
  8. package/dist/extendedSchemas.d.ts +7 -0
  9. package/dist/extendedSchemas.d.ts.map +1 -1
  10. package/dist/extendedSchemas.js +8 -0
  11. package/dist/extendedSchemas.js.map +1 -1
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/scenePulse.d.ts +66 -0
  17. package/dist/scenePulse.d.ts.map +1 -0
  18. package/dist/scenePulse.js +54 -0
  19. package/dist/scenePulse.js.map +1 -0
  20. package/dist/utils/service/__tests__/foodAndBeverageQuote.test.d.ts +2 -0
  21. package/dist/utils/service/__tests__/foodAndBeverageQuote.test.d.ts.map +1 -0
  22. package/dist/utils/service/__tests__/foodAndBeverageQuote.test.js +353 -0
  23. package/dist/utils/service/__tests__/foodAndBeverageQuote.test.js.map +1 -0
  24. package/dist/utils/service/foodAndBeverageQuote.d.ts +171 -0
  25. package/dist/utils/service/foodAndBeverageQuote.d.ts.map +1 -0
  26. package/dist/utils/service/foodAndBeverageQuote.js +467 -0
  27. package/dist/utils/service/foodAndBeverageQuote.js.map +1 -0
  28. package/package.json +1 -1
  29. package/prisma/schema.prisma +45 -0
  30. package/src/__tests__/scenePulse.test.ts +69 -0
  31. package/src/definitions.ts +5 -0
  32. package/src/extendedSchemas.ts +8 -0
  33. package/src/index.ts +6 -0
  34. package/src/scenePulse.ts +113 -0
  35. package/src/utils/service/__tests__/foodAndBeverageQuote.test.ts +502 -0
  36. package/src/utils/service/foodAndBeverageQuote.ts +613 -0
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Shared quote logic for Food & Beverage event-service listings.
3
+ *
4
+ * Rules:
5
+ * - Priced per-guest (PER_ATTENDEE pricingModel).
6
+ * - Headcount floors at minimumQuantity if set.
7
+ * - Dollar total floors at minimumChargeCents if set.
8
+ * - Packages and add-ons still stack on top.
9
+ * - No check-in true-up (promoter settlement) — headcount is snapshotted
10
+ * at booking time only.
11
+ *
12
+ * Keep this module pure (no Prisma, no React). Import it in bash-app, api,
13
+ * and frontendServiceBookingUtils via the index.
14
+ */
15
+ // ---------------------------------------------------------------------------
16
+ // Subtype detection
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * All FoodAndBeverageSubType values (mirrors the Prisma enum).
20
+ * Also include legacy/alias strings the wizard uses (Caterer, PrivateChef, etc.)
21
+ * so detection works before the DB normalises them.
22
+ */
23
+ export const FOOD_AND_BEVERAGE_SUBTYPES = new Set([
24
+ // Canonical FoodAndBeverageSubType values
25
+ "Caterer",
26
+ "FoodTruck",
27
+ "FoodCartStand",
28
+ "PrivateChefs",
29
+ "Bartenders",
30
+ "Baristas",
31
+ "Mixologist",
32
+ "Sommelier",
33
+ "DessertBar",
34
+ "BeverageCart",
35
+ "ConcessionStand",
36
+ "CelebrationCake",
37
+ "GrazingTable",
38
+ "FoodAndBeverageOther",
39
+ // Wizard aliases / legacy keys
40
+ "PrivateChef",
41
+ "Bartender",
42
+ "Barista",
43
+ ]);
44
+ /**
45
+ * Custom-order celebration cakes — consultation, serving-size packages,
46
+ * pickup/delivery. Not a dessert station (DessertBar) and not per-guest catering.
47
+ */
48
+ export function isCelebrationCakeEventService(opts) {
49
+ return opts.eventServiceSubType === "CelebrationCake";
50
+ }
51
+ /**
52
+ * Charcuterie boards and grazing tables — board/table packages, not per-guest catering.
53
+ */
54
+ export function isGrazingTableEventService(opts) {
55
+ return opts.eventServiceSubType === "GrazingTable";
56
+ }
57
+ /**
58
+ * Wedding / ceremony officiant. Hosting Support — not F&B, not Entertainment Emcee.
59
+ */
60
+ export function isOfficiantEventService(opts) {
61
+ return opts.eventServiceSubType === "Officiant";
62
+ }
63
+ /** Cake and grazing: CUSTOM starting price + ServicePackage tiers. Never per-guest. */
64
+ export const PACKAGE_PRICED_FOOD_SUBTYPES = new Set([
65
+ "CelebrationCake",
66
+ "GrazingTable",
67
+ ]);
68
+ export function isPackagePricedFoodEventService(opts) {
69
+ return (!!opts.eventServiceSubType &&
70
+ PACKAGE_PRICED_FOOD_SUBTYPES.has(opts.eventServiceSubType));
71
+ }
72
+ /**
73
+ * Return true when a listing is an F&B EventService — either by top-level
74
+ * type or by subtype string (for legacy/alias safety).
75
+ */
76
+ export function isFoodAndBeverageEventService(opts) {
77
+ if (opts.eventServiceType === "FoodAndBeverage")
78
+ return true;
79
+ if (opts.eventServiceSubType && FOOD_AND_BEVERAGE_SUBTYPES.has(opts.eventServiceSubType))
80
+ return true;
81
+ return false;
82
+ }
83
+ /**
84
+ * Per-guest F&B only. Cake and grazing use packages — never this path.
85
+ */
86
+ export function isPerGuestFoodAndBeverageEventService(opts) {
87
+ return (isFoodAndBeverageEventService(opts) && !isPackagePricedFoodEventService(opts));
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Fulfillment modes
91
+ // ---------------------------------------------------------------------------
92
+ export const EVENT_SERVICE_FULFILLMENT_MODES = [
93
+ "Pickup",
94
+ "Delivery",
95
+ "FullService",
96
+ ];
97
+ const FULFILLMENT_MODE_SET = new Set(EVENT_SERVICE_FULFILLMENT_MODES);
98
+ export function isFulfillmentMode(mode) {
99
+ return FULFILLMENT_MODE_SET.has(mode);
100
+ }
101
+ export function filterFulfillmentModes(modes) {
102
+ return (modes ?? []).filter(isFulfillmentMode);
103
+ }
104
+ export const FULFILLMENT_MODE_LABEL = {
105
+ Pickup: "Pickup",
106
+ Delivery: "Delivery",
107
+ FullService: "Full Service",
108
+ };
109
+ // ---------------------------------------------------------------------------
110
+ // Menu category chips per subtype
111
+ // ---------------------------------------------------------------------------
112
+ /**
113
+ * Suggested menu category chips shown in the wizard and on the listing.
114
+ * Providers can add custom values; these are just pre-populated starting points.
115
+ * Not SKUs — no flavors, quantities, or prices here.
116
+ */
117
+ export const FB_MENU_CATEGORY_SUGGESTIONS = {
118
+ Caterer: [
119
+ "Appetizers",
120
+ "Salads",
121
+ "Sandwiches",
122
+ "Mains",
123
+ "Sides",
124
+ "Desserts",
125
+ "Beverages",
126
+ "Vegetarian",
127
+ "Vegan",
128
+ "Gluten-Free",
129
+ ],
130
+ DessertBar: [
131
+ "Cakes",
132
+ "Cupcakes",
133
+ "Cookies",
134
+ "Dessert Table",
135
+ "Chocolate Fountain",
136
+ "Ice Cream",
137
+ "Candy Buffet",
138
+ "Gluten-Free",
139
+ "Vegan",
140
+ "Custom Orders",
141
+ ],
142
+ FoodTruck: [
143
+ "Street Food",
144
+ "Burgers",
145
+ "Tacos",
146
+ "Pizza",
147
+ "BBQ",
148
+ "Ethnic Cuisine",
149
+ "Desserts",
150
+ "Vegan",
151
+ "Gluten-Free",
152
+ ],
153
+ PrivateChef: [
154
+ "Appetizers",
155
+ "Multi-Course Dinner",
156
+ "Wine Pairing",
157
+ "Dietary Specialist",
158
+ "Tasting Menu",
159
+ "Family-Style",
160
+ ],
161
+ PrivateChefs: [
162
+ "Appetizers",
163
+ "Multi-Course Dinner",
164
+ "Wine Pairing",
165
+ "Dietary Specialist",
166
+ "Tasting Menu",
167
+ "Family-Style",
168
+ ],
169
+ Bartenders: [
170
+ "Cocktails",
171
+ "Beer & Wine",
172
+ "Open Bar",
173
+ "Mocktails",
174
+ "Craft Beer",
175
+ "Specialty Drinks",
176
+ ],
177
+ Bartender: [
178
+ "Cocktails",
179
+ "Beer & Wine",
180
+ "Open Bar",
181
+ "Mocktails",
182
+ "Craft Beer",
183
+ "Specialty Drinks",
184
+ ],
185
+ Baristas: ["Espresso", "Cold Brew", "Lattes", "Tea", "Specialty Beverages"],
186
+ Barista: ["Espresso", "Cold Brew", "Lattes", "Tea", "Specialty Beverages"],
187
+ FoodCartStand: [
188
+ "Street Food",
189
+ "Snacks",
190
+ "Desserts",
191
+ "Coffee",
192
+ "Specialty Station",
193
+ ],
194
+ BeverageCart: ["Coffee", "Tea", "Juice", "Smoothies", "Mocktails", "Water"],
195
+ };
196
+ /**
197
+ * Get suggested menu categories for a subtype.
198
+ * Falls back to a generic list if the subtype is not found.
199
+ */
200
+ export function getFoodAndBeverageMenuCategories(subtype) {
201
+ if (subtype && FB_MENU_CATEGORY_SUGGESTIONS[subtype]) {
202
+ return FB_MENU_CATEGORY_SUGGESTIONS[subtype];
203
+ }
204
+ return ["Mains", "Sides", "Desserts", "Beverages", "Dietary Options"];
205
+ }
206
+ /**
207
+ * The effective guest count used for the quote.
208
+ * Floors at minimumQuantity so the provider is never charged less than their floor.
209
+ */
210
+ export function quotedGuestCount(headcount, minimumQuantity) {
211
+ if (!minimumQuantity || minimumQuantity <= 0)
212
+ return Math.max(0, headcount);
213
+ return Math.max(headcount, minimumQuantity);
214
+ }
215
+ /**
216
+ * Total food & beverage quote in cents, before packages, add-ons, and fees.
217
+ *
218
+ * Calculation order (mirrors the brochure's structure):
219
+ * 1. Floor headcount at minimumQuantity → quotedGuests
220
+ * 2. Compute rate total: baseRateCents × quotedGuests
221
+ * 3. Floor at minimumChargeCents
222
+ *
223
+ * Packages and add-ons are NOT included here — they are added on top by
224
+ * frontendServiceBookingUtils (same as for all other service types).
225
+ */
226
+ export function foodAndBeverageQuoteCents({ baseRateCents, headcount, minimumQuantity, minimumChargeCents, }) {
227
+ if (baseRateCents <= 0 || headcount < 0)
228
+ return 0;
229
+ const guests = quotedGuestCount(headcount, minimumQuantity);
230
+ const rateTotalCents = baseRateCents * guests;
231
+ const floorCents = minimumChargeCents ?? 0;
232
+ return Math.max(rateTotalCents, floorCents);
233
+ }
234
+ /**
235
+ * Human-readable summary string, e.g. "$18 / guest · min 25 guests".
236
+ */
237
+ export function foodAndBeverageRateSummary(baseRateCents, minimumQuantity) {
238
+ const formatted = new Intl.NumberFormat("en-US", {
239
+ style: "currency",
240
+ currency: "USD",
241
+ minimumFractionDigits: 0,
242
+ maximumFractionDigits: 2,
243
+ }).format(baseRateCents / 100);
244
+ const perGuest = `${formatted} / guest`;
245
+ if (minimumQuantity && minimumQuantity > 0) {
246
+ return `${perGuest} · min ${minimumQuantity} guests`;
247
+ }
248
+ return perGuest;
249
+ }
250
+ /**
251
+ * Return true if the entered headcount is below the listing's minimum.
252
+ * Use this to block checkout with a human-readable message.
253
+ */
254
+ export function isBelowGuestMinimum(headcount, minimumQuantity) {
255
+ if (!minimumQuantity || minimumQuantity <= 0)
256
+ return false;
257
+ return headcount < minimumQuantity;
258
+ }
259
+ // ---------------------------------------------------------------------------
260
+ // Lead time
261
+ // ---------------------------------------------------------------------------
262
+ /**
263
+ * Human-readable lead time label, e.g. "Needs 7 days notice" or "Needs 2 weeks notice".
264
+ * Rounds partial weeks only when evenly divisible.
265
+ */
266
+ export function foodAndBeverageLeadTimeLabel(leadTimeDays) {
267
+ if (!leadTimeDays || leadTimeDays <= 0)
268
+ return null;
269
+ if (leadTimeDays % 7 === 0) {
270
+ const weeks = leadTimeDays / 7;
271
+ return `Needs ${weeks} ${weeks === 1 ? "week" : "weeks"} notice`;
272
+ }
273
+ return `Needs ${leadTimeDays} ${leadTimeDays === 1 ? "day" : "days"} notice`;
274
+ }
275
+ // ---------------------------------------------------------------------------
276
+ // Celebration cake chips (stored on goodsOrServices with lowercase prefixes)
277
+ // ---------------------------------------------------------------------------
278
+ export const CELEBRATION_CAKE_FULFILLMENT_MODES = ["Pickup", "Delivery"];
279
+ const CAKE_FULFILLMENT_MODE_SET = new Set(CELEBRATION_CAKE_FULFILLMENT_MODES);
280
+ export function isCelebrationCakeFulfillmentMode(mode) {
281
+ return CAKE_FULFILLMENT_MODE_SET.has(mode);
282
+ }
283
+ export function filterCelebrationCakeFulfillmentModes(modes) {
284
+ return (modes ?? []).filter(isCelebrationCakeFulfillmentMode);
285
+ }
286
+ export const CAKE_OPTION_KINDS = [
287
+ "flavor",
288
+ "frosting",
289
+ "filling",
290
+ "dietary",
291
+ ];
292
+ export const CELEBRATION_CAKE_FLAVORS = [
293
+ "Vanilla",
294
+ "Chocolate",
295
+ "Red Velvet",
296
+ "Marble",
297
+ "Carrot",
298
+ "Lemon",
299
+ "Almond",
300
+ "Funfetti",
301
+ "Spice",
302
+ "Coconut",
303
+ ];
304
+ export const CELEBRATION_CAKE_FROSTINGS = [
305
+ "Vanilla Buttercream",
306
+ "Chocolate Buttercream",
307
+ "Cream Cheese",
308
+ "Whipped Cream",
309
+ "Fondant",
310
+ "Ganache",
311
+ "Swiss Meringue",
312
+ ];
313
+ export const CELEBRATION_CAKE_FILLINGS = [
314
+ "Raspberry",
315
+ "Lemon Custard",
316
+ "Chocolate Mousse",
317
+ "Strawberry",
318
+ "Cream Cheese",
319
+ "Cannoli",
320
+ "Vanilla Buttercream",
321
+ ];
322
+ export const CELEBRATION_CAKE_DIETARY = [
323
+ "Gluten-Free",
324
+ "Vegan",
325
+ "Nut-Free",
326
+ "Sugar-Free",
327
+ "Dairy-Free",
328
+ ];
329
+ export function encodeCakeOption(kind, value) {
330
+ return `${kind}:${value}`;
331
+ }
332
+ /**
333
+ * Split prefixed cake chips back into grouped lists.
334
+ * Unprefixed leftovers land in `other` (caterer-style chips, if mixed).
335
+ */
336
+ export function parseCakeOptions(goodsOrServices) {
337
+ const result = {
338
+ flavors: [],
339
+ frostings: [],
340
+ fillings: [],
341
+ dietary: [],
342
+ other: [],
343
+ };
344
+ if (!goodsOrServices)
345
+ return result;
346
+ for (const raw of goodsOrServices) {
347
+ if (raw.startsWith("flavor:")) {
348
+ result.flavors.push(raw.slice("flavor:".length));
349
+ }
350
+ else if (raw.startsWith("frosting:")) {
351
+ result.frostings.push(raw.slice("frosting:".length));
352
+ }
353
+ else if (raw.startsWith("filling:")) {
354
+ result.fillings.push(raw.slice("filling:".length));
355
+ }
356
+ else if (raw.startsWith("dietary:")) {
357
+ result.dietary.push(raw.slice("dietary:".length));
358
+ }
359
+ else if (raw.length > 0) {
360
+ result.other.push(raw);
361
+ }
362
+ }
363
+ return result;
364
+ }
365
+ export function serializeCakeOptions(opts) {
366
+ return [
367
+ ...(opts.flavors ?? []).map((v) => encodeCakeOption("flavor", v)),
368
+ ...(opts.frostings ?? []).map((v) => encodeCakeOption("frosting", v)),
369
+ ...(opts.fillings ?? []).map((v) => encodeCakeOption("filling", v)),
370
+ ...(opts.dietary ?? []).map((v) => encodeCakeOption("dietary", v)),
371
+ ];
372
+ }
373
+ /**
374
+ * Listing line, e.g. "Cakes start at $350". Null when no starting price.
375
+ */
376
+ export function celebrationCakeStartingPriceLabel(minimumChargeCents) {
377
+ return startingPriceLabel("Cakes start at", minimumChargeCents);
378
+ }
379
+ function startingPriceLabel(prefix, minimumChargeCents) {
380
+ if (!minimumChargeCents || minimumChargeCents <= 0)
381
+ return null;
382
+ const formatted = new Intl.NumberFormat("en-US", {
383
+ style: "currency",
384
+ currency: "USD",
385
+ minimumFractionDigits: 0,
386
+ maximumFractionDigits: 2,
387
+ }).format(minimumChargeCents / 100);
388
+ return `${prefix} ${formatted}`;
389
+ }
390
+ export function grazingTableStartingPriceLabel(minimumChargeCents) {
391
+ return startingPriceLabel("Boards start at", minimumChargeCents);
392
+ }
393
+ export function packagePricedFoodStartingPriceLabel(eventServiceSubType, minimumChargeCents) {
394
+ if (eventServiceSubType === "GrazingTable") {
395
+ return grazingTableStartingPriceLabel(minimumChargeCents);
396
+ }
397
+ if (eventServiceSubType === "CelebrationCake") {
398
+ return celebrationCakeStartingPriceLabel(minimumChargeCents);
399
+ }
400
+ return null;
401
+ }
402
+ // ---------------------------------------------------------------------------
403
+ // Grazing / charcuterie chips (goodsOrServices with lowercase prefixes)
404
+ // ---------------------------------------------------------------------------
405
+ export const GRAZING_TABLE_FULFILLMENT_MODES = [
406
+ "Pickup",
407
+ "Delivery",
408
+ "FullService",
409
+ ];
410
+ export const GRAZING_OPTION_KINDS = ["style", "protein", "dietary"];
411
+ export const GRAZING_TABLE_STYLES = [
412
+ "Classic Charcuterie",
413
+ "Modern Grazing",
414
+ "Cheese Board",
415
+ "Dessert Board",
416
+ "Breakfast Board",
417
+ "Seasonal",
418
+ ];
419
+ export const GRAZING_TABLE_PROTEINS = [
420
+ "Cured Meats",
421
+ "Artisan Cheeses",
422
+ "Smoked Fish",
423
+ "Plant-Based",
424
+ "Crudité",
425
+ ];
426
+ export const GRAZING_TABLE_DIETARY = [
427
+ "Gluten-Free",
428
+ "Vegan",
429
+ "Nut-Free",
430
+ "Dairy-Free",
431
+ ];
432
+ export function encodeGrazingOption(kind, value) {
433
+ return `${kind}:${value}`;
434
+ }
435
+ export function parseGrazingOptions(goodsOrServices) {
436
+ const result = {
437
+ styles: [],
438
+ proteins: [],
439
+ dietary: [],
440
+ other: [],
441
+ };
442
+ if (!goodsOrServices)
443
+ return result;
444
+ for (const raw of goodsOrServices) {
445
+ if (raw.startsWith("style:")) {
446
+ result.styles.push(raw.slice("style:".length));
447
+ }
448
+ else if (raw.startsWith("protein:")) {
449
+ result.proteins.push(raw.slice("protein:".length));
450
+ }
451
+ else if (raw.startsWith("dietary:")) {
452
+ result.dietary.push(raw.slice("dietary:".length));
453
+ }
454
+ else if (raw.length > 0) {
455
+ result.other.push(raw);
456
+ }
457
+ }
458
+ return result;
459
+ }
460
+ export function serializeGrazingOptions(opts) {
461
+ return [
462
+ ...(opts.styles ?? []).map((v) => encodeGrazingOption("style", v)),
463
+ ...(opts.proteins ?? []).map((v) => encodeGrazingOption("protein", v)),
464
+ ...(opts.dietary ?? []).map((v) => encodeGrazingOption("dietary", v)),
465
+ ];
466
+ }
467
+ //# sourceMappingURL=foodAndBeverageQuote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foodAndBeverageQuote.js","sourceRoot":"","sources":["../../../src/utils/service/foodAndBeverageQuote.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash-app/bash-common",
3
- "version": "30.386.0",
3
+ "version": "30.391.0",
4
4
  "description": "Common data and scripts to use on the frontend and backend",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -5736,6 +5736,10 @@ model EventService {
5736
5736
  rateUnit String?
5737
5737
  minimumChargeCents Int?
5738
5738
  minimumQuantity Int?
5739
+ /// Delivery logistics for F&B listings: Pickup | Delivery | FullService.
5740
+ fulfillmentModes String[] @default([])
5741
+ /// Minimum advance-notice days required for F&B bookings (e.g. 7 = one week).
5742
+ leadTimeDays Int?
5739
5743
  crowdSize AmountOfGuests? @relation(fields: [crowdSizeId], references: [id], onDelete: Cascade)
5740
5744
  serviceRange ServiceRange? @relation(fields: [serviceRangeId], references: [id])
5741
5745
  service Service?
@@ -9490,6 +9494,8 @@ enum FoodAndBeverageSubType {
9490
9494
  DessertBar
9491
9495
  BeverageCart
9492
9496
  ConcessionStand
9497
+ CelebrationCake
9498
+ GrazingTable
9493
9499
  FoodAndBeverageOther
9494
9500
  }
9495
9501
 
@@ -9530,6 +9536,7 @@ enum RentalEquipmentSubType {
9530
9536
  BounceHouse
9531
9537
  WaterSlide
9532
9538
  CarnivalGames
9539
+ RestroomTrailer
9533
9540
  RentalEquipmentOther
9534
9541
  }
9535
9542
 
@@ -9617,6 +9624,7 @@ enum WellnessSubType {
9617
9624
  }
9618
9625
 
9619
9626
  enum HostingSupportSubType {
9627
+ Officiant
9620
9628
  HostingSupportOther
9621
9629
  }
9622
9630
 
@@ -9868,6 +9876,24 @@ enum DessertBarFormat {
9868
9876
  MultiStationDessert
9869
9877
  }
9870
9878
 
9879
+ enum CelebrationCakeFormat {
9880
+ WeddingCake
9881
+ BirthdayCake
9882
+ AnniversaryCake
9883
+ CorporateCake
9884
+ CustomOrder
9885
+ }
9886
+
9887
+ enum GrazingTableFormat {
9888
+ CharcuterieBoard
9889
+ CheeseBoard
9890
+ DessertBoard
9891
+ BreakfastBoard
9892
+ GrazingTableSetup
9893
+ CorporateSpread
9894
+ CustomOrder
9895
+ }
9896
+
9871
9897
  enum BeverageCartFormat {
9872
9898
  FullBarCart
9873
9899
  PremiumBarCart
@@ -10151,6 +10177,25 @@ enum TablesFormat {
10151
10177
  CustomTables
10152
10178
  }
10153
10179
 
10180
+ enum RestroomTrailerFormat {
10181
+ LuxuryTrailer
10182
+ StandardPortable
10183
+ ADAAccessible
10184
+ MultiStall
10185
+ HandwashStation
10186
+ }
10187
+
10188
+ enum OfficiantFormat {
10189
+ WeddingCeremony
10190
+ Elopement
10191
+ VowRenewal
10192
+ CommitmentCeremony
10193
+ Interfaith
10194
+ Nondenominational
10195
+ Civil
10196
+ CustomCeremony
10197
+ }
10198
+
10154
10199
  enum TentsFormat {
10155
10200
  ClearSpanTents
10156
10201
  FrameTents
@@ -0,0 +1,69 @@
1
+ /**
2
+ * parseScenePulseScope — city default, invalid → null.
3
+ */
4
+ import { describe, expect, test } from "@jest/globals";
5
+
6
+ import {
7
+ emptyScenePulseEnergy,
8
+ normalizeScenePulseGeoOverride,
9
+ parseScenePulseScope,
10
+ } from "../scenePulse.js";
11
+
12
+ describe("parseScenePulseScope", () => {
13
+ test("defaults missing to city", () => {
14
+ expect(parseScenePulseScope(undefined)).toBe("city");
15
+ expect(parseScenePulseScope(null)).toBe("city");
16
+ expect(parseScenePulseScope("")).toBe("city");
17
+ });
18
+
19
+ test("accepts city region world", () => {
20
+ expect(parseScenePulseScope("city")).toBe("city");
21
+ expect(parseScenePulseScope("Region")).toBe("region");
22
+ expect(parseScenePulseScope(" WORLD ")).toBe("world");
23
+ });
24
+
25
+ test("rejects unknown values", () => {
26
+ expect(parseScenePulseScope("you")).toBeNull();
27
+ expect(parseScenePulseScope(12)).toBeNull();
28
+ });
29
+ });
30
+
31
+ describe("normalizeScenePulseGeoOverride", () => {
32
+ test("returns null when city and state are blank", () => {
33
+ expect(normalizeScenePulseGeoOverride({})).toBeNull();
34
+ expect(
35
+ normalizeScenePulseGeoOverride({ city: " ", state: "" })
36
+ ).toBeNull();
37
+ });
38
+
39
+ test("trims city and state and ignores empty sibling", () => {
40
+ expect(
41
+ normalizeScenePulseGeoOverride({ city: " Denver ", state: " " })
42
+ ).toEqual({ city: "Denver", state: null });
43
+ expect(
44
+ normalizeScenePulseGeoOverride({ city: "", state: "CO" })
45
+ ).toEqual({ city: null, state: "CO" });
46
+ });
47
+
48
+ test("caps overly long values", () => {
49
+ const city = "A".repeat(90);
50
+ const parsed = normalizeScenePulseGeoOverride({ city, state: "Utah" });
51
+ expect(parsed?.city).toHaveLength(80);
52
+ expect(parsed?.state).toBe("Utah");
53
+ });
54
+ });
55
+
56
+ describe("emptyScenePulseEnergy", () => {
57
+ test("zeros every energy field", () => {
58
+ expect(emptyScenePulseEnergy()).toEqual({
59
+ liveNow: 0,
60
+ going: 0,
61
+ tonightBashes: 0,
62
+ tonightGoing: 0,
63
+ weekendBashes: 0,
64
+ weekendGoing: 0,
65
+ newThisWeek: 0,
66
+ namesInTheRoom: 0,
67
+ });
68
+ });
69
+ });
@@ -831,6 +831,11 @@ export type ApiServiceBookingParams = {
831
831
  bashEventId?: string;
832
832
  /** Booking-level bundled packages (e.g. "Gold DJ Package"), independent of hourly/daily rates. */
833
833
  packages?: ApiServicePackageParams[];
834
+ /**
835
+ * F&B per-guest bookings: host-entered guest count snapshotted at booking time.
836
+ * Saved to ServiceBooking.expectedAttendeesAtBooking. No promoter true-up.
837
+ */
838
+ foodAndBeverageHeadcount?: number;
834
839
  };
835
840
 
836
841
  export type ApiServiceCanBookParams = {} & ApiServiceBookingParams;
@@ -1054,6 +1054,14 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
1054
1054
  eventServiceSubType: true,
1055
1055
  formatOptions: true,
1056
1056
  crowdSize: true,
1057
+ // F&B per-guest pricing & logistics fields
1058
+ pricingModel: true,
1059
+ baseRateCents: true,
1060
+ minimumQuantity: true,
1061
+ minimumChargeCents: true,
1062
+ goodsOrServices: true,
1063
+ fulfillmentModes: true,
1064
+ leadTimeDays: true,
1057
1065
  },
1058
1066
  },
1059
1067
  entertainmentService: {