@bash-app/bash-common 30.215.0 → 30.217.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/definitions.d.ts +7 -6
  2. package/dist/definitions.d.ts.map +1 -1
  3. package/dist/definitions.js.map +1 -1
  4. package/dist/extendedSchemas.d.ts +100 -0
  5. package/dist/extendedSchemas.d.ts.map +1 -1
  6. package/dist/extendedSchemas.js +2 -0
  7. package/dist/extendedSchemas.js.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/legalTemplates.d.ts +11 -0
  13. package/dist/legalTemplates.d.ts.map +1 -0
  14. package/dist/legalTemplates.js +19 -0
  15. package/dist/legalTemplates.js.map +1 -0
  16. package/dist/utils/index.d.ts +0 -1
  17. package/dist/utils/index.d.ts.map +1 -1
  18. package/dist/utils/index.js +0 -1
  19. package/dist/utils/index.js.map +1 -1
  20. package/dist/utils/service/__tests__/apiServiceBookingApiUtils.test.d.ts +2 -0
  21. package/dist/utils/service/__tests__/apiServiceBookingApiUtils.test.d.ts.map +1 -0
  22. package/dist/utils/service/__tests__/apiServiceBookingApiUtils.test.js +101 -0
  23. package/dist/utils/service/__tests__/apiServiceBookingApiUtils.test.js.map +1 -0
  24. package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.d.ts +2 -0
  25. package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.d.ts.map +1 -0
  26. package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.js +125 -0
  27. package/dist/utils/service/__tests__/cancellationPolicyRefundResolver.test.js.map +1 -0
  28. package/dist/utils/service/__tests__/serviceBookingStatusUtils.test.d.ts +2 -0
  29. package/dist/utils/service/__tests__/serviceBookingStatusUtils.test.d.ts.map +1 -0
  30. package/dist/utils/service/__tests__/serviceBookingStatusUtils.test.js +226 -0
  31. package/dist/utils/service/__tests__/serviceBookingStatusUtils.test.js.map +1 -0
  32. package/dist/utils/service/__tests__/serviceRateUtils.test.d.ts +2 -0
  33. package/dist/utils/service/__tests__/serviceRateUtils.test.d.ts.map +1 -0
  34. package/dist/utils/service/__tests__/serviceRateUtils.test.js +237 -0
  35. package/dist/utils/service/__tests__/serviceRateUtils.test.js.map +1 -0
  36. package/dist/utils/service/serviceDBUtils.d.ts +11 -6
  37. package/dist/utils/service/serviceDBUtils.d.ts.map +1 -1
  38. package/dist/utils/service/serviceDBUtils.js +9 -4
  39. package/dist/utils/service/serviceDBUtils.js.map +1 -1
  40. package/dist/utils/service/serviceUtils.d.ts +5 -0
  41. package/dist/utils/service/serviceUtils.d.ts.map +1 -1
  42. package/dist/utils/service/serviceUtils.js +8 -1
  43. package/dist/utils/service/serviceUtils.js.map +1 -1
  44. package/dist/utmAttribution.d.ts +1 -5
  45. package/dist/utmAttribution.d.ts.map +1 -1
  46. package/dist/utmAttribution.js.map +1 -1
  47. package/dist/venueLoyaltyRedemption.d.ts +21 -0
  48. package/dist/venueLoyaltyRedemption.d.ts.map +1 -0
  49. package/dist/venueLoyaltyRedemption.js +5 -0
  50. package/dist/venueLoyaltyRedemption.js.map +1 -0
  51. package/package.json +1 -1
  52. package/prisma/schema.prisma +115 -3
  53. package/src/definitions.ts +7 -7
  54. package/src/extendedSchemas.ts +3 -0
  55. package/src/index.ts +2 -0
  56. package/src/legalTemplates.ts +21 -0
  57. package/src/utils/index.ts +0 -1
  58. package/src/utils/service/__tests__/apiServiceBookingApiUtils.test.ts +128 -0
  59. package/src/utils/service/__tests__/cancellationPolicyRefundResolver.test.ts +170 -0
  60. package/src/utils/service/__tests__/serviceBookingStatusUtils.test.ts +273 -0
  61. package/src/utils/service/__tests__/serviceRateUtils.test.ts +288 -0
  62. package/src/utils/service/serviceDBUtils.ts +13 -8
  63. package/src/utils/service/serviceUtils.ts +8 -0
  64. package/src/utmAttribution.ts +1 -5
  65. package/src/venueLoyaltyRedemption.ts +22 -0
