@bash-app/bash-common 30.186.0 → 30.196.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 (53) hide show
  1. package/dist/__tests__/stripeListingSubscriptionMessages.test.d.ts +2 -0
  2. package/dist/__tests__/stripeListingSubscriptionMessages.test.d.ts.map +1 -0
  3. package/dist/__tests__/stripeListingSubscriptionMessages.test.js +18 -0
  4. package/dist/__tests__/stripeListingSubscriptionMessages.test.js.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/stripeListingSubscriptionMessages.d.ts +13 -0
  10. package/dist/stripeListingSubscriptionMessages.d.ts.map +1 -0
  11. package/dist/stripeListingSubscriptionMessages.js +21 -0
  12. package/dist/stripeListingSubscriptionMessages.js.map +1 -0
  13. package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.d.ts +6 -0
  14. package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.d.ts.map +1 -0
  15. package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.js +104 -0
  16. package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.js.map +1 -0
  17. package/dist/utils/addressUtils.d.ts.map +1 -1
  18. package/dist/utils/addressUtils.js +157 -54
  19. package/dist/utils/addressUtils.js.map +1 -1
  20. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js +6 -2
  21. package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js.map +1 -1
  22. package/dist/utils/mediaClientDefaults.d.ts +7 -0
  23. package/dist/utils/mediaClientDefaults.d.ts.map +1 -0
  24. package/dist/utils/mediaClientDefaults.js +7 -0
  25. package/dist/utils/mediaClientDefaults.js.map +1 -0
  26. package/dist/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.d.ts +2 -0
  27. package/dist/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.d.ts.map +1 -0
  28. package/dist/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.js +18 -0
  29. package/dist/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.js.map +1 -0
  30. package/dist/utils/service/cancellationPolicyRefundResolver.d.ts +30 -0
  31. package/dist/utils/service/cancellationPolicyRefundResolver.d.ts.map +1 -0
  32. package/dist/utils/service/cancellationPolicyRefundResolver.js +82 -0
  33. package/dist/utils/service/cancellationPolicyRefundResolver.js.map +1 -0
  34. package/dist/utils/service/serviceUtils.d.ts +7 -0
  35. package/dist/utils/service/serviceUtils.d.ts.map +1 -1
  36. package/dist/utils/service/serviceUtils.js +178 -16
  37. package/dist/utils/service/serviceUtils.js.map +1 -1
  38. package/dist/utils/slugUtils.d.ts +5 -0
  39. package/dist/utils/slugUtils.d.ts.map +1 -1
  40. package/dist/utils/slugUtils.js +12 -0
  41. package/dist/utils/slugUtils.js.map +1 -1
  42. package/package.json +3 -2
  43. package/prisma/schema.prisma +6 -0
  44. package/src/__tests__/stripeListingSubscriptionMessages.test.ts +37 -0
  45. package/src/index.ts +3 -0
  46. package/src/stripeListingSubscriptionMessages.ts +29 -0
  47. package/src/utils/addressUtils.ts +175 -59
  48. package/src/utils/discountEngine/__tests__/eligibilityValidator.test.ts +6 -2
  49. package/src/utils/mediaClientDefaults.ts +6 -0
  50. package/src/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.ts +29 -0
  51. package/src/utils/service/cancellationPolicyRefundResolver.ts +112 -0
  52. package/src/utils/service/serviceUtils.ts +220 -20
  53. package/src/utils/slugUtils.ts +16 -0
