@bash-app/bash-common 30.408.0 → 30.410.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 (57) hide show
  1. package/dist/__tests__/partnerDealflow.test.d.ts +2 -0
  2. package/dist/__tests__/partnerDealflow.test.d.ts.map +1 -0
  3. package/dist/__tests__/partnerDealflow.test.js +51 -0
  4. package/dist/__tests__/partnerDealflow.test.js.map +1 -0
  5. package/dist/__tests__/partnerRevenueShareOptIn.test.d.ts +2 -0
  6. package/dist/__tests__/partnerRevenueShareOptIn.test.d.ts.map +1 -0
  7. package/dist/__tests__/partnerRevenueShareOptIn.test.js +55 -0
  8. package/dist/__tests__/partnerRevenueShareOptIn.test.js.map +1 -0
  9. package/dist/__tests__/reservedCustomBashEventType.test.d.ts +2 -0
  10. package/dist/__tests__/reservedCustomBashEventType.test.d.ts.map +1 -0
  11. package/dist/__tests__/reservedCustomBashEventType.test.js +19 -0
  12. package/dist/__tests__/reservedCustomBashEventType.test.js.map +1 -0
  13. package/dist/__tests__/serviceListingDealPreferences.test.d.ts +2 -0
  14. package/dist/__tests__/serviceListingDealPreferences.test.d.ts.map +1 -0
  15. package/dist/__tests__/serviceListingDealPreferences.test.js +84 -0
  16. package/dist/__tests__/serviceListingDealPreferences.test.js.map +1 -0
  17. package/dist/definitions.d.ts +5 -0
  18. package/dist/definitions.d.ts.map +1 -1
  19. package/dist/definitions.js +42 -0
  20. package/dist/definitions.js.map +1 -1
  21. package/dist/extendedSchemas.d.ts +78 -0
  22. package/dist/extendedSchemas.d.ts.map +1 -1
  23. package/dist/extendedSchemas.js +31 -0
  24. package/dist/extendedSchemas.js.map +1 -1
  25. package/dist/homepageSyntheticRows.d.ts +9 -0
  26. package/dist/homepageSyntheticRows.d.ts.map +1 -0
  27. package/dist/homepageSyntheticRows.js +26 -0
  28. package/dist/homepageSyntheticRows.js.map +1 -0
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +4 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/utils/service/partnerDealflow.d.ts +12 -0
  34. package/dist/utils/service/partnerDealflow.d.ts.map +1 -0
  35. package/dist/utils/service/partnerDealflow.js +33 -0
  36. package/dist/utils/service/partnerDealflow.js.map +1 -0
  37. package/dist/utils/service/partnerRevenueShareOptIn.d.ts +18 -0
  38. package/dist/utils/service/partnerRevenueShareOptIn.d.ts.map +1 -0
  39. package/dist/utils/service/partnerRevenueShareOptIn.js +46 -0
  40. package/dist/utils/service/partnerRevenueShareOptIn.js.map +1 -0
  41. package/dist/utils/service/serviceListingDealPreferences.d.ts +28 -0
  42. package/dist/utils/service/serviceListingDealPreferences.d.ts.map +1 -0
  43. package/dist/utils/service/serviceListingDealPreferences.js +126 -0
  44. package/dist/utils/service/serviceListingDealPreferences.js.map +1 -0
  45. package/package.json +5 -2
  46. package/prisma/schema.prisma +58 -0
  47. package/src/__tests__/partnerDealflow.test.ts +62 -0
  48. package/src/__tests__/partnerRevenueShareOptIn.test.ts +68 -0
  49. package/src/__tests__/reservedCustomBashEventType.test.ts +27 -0
  50. package/src/__tests__/serviceListingDealPreferences.test.ts +112 -0
  51. package/src/definitions.ts +51 -0
  52. package/src/extendedSchemas.ts +33 -0
  53. package/src/homepageSyntheticRows.ts +32 -0
  54. package/src/index.ts +4 -0
  55. package/src/utils/service/partnerDealflow.ts +47 -0
  56. package/src/utils/service/partnerRevenueShareOptIn.ts +70 -0
  57. package/src/utils/service/serviceListingDealPreferences.ts +184 -0