@@ -0,0 +1,288 @@
1
+ import { ServiceRatePricingType, ServiceRateType } from "@prisma/client";
2
+ import { DateTime } from "luxon";
3
+ import {
4
+ SERVICE_DAILY_RATE_HOURS_MIN,
5
+ serviceBookingCalcDayPriceBreakdown,
6
+ serviceBookingPriceBreakdownGetTypeText,
7
+ getServiceRatePricingInfo,
8
+ serviceRatesFilter,
9
+ serviceRatesHasRates,
10
+ } from "../serviceRateUtils";
11
+ import type { ServiceRatesLuxon } from "../serviceRateUtils";
12
+ import type { LuxonDateRange } from "../../luxonUtils";
13
+ import type { ServiceRate } from "@prisma/client";
14
+ import type { ServiceSpecialRatesExtT, ServiceDailyRatesExtT } from "../serviceRateTypes";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function makeRange(startISO: string, endISO: string): LuxonDateRange {
21
+ return {
22
+ start: DateTime.fromISO(startISO, { zone: "utc" }),
23
+ end: DateTime.fromISO(endISO, { zone: "utc" }),
24
+ };
25
+ }
26
+
27
+ function makeRate(overrides: Partial<ServiceRate> = {}): ServiceRate {
28
+ return {
29
+ id: "r1",
30
+ rateType: ServiceRateType.General,
31
+ hourlyRateCents: null,
32
+ dailyRateCents: null,
33
+ flatRateCents: null,
34
+ serviceRatesAssociationId: "sra1",
35
+ createdAt: new Date(),
36
+ updatedAt: new Date(),
37
+ ...overrides,
38
+ } as ServiceRate;
39
+ }
40
+
41
+ const RANGE_8H = makeRange("2025-07-01T09:00:00Z", "2025-07-01T17:00:00Z");
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // serviceBookingPriceBreakdownGetTypeText
45
+ // ---------------------------------------------------------------------------
46
+
47
+ describe("serviceBookingPriceBreakdownGetTypeText", () => {
48
+ it("returns rateType + space + pricingType", () => {
49
+ const info = {
50
+ rateType: ServiceRateType.General,
51
+ pricingType: ServiceRatePricingType.Hourly,
52
+ hours: 2,
53
+ piTotalCents: 200,
54
+ rateCents: 100,
55
+ unitCount: 2,
56
+ dateTimeRange: RANGE_8H,
57
+ };
58
+ expect(serviceBookingPriceBreakdownGetTypeText(info)).toBe("General Hourly");
59
+ });
60
+ });
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // serviceBookingCalcDayPriceBreakdown — flat rate
64
+ // ---------------------------------------------------------------------------
65
+
66
+ describe("serviceBookingCalcDayPriceBreakdown — flat rate", () => {
67
+ it("uses flat rate when flatRateCents is set, ignoring hourly", () => {
68
+ const rate = makeRate({ flatRateCents: 50000, hourlyRateCents: 10000 });
69
+ const result = serviceBookingCalcDayPriceBreakdown(RANGE_8H, [rate], false, 8);
70
+
71
+ expect(result.pricingType).toBe("Flat");
72
+ expect(result.rateCents).toBe(50000);
73
+ expect(result.piTotalCents).toBe(50000);
74
+ expect(result.unitCount).toBe(1);
75
+ });
76
+
77
+ it("assigns rateType from the matching rate", () => {
78
+ const rate = makeRate({ flatRateCents: 20000, rateType: ServiceRateType.Special });
79
+ const result = serviceBookingCalcDayPriceBreakdown(RANGE_8H, [rate], false, 4);
80
+ expect(result.rateType).toBe(ServiceRateType.Special);
81
+ });
82
+ });
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // serviceBookingCalcDayPriceBreakdown — hourly rate
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe("serviceBookingCalcDayPriceBreakdown — hourly rate", () => {
89
+ it("calculates hourly total correctly", () => {
90
+ const rate = makeRate({ hourlyRateCents: 5000 });
91
+ const result = serviceBookingCalcDayPriceBreakdown(RANGE_8H, [rate], false, 3);
92
+
93
+ expect(result.pricingType).toBe(ServiceRatePricingType.Hourly);
94
+ expect(result.rateCents).toBe(5000);
95
+ expect(result.unitCount).toBe(3);
96
+ expect(result.piTotalCents).toBe(15000);
97
+ });
98
+
99
+ it("returns zero total when no hourly rate available", () => {
100
+ const result = serviceBookingCalcDayPriceBreakdown(RANGE_8H, [], false, 3);
101
+ expect(result.piTotalCents).toBe(0);
102
+ });
103
+ });
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // serviceBookingCalcDayPriceBreakdown — daily rate
107
+ // ---------------------------------------------------------------------------
108
+
109
+ describe("serviceBookingCalcDayPriceBreakdown — daily rate", () => {
110
+ it("calculates daily total based on full-day slots", () => {
111
+ const rate = makeRate({ dailyRateCents: 40000 });
112
+ // 16 hours = floor(16/8) = 2 days
113
+ const result = serviceBookingCalcDayPriceBreakdown(RANGE_8H, [rate], true, 16);
114
+
115
+ expect(result.pricingType).toBe(ServiceRatePricingType.Daily);
116
+ expect(result.rateCents).toBe(40000);
117
+ expect(result.unitCount).toBe(2);
118
+ expect(result.piTotalCents).toBe(80000);
119
+ });
120
+
121
+ it("falls back to hourly when isFullDay=true but no daily rate available", () => {
122
+ const rate = makeRate({ hourlyRateCents: 3000 });
123
+ const result = serviceBookingCalcDayPriceBreakdown(RANGE_8H, [rate], true, 8);
124
+ expect(result.pricingType).toBe(ServiceRatePricingType.Hourly);
125
+ });
126
+ });
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // getServiceRatePricingInfo
130
+ // ---------------------------------------------------------------------------
131
+
132
+ describe("getServiceRatePricingInfo", () => {
133
+ it("returns hourly pricing when hours < SERVICE_DAILY_RATE_HOURS_MIN", () => {
134
+ const generalRate = makeRate({ hourlyRateCents: 2000 });
135
+ const result = getServiceRatePricingInfo(RANGE_8H, generalRate, [], [], "UTC", 4);
136
+ expect(result.pricingType).toBe(ServiceRatePricingType.Hourly);
137
+ expect(result.piTotalCents).toBe(8000);
138
+ });
139
+
140
+ it("returns daily pricing when hours >= SERVICE_DAILY_RATE_HOURS_MIN", () => {
141
+ const generalRate = makeRate({ dailyRateCents: 30000 });
142
+ const result = getServiceRatePricingInfo(RANGE_8H, generalRate, [], [], "UTC", SERVICE_DAILY_RATE_HOURS_MIN);
143
+ expect(result.pricingType).toBe(ServiceRatePricingType.Daily);
144
+ expect(result.piTotalCents).toBe(30000);
145
+ });
146
+
147
+ it("prefers special rate over general rate", () => {
148
+ const generalRate = makeRate({ hourlyRateCents: 1000 });
149
+ const specialRate = {
150
+ id: "sp1",
151
+ isAvailable: true,
152
+ serviceRate: makeRate({ hourlyRateCents: 9999 }),
153
+ dateTimeRange: RANGE_8H,
154
+ } as unknown as ServiceSpecialRatesExtT;
155
+
156
+ const result = getServiceRatePricingInfo(RANGE_8H, generalRate, [], [specialRate], "UTC", 2);
157
+ expect(result.rateCents).toBe(9999);
158
+ });
159
+ });
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // serviceRatesFilter
163
+ // ---------------------------------------------------------------------------
164
+
165
+ describe("serviceRatesFilter", () => {
166
+ const range = makeRange("2025-07-01T09:00:00Z", "2025-07-01T17:00:00Z");
167
+
168
+ it("keeps special rates that intersect and are available", () => {
169
+ const specialRate: ServiceSpecialRatesExtT = {
170
+ id: "sp1",
171
+ isAvailable: true,
172
+ serviceRate: makeRate({ hourlyRateCents: 5000 }),
173
+ dateTimeRange: makeRange("2025-07-01T10:00:00Z", "2025-07-01T14:00:00Z"),
174
+ } as unknown as ServiceSpecialRatesExtT;
175
+
176
+ const rates: ServiceRatesLuxon = {
177
+ generalRate: null,
178
+ specialRates: [specialRate],
179
+ dailyRates: [],
180
+ };
181
+
182
+ const result = serviceRatesFilter(rates, [range]);
183
+ expect(result.specialRates).toHaveLength(1);
184
+ });
185
+
186
+ it("excludes special rates that are unavailable", () => {
187
+ const blockedRate: ServiceSpecialRatesExtT = {
188
+ id: "sp2",
189
+ isAvailable: false,
190
+ serviceRate: makeRate(),
191
+ dateTimeRange: range,
192
+ } as unknown as ServiceSpecialRatesExtT;
193
+
194
+ const rates: ServiceRatesLuxon = {
195
+ generalRate: null,
196
+ specialRates: [blockedRate],
197
+ dailyRates: [],
198
+ };
199
+
200
+ const result = serviceRatesFilter(rates, [range]);
201
+ expect(result.specialRates).toHaveLength(0);
202
+ });
203
+
204
+ it("excludes special rates outside the requested range", () => {
205
+ const outsideRate: ServiceSpecialRatesExtT = {
206
+ id: "sp3",
207
+ isAvailable: true,
208
+ serviceRate: makeRate({ hourlyRateCents: 5000 }),
209
+ dateTimeRange: makeRange("2025-08-01T09:00:00Z", "2025-08-01T17:00:00Z"),
210
+ } as unknown as ServiceSpecialRatesExtT;
211
+
212
+ const rates: ServiceRatesLuxon = {
213
+ generalRate: null,
214
+ specialRates: [outsideRate],
215
+ dailyRates: [],
216
+ };
217
+
218
+ const result = serviceRatesFilter(rates, [range]);
219
+ expect(result.specialRates).toHaveLength(0);
220
+ });
221
+
222
+ it("preserves the generalRate unchanged", () => {
223
+ const generalRate = makeRate({ hourlyRateCents: 1000 });
224
+ const rates: ServiceRatesLuxon = {
225
+ generalRate,
226
+ specialRates: [],
227
+ dailyRates: [],
228
+ };
229
+ expect(serviceRatesFilter(rates, [range]).generalRate).toBe(generalRate);
230
+ });
231
+ });
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // serviceRatesHasRates
235
+ // ---------------------------------------------------------------------------
236
+
237
+ describe("serviceRatesHasRates", () => {
238
+ it("returns true when generalRate has hourly rate", () => {
239
+ const rates: ServiceRatesLuxon = {
240
+ generalRate: makeRate({ hourlyRateCents: 1000 }),
241
+ specialRates: [],
242
+ dailyRates: [],
243
+ };
244
+ expect(serviceRatesHasRates(rates)).toBe(true);
245
+ });
246
+
247
+ it("returns true when generalRate has flat rate", () => {
248
+ const rates: ServiceRatesLuxon = {
249
+ generalRate: makeRate({ flatRateCents: 5000 }),
250
+ specialRates: [],
251
+ dailyRates: [],
252
+ };
253
+ expect(serviceRatesHasRates(rates)).toBe(true);
254
+ });
255
+
256
+ it("returns false when all rates are zero/null", () => {
257
+ const rates: ServiceRatesLuxon = {
258
+ generalRate: makeRate(),
259
+ specialRates: [],
260
+ dailyRates: [],
261
+ };
262
+ expect(serviceRatesHasRates(rates)).toBe(false);
263
+ });
264
+
265
+ it("returns true when a special rate has a daily rate", () => {
266
+ const specialRate: ServiceSpecialRatesExtT = {
267
+ id: "sp1",
268
+ isAvailable: true,
269
+ serviceRate: makeRate({ dailyRateCents: 10000 }),
270
+ dateTimeRange: RANGE_8H,
271
+ } as unknown as ServiceSpecialRatesExtT;
272
+ const rates: ServiceRatesLuxon = {
273
+ generalRate: null,
274
+ specialRates: [specialRate],
275
+ dailyRates: [],
276
+ };
277
+ expect(serviceRatesHasRates(rates)).toBe(true);
278
+ });
279
+
280
+ it("returns falsy when generalRate is null and no special/daily rates", () => {
281
+ const rates: ServiceRatesLuxon = {
282
+ generalRate: null,
283
+ specialRates: [],
284
+ dailyRates: [],
285
+ };
286
+ expect(serviceRatesHasRates(rates)).toBeFalsy();
287
+ });
288
+ });
@@ -1,14 +1,19 @@
1
- import { ServiceExt } from "../../extendedSchemas.js";
2
- import { DeepPartial } from "../typeUtils.js";
1
+ import type { ServiceExt } from "../../extendedSchemas.js";
2
+ import type { DeepPartial } from "../typeUtils.js";
3
3
 