@@ -0,0 +1,112 @@
1
+ import { ServiceCancellationPolicy } from "@prisma/client";
2
+ import {
3
+ SERVICE_CANCELLATION_POLICY_DATA,
4
+ type ServiceCancellationRefundPolicy,
5
+ } from "./serviceUtils.js";
6
+
7
+ /**
8
+ * Converts a tier threshold from SERVICE_CANCELLATION_POLICY_DATA to hours so
9
+ * rules that mix `days` and `hours` are compared on one timeline.
10
+ */
11
+ export function cancellationRuleThresholdHours(
12
+ rule: ServiceCancellationRefundPolicy
13
+ ): number {
14
+ if (rule.days !== undefined) {
15
+ return rule.days * 24;
16
+ }
17
+ if (rule.hours !== undefined) {
18
+ return rule.hours;
19
+ }
20
+ return 0;
21
+ }
22
+
23
+ export type ResolveCancellationRefundOptions = {
24
+ /** Event start time (UTC or local — caller must be consistent). */
25
+ eventStart: Date;
26
+ /** Defaults to `new Date()`. */
27
+ now?: Date;
28
+ };
29
+
30
+ /**
31
+ * Hours from `now` until `eventStart` (non-negative). Returns 0 if the event
32
+ * has started or `eventStart` is invalid.
33
+ */
34
+ export function hoursUntilEventStart(
35
+ eventStart: Date,
36
+ now: Date = new Date()
37
+ ): number {
38
+ const ms = eventStart.getTime() - now.getTime();
39
+ if (ms <= 0 || Number.isNaN(ms)) {
40
+ return 0;
41
+ }
42
+ return ms / (1000 * 60 * 60);
43
+ }
44
+
45
+ /**
46
+ * Resolves refund as a fraction in [0, 1] from {@link SERVICE_CANCELLATION_POLICY_DATA}.
47
+ * For each rule, if time until start is at least that rule's threshold, the rule
48
+ * applies; when multiple apply, the highest `refundPercentage` wins (matches
49
+ * tiered "full → partial → none" semantics).
50
+ */
51
+ export function resolveCancellationRefundFraction(
52
+ policy: ServiceCancellationPolicy | null | undefined,
53
+ options: ResolveCancellationRefundOptions
54
+ ): number {
55
+ if (policy == null || policy === ServiceCancellationPolicy.None) {
56
+ return 0;
57
+ }
58
+ const data = SERVICE_CANCELLATION_POLICY_DATA[policy];
59
+ if (!data?.refundPolicy?.length) {
60
+ return 0;
61
+ }
62
+
63
+ const now = options.now ?? new Date();
64
+ const hoursUntil = hoursUntilEventStart(options.eventStart, now);
65
+ if (hoursUntil <= 0) {
66
+ return 0;
67
+ }
68
+
69
+ let bestPercent = 0;
70
+ for (const rule of data.refundPolicy) {
71
+ const thresholdHours = cancellationRuleThresholdHours(rule);
72
+ if (hoursUntil >= thresholdHours && rule.refundPercentage > bestPercent) {
73
+ bestPercent = rule.refundPercentage;
74
+ }
75
+ }
76
+
77
+ return bestPercent / 100;
78
+ }
79
+
80
+ /**
81
+ * The winning tier rule for display (e.g. service booking UI), or null if none.
82
+ */
83
+ export function getMatchingCancellationRefundRule(
84
+ policy: ServiceCancellationPolicy | null | undefined,
85
+ options: ResolveCancellationRefundOptions
86
+ ): ServiceCancellationRefundPolicy | null {
87
+ if (policy == null || policy === ServiceCancellationPolicy.None) {
88
+ return null;
89
+ }
90
+ const data = SERVICE_CANCELLATION_POLICY_DATA[policy];
91
+ if (!data?.refundPolicy?.length) {
92
+ return null;
93
+ }
94
+
95
+ const now = options.now ?? new Date();
96
+ const hoursUntil = hoursUntilEventStart(options.eventStart, now);
97
+ if (hoursUntil <= 0) {
98
+ return null;
99
+ }
100
+
101
+ let bestRule: ServiceCancellationRefundPolicy | null = null;
102
+ let bestPercent = 0;
103
+ for (const rule of data.refundPolicy) {
104
+ const thresholdHours = cancellationRuleThresholdHours(rule);
105
+ if (hoursUntil >= thresholdHours && rule.refundPercentage > bestPercent) {
106
+ bestPercent = rule.refundPercentage;
107
+ bestRule = rule;
108
+ }
109
+ }
110
+
111
+ return bestRule;
112
+ }
@@ -123,6 +123,91 @@ export const SERVICE_CANCELLATION_POLICY_DATA: ServiceCancellationPolicyMap = {
123
123
  ],
124
124
  description: "Strict policy for vendor bookings with longer notice periods",
125
125
  },
