@bash-app/bash-common 30.371.0 → 30.372.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 +6 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js.map +1 -1
- package/dist/utils/luxonUtils.d.ts +7 -7
- package/dist/utils/luxonUtils.d.ts.map +1 -1
- package/dist/utils/organizationEntitlements.d.ts +17 -14
- package/dist/utils/organizationEntitlements.d.ts.map +1 -1
- package/dist/utils/organizationEntitlements.js +12 -10
- package/dist/utils/organizationEntitlements.js.map +1 -1
- package/dist/utils/service/__tests__/apiServiceBookingApiUtils.test.js +66 -1
- package/dist/utils/service/__tests__/apiServiceBookingApiUtils.test.js.map +1 -1
- package/dist/utils/service/__tests__/frontendServiceBookingUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/frontendServiceBookingUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/frontendServiceBookingUtils.test.js +165 -0
- package/dist/utils/service/__tests__/frontendServiceBookingUtils.test.js.map +1 -0
- package/dist/utils/service/apiServiceBookingApiUtils.d.ts +5 -3
- package/dist/utils/service/apiServiceBookingApiUtils.d.ts.map +1 -1
- package/dist/utils/service/apiServiceBookingApiUtils.js +30 -1
- package/dist/utils/service/apiServiceBookingApiUtils.js.map +1 -1
- package/dist/utils/service/frontendServiceBookingUtils.d.ts +10 -4
- package/dist/utils/service/frontendServiceBookingUtils.d.ts.map +1 -1
- package/dist/utils/service/frontendServiceBookingUtils.js +16 -3
- package/dist/utils/service/frontendServiceBookingUtils.js.map +1 -1
- package/dist/utils/service/serviceBookingTypes.d.ts +2 -2
- package/dist/utils/service/serviceBookingTypes.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/definitions.ts +7 -0
- package/src/utils/organizationEntitlements.ts +17 -14
- package/src/utils/service/__tests__/apiServiceBookingApiUtils.test.ts +102 -2
- package/src/utils/service/__tests__/frontendServiceBookingUtils.test.ts +217 -0
- package/src/utils/service/apiServiceBookingApiUtils.ts +53 -1
- package/src/utils/service/frontendServiceBookingUtils.ts +33 -2
- package/src/utils/service/serviceBookingTypes.ts +2 -1
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
apiTransformAddOnsToAddonInputs,
|
|
3
|
+
apiTransformPackagesToPackageInputs,
|
|
4
|
+
servicePriceToBookToApiPackages,
|
|
3
5
|
serviceBookingParamsToLuxon,
|
|
4
6
|
} from "../apiServiceBookingApiUtils";
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
+
import type {
|
|
8
|
+
ApiServiceAddonParams,
|
|
9
|
+
ApiServiceBookingParams,
|
|
10
|
+
ApiServicePackageParams,
|
|
11
|
+
} from "../../../definitions";
|
|
12
|
+
import type { ServiceAddonExt, ServicePackageExt } from "../../../extendedSchemas";
|
|
13
|
+
import type { FrontendServiceGetPriceToBookResult } from "../frontendServiceBookingUtils";
|
|
7
14
|
|
|
8
15
|
// ---------------------------------------------------------------------------
|
|
9
16
|
// apiTransformAddOnsToAddonInputs
|
|
@@ -65,6 +72,99 @@ describe("apiTransformAddOnsToAddonInputs", () => {
|
|
|
65
72
|
});
|
|
66
73
|
});
|
|
67
74
|
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// apiTransformPackagesToPackageInputs
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
function makePackageExt(id: string, name = "Package"): ServicePackageExt {
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
name,
|
|
83
|
+
description: "",
|
|
84
|
+
priceCents: 5000,
|
|
85
|
+
serviceAddons: [],
|
|
86
|
+
} as unknown as ServicePackageExt;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function makePackageParam(
|
|
90
|
+
packageId: string,
|
|
91
|
+
qty = 1
|
|
92
|
+
): ApiServicePackageParams {
|
|
93
|
+
return { packageId, chosenQuantity: qty };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("apiTransformPackagesToPackageInputs", () => {
|
|
97
|
+
it("returns empty array when no packages are provided", () => {
|
|
98
|
+
expect(apiTransformPackagesToPackageInputs([], [])).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("maps package params to their matching ServicePackageExt", () => {
|
|
102
|
+
const servicePackages = [makePackageExt("p1"), makePackageExt("p2")];
|
|
103
|
+
const packagesApi = [makePackageParam("p1", 2)];
|
|
104
|
+
|
|
105
|
+
const result = apiTransformPackagesToPackageInputs(
|
|
106
|
+
packagesApi,
|
|
107
|
+
servicePackages
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(result).toHaveLength(1);
|
|
111
|
+
expect(result[0].id).toBe("p1");
|
|
112
|
+
expect(result[0].chosenQuantity).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("merges package params over the ServicePackageExt fields (params win)", () => {
|
|
116
|
+
const servicePackages = [makePackageExt("p1")];
|
|
117
|
+
const packagesApi = [
|
|
118
|
+
{ packageId: "p1", chosenQuantity: 5, extra: "field" } as any,
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const result = apiTransformPackagesToPackageInputs(
|
|
122
|
+
packagesApi,
|
|
123
|
+
servicePackages
|
|
124
|
+
);
|
|
125
|
+
expect(result[0].chosenQuantity).toBe(5);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("throws when a packageId does not match any ServicePackageExt", () => {
|
|
129
|
+
const servicePackages = [makePackageExt("p1")];
|
|
130
|
+
const packagesApi = [makePackageParam("ghost-id")];
|
|
131
|
+
|
|
132
|
+
expect(() =>
|
|
133
|
+
apiTransformPackagesToPackageInputs(packagesApi, servicePackages)
|
|
134
|
+
).toThrow(/Failed to match package with id: ghost-id/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// servicePriceToBookToApiPackages
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
describe("servicePriceToBookToApiPackages", () => {
|
|
143
|
+
it("returns empty array when priceToBook has no packages", () => {
|
|
144
|
+
const priceToBook = {
|
|
145
|
+
packages: [],
|
|
146
|
+
} as unknown as FrontendServiceGetPriceToBookResult;
|
|
147
|
+
|
|
148
|
+
expect(servicePriceToBookToApiPackages(priceToBook)).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("maps priceToBook packages to ApiServicePackageParams", () => {
|
|
152
|
+
const priceToBook = {
|
|
153
|
+
packages: [
|
|
154
|
+
{ packageId: "p1", chosenQuantity: 2, costPQCents: 5000 },
|
|
155
|
+
{ packageId: "p2", chosenQuantity: 1, costPQCents: 8000 },
|
|
156
|
+
],
|
|
157
|
+
} as unknown as FrontendServiceGetPriceToBookResult;
|
|
158
|
+
|
|
159
|
+
const result = servicePriceToBookToApiPackages(priceToBook);
|
|
160
|
+
|
|
161
|
+
expect(result).toEqual([
|
|
162
|
+
{ packageId: "p1", chosenQuantity: 2 },
|
|
163
|
+
{ packageId: "p2", chosenQuantity: 1 },
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
68
168
|
// ---------------------------------------------------------------------------
|
|
69
169
|
// serviceBookingParamsToLuxon
|
|
70
170
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
frontendServiceGetPriceToBook,
|
|
5
|
+
frontendServiceGetPriceToBookFees,
|
|
6
|
+
ServicePackageInput,
|
|
7
|
+
} from "../frontendServiceBookingUtils";
|
|
8
|
+
import type { ServiceAttendeeOption } from "../attendeeOptionUtils";
|
|
9
|
+
import type { ServiceRatesAssociationExt } from "../../../extendedSchemas";
|
|
10
|
+
import type { LuxonDateRange } from "../../luxonUtils";
|
|
11
|
+
|
|
12
|
+
function makeRange(startISO: string, endISO: string): LuxonDateRange {
|
|
13
|
+
return {
|
|
14
|
+
start: DateTime.fromISO(startISO, { zone: "utc" }),
|
|
15
|
+
end: DateTime.fromISO(endISO, { zone: "utc" }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeFlatRateAssociation(
|
|
20
|
+
flatRateCents = 20000
|
|
21
|
+
): ServiceRatesAssociationExt {
|
|
22
|
+
return {
|
|
23
|
+
serviceId: "svc-1",
|
|
24
|
+
serviceGeneralRates: {
|
|
25
|
+
flatRateCents,
|
|
26
|
+
hourlyRateCents: null,
|
|
27
|
+
dailyRateCents: null,
|
|
28
|
+
minimumTimeBlockHours: null,
|
|
29
|
+
cleaningFeePerBookingCents: 0,
|
|
30
|
+
},
|
|
31
|
+
serviceSpecialRates: [],
|
|
32
|
+
serviceDailyRates: [],
|
|
33
|
+
} as unknown as ServiceRatesAssociationExt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeTimeBasedAssociation(
|
|
37
|
+
hourlyRateCents = 5000
|
|
38
|
+
): ServiceRatesAssociationExt {
|
|
39
|
+
return {
|
|
40
|
+
serviceId: "svc-1",
|
|
41
|
+
serviceGeneralRates: {
|
|
42
|
+
flatRateCents: null,
|
|
43
|
+
hourlyRateCents,
|
|
44
|
+
dailyRateCents: null,
|
|
45
|
+
minimumTimeBlockHours: null,
|
|
46
|
+
cleaningFeePerBookingCents: 0,
|
|
47
|
+
},
|
|
48
|
+
serviceSpecialRates: [],
|
|
49
|
+
serviceDailyRates: [],
|
|
50
|
+
} as unknown as ServiceRatesAssociationExt;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makePackageInput(
|
|
54
|
+
overrides: Partial<ServicePackageInput> = {}
|
|
55
|
+
): ServicePackageInput {
|
|
56
|
+
return {
|
|
57
|
+
id: "pkg-1",
|
|
58
|
+
name: "Gold DJ Package",
|
|
59
|
+
description: "",
|
|
60
|
+
priceCents: 10000,
|
|
61
|
+
serviceAddons: [],
|
|
62
|
+
chosenQuantity: 1,
|
|
63
|
+
...overrides,
|
|
64
|
+
} as unknown as ServicePackageInput;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ATTENDEE_OPTION = null as unknown as ServiceAttendeeOption;
|
|
68
|
+
const RANGE_2H = makeRange("2025-07-01T09:00:00Z", "2025-07-01T11:00:00Z");
|
|
69
|
+
|
|
70
|
+
describe("frontendServiceGetPriceToBookFees — packages", () => {
|
|
71
|
+
it("returns an empty packages array and adds nothing to the subtotal when no packages are given", () => {
|
|
72
|
+
const result = frontendServiceGetPriceToBookFees(
|
|
73
|
+
makeFlatRateAssociation(20000),
|
|
74
|
+
{
|
|
75
|
+
daysToBookParams: [{ dateTimeRange: RANGE_2H, fees: [], addOns: [] }],
|
|
76
|
+
selectedAttendeeOption: ATTENDEE_OPTION,
|
|
77
|
+
additionalFees: [],
|
|
78
|
+
addOns: [],
|
|
79
|
+
timezone: "UTC",
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(result.packages).toEqual([]);
|
|
84
|
+
expect(result.subtotalBeforeTaxesCents).toBe(20000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("folds a single package's cost into the subtotal", () => {
|
|
88
|
+
const result = frontendServiceGetPriceToBookFees(
|
|
89
|
+
makeFlatRateAssociation(20000),
|
|
90
|
+
{
|
|
91
|
+
daysToBookParams: [{ dateTimeRange: RANGE_2H, fees: [], addOns: [] }],
|
|
92
|
+
selectedAttendeeOption: ATTENDEE_OPTION,
|
|
93
|
+
additionalFees: [],
|
|
94
|
+
addOns: [],
|
|
95
|
+
packages: [makePackageInput({ priceCents: 15000, chosenQuantity: 1 })],
|
|
96
|
+
timezone: "UTC",
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(result.packages).toHaveLength(1);
|
|
101
|
+
expect(result.packages[0]).toMatchObject({
|
|
102
|
+
packageId: "pkg-1",
|
|
103
|
+
chosenQuantity: 1,
|
|
104
|
+
costPQCents: 15000,
|
|
105
|
+
});
|
|
106
|
+
expect(result.subtotalBeforeTaxesCents).toBe(20000 + 15000);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("multiplies a package's cost by chosenQuantity", () => {
|
|
110
|
+
const result = frontendServiceGetPriceToBookFees(
|
|
111
|
+
makeFlatRateAssociation(20000),
|
|
112
|
+
{
|
|
113
|
+
daysToBookParams: [{ dateTimeRange: RANGE_2H, fees: [], addOns: [] }],
|
|
114
|
+
selectedAttendeeOption: ATTENDEE_OPTION,
|
|
115
|
+
additionalFees: [],
|
|
116
|
+
addOns: [],
|
|
117
|
+
packages: [makePackageInput({ priceCents: 15000, chosenQuantity: 3 })],
|
|
118
|
+
timezone: "UTC",
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(result.packages[0].chosenQuantity).toBe(3);
|
|
123
|
+
expect(result.subtotalBeforeTaxesCents).toBe(20000 + 15000 * 3);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("sums multiple distinct packages into the subtotal", () => {
|
|
127
|
+
const result = frontendServiceGetPriceToBookFees(
|
|
128
|
+
makeFlatRateAssociation(20000),
|
|
129
|
+
{
|
|
130
|
+
daysToBookParams: [{ dateTimeRange: RANGE_2H, fees: [], addOns: [] }],
|
|
131
|
+
selectedAttendeeOption: ATTENDEE_OPTION,
|
|
132
|
+
additionalFees: [],
|
|
133
|
+
addOns: [],
|
|
134
|
+
packages: [
|
|
135
|
+
makePackageInput({
|
|
136
|
+
id: "pkg-1",
|
|
137
|
+
priceCents: 15000,
|
|
138
|
+
chosenQuantity: 1,
|
|
139
|
+
}),
|
|
140
|
+
makePackageInput({
|
|
141
|
+
id: "pkg-2",
|
|
142
|
+
priceCents: 8000,
|
|
143
|
+
chosenQuantity: 2,
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
timezone: "UTC",
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(result.packages).toHaveLength(2);
|
|
151
|
+
expect(result.subtotalBeforeTaxesCents).toBe(20000 + 15000 + 8000 * 2);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("defaults chosenQuantity to 0 when not provided", () => {
|
|
155
|
+
const result = frontendServiceGetPriceToBookFees(
|
|
156
|
+
makeFlatRateAssociation(20000),
|
|
157
|
+
{
|
|
158
|
+
daysToBookParams: [{ dateTimeRange: RANGE_2H, fees: [], addOns: [] }],
|
|
159
|
+
selectedAttendeeOption: ATTENDEE_OPTION,
|
|
160
|
+
additionalFees: [],
|
|
161
|
+
addOns: [],
|
|
162
|
+
packages: [
|
|
163
|
+
makePackageInput({ priceCents: 15000, chosenQuantity: undefined }),
|
|
164
|
+
],
|
|
165
|
+
timezone: "UTC",
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(result.packages[0].chosenQuantity).toBe(0);
|
|
170
|
+
expect(result.subtotalBeforeTaxesCents).toBe(20000);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("applies package pricing on top of time-based (hourly) bookings too", () => {
|
|
174
|
+
const result = frontendServiceGetPriceToBookFees(
|
|
175
|
+
makeTimeBasedAssociation(5000),
|
|
176
|
+
{
|
|
177
|
+
daysToBookParams: [{ dateTimeRange: RANGE_2H, fees: [], addOns: [] }],
|
|
178
|
+
selectedAttendeeOption: ATTENDEE_OPTION,
|
|
179
|
+
additionalFees: [],
|
|
180
|
+
addOns: [],
|
|
181
|
+
packages: [makePackageInput({ priceCents: 12000, chosenQuantity: 1 })],
|
|
182
|
+
timezone: "UTC",
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// 2 hours @ $50/hr = $100 in day totals, plus the $120 package.
|
|
187
|
+
expect(result.daysTotalBeforeTaxesCents).toBe(10000);
|
|
188
|
+
expect(result.subtotalBeforeTaxesCents).toBe(10000 + 12000);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("frontendServiceGetPriceToBook — packages", () => {
|
|
193
|
+
it("passes packages through to frontendServiceGetPriceToBookFees", () => {
|
|
194
|
+
const result = frontendServiceGetPriceToBook(makeFlatRateAssociation(20000), {
|
|
195
|
+
addOns: [],
|
|
196
|
+
packages: [makePackageInput({ priceCents: 5000, chosenQuantity: 2 })],
|
|
197
|
+
selectedAttendeeOption: ATTENDEE_OPTION,
|
|
198
|
+
bookingDateTimeRanges: [RANGE_2H],
|
|
199
|
+
timezone: "UTC",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.packages).toHaveLength(1);
|
|
203
|
+
expect(result.subtotalBeforeTaxesCents).toBe(20000 + 5000 * 2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("defaults to an empty packages array when packages is omitted", () => {
|
|
207
|
+
const result = frontendServiceGetPriceToBook(makeFlatRateAssociation(20000), {
|
|
208
|
+
addOns: [],
|
|
209
|
+
selectedAttendeeOption: ATTENDEE_OPTION,
|
|
210
|
+
bookingDateTimeRanges: [RANGE_2H],
|
|
211
|
+
timezone: "UTC",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(result.packages).toEqual([]);
|
|
215
|
+
expect(result.subtotalBeforeTaxesCents).toBe(20000);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ApiServiceBookedDayParams,
|
|
6
6
|
ApiServiceBookedDayParamsLuxon,
|
|
7
7
|
ApiServiceBookingParams,
|
|
8
|
+
ApiServicePackageParams,
|
|
8
9
|
ApiServicePriceToBookResult,
|
|
9
10
|
} from "../../definitions.js";
|
|
10
11
|
import {
|
|
@@ -13,8 +14,10 @@ import {
|
|
|
13
14
|
ServiceBookingDayExt,
|
|
14
15
|
ServiceBookingExt,
|
|
15
16
|
ServiceBookingFeeExt,
|
|
17
|
+
ServiceBookingPackageExt,
|
|
16
18
|
ServiceBookingPriceBreakdownExt,
|
|
17
19
|
ServiceExt,
|
|
20
|
+
ServicePackageExt,
|
|
18
21
|
} from "../../extendedSchemas.js";
|
|
19
22
|
import { dateTimeFromString, dateTimeRangeFromDates } from "../luxonUtils.js";
|
|
20
23
|
import { serviceAttendeeOptions } from "./attendeeOptionUtils.js";
|
|
@@ -22,6 +25,7 @@ import {
|
|
|
22
25
|
FrontendServiceBookingDayInfo,
|
|
23
26
|
FrontendServiceGetPriceToBookResult,
|
|
24
27
|
ServiceAddonInput,
|
|
28
|
+
ServicePackageInput,
|
|
25
29
|
} from "./frontendServiceBookingUtils.js";
|
|
26
30
|
import {
|
|
27
31
|
ServiceBookingPriceBreakdownBaseT,
|
|
@@ -79,6 +83,31 @@ export function apiTransformAddOnsToAddonInputs<
|
|
|
79
83
|
});
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
export function apiTransformPackagesToPackageInputs<
|
|
87
|
+
PackageParams_t extends ApiServicePackageParams
|
|
88
|
+
>(
|
|
89
|
+
packagesApi: PackageParams_t[],
|
|
90
|
+
servicePackages: ServicePackageExt[]
|
|
91
|
+
): ServicePackageInput[] {
|
|
92
|
+
// Create a Map for faster lookup by packageId
|
|
93
|
+
const packageMap = new Map<string, ServicePackageExt>(
|
|
94
|
+
servicePackages.map((pkg) => [pkg.id, pkg])
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return packagesApi.map((packageApi) => {
|
|
98
|
+
const matchedPackage = packageMap.get(packageApi.packageId);
|
|
99
|
+
if (!matchedPackage) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Failed to match package with id: ${packageApi.packageId}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...matchedPackage,
|
|
106
|
+
...packageApi,
|
|
107
|
+
} as ServicePackageInput;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
export function apiTransformBookedDays(
|
|
83
112
|
priceToBook: ApiServicePriceToBookResult
|
|
84
113
|
): Partial<ServiceBookingDayExt>[] {
|
|
@@ -108,6 +137,8 @@ export function apiTransformBookedDays(
|
|
|
108
137
|
startDate: day.dateTimeRange.start.toJSDate(),
|
|
109
138
|
endDate: day.dateTimeRange.end.toJSDate(),
|
|
110
139
|
addOns: addOns,
|
|
140
|
+
// Packages are booking-level bundles (see servicePriceToBookToApiBookingBase),
|
|
141
|
+
// not tied to a specific day, so this stays empty here.
|
|
111
142
|
packages: [],
|
|
112
143
|
fees: day.fees as ServiceBookingFeeExt[],
|
|
113
144
|
priceBreakdown: priceBreakdown,
|
|
@@ -164,6 +195,7 @@ export function serviceGetPriceToBookFromBooking(
|
|
|
164
195
|
flatRateCents: booking.flatRateCents,
|
|
165
196
|
additionalFees: booking.additionalFees,
|
|
166
197
|
addOns: booking.addOns,
|
|
198
|
+
packages: booking.packages,
|
|
167
199
|
daysTotalBeforeTaxesCents: booking.daysTotalATBCents,
|
|
168
200
|
subtotalBeforeTaxesCents: booking.subtotalATBCents,
|
|
169
201
|
totalBeforeTaxesCents: booking.totalATBCents,
|
|
@@ -234,6 +266,18 @@ export function servicePriceToBookToApiDays(
|
|
|
234
266
|
|
|
235
267
|
return bookedDays;
|
|
236
268
|
}
|
|
269
|
+
|
|
270
|
+
export function servicePriceToBookToApiPackages(
|
|
271
|
+
priceToBook: FrontendServiceGetPriceToBookResult
|
|
272
|
+
): ApiServicePackageParams[] {
|
|
273
|
+
return priceToBook.packages.map(
|
|
274
|
+
(pkg): ApiServicePackageParams => ({
|
|
275
|
+
packageId: pkg.packageId,
|
|
276
|
+
chosenQuantity: pkg.chosenQuantity,
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
237
281
|
export interface ServicePriceToBookToApiBookingBaseArgs {
|
|
238
282
|
bookingType: ServiceBookingType;
|
|
239
283
|
status: ServiceBookingStatus;
|
|
@@ -260,6 +304,14 @@ export function servicePriceToBookToApiBookingBase({
|
|
|
260
304
|
costPQCents: addOn.costPQCents,
|
|
261
305
|
})
|
|
262
306
|
);
|
|
307
|
+
const packages = priceToBook.packages.map(
|
|
308
|
+
(pkg): Partial<ServiceBookingPackageExt> => ({
|
|
309
|
+
packageId: pkg.packageId,
|
|
310
|
+
package: pkg.package,
|
|
311
|
+
chosenQuantity: pkg.chosenQuantity,
|
|
312
|
+
costPQCents: pkg.costPQCents,
|
|
313
|
+
})
|
|
314
|
+
);
|
|
263
315
|
|
|
264
316
|
return {
|
|
265
317
|
bookingType: bookingType,
|
|
@@ -273,7 +325,7 @@ export function servicePriceToBookToApiBookingBase({
|
|
|
273
325
|
subtotalATBCents: priceToBook.subtotalBeforeTaxesCents,
|
|
274
326
|
totalATBCents: priceToBook.totalBeforeTaxesCents,
|
|
275
327
|
addOns: addOns as ServiceBookingAddOnExt[],
|
|
276
|
-
packages: [],
|
|
328
|
+
packages: packages as ServiceBookingPackageExt[],
|
|
277
329
|
allowPromiseToPay: allowPromiseToPay,
|
|
278
330
|
isFreeGuest: isFreeGuest,
|
|
279
331
|
// Phase 3 — providers opt in per-booking when they want BP-paid tickets
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ServiceBookingFeeExt,
|
|
11
11
|
ServiceBookingPriceBreakdownExt,
|
|
12
12
|
ServiceDailyRatesExt,
|
|
13
|
+
ServicePackageExt,
|
|
13
14
|
ServiceRatesAssociationExt,
|
|
14
15
|
ServiceSpecialRatesExt,
|
|
15
16
|
} from "../../extendedSchemas.js";
|
|
@@ -31,16 +32,20 @@ import {
|
|
|
31
32
|
ServiceBookingFeeBase,
|
|
32
33
|
ServiceBookingPriceBreakdownBaseT,
|
|
33
34
|
ServiceBookingAddOnBase,
|
|
35
|
+
ServiceBookingPackageBase,
|
|
34
36
|
} from "./serviceBookingTypes.js";
|
|
35
37
|
import {
|
|
36
38
|
calculateStripeProcessingFeeCents,
|
|
37
39
|
convertDollarsToCents,
|
|
38
40
|
} from "../paymentUtils.js";
|
|
39
|
-
|
|
40
41
|
export interface ServiceAddonInput extends ServiceAddon {
|
|
41
42
|
chosenQuantity?: number;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
export interface ServicePackageInput extends ServicePackageExt {
|
|
46
|
+
chosenQuantity?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
export interface FrontendServiceBookingDayInfoParams {
|
|
45
50
|
dateTimeRange: LuxonDateRange;
|
|
46
51
|
fees: ServiceBookingFeeBase[];
|
|
@@ -70,6 +75,7 @@ export interface FrontendServiceGetPriceToBookResult {
|
|
|
70
75
|
flatRateCents?: number; //only valid on flatRate
|
|
71
76
|
|
|
72
77
|
addOns: ServiceBookingAddOnBase[]; //for bookings that do not bill by the time. e.g. flat rate bookings. For time based bookings, addOns are part of the day
|
|
78
|
+
packages: ServiceBookingPackageBase[]; //booking-level bundled packages, independent of hourly/daily/flat rate option
|
|
73
79
|
|
|
74
80
|
daysTotalBeforeTaxesCents: number;
|
|
75
81
|
subtotalBeforeTaxesCents: number; //sum of all daysTotalBeforeTaxesCents, plus addOns/packages
|
|
@@ -197,6 +203,7 @@ export interface FrontendServiceGetPriceToBookFeesParams {
|
|
|
197
203
|
|
|
198
204
|
additionalFees: ServiceBookingFeeBase[];
|
|
199
205
|
addOns: ServiceAddonInput[]; //used for bookings that do not bill by the time. e.g. flat rate bookings
|
|
206
|
+
packages?: ServicePackageInput[]; //booking-level bundled packages, additive regardless of rate option
|
|
200
207
|
|
|
201
208
|
// overlappingBusinessHours: LuxonDateRange[] | null;
|
|
202
209
|
timezone: string | null | undefined;
|
|
@@ -208,6 +215,7 @@ export function frontendServiceGetPriceToBookFees(
|
|
|
208
215
|
daysToBookParams,
|
|
209
216
|
selectedAttendeeOption,
|
|
210
217
|
addOns,
|
|
218
|
+
packages = [],
|
|
211
219
|
additionalFees,
|
|
212
220
|
// overlappingBusinessHours,
|
|
213
221
|
timezone,
|
|
@@ -240,6 +248,19 @@ export function frontendServiceGetPriceToBookFees(
|
|
|
240
248
|
})
|
|
241
249
|
: [];
|
|
242
250
|
|
|
251
|
+
// Packages are booking-level bundled line items (e.g. "Gold DJ Package") and apply
|
|
252
|
+
// on top of the booking regardless of hourly/daily/flat rate option.
|
|
253
|
+
const usedPackages: ServiceBookingPackageBase[] = packages.map(
|
|
254
|
+
(pkg): ServiceBookingPackageBase => {
|
|
255
|
+
return {
|
|
256
|
+
package: pkg,
|
|
257
|
+
packageId: pkg.id,
|
|
258
|
+
chosenQuantity: pkg.chosenQuantity ?? 0,
|
|
259
|
+
costPQCents: pkg.priceCents,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
|
|
243
264
|
const bookingDayResults: FrontendServiceBookingDayInfo[] =
|
|
244
265
|
frontendServiceGetBookingDayInfo({
|
|
245
266
|
rateOption: rateOption,
|
|
@@ -286,7 +307,13 @@ export function frontendServiceGetPriceToBookFees(
|
|
|
286
307
|
0
|
|
287
308
|
);
|
|
288
309
|
|
|
289
|
-
const
|
|
310
|
+
const packagesTotalCents = usedPackages.reduce(
|
|
311
|
+
(sofar, pkg) => sofar + pkg.costPQCents * pkg.chosenQuantity,
|
|
312
|
+
0
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const subtotalBeforeTaxesCents =
|
|
316
|
+
daysTotalBeforeTaxesCents + addOnsTotalCents + packagesTotalCents;
|
|
290
317
|
|
|
291
318
|
if (!hasCustomProcessingFee) {
|
|
292
319
|
// Same pass-through formula as bash ticket/donation checkout (STRIPE_PROCESSING_FEE).
|
|
@@ -319,6 +346,7 @@ export function frontendServiceGetPriceToBookFees(
|
|
|
319
346
|
totalWithFees: totalBeforeTaxesCents, // Same as totalBeforeTaxesCents
|
|
320
347
|
additionalFees: allAdditionalFees,
|
|
321
348
|
addOns: usedAddons,
|
|
349
|
+
packages: usedPackages,
|
|
322
350
|
minimumTimeBlockHours: minimumTimeBlockHours,
|
|
323
351
|
rateOption: rateOption,
|
|
324
352
|
flatRateCents:
|
|
@@ -330,6 +358,7 @@ export function frontendServiceGetPriceToBookFees(
|
|
|
330
358
|
|
|
331
359
|
export interface FrontendServiceGetPriceToBookParams {
|
|
332
360
|
addOns: ServiceAddonInput[];
|
|
361
|
+
packages?: ServicePackageInput[];
|
|
333
362
|
|
|
334
363
|
selectedAttendeeOption: ServiceAttendeeOption;
|
|
335
364
|
|
|
@@ -343,6 +372,7 @@ export function frontendServiceGetPriceToBook(
|
|
|
343
372
|
serviceRatesAssociation: ServiceRatesAssociationExt | null | undefined,
|
|
344
373
|
{
|
|
345
374
|
addOns,
|
|
375
|
+
packages = [],
|
|
346
376
|
selectedAttendeeOption,
|
|
347
377
|
bookingDateTimeRanges, //list of dateTime ranges to book
|
|
348
378
|
// overlappingBusinessHours,
|
|
@@ -380,6 +410,7 @@ export function frontendServiceGetPriceToBook(
|
|
|
380
410
|
selectedAttendeeOption: selectedAttendeeOption,
|
|
381
411
|
additionalFees: [],
|
|
382
412
|
addOns: addOns,
|
|
413
|
+
packages: packages,
|
|
383
414
|
timezone: timezone,
|
|
384
415
|
});
|
|
385
416
|
}
|
|
@@ -2,6 +2,7 @@ import { ServiceBookingType, ServiceBookingStatus } from "@prisma/client";
|
|
|
2
2
|
import {
|
|
3
3
|
ServiceBookingAddOnExt,
|
|
4
4
|
ServiceBookingFeeExt,
|
|
5
|
+
ServiceBookingPackageExt,
|
|
5
6
|
ServiceBookingPriceBreakdownExt,
|
|
6
7
|
ServiceBookingDayExt,
|
|
7
8
|
ServiceDailyRatesExt,
|
|
@@ -21,7 +22,7 @@ export type ServiceBookingAddOnBase = Omit<
|
|
|
21
22
|
| "serviceBooking"
|
|
22
23
|
>;
|
|
23
24
|
export type ServiceBookingPackageBase = Omit<
|
|
24
|
-
|
|
25
|
+
ServiceBookingPackageExt,
|
|
25
26
|
| "id"
|
|
26
27
|
| "serviceBookingDayId"
|
|
27
28
|
| "serviceBookingDay"
|