4
- /** Converts service data for DB write. Currently a pass-through; rate conversion was removed. */
5
- export function serviceToDb(
6
- service: DeepPartial<ServiceExt>
7
- ): DeepPartial<ServiceExt> {
4
+ /**
5
+ * Maps a service record from the API / DB into the client `ServiceExt` shape
6
+ * (e.g. date normalization). Identity until a stricter transform is needed.
7
+ */
8
+ export function serviceFromDb<T extends ServiceExt | null | undefined>(
9
+ service: T
10
+ ): T {
8
11
  return service;
9
12
  }
10
13
 
11
- /** Converts service data from DB read. Currently a pass-through; rate conversion was removed. */
12
- export function serviceFromDb(service: ServiceExt): ServiceExt {
14
+ /**
15
+ * Maps a partial `ServiceExt` from the client for upsert payloads.
16
+ */
17
+ export function serviceToDb<T extends DeepPartial<ServiceExt>>(service: T): T {
13
18
  return service;
14
19
  }
@@ -915,3 +915,11 @@ export function serviceRelevantData(
915
915
  )
916
916
  );
917
917
  }
918
+
919
+ /**
920
+ * Returns the contextually correct label for a service's sellable items.
921
+ * Vendors sell "Products"; entertainers and other service types sell "Merch".
922
+ */
923
+ export function getVendorSalesLabel(serviceType: ServiceTypes): "Products" | "Merch" {
924
+ return serviceType === ServiceTypes.Vendors ? "Products" : "Merch";
925
+ }
@@ -16,11 +16,7 @@ const SESSION_KEYS = {
16
16
  } as const;
