@bash-app/bash-common 30.406.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.
- package/dist/__tests__/partnerDealflow.test.d.ts +2 -0
- package/dist/__tests__/partnerDealflow.test.d.ts.map +1 -0
- package/dist/__tests__/partnerDealflow.test.js +51 -0
- package/dist/__tests__/partnerDealflow.test.js.map +1 -0
- package/dist/__tests__/partnerRevenueShareOptIn.test.d.ts +2 -0
- package/dist/__tests__/partnerRevenueShareOptIn.test.d.ts.map +1 -0
- package/dist/__tests__/partnerRevenueShareOptIn.test.js +55 -0
- package/dist/__tests__/partnerRevenueShareOptIn.test.js.map +1 -0
- package/dist/__tests__/reservedCustomBashEventType.test.d.ts +2 -0
- package/dist/__tests__/reservedCustomBashEventType.test.d.ts.map +1 -0
- package/dist/__tests__/reservedCustomBashEventType.test.js +19 -0
- package/dist/__tests__/reservedCustomBashEventType.test.js.map +1 -0
- package/dist/__tests__/serviceListingDealPreferences.test.d.ts +2 -0
- package/dist/__tests__/serviceListingDealPreferences.test.d.ts.map +1 -0
- package/dist/__tests__/serviceListingDealPreferences.test.js +84 -0
- package/dist/__tests__/serviceListingDealPreferences.test.js.map +1 -0
- package/dist/definitions.d.ts +13 -2
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +57 -2
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +97 -4
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js +35 -0
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/homepageSyntheticRows.d.ts +9 -0
- package/dist/homepageSyntheticRows.d.ts.map +1 -0
- package/dist/homepageSyntheticRows.js +26 -0
- package/dist/homepageSyntheticRows.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/sideQuestTypes.d.ts +1 -1
- package/dist/sideQuestTypes.d.ts.map +1 -1
- package/dist/utils/service/partnerDealflow.d.ts +12 -0
- package/dist/utils/service/partnerDealflow.d.ts.map +1 -0
- package/dist/utils/service/partnerDealflow.js +33 -0
- package/dist/utils/service/partnerDealflow.js.map +1 -0
- package/dist/utils/service/partnerRevenueShareOptIn.d.ts +18 -0
- package/dist/utils/service/partnerRevenueShareOptIn.d.ts.map +1 -0
- package/dist/utils/service/partnerRevenueShareOptIn.js +46 -0
- package/dist/utils/service/partnerRevenueShareOptIn.js.map +1 -0
- package/dist/utils/service/serviceListingDealPreferences.d.ts +28 -0
- package/dist/utils/service/serviceListingDealPreferences.d.ts.map +1 -0
- package/dist/utils/service/serviceListingDealPreferences.js +126 -0
- package/dist/utils/service/serviceListingDealPreferences.js.map +1 -0
- package/package.json +5 -2
- package/prisma/schema.prisma +67 -1
- package/src/__tests__/partnerDealflow.test.ts +62 -0
- package/src/__tests__/partnerRevenueShareOptIn.test.ts +68 -0
- package/src/__tests__/reservedCustomBashEventType.test.ts +27 -0
- package/src/__tests__/serviceListingDealPreferences.test.ts +112 -0
- package/src/definitions.ts +67 -2
- package/src/extendedSchemas.ts +51 -4
- package/src/homepageSyntheticRows.ts +32 -0
- package/src/index.ts +4 -0
- package/src/sideQuestTypes.ts +1 -1
- package/src/utils/service/partnerDealflow.ts +47 -0
- package/src/utils/service/partnerRevenueShareOptIn.ts +70 -0
- package/src/utils/service/serviceListingDealPreferences.ts +184 -0
package/src/extendedSchemas.ts
CHANGED
|
@@ -667,6 +667,9 @@ type BashEventExtBase = Override<
|
|
|
667
667
|
dateOptions?: IdeaDateOption[];
|
|
668
668
|
/** Public aggregate when poll enabled (no PII) */
|
|
669
669
|
datePollPublicSummary?: IdeaDatePollPublicSummary;
|
|
670
|
+
hideCancellationPolicy?: boolean;
|
|
671
|
+
hideGroupUp?: boolean;
|
|
672
|
+
hideServicesOpenTo?: boolean;
|
|
670
673
|
/** GET /event/:id — tickets sold (non-cancelled), for social proof */
|
|
671
674
|
goingCount?: number;
|
|
672
675
|
/** GET /event/:id — true when at least one Budget line has `showOnBashDetailPage` set. Drives the "View budget" reveal on the public bash page; never carries dollar amounts. */
|
|
@@ -972,6 +975,24 @@ export const SERVICE_SUBTYPE_CROWD_RANGE_INCLUDE = {
|
|
|
972
975
|
serviceRange: true,
|
|
973
976
|
} as const;
|
|
974
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
|
+
|
|
975
996
|
export const VENDOR_DATA_TO_INCLUDE = {
|
|
976
997
|
...SERVICE_SUBTYPE_CROWD_RANGE_INCLUDE,
|
|
977
998
|
spaceRequirements: true,
|
|
@@ -1066,6 +1087,14 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
|
|
|
1066
1087
|
capacity: true,
|
|
1067
1088
|
amenities: true,
|
|
1068
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,
|
|
1069
1098
|
},
|
|
1070
1099
|
},
|
|
1071
1100
|
eventService: {
|
|
@@ -1083,6 +1112,7 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
|
|
|
1083
1112
|
goodsOrServices: true,
|
|
1084
1113
|
fulfillmentModes: true,
|
|
1085
1114
|
leadTimeDays: true,
|
|
1115
|
+
...SERVICE_PARTNER_REV_INKIND_SELECT,
|
|
1086
1116
|
},
|
|
1087
1117
|
},
|
|
1088
1118
|
entertainmentService: {
|
|
@@ -1091,6 +1121,7 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
|
|
|
1091
1121
|
entertainmentServiceTypes: true,
|
|
1092
1122
|
genre: true,
|
|
1093
1123
|
crowdSize: true,
|
|
1124
|
+
...SERVICE_PARTNER_REV_INKIND_SELECT,
|
|
1094
1125
|
},
|
|
1095
1126
|
},
|
|
1096
1127
|
vendor: {
|
|
@@ -1105,18 +1136,23 @@ export const SERVICE_PUBLIC_LIST_DATA_TO_INCLUDE = {
|
|
|
1105
1136
|
venueTypes: true,
|
|
1106
1137
|
detailsText: true,
|
|
1107
1138
|
crowdSize: true,
|
|
1139
|
+
preferences: {
|
|
1140
|
+
select: SERVICE_PARTNER_DEAL_PREF_SELECT,
|
|
1141
|
+
},
|
|
1108
1142
|
},
|
|
1109
1143
|
},
|
|
1110
1144
|
exhibitor: {
|
|
1111
1145
|
select: {
|
|
1112
1146
|
id: true,
|
|
1113
1147
|
crowdSize: true,
|
|
1148
|
+
...SERVICE_PARTNER_DEAL_PREF_SELECT,
|
|
1114
1149
|
},
|
|
1115
1150
|
},
|
|
1116
1151
|
sponsor: {
|
|
1117
1152
|
select: {
|
|
1118
1153
|
id: true,
|
|
1119
1154
|
crowdSize: true,
|
|
1155
|
+
...SERVICE_PARTNER_DEAL_PREF_SELECT,
|
|
1120
1156
|
},
|
|
1121
1157
|
},
|
|
1122
1158
|
} satisfies Prisma.ServiceInclude;
|
|
@@ -1329,6 +1365,10 @@ export const SERVICE_BOOKING_PUBLIC_DATA_TO_INCLUDE = {
|
|
|
1329
1365
|
title: true,
|
|
1330
1366
|
slug: true,
|
|
1331
1367
|
status: true,
|
|
1368
|
+
coverPhoto: true,
|
|
1369
|
+
startDateTime: true,
|
|
1370
|
+
endDateTime: true,
|
|
1371
|
+
timezone: true,
|
|
1332
1372
|
},
|
|
1333
1373
|
},
|
|
1334
1374
|
representedByOrganization: {
|
|
@@ -1363,7 +1403,14 @@ export const SERVICE_BOOKING_PRIVATE_DATA_TO_INCLUDE = {
|
|
|
1363
1403
|
// so call sites can cast prisma.upsert/findUnique results without needing `as unknown`.
|
|
1364
1404
|
export type ServiceBookingBashEventRef = Pick<
|
|
1365
1405
|
BashEvent,
|
|
1366
|
-
|
|
1406
|
+
| "id"
|
|
1407
|
+
| "title"
|
|
1408
|
+
| "slug"
|
|
1409
|
+
| "status"
|
|
1410
|
+
| "coverPhoto"
|
|
1411
|
+
| "startDateTime"
|
|
1412
|
+
| "endDateTime"
|
|
1413
|
+
| "timezone"
|
|
1367
1414
|
>;
|
|
1368
1415
|
|
|
1369
1416
|
export type ServiceBookingCheckoutRef = Pick<
|
|
@@ -2000,10 +2047,10 @@ export interface UserExt extends User {
|
|
|
2000
2047
|
socialEventCadenceDisplay?: string;
|
|
2001
2048
|
|
|
2002
2049
|
/**
|
|
2003
|
-
* Computed trust badges. Server
|
|
2004
|
-
* `showVerifiedBadgesOnProfile` and strips the underlying raw fields
|
|
2050
|
+
* Computed trust badges. Server strips the underlying raw fields
|
|
2005
2051
|
* (`paymentVerified`, `stripeAccountId`, `studentIdImageS3Key`, `schoolName`)
|
|
2006
|
-
* from the public response.
|
|
2052
|
+
* from the public response. `paymentVerified` stays public once true;
|
|
2053
|
+
* ID / host / student badges still require `showVerifiedBadgesOnProfile`.
|
|
2007
2054
|
*/
|
|
2008
2055
|
badges?: {
|
|
2009
2056
|
idVerified: boolean;
|
|
@@ -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";
|
package/src/sideQuestTypes.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ServiceExt } from "../../extendedSchemas";
|
|
2
|
+
import {
|
|
3
|
+
readPartnerDealflowFields,
|
|
4
|
+
serviceListingOpenToInKindDeal,
|
|
5
|
+
} from "./partnerDealflow.js";
|
|
6
|
+
import {
|
|
7
|
+
readPartnerRevenueShareFields,
|
|
8
|
+
REVENUE_SHARE_WITH_HOST_BASIS_LABEL,
|
|
9
|
+
serviceListingOpenToRevenueShare,
|
|
10
|
+
type RevenueShareWithHostBasis,
|
|
11
|
+
} from "./partnerRevenueShareOptIn.js";
|
|
12
|
+
|
|
13
|
+
export type ServiceListingDealBadge = {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ServiceListingDealDetailRow = {
|
|
19
|
+
label: string;
|
|
20
|
+
value: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function formatUsdFromCents(cents: number): string {
|
|
24
|
+
const dollars = cents / 100;
|
|
25
|
+
return dollars % 1 === 0 ? `$${dollars.toFixed(0)}` : `$${dollars.toFixed(2)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function minimumCashLabel(deal: ReturnType<typeof readPartnerDealflowFields>): string | null {
|
|
29
|
+
if (deal?.dealRequiresMinimumCash !== true) return null;
|
|
30
|
+
if (typeof deal.dealMinimumCashCents !== "number" || deal.dealMinimumCashCents <= 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return `${formatUsdFromCents(deal.dealMinimumCashCents)} min cash`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function truncateOffer(text: string, maxLen: number): string {
|
|
37
|
+
const t = text.trim();
|
|
38
|
+
if (t.length <= maxLen) return t;
|
|
39
|
+
return `${t.slice(0, maxLen - 1).trim()}…`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Compact chips for cards and list rows. */
|
|
43
|
+
export function getServiceListingDealBadges(
|
|
44
|
+
service: ServiceExt
|
|
45
|
+
): ServiceListingDealBadge[] {
|
|
46
|
+
const badges: ServiceListingDealBadge[] = [];
|
|
47
|
+
const deal = readPartnerDealflowFields(service);
|
|
48
|
+
|
|
49
|
+
if (serviceListingOpenToRevenueShare(service)) {
|
|
50
|
+
badges.push({ id: "rev-share", label: "Rev share" });
|
|
51
|
+
}
|
|
52
|
+
if (serviceListingOpenToInKindDeal(service)) {
|
|
53
|
+
badges.push({ id: "in-kind", label: "In-kind" });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const minCash = minimumCashLabel(deal);
|
|
57
|
+
if (minCash) {
|
|
58
|
+
badges.push({ id: "min-cash", label: minCash });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hostBidOpen =
|
|
62
|
+
service.vendor?.preferences?.openToHostBids === true ||
|
|
63
|
+
service.exhibitor?.openToHostBids === true ||
|
|
64
|
+
service.sponsor?.openToHostBids === true;
|
|
65
|
+
if (hostBidOpen) {
|
|
66
|
+
badges.push({ id: "host-bids", label: "Host bids" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return badges;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Longer copy for public detail pages. */
|
|
73
|
+
export function getServiceListingDealDetailRows(
|
|
74
|
+
service: ServiceExt
|
|
75
|
+
): ServiceListingDealDetailRow[] {
|
|
76
|
+
const rows: ServiceListingDealDetailRow[] = [];
|
|
77
|
+
const deal = readPartnerDealflowFields(service);
|
|
78
|
+
const rev = readPartnerRevenueShareFields(service);
|
|
79
|
+
const venue = service.venue;
|
|
80
|
+
|
|
81
|
+
if (venue?.revenueShareEnabled === true && venue.revenueShareHostPercentage != null) {
|
|
82
|
+
rows.push({
|
|
83
|
+
label: "Venue revenue share",
|
|
84
|
+
value: `Host keeps ${venue.revenueShareHostPercentage}% of agreed ticket or bar revenue.`,
|
|
85
|
+
});
|
|
86
|
+
} else if (rev?.openToRevenueShareWithHost === true) {
|
|
87
|
+
const pct = rev.revenueShareWithHostPercentage;
|
|
88
|
+
const basis = rev.revenueShareWithHostBasis as RevenueShareWithHostBasis | null;
|
|
89
|
+
const basisLabel =
|
|
90
|
+
basis && basis in REVENUE_SHARE_WITH_HOST_BASIS_LABEL
|
|
91
|
+
? REVENUE_SHARE_WITH_HOST_BASIS_LABEL[basis]
|
|
92
|
+
: null;
|
|
93
|
+
rows.push({
|
|
94
|
+
label: "Revenue share with host",
|
|
95
|
+
value:
|
|
96
|
+
typeof pct === "number"
|
|
97
|
+
? `Shares ${pct}% with the host${basisLabel ? ` (${basisLabel.toLowerCase()})` : ""}.`
|
|
98
|
+
: "Open to sharing revenue with the host (percentage not set).",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (serviceListingOpenToInKindDeal(service) && deal?.inKindDealOfferText) {
|
|
103
|
+
const est =
|
|
104
|
+
typeof deal.inKindEstimatedValueCents === "number" &&
|
|
105
|
+
deal.inKindEstimatedValueCents > 0
|
|
106
|
+
? ` Approx. value ${formatUsdFromCents(deal.inKindEstimatedValueCents)}.`
|
|
107
|
+
: "";
|
|
108
|
+
rows.push({
|
|
109
|
+
label: "In-kind / trade offer",
|
|
110
|
+
value: `${deal.inKindDealOfferText.trim()}${est}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const minCash = minimumCashLabel(deal);
|
|
115
|
+
if (minCash) {
|
|
116
|
+
rows.push({
|
|
117
|
+
label: "Minimum cash on hybrid deals",
|
|
118
|
+
value: `Requires ${minCash.replace(" min cash", "")} minimum cash alongside rev-share or in-kind terms so hosts know you're serious.`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return rows;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Card preview line for in-kind (truncated). */
|
|
126
|
+
export function getServiceListingInKindPreview(
|
|
127
|
+
service: ServiceExt,
|
|
128
|
+
maxLen = 80
|
|
129
|
+
): string | null {
|
|
130
|
+
const deal = readPartnerDealflowFields(service);
|
|
131
|
+
if (!serviceListingOpenToInKindDeal(service) || !deal?.inKindDealOfferText) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return truncateOffer(deal.inKindDealOfferText, maxLen);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type PartnerDealflowBookingSnapshot = {
|
|
138
|
+
openToRevenueShareWithHostSnapshot?: boolean;
|
|
139
|
+
revenueShareWithHostPercentageSnapshot?: number | null;
|
|
140
|
+
revenueShareWithHostBasisSnapshot?: string | null;
|
|
141
|
+
openToInKindDealsSnapshot?: boolean;
|
|
142
|
+
inKindDealOfferTextSnapshot?: string | null;
|
|
143
|
+
inKindEstimatedValueCentsSnapshot?: number | null;
|
|
144
|
+
dealRequiresMinimumCashSnapshot?: boolean;
|
|
145
|
+
dealMinimumCashCentsSnapshot?: number | null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Copy live listing deal prefs onto a booking at confirmation time. */
|
|
149
|
+
export function buildPartnerDealflowBookingSnapshot(
|
|
150
|
+
service: ServiceExt
|
|
151
|
+
): PartnerDealflowBookingSnapshot {
|
|
152
|
+
const rev = readPartnerRevenueShareFields(service);
|
|
153
|
+
const deal = readPartnerDealflowFields(service);
|
|
154
|
+
const venue = service.venue;
|
|
155
|
+
|
|
156
|
+
const snap: PartnerDealflowBookingSnapshot = {};
|
|
157
|
+
|
|
158
|
+
if (venue?.revenueShareEnabled === true) {
|
|
159
|
+
snap.openToRevenueShareWithHostSnapshot = true;
|
|
160
|
+
snap.revenueShareWithHostPercentageSnapshot =
|
|
161
|
+
venue.revenueShareHostPercentage ?? null;
|
|
162
|
+
snap.revenueShareWithHostBasisSnapshot = venue.revenueShareBasis ?? null;
|
|
163
|
+
} else if (rev?.openToRevenueShareWithHost === true) {
|
|
164
|
+
snap.openToRevenueShareWithHostSnapshot = true;
|
|
165
|
+
snap.revenueShareWithHostPercentageSnapshot =
|
|
166
|
+
rev.revenueShareWithHostPercentage ?? null;
|
|
167
|
+
snap.revenueShareWithHostBasisSnapshot =
|
|
168
|
+
rev.revenueShareWithHostBasis ?? null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (deal?.openToInKindDeals === true) {
|
|
172
|
+
snap.openToInKindDealsSnapshot = true;
|
|
173
|
+
snap.inKindDealOfferTextSnapshot = deal.inKindDealOfferText?.trim() ?? null;
|
|
174
|
+
snap.inKindEstimatedValueCentsSnapshot =
|
|
175
|
+
deal.inKindEstimatedValueCents ?? null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (deal?.dealRequiresMinimumCash === true) {
|
|
179
|
+
snap.dealRequiresMinimumCashSnapshot = true;
|
|
180
|
+
snap.dealMinimumCashCentsSnapshot = deal.dealMinimumCashCents ?? null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return snap;
|
|
184
|
+
}
|