@epztickets/common 1.88.0 → 1.90.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/build/events/class-category.d.ts +1 -5
- package/build/events/class-category.js +17 -30
- package/build/events/orientation-scheduled-event.d.ts +0 -29
- package/build/events/orientation-scheduled-event.js +0 -17
- package/build/events/orientation-updated-event.d.ts +1 -0
- package/build/events/subjects.d.ts +3 -1
- package/build/events/subjects.js +3 -0
- package/build/events/subscription-payment-events.d.ts +23 -0
- package/build/events/subscription-payment-events.js +2 -0
- package/build/index.d.ts +2 -1
- package/build/index.js +4 -1
- package/build/shared/business-hours.d.ts +26 -0
- package/build/shared/business-hours.js +118 -0
- package/build/shared/class-categories.d.ts +33 -0
- package/build/shared/class-categories.js +47 -0
- package/build/shared/index.d.ts +3 -0
- package/build/shared/index.js +24 -0
- package/build/shared/stripe-fee.d.ts +8 -0
- package/build/shared/stripe-fee.js +33 -0
- package/package.json +3 -2
- package/shared/package.json +6 -0
|
@@ -1,5 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export type ClassCategory = (typeof CLASS_CATEGORIES)[number]["value"];
|
|
3
|
-
export declare const CLASS_CATEGORY_VALUES: string[];
|
|
4
|
-
export declare const isClassCategory: (v: unknown) => v is ClassCategory;
|
|
5
|
-
export declare function inferClassCategory(title?: string | null): ClassCategory | undefined;
|
|
1
|
+
export * from "../shared/class-categories";
|
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
exports.CLASS_CATEGORY_VALUES = orientation_scheduled_event_1.CLASS_CATEGORIES.map((c) => c.value);
|
|
7
|
-
const isClassCategory = (v) => typeof v === "string" && exports.CLASS_CATEGORY_VALUES.includes(v);
|
|
8
|
-
exports.isClassCategory = isClassCategory;
|
|
9
|
-
// Best-effort guess of a category from a free-text class name
|
|
10
|
-
// ("Salsa basics" → dance, "BJJ open mat" → martial_arts). Used when a
|
|
11
|
-
// booking arrives without a category (AI chat agent, older clients)
|
|
12
|
-
// and to estimate categories for bookings made before categories
|
|
13
|
-
// existed. Returns undefined when nothing matches.
|
|
14
|
-
const KEYWORDS = [
|
|
15
|
-
["martial_arts", /\b(karate|judo|jiu[- ]?jitsu|bjj|taekwondo|tae kwon do|kung fu|boxing|kickbox\w*|mma|muay thai|self[- ]defen[cs]e|aikido|capoeira|krav maga|wrestling)\b/],
|
|
16
|
-
["yoga", /\b(yoga|pilates|meditat\w*|mindful\w*|stretch\w*|wellness|breathwork|sound bath|tai chi)\b/],
|
|
17
|
-
["dance", /\b(danc\w*|salsa|bachata|ballet|hip[- ]?hop|tango|merengue|cumbia|contemporary|choreo\w*|tap|jazz funk|k-?pop|twerk|ballroom|swing|flamenco|folklorico|breakdanc\w*|heels)\b/],
|
|
18
|
-
["fitness", /\b(fitness|hiit|boot ?camp|crossfit|zumba|workout|strength|spin(ning)?|cardio|barre|conditioning|circuit|bootcamp|personal training|training session)\b/],
|
|
19
|
-
["singing", /\b(sing\w*|vocal\w*|voice|choir|chorus|karaoke|a cappella)\b/],
|
|
20
|
-
["instrumental", /\b(guitar|piano|drum\w*|violin|viola|cello|bass|band|ukulele|sax\w*|trumpet|flute|clarinet|keyboard|instrument\w*|orchestra|jam session|dj)\b/],
|
|
21
|
-
["theater", /\b(theat(er|re)|acting|actor\w*|drama|improv|audition\w*|scene study|monologue|musical theat(er|re)|stage combat)\b/],
|
|
22
|
-
["workshop", /\b(workshop|lecture|seminar|talk|course|training|meeting|info session|q&a|panel)\b/],
|
|
23
|
-
];
|
|
24
|
-
function inferClassCategory(title) {
|
|
25
|
-
if (!title)
|
|
26
|
-
return undefined;
|
|
27
|
-
const t = title.toLowerCase();
|
|
28
|
-
for (const [cat, rx] of KEYWORDS)
|
|
29
|
-
if (rx.test(t))
|
|
30
|
-
return cat;
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
17
|
+
// Moved to shared/class-categories.ts (re-exported from the package
|
|
18
|
+
// root and from "@epztickets/common/shared").
|
|
19
|
+
__exportStar(require("../shared/class-categories"), exports);
|
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
import { Subjects } from "./subjects";
|
|
2
2
|
export type OrientationStatus = "scheduled" | "approved" | "no_show" | "declined" | "cancelled";
|
|
3
|
-
export declare const CLASS_CATEGORIES: readonly [{
|
|
4
|
-
readonly value: "singing";
|
|
5
|
-
readonly label: "Singing / vocal";
|
|
6
|
-
}, {
|
|
7
|
-
readonly value: "instrumental";
|
|
8
|
-
readonly label: "Instrumental";
|
|
9
|
-
}, {
|
|
10
|
-
readonly value: "dance";
|
|
11
|
-
readonly label: "Dance";
|
|
12
|
-
}, {
|
|
13
|
-
readonly value: "fitness";
|
|
14
|
-
readonly label: "Fitness";
|
|
15
|
-
}, {
|
|
16
|
-
readonly value: "theater";
|
|
17
|
-
readonly label: "Theater / acting";
|
|
18
|
-
}, {
|
|
19
|
-
readonly value: "yoga";
|
|
20
|
-
readonly label: "Yoga / Pilates / wellness";
|
|
21
|
-
}, {
|
|
22
|
-
readonly value: "martial_arts";
|
|
23
|
-
readonly label: "Martial arts";
|
|
24
|
-
}, {
|
|
25
|
-
readonly value: "workshop";
|
|
26
|
-
readonly label: "Workshop / lecture";
|
|
27
|
-
}, {
|
|
28
|
-
readonly value: "other";
|
|
29
|
-
readonly label: "Other";
|
|
30
|
-
}];
|
|
31
|
-
export declare const classCategoryLabel: (v?: string) => string | undefined;
|
|
32
3
|
export interface OrientationSnapshot {
|
|
33
4
|
id: string;
|
|
34
5
|
ownerUserId: string;
|
|
@@ -1,19 +1,2 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.classCategoryLabel = exports.CLASS_CATEGORIES = void 0;
|
|
4
|
-
// "Category of class" the renter picks on the orientation form.
|
|
5
|
-
// Shared by rooms (validation), notifications (email labels); the
|
|
6
|
-
// frontend keeps a matching copy in lib/orientations.ts.
|
|
7
|
-
exports.CLASS_CATEGORIES = [
|
|
8
|
-
{ value: "singing", label: "Singing / vocal" },
|
|
9
|
-
{ value: "instrumental", label: "Instrumental" },
|
|
10
|
-
{ value: "dance", label: "Dance" },
|
|
11
|
-
{ value: "fitness", label: "Fitness" },
|
|
12
|
-
{ value: "theater", label: "Theater / acting" },
|
|
13
|
-
{ value: "yoga", label: "Yoga / Pilates / wellness" },
|
|
14
|
-
{ value: "martial_arts", label: "Martial arts" },
|
|
15
|
-
{ value: "workshop", label: "Workshop / lecture" },
|
|
16
|
-
{ value: "other", label: "Other" },
|
|
17
|
-
];
|
|
18
|
-
const classCategoryLabel = (v) => { var _a, _b; return (_b = (_a = exports.CLASS_CATEGORIES.find((c) => c.value === v)) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : v; };
|
|
19
|
-
exports.classCategoryLabel = classCategoryLabel;
|
|
@@ -51,7 +51,9 @@ export declare enum Subjects {
|
|
|
51
51
|
RenterApprovalChanged = "events.orientation.renter-approval-changed",
|
|
52
52
|
OrientationScheduled = "events.orientation.scheduled",
|
|
53
53
|
OrientationUpdated = "events.orientation.updated",
|
|
54
|
-
OwnerPlanUpdated = "events.owner-plan.updated"
|
|
54
|
+
OwnerPlanUpdated = "events.owner-plan.updated",
|
|
55
|
+
SubscriptionPaymentFailed = "events.billing.payment-failed",
|
|
56
|
+
SubscriptionPaymentRecovered = "events.billing.payment-recovered"
|
|
55
57
|
}
|
|
56
58
|
export declare enum Streams {
|
|
57
59
|
Events = "events",
|
package/build/events/subjects.js
CHANGED
|
@@ -164,6 +164,9 @@ var Subjects;
|
|
|
164
164
|
// rate, negotiated per owner. orders mirrors the commission rate to
|
|
165
165
|
// snapshot it onto each booking.
|
|
166
166
|
Subjects["OwnerPlanUpdated"] = "events.owner-plan.updated";
|
|
167
|
+
// Monthly platform-fee payment outcomes (payments → notifications).
|
|
168
|
+
Subjects["SubscriptionPaymentFailed"] = "events.billing.payment-failed";
|
|
169
|
+
Subjects["SubscriptionPaymentRecovered"] = "events.billing.payment-recovered";
|
|
167
170
|
})(Subjects || (exports.Subjects = Subjects = {}));
|
|
168
171
|
var Streams;
|
|
169
172
|
(function (Streams) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Subjects } from "./subjects";
|
|
2
|
+
export interface SubscriptionPaymentFailedEvent {
|
|
3
|
+
subject: Subjects.SubscriptionPaymentFailed;
|
|
4
|
+
data: {
|
|
5
|
+
ownerUserId: string;
|
|
6
|
+
invoiceId: string;
|
|
7
|
+
amountCents: number;
|
|
8
|
+
attemptCount: number;
|
|
9
|
+
finalAttempt: boolean;
|
|
10
|
+
nextAttemptAt: string | null;
|
|
11
|
+
invoiceUrl: string | null;
|
|
12
|
+
occurredAt: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface SubscriptionPaymentRecoveredEvent {
|
|
16
|
+
subject: Subjects.SubscriptionPaymentRecovered;
|
|
17
|
+
data: {
|
|
18
|
+
ownerUserId: string;
|
|
19
|
+
invoiceId: string;
|
|
20
|
+
amountCents: number;
|
|
21
|
+
occurredAt: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
package/build/index.d.ts
CHANGED
|
@@ -37,8 +37,8 @@ export * from "./events/orientation-settings-updated-event";
|
|
|
37
37
|
export * from "./events/renter-approval-changed-event";
|
|
38
38
|
export * from "./events/orientation-scheduled-event";
|
|
39
39
|
export * from "./events/orientation-updated-event";
|
|
40
|
-
export * from "./events/class-category";
|
|
41
40
|
export * from "./events/owner-plan-updated-event";
|
|
41
|
+
export * from "./events/subscription-payment-events";
|
|
42
42
|
export * from "./events/order-cancelled-event";
|
|
43
43
|
export * from "./events/order-completed-event";
|
|
44
44
|
export * from "./events/order-created-event";
|
|
@@ -73,3 +73,4 @@ export * from "./events/user-self-booking-payment-reminder-requested-event";
|
|
|
73
73
|
export * from "./events/connect-disconnect-requested-event";
|
|
74
74
|
export * from "./events/connect-disconnect-approved-event";
|
|
75
75
|
export * from "./events/connect-disconnect-rejected-event";
|
|
76
|
+
export * from "./shared";
|
package/build/index.js
CHANGED
|
@@ -57,8 +57,9 @@ __exportStar(require("./events/orientation-settings-updated-event"), exports);
|
|
|
57
57
|
__exportStar(require("./events/renter-approval-changed-event"), exports);
|
|
58
58
|
__exportStar(require("./events/orientation-scheduled-event"), exports);
|
|
59
59
|
__exportStar(require("./events/orientation-updated-event"), exports);
|
|
60
|
-
|
|
60
|
+
// class categories now live in shared/ (re-exported below)
|
|
61
61
|
__exportStar(require("./events/owner-plan-updated-event"), exports);
|
|
62
|
+
__exportStar(require("./events/subscription-payment-events"), exports);
|
|
62
63
|
__exportStar(require("./events/order-cancelled-event"), exports);
|
|
63
64
|
__exportStar(require("./events/order-completed-event"), exports);
|
|
64
65
|
__exportStar(require("./events/order-created-event"), exports);
|
|
@@ -93,3 +94,5 @@ __exportStar(require("./events/user-self-booking-payment-reminder-requested-even
|
|
|
93
94
|
__exportStar(require("./events/connect-disconnect-requested-event"), exports);
|
|
94
95
|
__exportStar(require("./events/connect-disconnect-approved-event"), exports);
|
|
95
96
|
__exportStar(require("./events/connect-disconnect-rejected-event"), exports);
|
|
97
|
+
// Browser-safe helpers (also available as "@epztickets/common/shared")
|
|
98
|
+
__exportStar(require("./shared"), exports);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface DayHours {
|
|
2
|
+
closed: boolean;
|
|
3
|
+
open?: string;
|
|
4
|
+
close?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface BusinessHours {
|
|
7
|
+
mon: DayHours;
|
|
8
|
+
tue: DayHours;
|
|
9
|
+
wed: DayHours;
|
|
10
|
+
thu: DayHours;
|
|
11
|
+
fri: DayHours;
|
|
12
|
+
sat: DayHours;
|
|
13
|
+
sun: DayHours;
|
|
14
|
+
}
|
|
15
|
+
export interface HoursCheckResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
/** Human-readable reason. Only meaningful when ok=false. */
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Public check. Returns { ok: true } when the booking is allowed
|
|
22
|
+
* (either because there's no hours restriction, OR the window
|
|
23
|
+
* fits inside the day's open/close). Returns { ok: false, reason }
|
|
24
|
+
* with a message safe to surface to the caller when it doesn't.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkBusinessHours(hours: BusinessHours | undefined, startTime: Date, endTime: Date, timezone: string | undefined): HoursCheckResult;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkBusinessHours = checkBusinessHours;
|
|
4
|
+
// Server-side counterpart to reservation-frontend/lib/business-
|
|
5
|
+
// hours.ts. Given a Room replica's businessHours + the requested
|
|
6
|
+
// booking window, decides whether the window falls entirely
|
|
7
|
+
// within the room's open hours for the relevant day-of-week.
|
|
8
|
+
//
|
|
9
|
+
// Design decisions:
|
|
10
|
+
// - Backward compat: when businessHours is undefined the guard
|
|
11
|
+
// ALLOWS the booking. Matches the client's "no gate when
|
|
12
|
+
// hours aren't configured" behavior — an owner who hasn't
|
|
13
|
+
// set hours yet shouldn't accidentally lock renters out.
|
|
14
|
+
// - Timezone: businessHours are stored in the room's local
|
|
15
|
+
// time (e.g. "09:00" = 9am). At validation time we need the
|
|
16
|
+
// day-of-week and HH:MM of the requested start/end in that
|
|
17
|
+
// same local timezone. We use the ORDER'S buyer timezone
|
|
18
|
+
// as a proxy — in almost every case the buyer and the room
|
|
19
|
+
// are in the same physical location, so their timezones
|
|
20
|
+
// match. If they differ, worst-case the guard evaluates in
|
|
21
|
+
// the wrong timezone. That's a rare edge case we can revisit
|
|
22
|
+
// once we have real cross-timezone data.
|
|
23
|
+
// - Multi-day ranges: rejected. V1 doesn't support overnight
|
|
24
|
+
// bookings that cross a business-hour boundary. If an owner
|
|
25
|
+
// eventually wants "open Fri 8pm to Sat 2am," we'll add
|
|
26
|
+
// dedicated support then.
|
|
27
|
+
const JS_DAY_TO_KEY = [
|
|
28
|
+
"sun", "mon", "tue", "wed", "thu", "fri", "sat",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Extract { dayOfWeek, hh:mm } for a Date in a given IANA
|
|
32
|
+
* timezone, using Intl.DateTimeFormat. Falls back to UTC when
|
|
33
|
+
* the timezone is invalid or missing.
|
|
34
|
+
*/
|
|
35
|
+
function partsIn(date, timezone) {
|
|
36
|
+
var _a, _b, _c, _d;
|
|
37
|
+
const opts = Object.assign({ weekday: "short", hour: "2-digit", minute: "2-digit", hour12: false }, (timezone ? { timeZone: timezone } : { timeZone: "UTC" }));
|
|
38
|
+
let formatted;
|
|
39
|
+
try {
|
|
40
|
+
formatted = new Intl.DateTimeFormat("en-US", opts).formatToParts(date);
|
|
41
|
+
}
|
|
42
|
+
catch (_e) {
|
|
43
|
+
formatted = new Intl.DateTimeFormat("en-US", Object.assign(Object.assign({}, opts), { timeZone: "UTC" })).formatToParts(date);
|
|
44
|
+
}
|
|
45
|
+
const map = {};
|
|
46
|
+
for (const p of formatted)
|
|
47
|
+
map[p.type] = p.value;
|
|
48
|
+
const weekdayShort = (_a = map.weekday) !== null && _a !== void 0 ? _a : "Sun";
|
|
49
|
+
// Intl gives "Sun" ... "Sat" — map to our internal keys.
|
|
50
|
+
const dayKey = (_b = {
|
|
51
|
+
Sun: "sun", Mon: "mon", Tue: "tue", Wed: "wed",
|
|
52
|
+
Thu: "thu", Fri: "fri", Sat: "sat",
|
|
53
|
+
}[weekdayShort]) !== null && _b !== void 0 ? _b : "sun";
|
|
54
|
+
const hh = map.hour === "24" ? "00" : ((_c = map.hour) !== null && _c !== void 0 ? _c : "00");
|
|
55
|
+
const mm = (_d = map.minute) !== null && _d !== void 0 ? _d : "00";
|
|
56
|
+
return { dayKey: dayKey, hhmm: `${hh}:${mm}` };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Public check. Returns { ok: true } when the booking is allowed
|
|
60
|
+
* (either because there's no hours restriction, OR the window
|
|
61
|
+
* fits inside the day's open/close). Returns { ok: false, reason }
|
|
62
|
+
* with a message safe to surface to the caller when it doesn't.
|
|
63
|
+
*/
|
|
64
|
+
function checkBusinessHours(hours, startTime, endTime, timezone) {
|
|
65
|
+
// No hours configured — no gate. Same behavior as the
|
|
66
|
+
// frontend picker when businessHours is undefined.
|
|
67
|
+
if (!hours)
|
|
68
|
+
return { ok: true };
|
|
69
|
+
// Multi-day guard. This is intentionally strict — an overnight
|
|
70
|
+
// booking that would legitimately span two days (e.g. 8pm-2am)
|
|
71
|
+
// is out of scope for the current business-hours model.
|
|
72
|
+
const startParts = partsIn(startTime, timezone);
|
|
73
|
+
const endParts = partsIn(endTime, timezone);
|
|
74
|
+
if (startParts.dayKey !== endParts.dayKey) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
reason: "This room does not accept bookings that cross midnight. Please book within a single day.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const day = hours[startParts.dayKey];
|
|
81
|
+
if (!day)
|
|
82
|
+
return { ok: true };
|
|
83
|
+
if (day.closed) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: `This room is closed on ${dayLabel(startParts.dayKey)}. Please pick a day when it's open.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (!day.open || !day.close) {
|
|
90
|
+
// Malformed row — treat as no gate rather than reject a
|
|
91
|
+
// legitimate booking on bad server-side data.
|
|
92
|
+
return { ok: true };
|
|
93
|
+
}
|
|
94
|
+
// String comparison works for zero-padded 24-hour HH:MM.
|
|
95
|
+
if (startParts.hhmm < day.open || endParts.hhmm > day.close) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason: `This room is open ${to12h(day.open)} to ${to12h(day.close)} on ${dayLabel(startParts.dayKey)}. Please pick a time within the open window.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { ok: true };
|
|
102
|
+
}
|
|
103
|
+
// Small formatting helpers used only in the error message. Kept
|
|
104
|
+
// local so the guard has no external formatting dependency.
|
|
105
|
+
function dayLabel(key) {
|
|
106
|
+
return {
|
|
107
|
+
mon: "Monday", tue: "Tuesday", wed: "Wednesday", thu: "Thursday",
|
|
108
|
+
fri: "Friday", sat: "Saturday", sun: "Sunday",
|
|
109
|
+
}[key];
|
|
110
|
+
}
|
|
111
|
+
function to12h(hhmm) {
|
|
112
|
+
const [h, m] = hhmm.split(":").map(Number);
|
|
113
|
+
if (Number.isNaN(h) || Number.isNaN(m))
|
|
114
|
+
return hhmm;
|
|
115
|
+
const period = h < 12 ? "AM" : "PM";
|
|
116
|
+
const displayH = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
117
|
+
return `${displayH}:${String(m).padStart(2, "0")} ${period}`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const CLASS_CATEGORIES: readonly [{
|
|
2
|
+
readonly value: "singing";
|
|
3
|
+
readonly label: "Singing / vocal";
|
|
4
|
+
}, {
|
|
5
|
+
readonly value: "instrumental";
|
|
6
|
+
readonly label: "Instrumental";
|
|
7
|
+
}, {
|
|
8
|
+
readonly value: "dance";
|
|
9
|
+
readonly label: "Dance";
|
|
10
|
+
}, {
|
|
11
|
+
readonly value: "fitness";
|
|
12
|
+
readonly label: "Fitness";
|
|
13
|
+
}, {
|
|
14
|
+
readonly value: "theater";
|
|
15
|
+
readonly label: "Theater / acting";
|
|
16
|
+
}, {
|
|
17
|
+
readonly value: "yoga";
|
|
18
|
+
readonly label: "Yoga / Pilates / wellness";
|
|
19
|
+
}, {
|
|
20
|
+
readonly value: "martial_arts";
|
|
21
|
+
readonly label: "Martial arts";
|
|
22
|
+
}, {
|
|
23
|
+
readonly value: "workshop";
|
|
24
|
+
readonly label: "Workshop / lecture";
|
|
25
|
+
}, {
|
|
26
|
+
readonly value: "other";
|
|
27
|
+
readonly label: "Other";
|
|
28
|
+
}];
|
|
29
|
+
export declare const classCategoryLabel: (v?: string) => string | undefined;
|
|
30
|
+
export type ClassCategory = (typeof CLASS_CATEGORIES)[number]["value"];
|
|
31
|
+
export declare const CLASS_CATEGORY_VALUES: string[];
|
|
32
|
+
export declare const isClassCategory: (v: unknown) => v is ClassCategory;
|
|
33
|
+
export declare function inferClassCategory(title?: string | null): ClassCategory | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// "Category of class" — single source for services AND the frontend
|
|
3
|
+
// (via @epztickets/common/shared). Used by the orientation form, the
|
|
4
|
+
// Reserve modal, orders validation, and metrics trends.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isClassCategory = exports.CLASS_CATEGORY_VALUES = exports.classCategoryLabel = exports.CLASS_CATEGORIES = void 0;
|
|
7
|
+
exports.inferClassCategory = inferClassCategory;
|
|
8
|
+
exports.CLASS_CATEGORIES = [
|
|
9
|
+
{ value: "singing", label: "Singing / vocal" },
|
|
10
|
+
{ value: "instrumental", label: "Instrumental" },
|
|
11
|
+
{ value: "dance", label: "Dance" },
|
|
12
|
+
{ value: "fitness", label: "Fitness" },
|
|
13
|
+
{ value: "theater", label: "Theater / acting" },
|
|
14
|
+
{ value: "yoga", label: "Yoga / Pilates / wellness" },
|
|
15
|
+
{ value: "martial_arts", label: "Martial arts" },
|
|
16
|
+
{ value: "workshop", label: "Workshop / lecture" },
|
|
17
|
+
{ value: "other", label: "Other" },
|
|
18
|
+
];
|
|
19
|
+
const classCategoryLabel = (v) => { var _a, _b; return (_b = (_a = exports.CLASS_CATEGORIES.find((c) => c.value === v)) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : v; };
|
|
20
|
+
exports.classCategoryLabel = classCategoryLabel;
|
|
21
|
+
exports.CLASS_CATEGORY_VALUES = exports.CLASS_CATEGORIES.map((c) => c.value);
|
|
22
|
+
const isClassCategory = (v) => typeof v === "string" && exports.CLASS_CATEGORY_VALUES.includes(v);
|
|
23
|
+
exports.isClassCategory = isClassCategory;
|
|
24
|
+
// Best-effort guess of a category from a free-text class name
|
|
25
|
+
// ("Salsa basics" → dance, "BJJ open mat" → martial_arts). Used when a
|
|
26
|
+
// booking arrives without a category (AI chat agent, older clients)
|
|
27
|
+
// and to estimate categories for bookings made before categories
|
|
28
|
+
// existed. Returns undefined when nothing matches.
|
|
29
|
+
const KEYWORDS = [
|
|
30
|
+
["martial_arts", /\b(karate|judo|jiu[- ]?jitsu|bjj|taekwondo|tae kwon do|kung fu|boxing|kickbox\w*|mma|muay thai|self[- ]defen[cs]e|aikido|capoeira|krav maga|wrestling)\b/],
|
|
31
|
+
["yoga", /\b(yoga|pilates|meditat\w*|mindful\w*|stretch\w*|wellness|breathwork|sound bath|tai chi)\b/],
|
|
32
|
+
["dance", /\b(danc\w*|salsa|bachata|ballet|hip[- ]?hop|tango|merengue|cumbia|contemporary|choreo\w*|tap|jazz funk|k-?pop|twerk|ballroom|swing|flamenco|folklorico|breakdanc\w*|heels)\b/],
|
|
33
|
+
["fitness", /\b(fitness|hiit|boot ?camp|crossfit|zumba|workout|strength|spin(ning)?|cardio|barre|conditioning|circuit|bootcamp|personal training|training session)\b/],
|
|
34
|
+
["singing", /\b(sing\w*|vocal\w*|voice|choir|chorus|karaoke|a cappella)\b/],
|
|
35
|
+
["instrumental", /\b(guitar|piano|drum\w*|violin|viola|cello|bass|band|ukulele|sax\w*|trumpet|flute|clarinet|keyboard|instrument\w*|orchestra|jam session|dj)\b/],
|
|
36
|
+
["theater", /\b(theat(er|re)|acting|actor\w*|drama|improv|audition\w*|scene study|monologue|musical theat(er|re)|stage combat)\b/],
|
|
37
|
+
["workshop", /\b(workshop|lecture|seminar|talk|course|training|meeting|info session|q&a|panel)\b/],
|
|
38
|
+
];
|
|
39
|
+
function inferClassCategory(title) {
|
|
40
|
+
if (!title)
|
|
41
|
+
return undefined;
|
|
42
|
+
const t = title.toLowerCase();
|
|
43
|
+
for (const [cat, rx] of KEYWORDS)
|
|
44
|
+
if (rx.test(t))
|
|
45
|
+
return cat;
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// Browser-safe, dependency-free helpers shared by the services AND
|
|
18
|
+
// the Next.js frontend. Import from "@epztickets/common/shared" in the
|
|
19
|
+
// frontend (the package root pulls in server-only code: express,
|
|
20
|
+
// nats, pino…). Services can keep importing from the root, which
|
|
21
|
+
// re-exports everything here.
|
|
22
|
+
__exportStar(require("./stripe-fee"), exports);
|
|
23
|
+
__exportStar(require("./business-hours"), exports);
|
|
24
|
+
__exportStar(require("./class-categories"), exports);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const STRIPE_FEE_PCT = 0.029;
|
|
2
|
+
export declare const STRIPE_FEE_FIXED_CENTS = 30;
|
|
3
|
+
/** Total to charge (cents) for a base price (cents). 0 stays 0. */
|
|
4
|
+
export declare function totalWithStripeFeeCents(baseCents: number): number;
|
|
5
|
+
/** Dollars in → dollars out, rounded to cents. */
|
|
6
|
+
export declare function totalWithStripeFee(baseDollars: number): number;
|
|
7
|
+
/** The processing-fee portion (dollars) of a base price (dollars). */
|
|
8
|
+
export declare function stripeFeeFor(baseDollars: number): number;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Stripe processing-fee pass-through — THE single source of truth for
|
|
3
|
+
// what a renter pays at checkout. Used by:
|
|
4
|
+
// - payments (new-stripe-session): the amount actually charged
|
|
5
|
+
// - frontend ReserveModal: the price shown before booking
|
|
6
|
+
// - frontend order page: fallback total when Stripe's real charged
|
|
7
|
+
// amount isn't recorded yet
|
|
8
|
+
// If these ever disagreed, renters would see one price and pay another.
|
|
9
|
+
//
|
|
10
|
+
// Stripe takes 2.9% + 30¢ of the TOTAL charge. We gross up the room
|
|
11
|
+
// price so that after Stripe's cut the platform still receives the
|
|
12
|
+
// full base amount: total = ceil((base + 30) / (1 - 0.029)).
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.STRIPE_FEE_FIXED_CENTS = exports.STRIPE_FEE_PCT = void 0;
|
|
15
|
+
exports.totalWithStripeFeeCents = totalWithStripeFeeCents;
|
|
16
|
+
exports.totalWithStripeFee = totalWithStripeFee;
|
|
17
|
+
exports.stripeFeeFor = stripeFeeFor;
|
|
18
|
+
exports.STRIPE_FEE_PCT = 0.029;
|
|
19
|
+
exports.STRIPE_FEE_FIXED_CENTS = 30;
|
|
20
|
+
/** Total to charge (cents) for a base price (cents). 0 stays 0. */
|
|
21
|
+
function totalWithStripeFeeCents(baseCents) {
|
|
22
|
+
if (baseCents <= 0)
|
|
23
|
+
return 0;
|
|
24
|
+
return Math.ceil((baseCents + exports.STRIPE_FEE_FIXED_CENTS) / (1 - exports.STRIPE_FEE_PCT));
|
|
25
|
+
}
|
|
26
|
+
/** Dollars in → dollars out, rounded to cents. */
|
|
27
|
+
function totalWithStripeFee(baseDollars) {
|
|
28
|
+
return totalWithStripeFeeCents(Math.round(baseDollars * 100)) / 100;
|
|
29
|
+
}
|
|
30
|
+
/** The processing-fee portion (dollars) of a base price (dollars). */
|
|
31
|
+
function stripeFeeFor(baseDollars) {
|
|
32
|
+
return Math.round((totalWithStripeFee(baseDollars) - baseDollars) * 100) / 100;
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epztickets/common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.90.0",
|
|
4
4
|
"main": "./build/index.js",
|
|
5
5
|
"types": "./build/index.d.ts",
|
|
6
6
|
"files": [
|
|
7
|
-
"build/**/*"
|
|
7
|
+
"build/**/*",
|
|
8
|
+
"shared/package.json"
|
|
8
9
|
],
|
|
9
10
|
"scripts": {
|
|
10
11
|
"clean": "rimraf build",
|