@epztickets/common 1.98.0 → 1.100.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.
@@ -36,3 +36,15 @@ export interface RenterContractStatusChangedEvent {
36
36
  changedAt: string;
37
37
  };
38
38
  }
39
+ export interface ResourceSharedEvent {
40
+ subject: Subjects.ResourceShared;
41
+ data: {
42
+ resourceId: string;
43
+ title: string;
44
+ ownerUserId: string;
45
+ ownerName?: string;
46
+ buildingName?: string;
47
+ updated: boolean;
48
+ renterUserIds: string[];
49
+ };
50
+ }
@@ -0,0 +1,13 @@
1
+ import { Subjects } from "./subjects";
2
+ import { PricingRules } from "../shared/pricing";
3
+ export interface RoomPricingUpdatedEvent {
4
+ subject: Subjects.RoomPricingUpdated;
5
+ data: {
6
+ roomId: string;
7
+ ownerUserId: string;
8
+ baseRate: number;
9
+ rules: PricingRules | null;
10
+ source: "room" | "building" | "none";
11
+ updatedAt: string;
12
+ };
13
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -61,6 +61,8 @@ export declare enum Subjects {
61
61
  ContractSignRequested = "events.contract.sign-requested",
62
62
  ContractSigned = "events.contract.signed",
63
63
  RenterContractStatusChanged = "events.contract.renter-status-changed",
64
+ ResourceShared = "events.contract.resource-shared",
65
+ RoomPricingUpdated = "events.room.pricing-updated",
64
66
  OrderSeriesCancelled = "events.order-series.cancelled"
65
67
  }
66
68
  export declare enum Streams {
@@ -183,6 +183,9 @@ var Subjects;
183
183
  Subjects["ContractSignRequested"] = "events.contract.sign-requested";
184
184
  Subjects["ContractSigned"] = "events.contract.signed";
185
185
  Subjects["RenterContractStatusChanged"] = "events.contract.renter-status-changed";
186
+ Subjects["ResourceShared"] = "events.contract.resource-shared";
187
+ // Dynamic pricing (rooms → orders).
188
+ Subjects["RoomPricingUpdated"] = "events.room.pricing-updated";
186
189
  Subjects["OrderSeriesCancelled"] = "events.order-series.cancelled";
187
190
  })(Subjects || (exports.Subjects = Subjects = {}));
188
191
  var Streams;
package/build/index.d.ts CHANGED
@@ -57,6 +57,7 @@ export * from "./events/time-block-events";
57
57
  export * from "./events/order-series-events";
58
58
  export * from "./events/order-hold-events";
59
59
  export * from "./events/contract-events";
60
+ export * from "./events/room-pricing-updated-event";
60
61
  export * from "./events/order-rejected-event";
61
62
  export * from "./events/sellableunit-created-event";
62
63
  export * from "./events/sellableunit-updated-event";
package/build/index.js CHANGED
@@ -78,6 +78,7 @@ __exportStar(require("./events/time-block-events"), exports);
78
78
  __exportStar(require("./events/order-series-events"), exports);
79
79
  __exportStar(require("./events/order-hold-events"), exports);
80
80
  __exportStar(require("./events/contract-events"), exports);
81
+ __exportStar(require("./events/room-pricing-updated-event"), exports);
81
82
  __exportStar(require("./events/order-rejected-event"), exports);
82
83
  __exportStar(require("./events/sellableunit-created-event"), exports);
83
84
  __exportStar(require("./events/sellableunit-updated-event"), exports);
@@ -1,3 +1,4 @@
1
1
  export * from "./stripe-fee";
2
2
  export * from "./business-hours";
3
3
  export * from "./class-categories";
4
+ export * from "./pricing";
@@ -22,3 +22,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
22
22
  __exportStar(require("./stripe-fee"), exports);
23
23
  __exportStar(require("./business-hours"), exports);
24
24
  __exportStar(require("./class-categories"), exports);
