@bash-app/bash-common 30.233.0 → 30.235.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/definitions.d.ts +24 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +37 -0
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +16 -3
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js +2 -0
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/membershipDefinitions.d.ts +19 -1
- package/dist/membershipDefinitions.d.ts.map +1 -1
- package/dist/membershipDefinitions.js +12 -2
- package/dist/membershipDefinitions.js.map +1 -1
- package/dist/onSaleCapabilityRecommendations.d.ts +30 -0
- package/dist/onSaleCapabilityRecommendations.d.ts.map +1 -0
- package/dist/onSaleCapabilityRecommendations.js +92 -0
- package/dist/onSaleCapabilityRecommendations.js.map +1 -0
- package/dist/utils/__tests__/getBackendHost.test.js +51 -8
- package/dist/utils/__tests__/getBackendHost.test.js.map +1 -1
- package/dist/utils/urlUtils.d.ts.map +1 -1
- package/dist/utils/urlUtils.js +18 -4
- package/dist/utils/urlUtils.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +51 -0
- package/src/definitions.ts +61 -0
- package/src/extendedSchemas.ts +16 -3
- package/src/index.ts +2 -0
- package/src/membershipDefinitions.ts +15 -4
- package/src/onSaleCapabilityRecommendations.ts +131 -0
- package/src/utils/__tests__/getBackendHost.test.ts +52 -8
- package/src/utils/urlUtils.ts +21 -4
package/prisma/schema.prisma
CHANGED
|
@@ -669,6 +669,8 @@ model BashEvent {
|
|
|
669
669
|
subtitle String?
|
|
670
670
|
isFeatured Boolean?
|
|
671
671
|
featuredUntil DateTime? // When paid feature expires (null = no expiry / super-user pinned)
|
|
672
|
+
/// Paid discovery — carousel impression counter
|
|
673
|
+
featuredImpressions Int @default(0)
|
|
672
674
|
isTrending Boolean?
|
|
673
675
|
venueId String?
|
|
674
676
|
averageRating Float? @default(0)
|
|
@@ -2247,6 +2249,8 @@ model User {
|
|
|
2247
2249
|
createdOn DateTime @default(now())
|
|
2248
2250
|
stripeCustomerId String? @unique
|
|
2249
2251
|
stripeAccountId String? @unique
|
|
2252
|
+
/// Preferred processor for dual-rail onboarding (optional; legacy Stripe fields remain source of truth until fully migrated).
|
|
2253
|
+
paymentProvider PaymentProvider @default(STRIPE)
|
|
2250
2254
|
isSuperUser Boolean @default(false)
|
|
2251
2255
|
isSuspended Boolean @default(false)
|
|
2252
2256
|
intent UserIntent?
|
|
@@ -2508,6 +2512,7 @@ model User {
|
|
|
2508
2512
|
sponsorBookingRequestsAsHost SponsorBookingRequest[] @relation("SponsorBookingHost")
|
|
2509
2513
|
sponsorships SponsoredEvent[]
|
|
2510
2514
|
stripeAccounts StripeAccount[]
|
|
2515
|
+
paymentProviderAccounts PaymentProviderAccount[]
|
|
2511
2516
|
ticketsISent Ticket[] @relation("TicketsISent")
|
|
2512
2517
|
ticketsIOwn Ticket[] @relation("TicketsIOwn")
|
|
2513
2518
|
bashEventDonations BashEventDonation[]
|
|
@@ -3073,6 +3078,7 @@ model Service {
|
|
|
3073
3078
|
serviceRating Float? @default(0)
|
|
3074
3079
|
totalRatings Int? @default(0)
|
|
3075
3080
|
stripeAccountId String?
|
|
3081
|
+
paymentAccountId String?
|
|
3076
3082
|
targetAudienceId String? @unique
|
|
3077
3083
|
visibility VisibilityPreference @default(Public)
|
|
3078
3084
|
bashesNotInterestedIn BashEventType[]
|
|
@@ -3113,6 +3119,7 @@ model Service {
|
|
|
3113
3119
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
3114
3120
|
owner User? @relation("OwnedService", fields: [ownerId], references: [id])
|
|
3115
3121
|
sponsor Sponsor? @relation(fields: [sponsorId], references: [id], onDelete: Cascade)
|
|
3122
|
+
paymentAccount PaymentProviderAccount? @relation(fields: [paymentAccountId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
|
3116
3123
|
stripeAccount StripeAccount? @relation(fields: [stripeAccountId], references: [id], onDelete: Restrict)
|
|
3117
3124
|
targetAudience TargetAudience? @relation(fields: [targetAudienceId], references: [id], onDelete: Cascade)
|
|
3118
3125
|
vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: Cascade)
|
|
@@ -3140,6 +3147,7 @@ model Service {
|
|
|
3140
3147
|
serviceBudgets Budget[] @relation("ServiceBudgets")
|
|
3141
3148
|
|
|
3142
3149
|
@@index([serviceListingStripeSubscriptionId])
|
|
3150
|
+
@@index([paymentAccountId])
|
|
3143
3151
|
@@index([isFreeFirstListing])
|
|
3144
3152
|
@@index([isVerified])
|
|
3145
3153
|
@@index([isLicensed])
|
|
@@ -3161,6 +3169,32 @@ model StripeAccount {
|
|
|
3161
3169
|
@@index([ownerId])
|
|
3162
3170
|
}
|
|
3163
3171
|
|
|
3172
|
+
/// Connects a user to Stripe and/or Square rails for provider payouts (dual-processor layer).
|
|
3173
|
+
model PaymentProviderAccount {
|
|
3174
|
+
id String @id @default(cuid())
|
|
3175
|
+
userId String
|
|
3176
|
+
provider PaymentProvider @default(STRIPE)
|
|
3177
|
+
stripeAccountId String? @unique
|
|
3178
|
+
stripeCustomerId String? @unique
|
|
3179
|
+
squareMerchantId String? @unique
|
|
3180
|
+
squareLocationId String?
|
|
3181
|
+
squareAccessToken String? @db.Text
|
|
3182
|
+
squareRefreshToken String? @db.Text
|
|
3183
|
+
squareTokenExpiresAt DateTime?
|
|
3184
|
+
accountVerified Boolean @default(false)
|
|
3185
|
+
payoutsEnabled Boolean @default(false)
|
|
3186
|
+
accountStatus String?
|
|
3187
|
+
createdAt DateTime @default(now())
|
|
3188
|
+
updatedAt DateTime @updatedAt
|
|
3189
|
+
lastVerifiedAt DateTime?
|
|
3190
|
+
user User @relation(fields: [userId], references: [id], onDelete: Restrict, onUpdate: Cascade)
|
|
3191
|
+
services Service[]
|
|
3192
|
+
|
|
3193
|
+
@@unique([userId, provider])
|
|
3194
|
+
@@index([userId])
|
|
3195
|
+
@@index([provider])
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3164
3198
|
model StripeWebhookEvent {
|
|
3165
3199
|
id String @id
|
|
3166
3200
|
createdAt DateTime @default(now())
|
|
@@ -3905,10 +3939,22 @@ model ServiceAddon {
|
|
|
3905
3939
|
ServiceBookingAddOn ServiceBookingAddOn[]
|
|
3906
3940
|
convertedFromInclude ServiceInclude[] @relation("IncludeConvertedToAddon")
|
|
3907
3941
|
convertedFromRequest ServiceAddonRequest[] @relation("RequestConvertedToAddon")
|
|
3942
|
+
serviceMerchSquareFulfillments ServiceMerchSquareFulfillment[]
|
|
3908
3943
|
|
|
3909
3944
|
@@index([imageUrl])
|
|
3910
3945
|
}
|
|
3911
3946
|
|
|
3947
|
+
/// Idempotency ledger: one row per Square payment applied to service merch inventory (public hired-service SKUs).
|
|
3948
|
+
model ServiceMerchSquareFulfillment {
|
|
3949
|
+
id String @id @default(cuid())
|
|
3950
|
+
squarePaymentId String @unique
|
|
3951
|
+
serviceAddonId String
|
|
3952
|
+
createdAt DateTime @default(now())
|
|
3953
|
+
serviceAddon ServiceAddon @relation(fields: [serviceAddonId], references: [id], onDelete: Restrict, onUpdate: Cascade)
|
|
3954
|
+
|
|
3955
|
+
@@index([serviceAddonId])
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3912
3958
|
model ServiceInclude {
|
|
3913
3959
|
id String @id @default(cuid())
|
|
3914
3960
|
serviceId String
|
|
@@ -6728,6 +6774,11 @@ enum ServiceTypes {
|
|
|
6728
6774
|
Volunteers
|
|
6729
6775
|
}
|
|
6730
6776
|
|
|
6777
|
+
enum PaymentProvider {
|
|
6778
|
+
STRIPE
|
|
6779
|
+
SQUARE
|
|
6780
|
+
}
|
|
6781
|
+
|
|
6731
6782
|
enum PricingModel {
|
|
6732
6783
|
FLAT_FEE // Fixed amount: "$500 for vendor booth"
|
|
6733
6784
|
PER_ATTENDEE // Per person: "$50 per expected attendee"
|
package/src/definitions.ts
CHANGED
|
@@ -71,6 +71,67 @@ export const BASH_PASS_PRICE_ENV_KEYS: Record<BashPassTier, string> = {
|
|
|
71
71
|
[BashPassTier.UNLIMITED]: "STRIPE_BASH_PASS_UNLIMITED_PRICE_ID",
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
// ─── Ticket.purchaseType values (string column; keep in sync with API writers) ───
|
|
75
|
+
|
|
76
|
+
export const TICKET_PURCHASE_TYPE_USD = "USD" as const;
|
|
77
|
+
export const TICKET_PURCHASE_TYPE_BASHPOINTS = "BashPoints" as const;
|
|
78
|
+
export const TICKET_PURCHASE_TYPE_FREE = "Free" as const;
|
|
79
|
+
export const TICKET_PURCHASE_TYPE_COMP = "Comp" as const;
|
|
80
|
+
export const TICKET_PURCHASE_TYPE_BASHPASS = "BashPass" as const;
|
|
81
|
+
/** Unpaid reservation — pay at venue (RSVP & Pay at Door). */
|
|
82
|
+
export const TICKET_PURCHASE_TYPE_RSVP_AT_DOOR = "RSVPAtDoor" as const;
|
|
83
|
+
/** Cash collected at door (staff-marked). */
|
|
84
|
+
export const TICKET_PURCHASE_TYPE_CASH = "Cash" as const;
|
|
85
|
+
|
|
86
|
+
export type TicketPurchaseTypeStored =
|
|
87
|
+
| typeof TICKET_PURCHASE_TYPE_USD
|
|
88
|
+
| typeof TICKET_PURCHASE_TYPE_BASHPOINTS
|
|
89
|
+
| typeof TICKET_PURCHASE_TYPE_FREE
|
|
90
|
+
| typeof TICKET_PURCHASE_TYPE_COMP
|
|
91
|
+
| typeof TICKET_PURCHASE_TYPE_BASHPASS
|
|
92
|
+
| typeof TICKET_PURCHASE_TYPE_RSVP_AT_DOOR
|
|
93
|
+
| typeof TICKET_PURCHASE_TYPE_CASH;
|
|
94
|
+
|
|
95
|
+
/** Stripe Checkout metadata: settle an existing RSVP ticket after card pay. */
|
|
96
|
+
export const CHECKOUT_PURPOSE_RSVP_DOOR_CHECKOUT = "rsvp_door_checkout" as const;
|
|
97
|
+
|
|
98
|
+
/** Door scanner: ticket QR verification result. */
|
|
99
|
+
export const DOOR_TICKET_SCAN_OUTCOMES = [
|
|
100
|
+
"paid_ok",
|
|
101
|
+
"payment_required",
|
|
102
|
+
"invalid_or_expired",
|
|
103
|
+
"wrong_event",
|
|
104
|
+
"already_checked_in",
|
|
105
|
+
] as const;
|
|
106
|
+
export type DoorTicketScanOutcome = (typeof DOOR_TICKET_SCAN_OUTCOMES)[number];
|
|
107
|
+
|
|
108
|
+
/** Guest may enter without an additional payment step at the door. */
|
|
109
|
+
export function ticketIsPaidForEventAdmission(
|
|
110
|
+
ticket: { purchaseType: string | null; paidOn: Date | string | null },
|
|
111
|
+
tierPriceCents: number
|
|
112
|
+
): boolean {
|
|
113
|
+
if (tierPriceCents <= 0) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (ticket.purchaseType === TICKET_PURCHASE_TYPE_COMP) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (ticket.purchaseType === TICKET_PURCHASE_TYPE_RSVP_AT_DOOR) {
|
|
120
|
+
return ticket.paidOn != null;
|
|
121
|
+
}
|
|
122
|
+
return ticket.paidOn != null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isUnpaidRsvpAtDoorTicket(ticket: {
|
|
126
|
+
purchaseType: string | null;
|
|
127
|
+
paidOn: Date | string | null;
|
|
128
|
+
}): boolean {
|
|
129
|
+
return (
|
|
130
|
+
ticket.purchaseType === TICKET_PURCHASE_TYPE_RSVP_AT_DOOR &&
|
|
131
|
+
ticket.paidOn == null
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
74
135
|
export const URL_PARAMS_BASH_EVENT_ID = "bashEventId" as const;
|
|
75
136
|
export const URL_PARAMS_BASH_EVENT_TITLE = "bashEventTitle" as const;
|
|
76
137
|
export const URL_PARAMS_BASH_EVENT_DESC = "bashEventDesc" as const;
|
package/src/extendedSchemas.ts
CHANGED
|
@@ -201,6 +201,8 @@ export const FRONT_END_USER_DATA_TO_SELECT = {
|
|
|
201
201
|
|
|
202
202
|
export const PRIVATE_USER_ACCOUNT_TO_SELECT = {
|
|
203
203
|
...FRONT_END_USER_DATA_TO_SELECT,
|
|
204
|
+
/** Self session only — preferred payment rail (Stripe vs Square). */
|
|
205
|
+
paymentProvider: true,
|
|
204
206
|
// Self-only sensitive demographics — re-added so /me can populate edit forms.
|
|
205
207
|
gender: true,
|
|
206
208
|
genderIdentity: true,
|
|
@@ -263,22 +265,33 @@ export type BashEventMerchCatalog = {
|
|
|
263
265
|
settings: BashEventHostMerchSettings;
|
|
264
266
|
};
|
|
265
267
|
|
|
266
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Service profile merch purchase options (ServiceRatesAssociation.merchSettings JSON).
|
|
270
|
+
*
|
|
271
|
+
* **Hired services → Square:** Only `showShipping` and `showPreOrder` produce checkout
|
|
272
|
+
* buttons. `showAddToCheckout` is for Stripe host/ticket-cart flows on the event (`BashMerchSection`);
|
|
273
|
+
* it is not used for Square Payment Links under hired services.
|
|
274
|
+
*/
|
|
267
275
|
export type ServiceMerchSettings = {
|
|
268
276
|
showShipping: boolean;
|
|
269
277
|
showPreOrder: boolean;
|
|
278
|
+
/** Stripe ticket-cart bundle option for host merch flows—not used for hired-service Square checkout. */
|
|
270
279
|
showAddToCheckout: boolean;
|
|
271
280
|
purchaseNote?: string | null;
|
|
272
281
|
};
|
|
273
282
|
|
|
274
283
|
/** Public bash detail: hired service bookings + merch SKUs for the event (GET /event/:id, /public-event/:id) */
|
|
275
|
-
export
|
|
284
|
+
export type BashEventHiredServicePublic = {
|
|
276
285
|
bookingId: string;
|
|
277
286
|
serviceId: string;
|
|
278
287
|
specificId: string;
|
|
279
288
|
serviceName: string | null;
|
|
280
289
|
serviceType: ServiceTypes;
|
|
281
290
|
coverPhoto: string | null;
|
|
291
|
+
/** When true, standalone Square checkout is available for merchandise SKUs below. */
|
|
292
|
+
squareMerchCheckoutEnabled: boolean;
|
|
293
|
+
/** Merch options from provider profile—Square hired-service checkout reads ship + pickup only. */
|
|
294
|
+
merchSettings: ServiceMerchSettings | null;
|
|
282
295
|
merchandiseAddons: Array<{
|
|
283
296
|
id: string;
|
|
284
297
|
name: string;
|
|
@@ -288,7 +301,7 @@ export interface BashEventHiredServicePublic {
|
|
|
288
301
|
imageAlt: string | null;
|
|
289
302
|
quantity: number;
|
|
290
303
|
}>;
|
|
291
|
-
}
|
|
304
|
+
};
|
|
292
305
|
|
|
293
306
|
export interface BashEventExt extends Omit<
|
|
294
307
|
BashEvent,
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./extendedSchemas.js";
|
|
|
7
7
|
export * from "./partnerStoreTypes.js";
|
|
8
8
|
export * from "./bashFeedTypes.js";
|
|
9
9
|
export * from "./membershipDefinitions.js";
|
|
10
|
+
export * from "./onSaleCapabilityRecommendations.js";
|
|
10
11
|
export * from "./aiApproval.js";
|
|
11
12
|
export * from "./userReportTypes.js";
|
|
12
13
|
|
|
@@ -148,6 +149,7 @@ export {
|
|
|
148
149
|
PrizePaymentMethod,
|
|
149
150
|
PrizePaymentStatus,
|
|
150
151
|
PartnerTier,
|
|
152
|
+
PaymentProvider,
|
|
151
153
|
PrizeType,
|
|
152
154
|
ProjectorSetupFormat,
|
|
153
155
|
PromoterFormat,
|
|
@@ -114,19 +114,30 @@ export const MEMBERSHIP_PRICING = {
|
|
|
114
114
|
* When they exhaust free slots or want more, they pay these rates.
|
|
115
115
|
*/
|
|
116
116
|
export const FEATURE_BOOST_DURATIONS = [
|
|
117
|
-
{ id: "24h", hours: 24,
|
|
118
|
-
{ id: "72h", hours: 72,
|
|
119
|
-
{ id: "7d", hours: 168,
|
|
117
|
+
{ id: "24h", hours: 24, label: "24 hours", priceCents: 500 }, // $5
|
|
118
|
+
{ id: "72h", hours: 72, label: "3 days", priceCents: 1200 }, // $12
|
|
119
|
+
{ id: "7d", hours: 168, label: "7 days", priceCents: 2500 }, // $25 — typical “best value” tier in UI
|
|
120
|
+
{ id: "14d", hours: 336, label: "14 days", priceCents: 4500 }, // $45
|
|
121
|
+
{ id: "30d", hours: 720, label: "30 days", priceCents: 8000 }, // $80
|
|
120
122
|
] as const;
|
|
121
123
|
|
|
122
124
|
export type FeatureBoostDurationId = typeof FEATURE_BOOST_DURATIONS[number]["id"];
|
|
123
125
|
|
|
124
|
-
/** Applies to
|
|
126
|
+
/** Applies to event, feed-post, and service profile feature boosts */
|
|
125
127
|
export const FEATURE_BOOST_STRIPE_DESCRIPTION = {
|
|
126
128
|
event: (durationLabel: string) => `Featured Event — ${durationLabel}`,
|
|
127
129
|
feedPost: (durationLabel: string) => `Featured Feed Post — ${durationLabel}`,
|
|
130
|
+
service: (durationLabel: string) => `Featured Service — ${durationLabel}`,
|
|
128
131
|
};
|
|
129
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Free “featured event” slots from membership (`featuredEventsPerMonth` in {@link MEMBERSHIP_LIMITS})
|
|
135
|
+
* are **not** tied to the calendar month. Usage is tracked in `User.membershipFeaturedEventsUsed` and
|
|
136
|
+
* reset when the membership subscription period renews (see Stripe `invoice.payment_succeeded` handling
|
|
137
|
+
* in the API — same path that can reset `membershipFeaturedEventsUsed`). Host UI should describe this as
|
|
138
|
+
* “resets when your membership renews,” not “resets on the 1st.”
|
|
139
|
+
*/
|
|
140
|
+
|
|
130
141
|
// Helper: Get monthly price in cents for a tier (for BashPoints calculations)
|
|
131
142
|
export const MEMBERSHIP_MONTHLY_PRICE_CENTS: Record<MembershipTier, number> = {
|
|
132
143
|
Basic: MEMBERSHIP_PRICING.Basic.monthly,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic defaults for festival-style on-sale capabilities (wizard UI).
|
|
3
|
+
* Pure functions — safe for SSR/tests; no IO.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type CapabilityKey =
|
|
7
|
+
| "queue"
|
|
8
|
+
| "installments"
|
|
9
|
+
| "sms"
|
|
10
|
+
| "namedTickets";
|
|
11
|
+
|
|
12
|
+
export interface CapabilitySignals {
|
|
13
|
+
expectedAttendance: number | null;
|
|
14
|
+
highestTicketPriceCents: number | null;
|
|
15
|
+
lowestTicketPriceCents: number | null;
|
|
16
|
+
multiDay: boolean;
|
|
17
|
+
ticketTierCount: number;
|
|
18
|
+
onSaleOpensAt: string | null;
|
|
19
|
+
/** ISO currency lowercase, e.g. "usd" */
|
|
20
|
+
currency: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CapabilityRecommendation {
|
|
24
|
+
capability: CapabilityKey;
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
/** Human-readable bullets for inline “Recommended because …”. */
|
|
27
|
+
rationale: string[];
|
|
28
|
+
suggestedConfig?: {
|
|
29
|
+
queueAdmitRatePerSecond?: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function admitRatePerSecond(attendance: number | null): number {
|
|
34
|
+
if (attendance == null) return 3;
|
|
35
|
+
if (attendance < 400) return 8;
|
|
36
|
+
if (attendance < 1500) return 5;
|
|
37
|
+
if (attendance < 5000) return 3;
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Inspect draft event signals and suggest which toggles to pre-check on first load.
|
|
43
|
+
* The UI always treats checkbox state as source of truth; this only informs defaults.
|
|
44
|
+
*/
|
|
45
|
+
export function computeCapabilityRecommendations(
|
|
46
|
+
signals: CapabilitySignals
|
|
47
|
+
): CapabilityRecommendation[] {
|
|
48
|
+
const {
|
|
49
|
+
expectedAttendance,
|
|
50
|
+
highestTicketPriceCents,
|
|
51
|
+
ticketTierCount,
|
|
52
|
+
multiDay,
|
|
53
|
+
currency,
|
|
54
|
+
} = signals;
|
|
55
|
+
|
|
56
|
+
const attendance = expectedAttendance ?? 0;
|
|
57
|
+
const highCents = highestTicketPriceCents ?? 0;
|
|
58
|
+
const tierCount = ticketTierCount;
|
|
59
|
+
|
|
60
|
+
const queueRationale: string[] = [];
|
|
61
|
+
let queueOn = false;
|
|
62
|
+
if (attendance >= 350) {
|
|
63
|
+
queueOn = true;
|
|
64
|
+
queueRationale.push("Expected headcount is high enough that a waiting room reduces checkout failures.");
|
|
65
|
+
}
|
|
66
|
+
if (multiDay) {
|
|
67
|
+
queueOn = true;
|
|
68
|
+
queueRationale.push("Multi-day events often see concentrated on-sale traffic.");
|
|
69
|
+
}
|
|
70
|
+
if (tierCount >= 4) {
|
|
71
|
+
queueOn = true;
|
|
72
|
+
queueRationale.push("Several ticket tiers usually means more buyers hitting the page at once.");
|
|
73
|
+
}
|
|
74
|
+
if (highCents >= 8_000) {
|
|
75
|
+
queueOn = true;
|
|
76
|
+
queueRationale.push("Higher ticket prices tend to drive more simultaneous purchase attempts.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const installmentsRationale: string[] = [];
|
|
80
|
+
let installmentsOn = false;
|
|
81
|
+
if (currency === "usd" && highCents >= 4_500) {
|
|
82
|
+
installmentsOn = true;
|
|
83
|
+
installmentsRationale.push("Installments help convert for tickets around $45+ in supported currencies.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const smsRationale: string[] = [];
|
|
87
|
+
let smsOn = false;
|
|
88
|
+
if (attendance >= 500 || highCents >= 10_000) {
|
|
89
|
+
smsOn = true;
|
|
90
|
+
smsRationale.push("Larger or higher-stakes sales benefit from a clear confirmation channel.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const namedRationale: string[] = [];
|
|
94
|
+
let namedOn = false;
|
|
95
|
+
if (tierCount >= 2 && highCents >= 6_000) {
|
|
96
|
+
namedOn = true;
|
|
97
|
+
namedRationale.push("Named tickets help groups and higher-value tiers stay organized at the door.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const queue: CapabilityRecommendation = {
|
|
101
|
+
capability: "queue",
|
|
102
|
+
enabled: queueOn,
|
|
103
|
+
rationale: queueRationale,
|
|
104
|
+
...(queueOn
|
|
105
|
+
? {
|
|
106
|
+
suggestedConfig: {
|
|
107
|
+
queueAdmitRatePerSecond: admitRatePerSecond(expectedAttendance),
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return [
|
|
114
|
+
queue,
|
|
115
|
+
{
|
|
116
|
+
capability: "installments",
|
|
117
|
+
enabled: installmentsOn,
|
|
118
|
+
rationale: installmentsRationale,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
capability: "sms",
|
|
122
|
+
enabled: smsOn,
|
|
123
|
+
rationale: smsRationale,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
capability: "namedTickets",
|
|
127
|
+
enabled: namedOn,
|
|
128
|
+
rationale: namedRationale,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* getBackendHost — chooses the API origin (api.bash.community vs
|
|
2
|
+
* getBackendHost — chooses the API origin (api.bash.community vs Vite dev same-origin
|
|
3
3
|
* vs the bundled API_HOST for native Capacitor builds).
|
|
4
4
|
*
|
|
5
5
|
* This file pins the contract so a future "let's tidy up env handling" change
|
|
@@ -42,35 +42,79 @@ afterEach(() => {
|
|
|
42
42
|
clearWindow();
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
describe("getBackendHost — local dev", () => {
|
|
46
|
-
|
|
45
|
+
describe("getBackendHost — local dev (Vite NODE_ENV=development)", () => {
|
|
46
|
+
let origNodeEnv: string | undefined;
|
|
47
|
+
|
|
48
|
+
beforeAll(() => {
|
|
49
|
+
origNodeEnv = process.env.NODE_ENV;
|
|
50
|
+
process.env.NODE_ENV = "development";
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
if (origNodeEnv === undefined) {
|
|
55
|
+
delete process.env.NODE_ENV;
|
|
56
|
+
} else {
|
|
57
|
+
process.env.NODE_ENV = origNodeEnv;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns same origin as the page (Vite proxies /api to Express)", () => {
|
|
47
62
|
setWindow({
|
|
48
63
|
protocol: "http:",
|
|
49
64
|
hostname: "localhost",
|
|
50
65
|
host: "localhost:3000",
|
|
51
66
|
port: "3000",
|
|
52
67
|
});
|
|
53
|
-
expect(getBackendHost()).toBe("http://localhost:
|
|
68
|
+
expect(getBackendHost()).toBe("http://localhost:3000");
|
|
54
69
|
});
|
|
55
70
|
|
|
56
|
-
it("returns
|
|
71
|
+
it("returns 127.0.0.1 with the page port (not a hard-coded API port)", () => {
|
|
57
72
|
setWindow({
|
|
58
73
|
protocol: "http:",
|
|
59
74
|
hostname: "127.0.0.1",
|
|
60
75
|
host: "127.0.0.1:3000",
|
|
61
76
|
port: "3000",
|
|
62
77
|
});
|
|
63
|
-
expect(getBackendHost()).toBe("http://127.0.0.1:
|
|
78
|
+
expect(getBackendHost()).toBe("http://127.0.0.1:3000");
|
|
64
79
|
});
|
|
65
80
|
|
|
66
|
-
it("uses the LAN
|
|
81
|
+
it("uses the LAN host:port of the dev server (phone on same Wi‑Fi → same origin + proxy)", () => {
|
|
67
82
|
setWindow({
|
|
68
83
|
protocol: "http:",
|
|
69
84
|
hostname: "192.168.1.42",
|
|
70
85
|
host: "192.168.1.42:3000",
|
|
71
86
|
port: "3000",
|
|
72
87
|
});
|
|
73
|
-
expect(getBackendHost()).toBe("http://192.168.1.42:
|
|
88
|
+
expect(getBackendHost()).toBe("http://192.168.1.42:3000");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("matches page origin when Vite listens on PORT=3500 — set VITE_API_PROXY_TARGET so /api proxies to Express (not the same TCP port)", () => {
|
|
92
|
+
setWindow({
|
|
93
|
+
protocol: "http:",
|
|
94
|
+
hostname: "localhost",
|
|
95
|
+
host: "localhost:3500",
|
|
96
|
+
port: "3500",
|
|
97
|
+
});
|
|
98
|
+
expect(getBackendHost()).toBe("http://localhost:3500");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("getBackendHost — local non-dev (e.g. jest NODE_ENV=test)", () => {
|
|
103
|
+
it("falls back to canonical API port :3500 when not in Vite development bundle", () => {
|
|
104
|
+
const orig = process.env.NODE_ENV;
|
|
105
|
+
process.env.NODE_ENV = "test";
|
|
106
|
+
try {
|
|
107
|
+
setWindow({
|
|
108
|
+
protocol: "http:",
|
|
109
|
+
hostname: "localhost",
|
|
110
|
+
host: "localhost:3000",
|
|
111
|
+
port: "3000",
|
|
112
|
+
});
|
|
113
|
+
expect(getBackendHost()).toBe("http://localhost:3500");
|
|
114
|
+
} finally {
|
|
115
|
+
if (orig === undefined) delete process.env.NODE_ENV;
|
|
116
|
+
else process.env.NODE_ENV = orig;
|
|
117
|
+
}
|
|
74
118
|
});
|
|
75
119
|
});
|
|
76
120
|
|
package/src/utils/urlUtils.ts
CHANGED
|
@@ -56,21 +56,38 @@ export function getFrontendHost(): string {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export function getBackendHost(): string {
|
|
59
|
+
if (typeof window === "undefined") {
|
|
60
|
+
return API_HOST;
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
// Capacitor loads the app from a bundled origin whose hostname is `localhost`; we must
|
|
60
64
|
// still use the baked-in API URL (REACT_APP_API), not localhost:3500.
|
|
61
65
|
if (isCapacitorNativePlatform()) {
|
|
62
66
|
return API_HOST;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
/**
|
|
70
|
+
* In Vite dev only (`NODE_ENV === "development"`), call `/api` on the same origin so
|
|
71
|
+
* `server.proxy['/api']` forwards to Express. Use port :3500 for local preview/tests
|
|
72
|
+
* where there's no proxy.
|
|
73
|
+
*
|
|
74
|
+
* Hard-coding `:3500` alone breaks when `PORT=3500` for Vite: fetches targeted Vite itself
|
|
75
|
+
* (404 on POST /public-event/…/door-qr instead of Express).
|
|
76
|
+
*/
|
|
66
77
|
const isLocalDev =
|
|
67
78
|
window.location.hostname === "localhost" ||
|
|
68
79
|
window.location.hostname === "127.0.0.1" ||
|
|
69
80
|
window.location.hostname.includes("192.168");
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
if (!isLocalDev) {
|
|
83
|
+
return API_HOST;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (process.env.NODE_ENV === "development") {
|
|
87
|
+
return `${window.location.protocol}//${window.location.host}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `${window.location.protocol}//${window.location.hostname}:3500`;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
export function getSsrBashDetailUrl(bashEventId: string | undefined): string {
|