126
+ // Exhibitor / Sponsor: same tier math as vendor; separate enum + copy for role clarity
127
+ [ServiceCancellationPolicyOption.ExhibitorFlexible]: {
128
+ name: "Exhibitor Flexible",
129
+ refundPolicy: [
130
+ {
131
+ days: 7,
132
+ refundPercentage: 100,
133
+ },
134
+ {
135
+ hours: 24,
136
+ refundPercentage: 50,
137
+ },
138
+ ],
139
+ description: "Flexible policy for exhibitor bookings",
140
+ },
141
+ [ServiceCancellationPolicyOption.ExhibitorStandard]: {
142
+ name: "Exhibitor Standard",
143
+ refundPolicy: [
144
+ {
145
+ days: 14,
146
+ refundPercentage: 100,
147
+ },
148
+ {
149
+ days: 3,
150
+ refundPercentage: 50,
151
+ },
152
+ ],
153
+ description: "Standard policy for exhibitor bookings",
154
+ },
155
+ [ServiceCancellationPolicyOption.ExhibitorStrict]: {
156
+ name: "Exhibitor Strict",
157
+ refundPolicy: [
158
+ {
159
+ days: 30,
160
+ refundPercentage: 100,
161
+ },
162
+ {
163
+ days: 7,
164
+ refundPercentage: 25,
165
+ },
166
+ ],
167
+ description: "Strict policy for exhibitor bookings",
168
+ },
169
+ [ServiceCancellationPolicyOption.SponsorFlexible]: {
170
+ name: "Sponsor Flexible",
171
+ refundPolicy: [
172
+ {
173
+ days: 7,
174
+ refundPercentage: 100,
175
+ },
176
+ {
177
+ hours: 24,
178
+ refundPercentage: 50,
179
+ },
180
+ ],
181
+ description: "Flexible policy for sponsorship bookings",
182
+ },
183
+ [ServiceCancellationPolicyOption.SponsorStandard]: {
184
+ name: "Sponsor Standard",
185
+ refundPolicy: [
186
+ {
187
+ days: 14,
188
+ refundPercentage: 100,
189
+ },
190
+ {
191
+ days: 3,
192
+ refundPercentage: 50,
193
+ },
194
+ ],
195
+ description: "Standard policy for sponsorship bookings",
196
+ },
197
+ [ServiceCancellationPolicyOption.SponsorStrict]: {
198
+ name: "Sponsor Strict",
199
+ refundPolicy: [
200
+ {
201
+ days: 30,
202
+ refundPercentage: 100,
203
+ },
204
+ {
205
+ days: 7,
206
+ refundPercentage: 25,
207
+ },
208
+ ],
209
+ description: "Strict policy for sponsorship bookings",
210
+ },
126
211
  } as const;
127
212
 
128
213
  function generateDescription(
@@ -140,9 +225,10 @@ function generateDescription(
140
225
  if (index === 0) {
141
226
  return `Guests may cancel their Booking until ${timeValue} ${unit} before the event start time and will receive ${refundText} of their Booking Price.`;
142
227
  } else {
143
- const previousTime =
144
- refundPolicy[index - 1].days ?? refundPolicy[index - 1].hours;
145
- return `Guests may cancel their Booking between ${timeValue} ${unit} and ${previousTime} ${unit} before the event start time and receive ${refundText} of their Booking Price.`;
228
+ const prevPolicy = refundPolicy[index - 1];
229
+ const prevUnit = prevPolicy.days ? "days" : "hours";
230
+ const prevTime = prevPolicy.days ?? prevPolicy.hours;
231
+ return `Guests may cancel their Booking between ${prevTime} ${prevUnit} and ${timeValue} ${unit} before the event start time and receive ${refundText} of their Booking Price.`;
146
232
  }
147
233
  });
148
234
 
@@ -158,6 +244,12 @@ function generateDescription(
158
244
  return descriptions.join(" ");
159
245
  }
160
246
 
247
+ /**
248
+ * Vendor cancellation policy copy shown in the service wizard. Hosts send
249
+ * **booking requests**; after acceptance and payment, the vendor's obligation
250
+ * is a **verified booking**. Use "verified booking" for vendor-initiated
251
+ * cancellation—never "booking request" in this generator.
252
+ */
161
253
  function generateVendorDescription(
162
254
  refundPolicy: ServiceCancellationRefundPolicy[]
163
255
  ): string {
@@ -171,11 +263,12 @@ function generateVendorDescription(
171
263
  : `${policy.refundPercentage}% refund from the Host (excluding Fees)`;
172
264
 
173
265
  if (index === 0) {
174
- return `Vendors may cancel their booking request until ${timeValue} ${unit} before the event start time and will receive ${refundText} of their payment.`;
266
+ return `If you (the vendor) cancel your verified booking at least ${timeValue} ${unit} before the event start time, you will receive ${refundText} of what you paid the Host.`;
175
267
  } else {
176
- const previousTime =
177
- refundPolicy[index - 1].days ?? refundPolicy[index - 1].hours;
178
- return `Vendors may cancel their booking request between ${timeValue} ${unit} and ${previousTime} ${unit} before the event start time and receive ${refundText} of their payment.`;
268
+ const prevPolicy = refundPolicy[index - 1];
269
+ const prevUnit = prevPolicy.days ? "days" : "hours";
270
+ const prevTime = prevPolicy.days ?? prevPolicy.hours;
271
+ return `If you (the vendor) cancel your verified booking between ${prevTime} ${prevUnit} and ${timeValue} ${unit} before the event start time, you will receive ${refundText} of what you paid the Host.`;
179
272
  }
180
273
  });