17
17
 
18
18
  /** Normalize optional UTM strings from API args (Stripe checkout / payment intent body). */
19
- export function utmFieldsFromCheckoutArgs(args: {
20
- utmSource?: string;
21
- utmMedium?: string;
22
- utmCampaign?: string;
23
- }): UtmFields | undefined {
19
+ export function utmFieldsFromCheckoutArgs(args: UtmFields): UtmFields | undefined {
24
20
  const s = args.utmSource?.trim();
25
21
  const m = args.utmMedium?.trim();
26
22
  const c = args.utmCampaign?.trim();
@@ -0,0 +1,22 @@
1
+ /**
2
+ * API shapes for venue loyalty perk requests (guest claim + host list/redeem).
3
+ */
4
+
5
+ export interface VenueLoyaltyRedemptionGuestDto {
6
+ id: string;
7
+ venueId: string;
8
+ userId: string;
9
+ templateId: string;
10
+ claimedAt: string;
11
+ redeemedAt: string | null;
12
+ guestNotes: string | null;
13
+ hostNotes: string | null;
14
+ }
15
+
16
+ export interface VenueLoyaltyRedemptionListItem extends VenueLoyaltyRedemptionGuestDto {
17
+ user: {
18
+ email: string;
19
+ givenName: string | null;
20
+ familyName: string | null;
21
+ };
22
+ }