@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,170 @@
1
+ import { ServiceCancellationPolicy } from "@prisma/client";
2
+ import {
3
+ hoursUntilEventStart,
4
+ cancellationRuleThresholdHours,
5
+ resolveCancellationRefundFraction,
6
+ getMatchingCancellationRefundRule,
7
+ } from "../cancellationPolicyRefundResolver";
8
+ import type { ServiceCancellationRefundPolicy } from "../serviceUtils";
9
+
10
+ // Fixed reference for "now"
11
+ const NOW = new Date("2025-06-01T12:00:00.000Z");
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // cancellationRuleThresholdHours
15
+ // ---------------------------------------------------------------------------
16
+
17
+ describe("cancellationRuleThresholdHours", () => {
18
+ it("converts days to hours", () => {
19
+ const rule: ServiceCancellationRefundPolicy = { days: 7, refundPercentage: 100 };
20
+ expect(cancellationRuleThresholdHours(rule)).toBe(168);
21
+ });
22
+ it("returns hours directly", () => {
23
+ const rule: ServiceCancellationRefundPolicy = { hours: 24, refundPercentage: 50 };
24
+ expect(cancellationRuleThresholdHours(rule)).toBe(24);
25
+ });
26
+ it("returns 0 when neither days nor hours is set", () => {
27
+ const rule = { refundPercentage: 100 } as ServiceCancellationRefundPolicy;
28
+ expect(cancellationRuleThresholdHours(rule)).toBe(0);
29
+ });
30
+ });
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // hoursUntilEventStart
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe("hoursUntilEventStart", () => {
37
+ it("returns 0 when event has already started", () => {
38
+ const past = new Date(NOW.getTime() - 1000);
39
+ expect(hoursUntilEventStart(past, NOW)).toBe(0);
40
+ });
41
+ it("returns 0 for a current-moment event", () => {
42
+ expect(hoursUntilEventStart(NOW, NOW)).toBe(0);
43
+ });
44
+ it("returns correct hours for 48-hour future event", () => {
45
+ const future = new Date(NOW.getTime() + 48 * 60 * 60 * 1000);
46
+ expect(hoursUntilEventStart(future, NOW)).toBe(48);
47
+ });
48
+ it("returns fractional hours", () => {
49
+ const future = new Date(NOW.getTime() + 1.5 * 60 * 60 * 1000);
50
+ expect(hoursUntilEventStart(future, NOW)).toBeCloseTo(1.5);
51
+ });
52
+ });
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // resolveCancellationRefundFraction
56
+ // ---------------------------------------------------------------------------
57
+
58
+ describe("resolveCancellationRefundFraction", () => {
59
+ it("returns 0 for null policy", () => {
60
+ expect(
61
+ resolveCancellationRefundFraction(null, { eventStart: new Date(NOW.getTime() + 48 * 3600_000), now: NOW })
62
+ ).toBe(0);
63
+ });
64
+ it("returns 0 for None policy", () => {
65
+ expect(
66
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.None, { eventStart: new Date(NOW.getTime() + 48 * 3600_000), now: NOW })
67
+ ).toBe(0);
68
+ });
69
+ it("returns 0 when event has already started", () => {
70
+ const past = new Date(NOW.getTime() - 1000);
71
+ expect(
72
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.VeryFlexible, { eventStart: past, now: NOW })
73
+ ).toBe(0);
74
+ });
75
+
76
+ // VeryFlexible: 100% refund if >= 24 hours out
77
+ it("returns 1 for VeryFlexible policy >= 24h before start", () => {
78
+ const future48h = new Date(NOW.getTime() + 48 * 3600_000);
79
+ expect(
80
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.VeryFlexible, { eventStart: future48h, now: NOW })
81
+ ).toBe(1);
82
+ });
83
+ it("returns 0 for VeryFlexible policy < 24h before start", () => {
84
+ const future12h = new Date(NOW.getTime() + 12 * 3600_000);
85
+ expect(
86
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.VeryFlexible, { eventStart: future12h, now: NOW })
87
+ ).toBe(0);
88
+ });
89
+
90
+ // Flexible: 100% if >= 7 days, 50% if >= 24h
91
+ it("returns 1 for Flexible policy >= 7 days before start", () => {
92
+ const future8Days = new Date(NOW.getTime() + 8 * 24 * 3600_000);
93
+ expect(
94
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.Flexible, { eventStart: future8Days, now: NOW })
95
+ ).toBe(1);
96
+ });
97
+ it("returns 0.5 for Flexible policy >= 24h but < 7 days before start", () => {
98
+ const future3Days = new Date(NOW.getTime() + 3 * 24 * 3600_000);
99
+ expect(
100
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.Flexible, { eventStart: future3Days, now: NOW })
101
+ ).toBe(0.5);
102
+ });
103
+ it("returns 0 for Flexible policy < 24h before start", () => {
104
+ const future12h = new Date(NOW.getTime() + 12 * 3600_000);
105
+ expect(
106
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.Flexible, { eventStart: future12h, now: NOW })
107
+ ).toBe(0);
108
+ });
109
+
110
+ // Standard30Day: 100% if >= 30 days, 50% if >= 7 days
111
+ it("returns 1 for Standard30Day policy >= 30 days before start", () => {
112
+ const future31Days = new Date(NOW.getTime() + 31 * 24 * 3600_000);
113
+ expect(
114
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.Standard30Day, { eventStart: future31Days, now: NOW })
115
+ ).toBe(1);
116
+ });
117
+ it("returns 0.5 for Standard30Day policy between 7 and 30 days before start", () => {
118
+ const future10Days = new Date(NOW.getTime() + 10 * 24 * 3600_000);
119
+ expect(
120
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.Standard30Day, { eventStart: future10Days, now: NOW })
121
+ ).toBe(0.5);
122
+ });
123
+ it("returns 0 for Standard30Day policy < 7 days before start", () => {
124
+ const future2Days = new Date(NOW.getTime() + 2 * 24 * 3600_000);
125
+ expect(
126
+ resolveCancellationRefundFraction(ServiceCancellationPolicy.Standard30Day, { eventStart: future2Days, now: NOW })
127
+ ).toBe(0);
128
+ });
129
+ });
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // getMatchingCancellationRefundRule
133
+ // ---------------------------------------------------------------------------
134
+
135
+ describe("getMatchingCancellationRefundRule", () => {
136
+ it("returns null for null policy", () => {
137
+ expect(
138
+ getMatchingCancellationRefundRule(null, { eventStart: new Date(NOW.getTime() + 48 * 3600_000), now: NOW })
139
+ ).toBeNull();
140
+ });
141
+ it("returns null for None policy", () => {
142
+ expect(
143
+ getMatchingCancellationRefundRule(ServiceCancellationPolicy.None, { eventStart: new Date(NOW.getTime() + 48 * 3600_000), now: NOW })
144
+ ).toBeNull();
145
+ });
146
+ it("returns null when event has already started", () => {
147
+ const past = new Date(NOW.getTime() - 1000);
148
+ expect(
149
+ getMatchingCancellationRefundRule(ServiceCancellationPolicy.Flexible, { eventStart: past, now: NOW })
150
+ ).toBeNull();
151
+ });
152
+ it("returns the 100% rule for VeryFlexible >= 24h", () => {
153
+ const future48h = new Date(NOW.getTime() + 48 * 3600_000);
154
+ const rule = getMatchingCancellationRefundRule(ServiceCancellationPolicy.VeryFlexible, { eventStart: future48h, now: NOW });
155
+ expect(rule).not.toBeNull();
156
+ expect(rule!.refundPercentage).toBe(100);
157
+ });
158
+ it("returns the 100% rule for Flexible >= 7 days", () => {
159
+ const future8Days = new Date(NOW.getTime() + 8 * 24 * 3600_000);
160
+ const rule = getMatchingCancellationRefundRule(ServiceCancellationPolicy.Flexible, { eventStart: future8Days, now: NOW });
161
+ expect(rule!.refundPercentage).toBe(100);
162
+ expect(rule!.days).toBe(7);
163
+ });
164
+ it("returns the 50% rule for Flexible 24h-7days window", () => {
165
+ const future3Days = new Date(NOW.getTime() + 3 * 24 * 3600_000);
166
+ const rule = getMatchingCancellationRefundRule(ServiceCancellationPolicy.Flexible, { eventStart: future3Days, now: NOW });
167
+ expect(rule!.refundPercentage).toBe(50);
168
+ expect(rule!.hours).toBe(24);
169
+ });
170
+ });
@@ -0,0 +1,273 @@
1
+ import {
2
+ serviceBookingIsRequest,
3
+ serviceBookingIsConfirmed,
4
+ serviceBookingIsPaid,
5
+ serviceBookingIsRefunded,
6
+ serviceBookingAllowPromisePay,
7
+ serviceBookingIsCanceled,
8
+ serviceBookingIsApproved,
9
+ serviceBookingIsDeclined,
10
+ serviceBookingIsPending,
11
+ serviceBookingHasApprovalDecision,
12
+ serviceBookingCanBePaid,
13
+ serviceBookingCanBeRefunded,
14
+ serviceBookingCanHaveApprovalDecision,
15
+ serviceBookingCanBeCanceled,
16
+ serviceBookingCanBeConfirmed,
17
+ } from "../serviceBookingStatusUtils";
18
+ import type { ServiceBookingExt } from "../../../extendedSchemas";
19
+
20
+ // Minimal booking factory — only supply the fields each predicate inspects
21
+ function makeBooking(overrides: Partial<ServiceBookingExt> = {}): ServiceBookingExt {
22
+ return {
23
+ id: "b1",
24
+ status: "Pending",
25
+ bookingType: "Instant",
26
+ allowPromiseToPay: false,
27
+ checkout: null,
28
+ ...overrides,
29
+ } as unknown as ServiceBookingExt;
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Simple predicates
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe("serviceBookingIsRequest", () => {
37
+ it("returns true when bookingType is Request", () => {
38
+ expect(serviceBookingIsRequest(makeBooking({ bookingType: "Request" }))).toBe(true);
39
+ });
40
+ it("returns false when bookingType is Instant", () => {
41
+ expect(serviceBookingIsRequest(makeBooking({ bookingType: "Instant" }))).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe("serviceBookingIsConfirmed", () => {
46
+ it("returns true for Confirmed status", () => {
47
+ expect(serviceBookingIsConfirmed(makeBooking({ status: "Confirmed" }))).toBe(true);
48
+ });
49
+ it("returns false for non-Confirmed statuses", () => {
50
+ for (const status of ["Pending", "Approved", "Rejected", "Canceled"]) {
51
+ expect(serviceBookingIsConfirmed(makeBooking({ status } as Partial<ServiceBookingExt>))).toBe(false);
52
+ }
53
+ });
54
+ });
55
+
56
+ describe("serviceBookingIsPaid", () => {
57
+ it("returns true when checkout.paidOn is set", () => {
58
+ expect(serviceBookingIsPaid(makeBooking({ checkout: { paidOn: new Date() } } as Partial<ServiceBookingExt>))).toBe(true);
59
+ });
60
+ it("returns false when checkout is null", () => {
61
+ expect(serviceBookingIsPaid(makeBooking({ checkout: null }))).toBe(false);
62
+ });
63
+ it("returns false when paidOn is null", () => {
64
+ expect(serviceBookingIsPaid(makeBooking({ checkout: { paidOn: null } } as Partial<ServiceBookingExt>))).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe("serviceBookingIsRefunded", () => {
69
+ it("returns true when checkout.refundedOn is set", () => {
70
+ expect(serviceBookingIsRefunded(makeBooking({ checkout: { refundedOn: new Date() } } as Partial<ServiceBookingExt>))).toBe(true);
71
+ });
72
+ it("returns false when checkout is null", () => {
73
+ expect(serviceBookingIsRefunded(makeBooking({ checkout: null }))).toBe(false);
74
+ });
75
+ it("returns false when refundedOn is null", () => {
76
+ expect(serviceBookingIsRefunded(makeBooking({ checkout: { refundedOn: null } } as Partial<ServiceBookingExt>))).toBe(false);
77
+ });
78
+ });
79
+
80
+ describe("serviceBookingAllowPromisePay", () => {
81
+ it("returns true when allowPromiseToPay is true", () => {
82
+ expect(serviceBookingAllowPromisePay(makeBooking({ allowPromiseToPay: true }))).toBe(true);
83
+ });
84
+ it("returns false when allowPromiseToPay is false", () => {
85
+ expect(serviceBookingAllowPromisePay(makeBooking({ allowPromiseToPay: false }))).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe("serviceBookingIsCanceled", () => {
90
+ it("returns true for Canceled status", () => {
91
+ expect(serviceBookingIsCanceled(makeBooking({ status: "Canceled" }))).toBe(true);
92
+ });
93
+ it("returns false for other statuses", () => {
94
+ expect(serviceBookingIsCanceled(makeBooking({ status: "Pending" }))).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe("serviceBookingIsApproved", () => {
99
+ it("returns true for Approved status", () => {
100
+ expect(serviceBookingIsApproved(makeBooking({ status: "Approved" }))).toBe(true);
101
+ });
102
+ it("returns false for Rejected", () => {
103
+ expect(serviceBookingIsApproved(makeBooking({ status: "Rejected" }))).toBe(false);
104
+ });
105
+ });
106
+
107
+ describe("serviceBookingIsDeclined", () => {
108
+ it("returns true for Rejected status", () => {
109
+ expect(serviceBookingIsDeclined(makeBooking({ status: "Rejected" }))).toBe(true);
110
+ });
111
+ it("returns false for Approved", () => {
112
+ expect(serviceBookingIsDeclined(makeBooking({ status: "Approved" }))).toBe(false);
113
+ });
114
+ });
115
+
116
+ describe("serviceBookingIsPending", () => {
117
+ it("returns true for Pending status", () => {
118
+ expect(serviceBookingIsPending(makeBooking({ status: "Pending" }))).toBe(true);
119
+ });
120
+ it("returns false for Approved", () => {
121
+ expect(serviceBookingIsPending(makeBooking({ status: "Approved" }))).toBe(false);
122
+ });
123
+ });
124
+
125
+ describe("serviceBookingHasApprovalDecision", () => {
126
+ it("returns true when Rejected", () => {
127
+ expect(serviceBookingHasApprovalDecision(makeBooking({ status: "Rejected" }))).toBe(true);
128
+ });
129
+ it("returns true when Approved", () => {
130
+ expect(serviceBookingHasApprovalDecision(makeBooking({ status: "Approved" }))).toBe(true);
131
+ });
132
+ it("returns false when Pending", () => {
133
+ expect(serviceBookingHasApprovalDecision(makeBooking({ status: "Pending" }))).toBe(false);
134
+ });
135
+ });
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Compound predicates — canBePaid
139
+ // ---------------------------------------------------------------------------
140
+
141
+ describe("serviceBookingCanBePaid", () => {
142
+ it("returns valid for an Approved, unpaid, non-canceled booking", () => {
143
+ expect(serviceBookingCanBePaid(makeBooking({ status: "Approved" }))).toEqual({ valid: true });
144
+ });
145
+ it("blocks payment for a Canceled booking", () => {
146
+ const result = serviceBookingCanBePaid(makeBooking({ status: "Canceled" }));
147
+ expect(result.valid).toBe(false);
148
+ expect(result.errorMessage).toMatch(/canceled/i);
149
+ });
150
+ it("blocks payment when already paid", () => {
151
+ const result = serviceBookingCanBePaid(
152
+ makeBooking({ status: "Approved", checkout: { paidOn: new Date() } } as Partial<ServiceBookingExt>)
153
+ );
154
+ expect(result.valid).toBe(false);
155
+ expect(result.errorMessage).toMatch(/already been paid/i);
156
+ });
157
+ it("blocks payment for a Rejected booking", () => {
158
+ const result = serviceBookingCanBePaid(makeBooking({ status: "Rejected" }));
159
+ expect(result.valid).toBe(false);
160
+ expect(result.errorMessage).toMatch(/declined/i);
161
+ });
162
+ it("blocks payment for a Pending Request", () => {
163
+ const result = serviceBookingCanBePaid(makeBooking({ status: "Pending", bookingType: "Request" }));
164
+ expect(result.valid).toBe(false);
165
+ expect(result.errorMessage).toMatch(/pending booking request/i);
166
+ });
167
+ it("allows payment for a Pending non-Request booking", () => {
168
+ expect(serviceBookingCanBePaid(makeBooking({ status: "Pending", bookingType: "Instant" }))).toEqual({ valid: true });
169
+ });
170
+ });
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // canBeRefunded
174
+ // ---------------------------------------------------------------------------
175
+
176
+ describe("serviceBookingCanBeRefunded", () => {
177
+ const paidBooking = makeBooking({
178
+ status: "Approved",
179
+ checkout: { paidOn: new Date() },
180
+ } as Partial<ServiceBookingExt>);
181
+
182
+ it("returns valid for a paid, non-canceled, non-refunded booking", () => {
183
+ expect(serviceBookingCanBeRefunded(paidBooking)).toEqual({ valid: true });
184
+ });
185
+ it("blocks refund for a Canceled booking", () => {
186
+ const result = serviceBookingCanBeRefunded(makeBooking({ status: "Canceled" }));
187
+ expect(result.valid).toBe(false);
188
+ expect(result.errorMessage).toMatch(/already canceled/i);
189
+ });
190
+ it("blocks refund for an already refunded booking", () => {
191
+ const result = serviceBookingCanBeRefunded(
192
+ makeBooking({ checkout: { paidOn: new Date(), refundedOn: new Date() } } as Partial<ServiceBookingExt>)
193
+ );
194
+ expect(result.valid).toBe(false);
195
+ expect(result.errorMessage).toMatch(/already refunded/i);
196
+ });
197
+ it("blocks refund for an unpaid booking", () => {
198
+ const result = serviceBookingCanBeRefunded(makeBooking({ checkout: null }));
199
+ expect(result.valid).toBe(false);
200
+ expect(result.errorMessage).toMatch(/not been paid/i);
201
+ });
202
+ });
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // canHaveApprovalDecision
206
+ // ---------------------------------------------------------------------------
207
+
208
+ describe("serviceBookingCanHaveApprovalDecision", () => {
209
+ it("returns valid for a Pending Request that is not canceled or confirmed", () => {
210
+ const booking = makeBooking({ bookingType: "Request", status: "Pending" });
211
+ expect(serviceBookingCanHaveApprovalDecision(booking)).toEqual({ valid: true });
212
+ });
213
+ it("blocks non-Request bookings", () => {
214
+ const result = serviceBookingCanHaveApprovalDecision(makeBooking({ bookingType: "Instant" }));
215
+ expect(result.valid).toBe(false);
216
+ expect(result.errorMessage).toMatch(/booking requests/i);
217
+ });
218
+ it("blocks Canceled requests", () => {
219
+ const result = serviceBookingCanHaveApprovalDecision(makeBooking({ bookingType: "Request", status: "Canceled" }));
220
+ expect(result.valid).toBe(false);
221
+ expect(result.errorMessage).toMatch(/canceled/i);
222
+ });
223
+ it("blocks Confirmed requests", () => {
224
+ const result = serviceBookingCanHaveApprovalDecision(makeBooking({ bookingType: "Request", status: "Confirmed" }));
225
+ expect(result.valid).toBe(false);
226
+ expect(result.errorMessage).toMatch(/confirmed/i);
227
+ });
228
+ });
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // canBeCanceled
232
+ // ---------------------------------------------------------------------------
233
+
234
+ describe("serviceBookingCanBeCanceled", () => {
235
+ const service = {} as any;
236
+
237
+ it("returns valid for an active Pending booking", () => {
238
+ expect(serviceBookingCanBeCanceled(service, makeBooking({ status: "Pending" }))).toEqual({ valid: true });
239
+ });
240
+ it("blocks canceling an already Canceled booking", () => {
241
+ const result = serviceBookingCanBeCanceled(service, makeBooking({ status: "Canceled" }));
242
+ expect(result.valid).toBe(false);
243
+ expect(result.errorMessage).toMatch(/may not be canceled/i);
244
+ });
245
+ it("blocks canceling a refunded booking", () => {
246
+ const result = serviceBookingCanBeCanceled(
247
+ service,
248
+ makeBooking({ checkout: { refundedOn: new Date() } } as Partial<ServiceBookingExt>)
249
+ );
250
+ expect(result.valid).toBe(false);
251
+ expect(result.errorMessage).toMatch(/refunded/i);
252
+ });
253
+ it("blocks canceling a Rejected booking", () => {
254
+ const result = serviceBookingCanBeCanceled(service, makeBooking({ status: "Rejected" }));
255
+ expect(result.valid).toBe(false);
256
+ expect(result.errorMessage).toMatch(/declined/i);
257
+ });
258
+ });
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // canBeConfirmed
262
+ // ---------------------------------------------------------------------------
263
+
264
+ describe("serviceBookingCanBeConfirmed", () => {
265
+ it("returns valid for an Approved booking", () => {
266
+ expect(serviceBookingCanBeConfirmed(makeBooking({ status: "Approved" }))).toEqual({ valid: true });
267
+ });
268
+ it("blocks confirming a Canceled booking", () => {
269
+ const result = serviceBookingCanBeConfirmed(makeBooking({ status: "Canceled" }));
270
+ expect(result.valid).toBe(false);
271
+ expect(result.errorMessage).toMatch(/canceled/i);
272
+ });
273
+ });