181
274
 
@@ -183,14 +276,97 @@ function generateVendorDescription(
183
276
  refundPolicy[refundPolicy.length - 1].days ??
184
277
  refundPolicy[refundPolicy.length - 1].hours;
185
278
  descriptions.push(
186
- `Vendor cancellations submitted less than ${lastTime} ${
279
+ `If you (the vendor) cancel your verified booking less than ${lastTime} ${
187
280
  refundPolicy[refundPolicy.length - 1].days ? "days" : "hours"
188
- } before the Event start time are not refundable.`
281
+ } before the event start time, your payment is not refundable under this policy.`
189
282
  );
190
283
 
191
- // Add host cancellation policy
192
284
  descriptions.push(
193
- `If the Host cancels the event or vendor booking, the Vendor will receive a full refund regardless of timing, unless the cancellation is due to vendor non-compliance with event requirements.`
285
+ `If the Host cancels the event or your verified booking, you will generally receive a full refund of what you paid, unless the cancellation was due to your non-compliance with the Host's or event's requirements.`
286
+ );
287
+
288
+ return descriptions.join(" ");
289
+ }
290
+
291
+ /**
292
+ * Exhibitor-facing copy: hosts send booking requests; after accept + pay, use
293
+ * "verified booking" for exhibitor-initiated cancellation (same convention as vendors).
294
+ */
295
+ function generateExhibitorDescription(
296
+ refundPolicy: ServiceCancellationRefundPolicy[]
297
+ ): string {
298
+ const descriptions = refundPolicy.map((policy, index) => {
299
+ const unit = policy.days ? "days" : "hours";
300
+ const timeValue = policy.days ?? policy.hours;
301
+
302
+ const refundText =
303
+ policy.refundPercentage === 100
304
+ ? "a full refund from the Host (including all Fees)"
305
+ : `${policy.refundPercentage}% refund from the Host (excluding Fees)`;
306
+
307
+ if (index === 0) {
308
+ return `If you (the exhibitor) cancel your verified booking at least ${timeValue} ${unit} before the event start time, you will receive ${refundText} of what you paid the Host.`;
309
+ } else {
310
+ const prevPolicy = refundPolicy[index - 1];
311
+ const prevUnit = prevPolicy.days ? "days" : "hours";
312
+ const prevTime = prevPolicy.days ?? prevPolicy.hours;
313
+ return `If you (the exhibitor) cancel your verified booking between ${prevTime} ${prevUnit} and ${timeValue} ${unit} before the event start time, you will receive ${refundText} of what you paid the Host.`;
314
+ }
315
+ });
316
+
317
+ const lastTime =
318
+ refundPolicy[refundPolicy.length - 1].days ??
319
+ refundPolicy[refundPolicy.length - 1].hours;
320
+ descriptions.push(
321
+ `If you (the exhibitor) cancel your verified booking less than ${lastTime} ${
322
+ refundPolicy[refundPolicy.length - 1].days ? "days" : "hours"
323
+ } before the event start time, your payment is not refundable under this policy.`
324
+ );
325
+
326
+ descriptions.push(
327
+ `If the Host cancels the event or your verified booking, you will generally receive a full refund of what you paid, unless the cancellation was due to your non-compliance with the Host's or event's requirements.`
328
+ );
329
+
330
+ return descriptions.join(" ");
331
+ }
332
+
333
+ /**
334
+ * Sponsor-facing copy: same payment-to-host model; use "sponsorship commitment" where
335
+ * "verified booking" would be awkward for sponsors.
336
+ */
337
+ function generateSponsorDescription(
338
+ refundPolicy: ServiceCancellationRefundPolicy[]
339
+ ): string {
340
+ const descriptions = refundPolicy.map((policy, index) => {
341
+ const unit = policy.days ? "days" : "hours";
342
+ const timeValue = policy.days ?? policy.hours;
343
+
344
+ const refundText =
345
+ policy.refundPercentage === 100
346
+ ? "a full refund from the Host (including all Fees)"
347
+ : `${policy.refundPercentage}% refund from the Host (excluding Fees)`;
348
+
349
+ if (index === 0) {
350
+ return `If you (the sponsor) cancel your verified sponsorship commitment at least ${timeValue} ${unit} before the event start time, you will receive ${refundText} of what you paid the Host.`;
351
+ } else {
352
+ const prevPolicy = refundPolicy[index - 1];
353
+ const prevUnit = prevPolicy.days ? "days" : "hours";
354
+ const prevTime = prevPolicy.days ?? prevPolicy.hours;
355
+ return `If you (the sponsor) cancel your verified sponsorship commitment between ${prevTime} ${prevUnit} and ${timeValue} ${unit} before the event start time, you will receive ${refundText} of what you paid the Host.`;
356
+ }
357
+ });
358
+
359
+ const lastTime =
360
+ refundPolicy[refundPolicy.length - 1].days ??
361
+ refundPolicy[refundPolicy.length - 1].hours;
362
+ descriptions.push(
363
+ `If you (the sponsor) cancel your verified sponsorship commitment less than ${lastTime} ${
364
+ refundPolicy[refundPolicy.length - 1].days ? "days" : "hours"
365
+ } before the event start time, your payment is not refundable under this policy.`
366
+ );
367
+
368
+ descriptions.push(
369
+ `If the Host cancels the event or your sponsorship, you will generally receive a full refund of what you paid, unless the cancellation was due to your non-compliance with the Host's or event's requirements.`
194
370
  );
195
371
 
196
372
  return descriptions.join(" ");
@@ -207,11 +383,14 @@ Object.keys(SERVICE_CANCELLATION_POLICY_DATA)
207
383
  SERVICE_CANCELLATION_POLICY_DATA[
208
384
  policyKey as ServiceCancellationPolicyOption
209
385
  ];
210
-
211
- // Use vendor-specific description for vendor policies
212
- const isVendorPolicy = (policyKey as string).startsWith('Vendor');
213
- if (isVendorPolicy) {
386
+
387
+ const keyStr = policyKey as string;
388
+ if (keyStr.startsWith("Vendor")) {
214
389
  policy.description = generateVendorDescription(policy.refundPolicy);
390
+ } else if (keyStr.startsWith("Exhibitor")) {
391
+ policy.description = generateExhibitorDescription(policy.refundPolicy);
392
+ } else if (keyStr.startsWith("Sponsor")) {
393
+ policy.description = generateSponsorDescription(policy.refundPolicy);
215
394
  } else {
216
395
  policy.description = generateDescription(policy.refundPolicy);
217
396
  }
@@ -277,6 +456,27 @@ export function serviceWizardUrl(
277
456
  return `/wz/services/${serviceId}/${serviceType}/${specificId}/${step}/${substep}`;
278
457
  }
279
458
 
459
+ /**
460
+ * Vendor, Exhibitor, and Sponsor profile wizards share this layout: major step 6 = "How",
461
+ * sub-step 3 = "Payment method" (partner profile wizard payment step).
462
+ */
463
+ export const PARTNER_SERVICE_PROFILE_WIZARD_PAYMENT_METHOD_MAJOR_STEP = 6;
464
+ export const PARTNER_SERVICE_PROFILE_WIZARD_PAYMENT_METHOD_SUB_STEP = 3;
465
+
466
+ export function partnerServiceProfileWizardPaymentMethodUrl(
467
+ serviceId: string,
468
+ serviceType: string,
469
+ specificId: string
470
+ ): string {
471
+ return serviceWizardUrl(
472
+ serviceId,
473
+ serviceType,
474
+ specificId,
475
+ PARTNER_SERVICE_PROFILE_WIZARD_PAYMENT_METHOD_MAJOR_STEP,
476
+ PARTNER_SERVICE_PROFILE_WIZARD_PAYMENT_METHOD_SUB_STEP
477
+ );
478
+ }
479
+
280
480
  export function serviceCheckoutUrl(
281
481
  serviceId: string,
282
482
  serviceType: string,
@@ -440,14 +640,14 @@ export const serviceSpecificInfoAI = {
440
640
  isArray: false,
441
641
  },
442
642
  vendorType: {
443
- description: `vendorType (set to "products" if selling physical goods, or "services" if providing vendor services like face painting)`,
643
+ description: `vendorType: use "products" for physical goods (retail food, merchandise, packaged goods) and "services" for experiential booth activities (face painting, henna, etc.). If the prompt is only a well-known business or brand name, infer the correct mode: restaurant chains, bakeries, coffee shops, snack brands → "products"; activity-only brands → "services" when appropriate.`,
444
644
  example: `"products"`,
445
645
  type: "string",
446
646
  isArray: false,
447
647
  },
448
648
  vendorProductTypes: {
449
- description: `vendorProductTypes (array of VendedProductType enum values for PRODUCT vendors: Art, HandcraftedItems, Pottery, Woodwork, LeatherGoods, Textiles, BakedGoods, Beverages, FoodItems, HoneyAndJams, PopcornAndSnacks, SweetTreats, Spices, Sauces, MakeupCosmetics, SkincareProducts, BodyCareProducts, Soaps, Perfumes, HairCareProducts, Clothing, TShirtsAndApparel, Jewelry, Accessories, Hats, Bags, Shoes, HomeDecor, Candles, Plants, Succulents, Furniture, Kitchenware, Books, Crystals, SpiritualItems, HerbalRemedies, VintageAntiques, Collectibles, PhotographyPrints, StickersAndStationery, Prints, Zines, Toys, BabyItems, PetProducts, TechAccessories, Other). Only use for vendors with vendorType="products".`,
450
- example: `["Jewelry", "HandcraftedItems"]`,
649
+ description: `vendorProductTypes (array of VendedProductType enum values for PRODUCT vendors: Art, HandcraftedItems, Pottery, Woodwork, LeatherGoods, Textiles, BakedGoods, Beverages, FoodItems, HoneyAndJams, PopcornAndSnacks, SweetTreats, Spices, Sauces, MakeupCosmetics, SkincareProducts, BodyCareProducts, Soaps, Perfumes, HairCareProducts, Clothing, TShirtsAndApparel, Jewelry, Accessories, Hats, Bags, Shoes, HomeDecor, Candles, Plants, Succulents, Furniture, Kitchenware, Books, Crystals, SpiritualItems, HerbalRemedies, VintageAntiques, Collectibles, PhotographyPrints, StickersAndStationery, Prints, Zines, Toys, BabyItems, PetProducts, TechAccessories, Other). Only use for vendors with vendorType="products". For famous food/dessert brands (e.g. doughnuts, coffee), use FoodItems and/or SweetTreats (both are valid when appropriate).`,
650
+ example: `["FoodItems", "SweetTreats"]`,
451
651
  type: "string",
452
652
  isArray: true,
453
653
  },
@@ -458,8 +658,8 @@ export const serviceSpecificInfoAI = {
458
658
  isArray: true,
459
659
  },
460
660
  goodsOrServices: {
461
- description: `goodsOrServices (array of specific products/services offered - the actual items, not categories)`,
462
- example: `["bracelets", "jewelry", "ponchos", "blankets"]`,
661
+ description: `goodsOrServices (array of specific items offered e.g. "Donuts", "Coffee", "Glazed doughnuts", not just category names). When the user names a famous brand, infer typical menu or product lines for that brand.`,
662
+ example: `["Donuts", "Coffee", "Seasonal flavors"]`,
463
663
  type: "string",
464
664
  isArray: true,
465
665
  },
@@ -1,4 +1,5 @@
1
1
  import slugify from 'slugify';
2
+ import { BASH_DETAIL_URL } from '../definitions.js';
2
3
 
3
4
  export function generateSlug(title: string): string {
4
5
  return slugify(title, {
@@ -31,6 +32,21 @@ export function generateBashDetailUrl(bashEventId: string, slug: string): string
31
32
  return `/bash/${bashEventId}-${slug}`;
32
33
  }
33
34
 
35
+ /**
36
+ * Public event path for links shared to social / QR (canonical frontend URL).
37
+ * With a slug: `/bash/{id}-{slug}`; without: `/bash/{id}` (legacy id-only segment).
38
+ */
39
+ export function getPublicBashDetailPath(
40
+ bashEventId: string,
41
+ slug: string | null | undefined
42
+ ): string {
43
+ const s = slug?.trim();
44
+ if (s) {
45
+ return generateBashDetailUrl(bashEventId, s);
46
+ }
47
+ return `${BASH_DETAIL_URL}/${bashEventId}`;
48
+ }
49
+
34
50
  export function parseBashUrlParams(param: string): { id: string; slug: string | null } | null {
35
51
  // Expected format: "id-slug" where id is first part before first dash
36
52
  const dashIndex = param.indexOf('-');