@@ -5900,6 +5900,14 @@ model EventService {
5900
5900
  fulfillmentModes String[] @default([])
5901
5901
  /// Minimum advance-notice days required for F&B bookings (e.g. 7 = one week).
5902
5902
  leadTimeDays Int?
5903
+ openToRevenueShareWithHost Boolean @default(false)
5904
+ revenueShareWithHostPercentage Int?
5905
+ revenueShareWithHostBasis String?
5906
+ openToInKindDeals Boolean @default(false)
5907
+ inKindDealOfferText String?
5908
+ inKindEstimatedValueCents Int?
5909
+ dealRequiresMinimumCash Boolean @default(false)
5910
+ dealMinimumCashCents Int?
5903
5911
  crowdSize AmountOfGuests? @relation(fields: [crowdSizeId], references: [id], onDelete: Cascade)
5904
5912
  serviceRange ServiceRange? @relation(fields: [serviceRangeId], references: [id])
5905
5913
  service Service?
@@ -5958,6 +5966,14 @@ model EntertainmentService {
5958
5966
  minimumQuantity Int?
5959
5967
  tierPricing Json?
5960
5968
  pricingNotes String?
5969
+ openToRevenueShareWithHost Boolean @default(false)
5970
+ revenueShareWithHostPercentage Int?
5971
+ revenueShareWithHostBasis String?
5972
+ openToInKindDeals Boolean @default(false)
5973
+ inKindDealOfferText String?
5974
+ inKindEstimatedValueCents Int?
5975
+ dealRequiresMinimumCash Boolean @default(false)
5976
+ dealMinimumCashCents Int?
5961
5977
  crowdSize AmountOfGuests? @relation(fields: [crowdSizeId], references: [id], onDelete: Cascade)
5962
5978
  serviceRange ServiceRange? @relation(fields: [serviceRangeId], references: [id])
5963
5979
  service Service?
@@ -6072,6 +6088,18 @@ model VendorPreferences {
6072
6088
  bidMinLeadDays Int?
6073
6089
  /// small | medium | large — typical event size the vendor prefers.
6074
6090
  bidProjectSize String?
6091
+ /// Partner offers a revenue share with the host instead of (or in addition to) upfront fees.
6092
+ openToRevenueShareWithHost Boolean @default(false)
6093
+ /// 0..100 — share of the partner's relevant sales paid to the host.
6094
+ revenueShareWithHostPercentage Int?
6095
+ /// ALL_SALES | REFERRAL_ATTRIBUTED — which sales count toward the share.
6096
+ revenueShareWithHostBasis String?
6097
+ openToInKindDeals Boolean @default(false)
6098
+ inKindDealOfferText String?
6099
+ inKindEstimatedValueCents Int?
6100
+ /// Minimum cash (cents) required alongside rev-share or in-kind deals.
6101
+ dealRequiresMinimumCash Boolean @default(false)
6102
+ dealMinimumCashCents Int?
6075
6103
  createdAt DateTime @default(now())
6076
6104
  updatedAt DateTime @updatedAt
6077
6105
  vendor Vendor @relation(fields: [vendorId], references: [id], onDelete: Cascade)
@@ -6150,6 +6178,14 @@ model Exhibitor {
6150
6178
  bidMinLeadDays Int?
6151
6179
  /// small | medium | large — typical event size the exhibitor is comfortable with.
6152
6180
  bidProjectSize String?
6181
+ openToRevenueShareWithHost Boolean @default(false)
6182
+ revenueShareWithHostPercentage Int?
6183
+ revenueShareWithHostBasis String?
6184
+ openToInKindDeals Boolean @default(false)
6185
+ inKindDealOfferText String?
6186
+ inKindEstimatedValueCents Int?
6187
+ dealRequiresMinimumCash Boolean @default(false)
6188
+ dealMinimumCashCents Int?
6153
6189
 
6154
6190
  crowdSize AmountOfGuests? @relation(fields: [crowdSizeId], references: [id], onDelete: Cascade)
6155
6191
  serviceRange ServiceRange? @relation(fields: [serviceRangeId], references: [id])
@@ -6186,6 +6222,14 @@ model Sponsor {
6186
6222
  bidMinLeadDays Int?
6187
6223
  /// small | medium | large — typical event size the sponsor is comfortable with.
6188
6224
  bidProjectSize String?
6225
+ openToRevenueShareWithHost Boolean @default(false)
6226
+ revenueShareWithHostPercentage Int?
6227
+ revenueShareWithHostBasis String?
6228
+ openToInKindDeals Boolean @default(false)
6229
+ inKindDealOfferText String?
6230
+ inKindEstimatedValueCents Int?
6231
+ dealRequiresMinimumCash Boolean @default(false)
6232
+ dealMinimumCashCents Int?
6189
6233
 
6190
6234
  service Service?
6191
6235
  crowdSize AmountOfGuests? @relation(fields: [crowdSizeId], references: [id])
@@ -6277,6 +6321,11 @@ model Venue {
6277
6321
  revenueShareBasis String?
6278
6322
  /// Free-form notes shown next to the rev-share split.
6279
6323
  revenueShareNotes String?
6324
+ openToInKindDeals Boolean @default(false)
6325
+ inKindDealOfferText String?
6326
+ inKindEstimatedValueCents Int?
6327
+ dealRequiresMinimumCash Boolean @default(false)
6328
+ dealMinimumCashCents Int?
6280
6329
 
6281
6330
  fbCarryoverCredits VenueFBCarryoverCredit[]
6282
6331
  fbMinimumOverrides VenueFBMinimumOverride[]
@@ -8075,6 +8124,15 @@ model ServiceBooking {
8075
8124
  revenueShareHostPctSnapshot Int?
8076
8125
  /// Snapshot of `Venue.revenueShareBasis`.
8077
8126
  revenueShareBasisSnapshot String?
8127
+ /// Partner / venue deal prefs at confirmation (rev-share, in-kind, min cash).
8128
+ openToRevenueShareWithHostSnapshot Boolean?
8129
+ revenueShareWithHostPercentageSnapshot Int?
8130
+ revenueShareWithHostBasisSnapshot String?
8131
+ openToInKindDealsSnapshot Boolean?
8132
+ inKindDealOfferTextSnapshot String?
8133
+ inKindEstimatedValueCentsSnapshot Int?
8134
+ dealRequiresMinimumCashSnapshot Boolean?
8135
+ dealMinimumCashCentsSnapshot Int?
8078
8136
  /// Computed venue rev-share owed (cents) at settlement.
8079
8137
  revenueShareCalculatedCents Int?
8080
8138
  // Settlement lifecycle state (drives dunning, retries, dispute window).
@@ -0,0 +1,62 @@
1
+ import type { ServiceExt } from "../extendedSchemas";
2
+ import {
3
+ readPartnerDealflowFields,
4
+ serviceListingOpenToInKindDeal,
5
+ } from "../utils/service/partnerDealflow";
6
+
7
+ describe("partnerDealflow", () => {
8
+ it("detects in-kind listings with non-empty offer text", () => {
9
+ const service = {
10
+ sponsor: {
11
+ openToInKindDeals: true,
12
+ inKindDealOfferText: "100 free socks",
13
+ },
14
+ } as ServiceExt;
15
+
16
+ expect(readPartnerDealflowFields(service)?.openToInKindDeals).toBe(true);
17
+ expect(serviceListingOpenToInKindDeal(service)).toBe(true);
18
+ });
19
+
20
+ it("rejects in-kind when offer text is blank", () => {
21
+ const service = {
22
+ sponsor: {
23
+ openToInKindDeals: true,
24
+ inKindDealOfferText: " ",
25
+ },
26
+ } as ServiceExt;
27
+
28
+ expect(serviceListingOpenToInKindDeal(service)).toBe(false);
29
+ });
30
+
31
+ it("reads dealflow from vendor preferences before flat partner rows", () => {
32
+ const service = {
33
+ vendor: {
34
+ preferences: {
35
+ openToInKindDeals: true,
36
+ inKindDealOfferText: "Sample trays",
37
+ dealRequiresMinimumCash: true,
38
+ dealMinimumCashCents: 5000,
39
+ },
40
+ },
41
+ sponsor: {
42
+ openToInKindDeals: true,
43
+ inKindDealOfferText: "should not win",
44
+ },
45
+ } as ServiceExt;
46
+
47
+ const row = readPartnerDealflowFields(service);
48
+ expect(row?.inKindDealOfferText).toBe("Sample trays");
49
+ expect(row?.dealMinimumCashCents).toBe(5000);
50
+ });
51
+
52
+ it("does not treat in-kind flag alone as a listing when toggle is off", () => {
53
+ const service = {
54
+ venue: {
55
+ openToInKindDeals: false,
56
+ inKindDealOfferText: "Free room night",
57
+ },
58
+ } as ServiceExt;
59
+
60
+ expect(serviceListingOpenToInKindDeal(service)).toBe(false);
61
+ });
62
+ });
@@ -0,0 +1,68 @@
1
+ import type { ServiceExt } from "../extendedSchemas";
2
+ import {
3
+ readPartnerRevenueShareFields,
4
+ serviceListingOpenToRevenueShare,
5
+ } from "../utils/service/partnerRevenueShareOptIn";
6
+
7
+ describe("partnerRevenueShareOptIn", () => {
8
+ it("treats venue ticket/bar rev share as discoverable without partner opt-in", () => {
9
+ const service = {
10
+ venue: {
11
+ revenueShareEnabled: true,
12
+ revenueShareHostPercentage: 10,
13
+ },
14
+ } as ServiceExt;
15
+
16
+ expect(serviceListingOpenToRevenueShare(service)).toBe(true);
17
+ expect(readPartnerRevenueShareFields(service)).toBeNull();
18
+ });
19
+
20
+ it("requires partner opt-in plus a non-null percentage", () => {
21
+ const openNoPct = {
22
+ vendor: {
23
+ preferences: {
24
+ openToRevenueShareWithHost: true,
25
+ revenueShareWithHostPercentage: null,
26
+ },
27
+ },
28
+ } as ServiceExt;
29
+
30
+ expect(serviceListingOpenToRevenueShare(openNoPct)).toBe(false);
31
+
32
+ const closedWithPct = {
33
+ vendor: {
34
+ preferences: {
35
+ openToRevenueShareWithHost: false,
36
+ revenueShareWithHostPercentage: 20,
37
+ },
38
+ },
39
+ } as ServiceExt;
40
+
41
+ expect(serviceListingOpenToRevenueShare(closedWithPct)).toBe(false);
42
+
43
+ const openZero = {
44
+ exhibitor: {
45
+ openToRevenueShareWithHost: true,
46
+ revenueShareWithHostPercentage: 0,
47
+ },
48
+ } as ServiceExt;
49
+
50
+ expect(serviceListingOpenToRevenueShare(openZero)).toBe(true);
51
+ });
52
+
53
+ it("reads rev-share fields from the first matching partner subtype", () => {
54
+ const service = {
55
+ eventService: {
56
+ openToRevenueShareWithHost: true,
57
+ revenueShareWithHostPercentage: 12,
58
+ revenueShareWithHostBasis: "REFERRAL_ATTRIBUTED",
59
+ },
60
+ } as ServiceExt;
61
+
62
+ expect(readPartnerRevenueShareFields(service)).toEqual({
63
+ openToRevenueShareWithHost: true,
64
+ revenueShareWithHostPercentage: 12,
65
+ revenueShareWithHostBasis: "REFERRAL_ATTRIBUTED",
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,27 @@
1
+ import {
2
+ isReservedCustomBashEventTypeDisplayName,
3
+ } from "../definitions.js";
4
+
5
+ describe("isReservedCustomBashEventTypeDisplayName", () => {
6
+ it("blocks homepage synthetic section titles", () => {
7
+ expect(isReservedCustomBashEventTypeDisplayName("Featured")).toBe(true);
8
+ expect(isReservedCustomBashEventTypeDisplayName("Popular")).toBe(true);
9
+ expect(isReservedCustomBashEventTypeDisplayName("Trending")).toBe(true);
10
+ expect(isReservedCustomBashEventTypeDisplayName("Ideas")).toBe(true);
11
+ expect(isReservedCustomBashEventTypeDisplayName("Pro Events")).toBe(true);
12
+ expect(isReservedCustomBashEventTypeDisplayName("The crew's going")).toBe(
13
+ true
14
+ );
15
+ });
16
+
17
+ it("blocks built-in bash type labels", () => {
18
+ expect(isReservedCustomBashEventTypeDisplayName("Birthday")).toBe(true);
19
+ expect(isReservedCustomBashEventTypeDisplayName("House Party")).toBe(true);
20
+ });
21
+
22
+ it("allows novel custom labels", () => {
23
+ expect(isReservedCustomBashEventTypeDisplayName("Rooftop Party")).toBe(
24
+ false
25
+ );
26
+ });
27
+ });
@@ -0,0 +1,112 @@
1
+ import type { ServiceExt } from "../extendedSchemas";
2
+ import {
3
+ buildPartnerDealflowBookingSnapshot,
4
+ getServiceListingDealBadges,
5
+ getServiceListingDealDetailRows,
6
+ } from "../utils/service/serviceListingDealPreferences";
7
+
8
+ describe("serviceListingDealPreferences", () => {
9
+ it("shows min cash badge only when amount is set", () => {
10
+ const withMin = {
11
+ sponsor: {
12
+ dealRequiresMinimumCash: true,
13
+ dealMinimumCashCents: 25000,
14
+ },
15
+ } as ServiceExt;
16
+
17
+ expect(getServiceListingDealBadges(withMin).some((b) => b.id === "min-cash")).toBe(
18
+ true
19
+ );
20
+
21
+ const checkboxOnly = {
22
+ sponsor: {
23
+ dealRequiresMinimumCash: true,
24
+ dealMinimumCashCents: null,
25
+ },
26
+ } as ServiceExt;
27
+
28
+ expect(
29
+ getServiceListingDealBadges(checkboxOnly).some((b) => b.id === "min-cash")
30
+ ).toBe(false);
31
+ });
32
+
33
+ it("snapshots venue rev share instead of partner fields when venue is enabled", () => {
34
+ const service = {
35
+ venue: {
36
+ revenueShareEnabled: true,
37
+ revenueShareHostPercentage: 18,
38
+ revenueShareBasis: "ALL_SALES",
39
+ },
40
+ eventService: {
41
+ openToRevenueShareWithHost: true,
42
+ revenueShareWithHostPercentage: 99,
43
+ },
44
+ } as ServiceExt;
45
+
46
+ const snap = buildPartnerDealflowBookingSnapshot(service);
47
+
48
+ expect(snap.openToRevenueShareWithHostSnapshot).toBe(true);
49
+ expect(snap.revenueShareWithHostPercentageSnapshot).toBe(18);
50
+ expect(snap.revenueShareWithHostBasisSnapshot).toBe("ALL_SALES");
51
+ });
52
+
53
+ it("snapshots hybrid min-cash flag even when cents are unset (Stripe amount is separate)", () => {
54
+ const service = {
55
+ sponsor: {
56
+ dealRequiresMinimumCash: true,
57
+ dealMinimumCashCents: null,
58
+ openToInKindDeals: true,
59
+ inKindDealOfferText: "Branded socks",
60
+ },
61
+ } as ServiceExt;
62
+
63
+ const snap = buildPartnerDealflowBookingSnapshot(service);
64
+
65
+ expect(snap.dealRequiresMinimumCashSnapshot).toBe(true);
66
+ expect(snap.dealMinimumCashCentsSnapshot).toBeNull();
67
+ expect(snap.openToInKindDealsSnapshot).toBe(true);
68
+ expect(snap.inKindDealOfferTextSnapshot).toBe("Branded socks");
69
+ });
70
+
71
+ it("snapshots rev-share opt-in with null percentage when partner saved incomplete prefs", () => {
72
+ const service = {
73
+ sponsor: {
74
+ openToRevenueShareWithHost: true,
75
+ revenueShareWithHostPercentage: null,
76
+ },
77
+ } as ServiceExt;
78
+
79
+ expect(buildPartnerDealflowBookingSnapshot(service)).toEqual({
80
+ openToRevenueShareWithHostSnapshot: true,
81
+ revenueShareWithHostPercentageSnapshot: null,
82
+ revenueShareWithHostBasisSnapshot: null,
83
+ });
84
+ });
85
+
86
+ it("includes min-cash detail copy only when cents are positive", () => {
87
+ const hybrid = {
88
+ sponsor: {
89
+ dealRequiresMinimumCash: true,
90
+ dealMinimumCashCents: 25000,
91
+ openToInKindDeals: true,
92
+ inKindDealOfferText: "100 pairs",
93
+ },
94
+ } as ServiceExt;
95
+
96
+ const rows = getServiceListingDealDetailRows(hybrid);
97
+ expect(rows.some((r) => r.label.includes("Minimum cash"))).toBe(true);
98
+
99
+ const flagOnly = {
100
+ sponsor: {
101
+ dealRequiresMinimumCash: true,
102
+ dealMinimumCashCents: 0,
103
+ },
104
+ } as ServiceExt;
105
+
106
+ expect(
107
+ getServiceListingDealDetailRows(flagOnly).some((r) =>
108
+ r.label.includes("Minimum cash")
109
+ )
110
+ ).toBe(false);
111
+ });
112
+ });
@@ -32,6 +32,8 @@ import {
32
32
  ServiceBookingExt,
33
33
  type UserExt,
34
34
  } from "./extendedSchemas.js";
35
+ import { DISCOVERY_SCOPE_SECTION_TITLE } from "./discoveryScope.js";
36
+ import { HOMEPAGE_SYNTHETIC_SECTION_ROW_KEYS } from "./homepageSyntheticRows.js";
35
37
  import { LuxonDateRange } from "./utils/luxonUtils.js";
36
38
  import { ApiServiceCantBookReason } from "./utils/service/apiServiceBookingApiUtils.js";
37
39
  import { ServiceAttendeeOption } from "./utils/service/attendeeOptionUtils.js";
@@ -2182,6 +2184,55 @@ export function bashSectionTitleForBashType(bashType: string): string {
2182
2184
  return pluralizeBashEventSingularLabel(spaced);
2183
2185
  }
2184
2186
 
2187
+ function normalizeReservedCustomBashEventTypeLabel(label: string): string {
2188
+ return label.trim().toLowerCase().replace(/\s+/g, " ");
2189
+ }
2190
+
2191
+ let reservedCustomBashEventTypeLabels: Set<string> | null = null;
2192
+
2193
+ function reservedCustomBashEventTypeLabelSet(): Set<string> {
2194
+ if (reservedCustomBashEventTypeLabels) {
2195
+ return reservedCustomBashEventTypeLabels;
2196
+ }
2197
+ const set = new Set<string>();
2198
+ const add = (value: string) => {
2199
+ const normalized = normalizeReservedCustomBashEventTypeLabel(value);
2200
+ if (normalized) set.add(normalized);
2201
+ };
2202
+
2203
+ for (const key of HOMEPAGE_SYNTHETIC_SECTION_ROW_KEYS) {
2204
+ add(key);
2205
+ add(bashSectionTitleForBashType(key));
2206
+ }
2207
+ for (const title of Object.values(DISCOVERY_SCOPE_SECTION_TITLE)) {
2208
+ add(title);
2209
+ }
2210
+ for (const title of Object.values(SYNTHETIC_BASH_SECTION_TITLE)) {
2211
+ add(title);
2212
+ }
2213
+ for (const enumKey of BASH_EVENT_TYPE_ENUM_VALUES) {
2214
+ const k = enumKey as BashEventType;
2215
+ add(enumKey);
2216
+ add(BashEventTypeToString[k]);
2217
+ add(bashSectionTitleForBashType(enumKey));
2218
+ }
2219
+
2220
+ reservedCustomBashEventTypeLabels = set;
2221
+ return set;
2222
+ }
2223
+
2224
+ /**
2225
+ * Homepage section titles and built-in bash types cannot be created as custom
2226
+ * types in the wizard (would collide with synthetic discovery rows).
2227
+ */
2228
+ export function isReservedCustomBashEventTypeDisplayName(
2229
+ displayName: string
2230
+ ): boolean {
2231
+ return reservedCustomBashEventTypeLabelSet().has(
2232
+ normalizeReservedCustomBashEventTypeLabel(displayName)
2233
+ );
2234
+ }
2235
+
2185
2236
  export type RecordKey = string | number | symbol;
2186
2237
  export type PartialExcept<T, K extends keyof T> = Partial<T> & {
2187
2238
  [P in K]: T[P];
@@ -975,6 +975,24 @@ export const SERVICE_SUBTYPE_CROWD_RANGE_INCLUDE = {
975
975
  serviceRange: true,
976
976
  } as const;
977
977
 
978
+ /** Rev-share, in-kind, and min-cash (all partner subtype tables + venue). */
979
+ export const SERVICE_PARTNER_REV_INKIND_SELECT = {
980
+ openToRevenueShareWithHost: true,
981
+ revenueShareWithHostPercentage: true,
982
+ revenueShareWithHostBasis: true,
983
+ openToInKindDeals: true,
984
+ inKindDealOfferText: true,
985
+ inKindEstimatedValueCents: true,
986
+ dealRequiresMinimumCash: true,
987
+ dealMinimumCashCents: true,
988
+ } as const;
989
+
990
+ /** Includes host-bid opt-in (vendor prefs, exhibitor, sponsor only). */
991
+ export const SERVICE_PARTNER_DEAL_PREF_SELECT = {
992
+ ...SERVICE_PARTNER_REV_INKIND_SELECT,
993
+ openToHostBids: true,
994
+ } as const;
995
+
978
996
  export const VENDOR_DATA_TO_INCLUDE = {
979
997
  ...SERVICE_SUBTYPE_CROWD_RANGE_INCLUDE,
980
998
  spaceRequirements: true,
@@ -1069,6 +1087,14 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
1069
1087
  capacity: true,
1070
1088
  amenities: true,
1071
1089
  features: true,
1090
+ revenueShareEnabled: true,
1091
+ revenueShareHostPercentage: true,
1092
+ revenueShareBasis: true,
1093
+ openToInKindDeals: true,
1094
+ inKindDealOfferText: true,
1095
+ inKindEstimatedValueCents: true,
1096
+ dealRequiresMinimumCash: true,
1097
+ dealMinimumCashCents: true,
1072
1098
  },
1073
1099
  },
1074
1100
  eventService: {
@@ -1086,6 +1112,7 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
1086
1112
  goodsOrServices: true,
1087
1113
  fulfillmentModes: true,
1088
1114
  leadTimeDays: true,
1115
+ ...SERVICE_PARTNER_REV_INKIND_SELECT,
1089
1116
  },
1090
1117
  },
1091
1118
  entertainmentService: {
@@ -1094,6 +1121,7 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
1094
1121
  entertainmentServiceTypes: true,
1095
1122
  genre: true,
1096
1123
  crowdSize: true,
1124
+ ...SERVICE_PARTNER_REV_INKIND_SELECT,
1097
1125
  },
1098
1126
  },
1099
1127
  vendor: {
@@ -1108,18 +1136,23 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
1108
1136
  venueTypes: true,
1109
1137
  detailsText: true,
1110
1138
  crowdSize: true,
1139
+ preferences: {
1140
+ select: SERVICE_PARTNER_DEAL_PREF_SELECT,
1141
+ },
1111
1142
  },
1112
1143
  },
1113
1144
  exhibitor: {
1114
1145
  select: {
1115
1146
  id: true,
1116
1147
  crowdSize: true,
1148
+ ...SERVICE_PARTNER_DEAL_PREF_SELECT,
1117
1149
  },
1118
1150
  },
1119
1151
  sponsor: {
1120
1152
  select: {
1121
1153
  id: true,
1122
1154
  crowdSize: true,
1155
+ ...SERVICE_PARTNER_DEAL_PREF_SELECT,
1123
1156
  },
1124
1157
  },
1125
1158
  } satisfies Prisma.ServiceInclude;
@@ -0,0 +1,32 @@
1
+ import { DISCOVERY_SCOPE_ROW_KEYS } from "./discoveryScope.js";
2
+
3
+ /** Algorithm / geo / social rows — not host-picked `BashEventType` values. */
4
+ export const HOMEPAGE_DISCOVERY_SYNTHETIC_ROW_KEYS = [
5
+ "Ideas",
6
+ "Featured",
7
+ "Popular",
8
+ "Trending",
9
+ "CrewGoing",
10
+ ...DISCOVERY_SCOPE_ROW_KEYS,
11
+ ] as const;
12
+
13
+ /** Membership-tier inventory rows (creator tier, not event type). */
14
+ export const MEMBERSHIP_TIER_HOMEPAGE_ROW_KEYS = [
15
+ "Pro Events",
16
+ "Elite Events",
17
+ "Legend Events",
18
+ ] as const;
19
+
20
+ export const HOMEPAGE_SYNTHETIC_SECTION_ROW_KEYS: readonly string[] = [
21
+ ...HOMEPAGE_DISCOVERY_SYNTHETIC_ROW_KEYS,
22
+ ...MEMBERSHIP_TIER_HOMEPAGE_ROW_KEYS,
23
+ ];
24
+
25
+ export function isHomepageSyntheticSectionRowKey(key: string): boolean {
26
+ return (HOMEPAGE_SYNTHETIC_SECTION_ROW_KEYS as readonly string[]).includes(
27
+ key
28
+ );
29
+ }
30
+
31
+ /** @deprecated Prefer `HOMEPAGE_DISCOVERY_SYNTHETIC_ROW_KEYS` — kept for app imports. */
32
+ export const HOMEPAGE_DISCOVERY_ROW_KEYS = HOMEPAGE_DISCOVERY_SYNTHETIC_ROW_KEYS;
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./sideQuestTypes.js";
15
15
  export * from "./icebreakerPrompts.js";
16
16
  export * from "./causeTags.js";
17
17
  export * from "./discoveryScope.js";
18
+ export * from "./homepageSyntheticRows.js";
18
19
  export * from "./userTaste.js";
19
20
  export * from "./crewSocialCopy.js";
20
21
  export * from "./suggestionScore.js";
@@ -407,6 +408,9 @@ export * from "./utils/agencyBookingCheckoutCopy.js";
407
408
  export * from "./utils/service/serviceUtils.js";
408
409
  export * from "./utils/service/serviceListingMediaUtils.js";
409
410
  export * from "./utils/service/venueSettlementUtils.js";
411
+ export * from "./utils/service/partnerRevenueShareOptIn.js";
412
+ export * from "./utils/service/partnerDealflow.js";
413
+ export * from "./utils/service/serviceListingDealPreferences.js";
410
414
  export * from "./utils/service/offerAllowListUtils.js";
411
415
  export * from "./utils/service/offerComparisonUtils.js";
412
416
  export * from "./utils/service/unifiedPricingResolver.js";
@@ -0,0 +1,47 @@
1
+ import type { ServiceExt } from "../../extendedSchemas";
2
+
3
+ export type PartnerDealflowFields = {
4
+ openToInKindDeals?: boolean | null;
5
+ inKindDealOfferText?: string | null;
6
+ inKindEstimatedValueCents?: number | null;
7
+ dealRequiresMinimumCash?: boolean | null;
8
+ dealMinimumCashCents?: number | null;
9
+ };
10
+
11
+ function rowFromPartner(
12
+ row: PartnerDealflowFields | null | undefined
13
+ ): PartnerDealflowFields | null {
14
+ if (!row) return null;
15
+ return row;
16
+ }
17
+
18
+ /** Reads in-kind + hybrid cash prefs from any partner subtype payload. */
19
+ export function readPartnerDealflowFields(
20
+ service: ServiceExt
21
+ ): PartnerDealflowFields | null {
22
+ if (service.vendor?.preferences) {
23
+ return rowFromPartner(service.vendor.preferences);
24
+ }
25
+ if (service.exhibitor) {
26
+ return rowFromPartner(service.exhibitor);
27
+ }
28
+ if (service.sponsor) {
29
+ return rowFromPartner(service.sponsor);
30
+ }
31
+ if (service.entertainmentService) {
32
+ return rowFromPartner(service.entertainmentService);
33
+ }
34
+ if (service.eventService) {
35
+ return rowFromPartner(service.eventService);
36
+ }
37
+ if (service.venue) {
38
+ return rowFromPartner(service.venue);
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export function serviceListingOpenToInKindDeal(service: ServiceExt): boolean {
44
+ const row = readPartnerDealflowFields(service);
45
+ const text = row?.inKindDealOfferText?.trim();
46
+ return row?.openToInKindDeals === true && Boolean(text && text.length > 0);
47
+ }
@@ -0,0 +1,70 @@
1
+ import type { ServiceExt } from "../../extendedSchemas";
2
+
3
+ /** Which sales count when a partner shares revenue with the host. */
4
+ export const REVENUE_SHARE_WITH_HOST_BASIS = {
5
+ ALL_SALES: "ALL_SALES",
6
+ REFERRAL_ATTRIBUTED: "REFERRAL_ATTRIBUTED",
7
+ } as const;
8
+
9
+ export type RevenueShareWithHostBasis =
10
+ (typeof REVENUE_SHARE_WITH_HOST_BASIS)[keyof typeof REVENUE_SHARE_WITH_HOST_BASIS];
11
+
12
+ export const REVENUE_SHARE_WITH_HOST_BASIS_LABEL: Record<
13
+ RevenueShareWithHostBasis,
14
+ string
15
+ > = {
16
+ ALL_SALES: "All sales at the bash",
17
+ REFERRAL_ATTRIBUTED: "Sales from guests using my code only",
18
+ };
19
+
20
+ export type PartnerRevenueShareFields = {
21
+ openToRevenueShareWithHost?: boolean | null;
22
+ revenueShareWithHostPercentage?: number | null;
23
+ revenueShareWithHostBasis?: string | null;
24
+ };
25
+
26
+ function partnerFieldsFromRecord(
27
+ row: PartnerRevenueShareFields | null | undefined
28
+ ): PartnerRevenueShareFields | null {
29
+ if (!row) return null;
30
+ return row;
31
+ }
32
+
33
+ /** Reads partner rev-share opt-in from any service subtype payload. */
34
+ export function readPartnerRevenueShareFields(
35
+ service: ServiceExt
36
+ ): PartnerRevenueShareFields | null {
37
+ if (service.vendor?.preferences) {
38
+ return partnerFieldsFromRecord(service.vendor.preferences);
39
+ }
40
+ if (service.exhibitor) {
41
+ return partnerFieldsFromRecord(service.exhibitor);
42
+ }
43
+ if (service.sponsor) {
44
+ return partnerFieldsFromRecord(service.sponsor);
45
+ }
46
+ if (service.entertainmentService) {
47
+ return partnerFieldsFromRecord(service.entertainmentService);
48
+ }
49
+ if (service.eventService) {
50
+ return partnerFieldsFromRecord(service.eventService);
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /** True when listing is open to a revenue-share deal (partner or venue ticket split). */
56
+ export function serviceListingOpenToRevenueShare(service: ServiceExt): boolean {
57
+ const venue = service.venue;
58
+ if (
59
+ venue?.revenueShareEnabled === true &&
60
+ venue.revenueShareHostPercentage != null
61
+ ) {
62
+ return true;
63
+ }
64
+ const partner = readPartnerRevenueShareFields(service);
65
+ return (
66
+ partner?.openToRevenueShareWithHost === true &&
67
+ partner.revenueShareWithHostPercentage != null &&
68
+ partner.revenueShareWithHostPercentage >= 0
69
+ );
70
+ }