25
+ __exportStar(require("./pricing"), exports);
@@ -0,0 +1,36 @@
1
+ export interface PriceWindow {
2
+ name: string;
3
+ days: number[];
4
+ start: string;
5
+ end: string;
6
+ kind: "percent" | "fixed";
7
+ value: number;
8
+ }
9
+ export interface PricingRules {
10
+ windows: PriceWindow[];
11
+ minRate?: number;
12
+ maxRate?: number;
13
+ timezone: string;
14
+ }
15
+ export interface PriceSegment {
16
+ start: string;
17
+ end: string;
18
+ label: string;
19
+ rate: number;
20
+ amount: number;
21
+ }
22
+ export interface PriceQuote {
23
+ total: number;
24
+ hours: number;
25
+ effectiveRate: number;
26
+ segments: PriceSegment[];
27
+ }
28
+ export declare const STANDARD_LABEL = "Standard";
29
+ export declare function rateAt(baseRate: number, rules: PricingRules | undefined | null, at: Date): {
30
+ rate: number;
31
+ label: string;
32
+ };
33
+ export declare function quotePrice(baseRate: number, rules: PricingRules | undefined | null, start: Date, end: Date): PriceQuote;
34
+ export declare function lockedQuote(rate: number, start: Date, end: Date, label?: string): PriceQuote;
35
+ export declare const isDynamicQuote: (q: PriceQuote) => boolean;
36
+ export declare function pricingRulesProblem(rules: PricingRules): string | null;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ // Dynamic pricing — "price windows" (browser-safe; shared by the
3
+ // frontend and the services so the price shown is the price charged).
4
+ //
5
+ // A room has a base hourly rate (Room.price). Its PricingRules (set
6
+ // on the building, or overridden per room) list price windows:
7
+ // { name: "Peak", days: [1,2,3,4], start: "17:00", end: "21:00",
8
+ // kind: "percent", value: 25 } → +25% Mon–Thu 5–9 PM
9
+ // { kind: "fixed", value: 45 } → $45/hr in that window
10
+ // Time outside every window = the base rate ("Standard"). When windows
11
+ // overlap, the LATER one in the list wins. minRate / maxRate clamp the
12
+ // resulting hourly rate. Windows are in the building's own timezone.
13
+ //
14
+ // Prices are worked out in 15-minute steps, so a booking that crosses
15
+ // a window edge is priced part at each rate.
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.isDynamicQuote = exports.STANDARD_LABEL = void 0;
18
+ exports.rateAt = rateAt;
19
+ exports.quotePrice = quotePrice;
20
+ exports.lockedQuote = lockedQuote;
21
+ exports.pricingRulesProblem = pricingRulesProblem;
22
+ exports.STANDARD_LABEL = "Standard";
23
+ const STEP_MS = 15 * 60 * 1000;
24
+ const round2 = (n) => Math.round(n * 100) / 100;
25
+ const toMin = (hhmm) => {
26
+ const m = /^(\d{2}):(\d{2})$/.exec(hhmm);
27
+ if (!m)
28
+ return NaN;
29
+ return Number(m[1]) * 60 + Number(m[2]);
30
+ };
31
+ // Local weekday + minute-of-day of an instant in a timezone.
32
+ const fmtCache = new Map();
33
+ function localParts(d, tz) {
34
+ var _a, _b, _c, _d, _e, _f;
35
+ let f = fmtCache.get(tz);
36
+ if (!f) {
37
+ f = new Intl.DateTimeFormat("en-US", {
38
+ timeZone: tz,
39
+ weekday: "short",
40
+ hour: "2-digit",
41
+ minute: "2-digit",
42
+ hourCycle: "h23",
43
+ });
44
+ fmtCache.set(tz, f);
45
+ }
46
+ const parts = f.formatToParts(d);
47
+ const wd = (_b = (_a = parts.find((p) => p.type === "weekday")) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : "Sun";
48
+ const h = Number((_d = (_c = parts.find((p) => p.type === "hour")) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : 0);
49
+ const m = Number((_f = (_e = parts.find((p) => p.type === "minute")) === null || _e === void 0 ? void 0 : _e.value) !== null && _f !== void 0 ? _f : 0);
50
+ const day = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(wd);
51
+ return { day: day < 0 ? 0 : day, minute: h * 60 + m };
52
+ }
53
+ // The hourly rate (and its label) at one instant.
54
+ function rateAt(baseRate, rules, at) {
55
+ let rate = baseRate;
56
+ let label = exports.STANDARD_LABEL;
57
+ if (rules && rules.windows.length) {
58
+ const { day, minute } = localParts(at, rules.timezone || "UTC");
59
+ for (const w of rules.windows) {
60
+ const s = toMin(w.start);
61
+ const e = w.end === "24:00" ? 1440 : toMin(w.end);
62
+ if (w.days.includes(day) && minute >= s && minute < e) {
63
+ rate = w.kind === "fixed" ? w.value : baseRate * (1 + w.value / 100);
64
+ label = w.name;
65
+ }
66
+ }
67
+ }
68
+ if ((rules === null || rules === void 0 ? void 0 : rules.minRate) !== undefined && rules.minRate !== null)
69
+ rate = Math.max(rate, rules.minRate);
70
+ if ((rules === null || rules === void 0 ? void 0 : rules.maxRate) !== undefined && rules.maxRate !== null)
71
+ rate = Math.min(rate, rules.maxRate);
72
+ return { rate: round2(Math.max(0, rate)), label };
73
+ }
74
+ // Price for [start, end) with a segment breakdown.
75
+ function quotePrice(baseRate, rules, start, end) {
76
+ const segments = [];
77
+ let total = 0;
78
+ for (let t = start.getTime(); t < end.getTime(); t += STEP_MS) {
79
+ const stepEnd = Math.min(t + STEP_MS, end.getTime());
80
+ const { rate, label } = rateAt(baseRate, rules, new Date(t));
81
+ const amount = (rate * (stepEnd - t)) / 3600000;
82
+ total += amount;
83
+ const last = segments[segments.length - 1];
84
+ if (last && last.label === label && last.rate === rate) {
85
+ last.end = new Date(stepEnd).toISOString();
86
+ last.amount += amount;
87
+ }
88
+ else {
89
+ segments.push({ start: new Date(t).toISOString(), end: new Date(stepEnd).toISOString(), label, rate, amount });
90
+ }
91
+ }
92
+ for (const s of segments)
93
+ s.amount = round2(s.amount);
94
+ const hours = (end.getTime() - start.getTime()) / 3600000;
95
+ total = round2(total);
96
+ return { total, hours, effectiveRate: hours > 0 ? round2(total / hours) : baseRate, segments };
97
+ }
98
+ // A recurring series keeps its first date's hourly rate for every date.
99
+ function lockedQuote(rate, start, end, label = "Series rate") {
100
+ const hours = (end.getTime() - start.getTime()) / 3600000;
101
+ const total = round2(rate * hours);
102
+ return {
103
+ total,
104
+ hours,
105
+ effectiveRate: rate,
106
+ segments: [{ start: start.toISOString(), end: end.toISOString(), label, rate, amount: total }],
107
+ };
108
+ }
109
+ // Does this booking touch any price window? (UI: show the breakdown.)
110
+ const isDynamicQuote = (q) => q.segments.some((s) => s.label !== exports.STANDARD_LABEL);
111
+ exports.isDynamicQuote = isDynamicQuote;
112
+ // Validation shared by the owner form and the rooms service.
113
+ // Returns a readable problem or null.
114
+ function pricingRulesProblem(rules) {
115
+ if (!rules || !Array.isArray(rules.windows))
116
+ return "Price windows are missing.";
117
+ if (rules.windows.length > 20)
118
+ return "Use at most 20 price windows.";
119
+ try {
120
+ new Intl.DateTimeFormat("en-US", { timeZone: rules.timezone });
121
+ }
122
+ catch (_a) {
123
+ return "Unknown timezone.";
124
+ }
125
+ for (const [i, w] of rules.windows.entries()) {
126
+ const n = `Price window ${i + 1}`;
127
+ if (!w.name || !w.name.trim() || w.name.trim().length > 40)
128
+ return `${n}: give it a name (up to 40 characters).`;
129
+ if (!Array.isArray(w.days) || !w.days.length || w.days.some((d) => !Number.isInteger(d) || d < 0 || d > 6)) {
130
+ return `${n}: pick at least one day.`;
131
+ }
132
+ const s = toMin(w.start);
133
+ const e = w.end === "24:00" ? 1440 : toMin(w.end);
134
+ if (!Number.isFinite(s) || !Number.isFinite(e) || s % 15 || e % 15 || s < 0 || e > 1440) {
135
+ return `${n}: times must be on 15-minute steps.`;
136
+ }
137
+ if (e <= s)
138
+ return `${n}: the end time must be after the start time.`;
139
+ if (w.kind === "percent") {
140
+ if (!(w.value >= -90 && w.value <= 300))
141
+ return `${n}: the change must be between -90% and +300%.`;
142
+ }
143
+ else if (w.kind === "fixed") {
144
+ if (!(w.value > 0 && w.value <= 10000))
145
+ return `${n}: enter an hourly price above $0.`;
146
+ }
147
+ else {
148
+ return `${n}: choose percent or fixed price.`;
149
+ }
150
+ }
151
+ const { minRate, maxRate } = rules;
152
+ if (minRate !== undefined && minRate !== null && !(minRate >= 0))
153
+ return "The minimum hourly price can't be negative.";
154
+ if (maxRate !== undefined && maxRate !== null && !(maxRate > 0))
155
+ return "The maximum hourly price must be above $0.";
156
+ if (minRate != null && maxRate != null && minRate > maxRate)
157
+ return "The minimum price is higher than the maximum.";
158
+ return null;
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epztickets/common",
3
- "version": "1.98.0",
3
+ "version": "1.100.0",
4
4
  "main": "./build/index.js",
5
5
  "types": "./build/index.d.ts",
6
6
  "files": [
@@ -15,7 +15,7 @@
15
15
  "version:major": "npm version major",
16
16
  "release:minor": "npm version minor && npm run build && npm publish",
17
17
  "pub:patch": "npm run build && git add . && git commit -m \"chore: patch release\" && npm run version:patch && npm publish",
18
- "pub:minor": "npm run build && git add . && git commit -m \"feat: nats support\" && npm run version:minor && npm publish"
18
+ "pub:minor": "npm run build && git add . && git commit -m \"feat: version improvement\" && npm run version:minor && npm publish"
19
19
  },
20
20
  "keywords": [],
21
21
  "author": "",