@wix/headless-bookings 1.0.1 → 1.0.3

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.
@@ -296,9 +296,13 @@ function PaymentData(props) {
296
296
  const { children } = props;
297
297
  const bookingService = (0, services_manager_react_1.useService)(booking_js_1.BookingServiceDefinition);
298
298
  const serviceSelections = bookingService.serviceSelections.get();
299
+ const timezone = bookingService.timezone.get();
299
300
  const pricingServiceSelections = serviceSelections.map((selection) => ({
300
301
  service: selection.service,
301
302
  lineItemId: selection.instanceId,
303
+ localStartDate: selection.timeSlot?.startDate,
304
+ localEndDate: selection.timeSlot?.endDate,
305
+ timeZone: timezone,
302
306
  }));
303
307
  return children({ pricingServiceSelections });
304
308
  }
@@ -48,6 +48,12 @@ export interface PricingServiceSelection {
48
48
  service: Service;
49
49
  /** Optional line item ID. If not provided, one will be generated. */
50
50
  lineItemId?: string;
51
+ /** Local start date of the selected slot in ISO-8601 format (e.g. "2024-01-30T13:30:00"). */
52
+ localStartDate?: string;
53
+ /** Local end date of the selected slot in ISO-8601 format (e.g. "2024-01-30T14:30:00"). */
54
+ localEndDate?: string;
55
+ /** IANA time zone identifier for the selected slot (e.g. "America/New_York"). */
56
+ timeZone?: string;
51
57
  }
52
58
  /**
53
59
  * Public config for PaymentService (SSR mode only):
@@ -10,6 +10,7 @@ exports.loadPaymentConfig = loadPaymentConfig;
10
10
  const services_definitions_1 = require("@wix/services-definitions");
11
11
  const signals_1 = require("@wix/services-definitions/core-services/signals");
12
12
  const auto_sdk_ecom_totals_calculator_1 = require("@wix/auto_sdk_ecom_totals-calculator");
13
+ const auto_sdk_bookings_pricing_1 = require("@wix/auto_sdk_bookings_pricing");
13
14
  const booking_js_1 = require("../booking/booking.js");
14
15
  const auto_sdk_bookings_services_1 = require("@wix/auto_sdk_bookings_services");
15
16
  const payment_def_js_1 = require("./payment.def.js");
@@ -91,13 +92,57 @@ function buildLineItem(service, totalParticipants, lineItemId) {
91
92
  paymentOption,
92
93
  };
93
94
  }
95
+ // durationRange is an @internal field on AvailabilityConstraints (bookings services SDK v1.0.226+).
96
+ // It signals that the service uses flexible duration pricing (hourly/daily rentals),
97
+ // where the correct price must be fetched via previewPrice() rather than the fixed unit price.
98
+ function getDurationRange(service) {
99
+ return service.schedule?.availabilityConstraints?.durationRange;
100
+ }
101
+ async function getPreviewedPrice(service, localStartDate, localEndDate, timeZone, numberOfParticipants) {
102
+ if (!service._id)
103
+ return null;
104
+ const lineItemInput = {
105
+ serviceId: service._id,
106
+ localStartDate,
107
+ localEndDate,
108
+ numberOfParticipants,
109
+ ...(timeZone ? { timeZone } : {}),
110
+ };
111
+ const result = await (0, auto_sdk_bookings_pricing_1.previewPrice)([lineItemInput]);
112
+ return result.priceInfo?.calculatedPrice ?? null;
113
+ }
114
+ async function buildLineItemWithPreview(item, totalParticipants) {
115
+ if (item.localStartDate &&
116
+ item.localEndDate &&
117
+ getDurationRange(item.service)) {
118
+ const previewedPrice = await getPreviewedPrice(item.service, item.localStartDate, item.localEndDate, item.timeZone, totalParticipants).catch(() => null);
119
+ if (previewedPrice != null) {
120
+ const depositValue = item.service.payment?.fixed?.deposit?.value;
121
+ const hasDeposit = !!depositValue && Number(depositValue) > 0;
122
+ const deposit = hasDeposit
123
+ ? Number(depositValue) * totalParticipants
124
+ : undefined;
125
+ return {
126
+ _id: item.lineItemId,
127
+ catalogReference: {
128
+ catalogItemId: item.service._id || '',
129
+ appId: constants_js_1.BOOKING_APP_ID,
130
+ },
131
+ price: String(previewedPrice),
132
+ ...(deposit ? { depositAmount: String(deposit) } : {}),
133
+ quantity: 1,
134
+ paymentOption: getPaymentOption(item.service, hasDeposit),
135
+ };
136
+ }
137
+ }
138
+ return buildLineItem(item.service, totalParticipants, item.lineItemId);
139
+ }
94
140
  async function calculatePaymentDetails(pricingServiceSelections, totalParticipants) {
95
- // Ensure all booking services have a lineItemId
96
141
  const data = pricingServiceSelections.map((selection) => ({
97
142
  ...selection,
98
143
  lineItemId: selection.lineItemId || (0, guid_js_1.generateLineItemId)(),
99
144
  }));
100
- const lineItems = data.map((item) => buildLineItem(item.service, totalParticipants, item.lineItemId));
145
+ const lineItems = await Promise.all(data.map((item) => buildLineItemWithPreview(item, totalParticipants)));
101
146
  const response = await (0, auto_sdk_ecom_totals_calculator_1.calculateTotals)({
102
147
  lineItems,
103
148
  });
@@ -168,22 +213,29 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
168
213
  }
169
214
  // Mode 3: Reactive - observe BookingService signals (client-side only)
170
215
  else if (typeof window !== 'undefined' && bookingService) {
171
- // Computed signal that only changes when services change (not participants)
216
+ // Computed signal that changes when services or selected time slots change (not participants)
172
217
  const servicesKey = signalsService.computed(() => {
173
218
  const selections = bookingService?.serviceSelections.get() || [];
174
- return selections.map((s) => s.service._id).join(',');
219
+ const timezone = bookingService?.timezone.get();
220
+ return (selections
221
+ .map((s) => `${s.service._id}:${s.timeSlot?.startDate ?? ''}:${s.timeSlot?.endDate ?? ''}`)
222
+ .join(',') + `:${timezone ?? ''}`);
175
223
  });
176
224
  signalsService.effect(() => {
177
- // Subscribe to servicesKey to trigger effect only when services change
225
+ // Subscribe to servicesKey to trigger effect when service or time slot changes
178
226
  servicesKey.get();
179
227
  // Read current selections without subscribing (peek)
180
228
  const selections = bookingService?.serviceSelections.peek() || [];
229
+ const timezone = bookingService?.timezone.peek();
181
230
  // For single service mode, take the first selection
182
231
  const firstSelection = selections[0];
183
232
  const numParticipants = firstSelection?.totalParticipants || 1;
184
233
  const pricingServiceSelections = selections.map((selection) => ({
185
234
  service: selection.service,
186
235
  lineItemId: selection.instanceId,
236
+ localStartDate: selection.timeSlot?.startDate,
237
+ localEndDate: selection.timeSlot?.endDate,
238
+ timeZone: timezone,
187
239
  }));
188
240
  if (pricingServiceSelections.length) {
189
241
  serviceOptionsSignal.set(deriveServiceOptions(pricingServiceSelections));
@@ -284,9 +284,13 @@ export function PaymentData(props) {
284
284
  const { children } = props;
285
285
  const bookingService = useService(BookingServiceDefinition);
286
286
  const serviceSelections = bookingService.serviceSelections.get();
287
+ const timezone = bookingService.timezone.get();
287
288
  const pricingServiceSelections = serviceSelections.map((selection) => ({
288
289
  service: selection.service,
289
290
  lineItemId: selection.instanceId,
291
+ localStartDate: selection.timeSlot?.startDate,
292
+ localEndDate: selection.timeSlot?.endDate,
293
+ timeZone: timezone,
290
294
  }));
291
295
  return children({ pricingServiceSelections });
292
296
  }
@@ -48,6 +48,12 @@ export interface PricingServiceSelection {
48
48
  service: Service;
49
49
  /** Optional line item ID. If not provided, one will be generated. */
50
50
  lineItemId?: string;
51
+ /** Local start date of the selected slot in ISO-8601 format (e.g. "2024-01-30T13:30:00"). */
52
+ localStartDate?: string;
53
+ /** Local end date of the selected slot in ISO-8601 format (e.g. "2024-01-30T14:30:00"). */
54
+ localEndDate?: string;
55
+ /** IANA time zone identifier for the selected slot (e.g. "America/New_York"). */
56
+ timeZone?: string;
51
57
  }
52
58
  /**
53
59
  * Public config for PaymentService (SSR mode only):
@@ -6,8 +6,9 @@
6
6
  import { implementService } from '@wix/services-definitions';
7
7
  import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
8
8
  import { calculateTotals, } from '@wix/auto_sdk_ecom_totals-calculator';
9
+ import { previewPrice, } from '@wix/auto_sdk_bookings_pricing';
9
10
  import { BookingServiceDefinition } from '../booking/booking.js';
10
- import { RateType } from '@wix/auto_sdk_bookings_services';
11
+ import { RateType, } from '@wix/auto_sdk_bookings_services';
11
12
  import { PaymentServiceDefinition, } from './payment.def.js';
12
13
  import { generateLineItemId } from '../../utils/guid.js';
13
14
  export { PaymentServiceDefinition, } from './payment.def.js';
@@ -86,13 +87,57 @@ function buildLineItem(service, totalParticipants, lineItemId) {
86
87
  paymentOption,
87
88
  };
88
89
  }
90
+ // durationRange is an @internal field on AvailabilityConstraints (bookings services SDK v1.0.226+).
91
+ // It signals that the service uses flexible duration pricing (hourly/daily rentals),
92
+ // where the correct price must be fetched via previewPrice() rather than the fixed unit price.
93
+ function getDurationRange(service) {
94
+ return service.schedule?.availabilityConstraints?.durationRange;
95
+ }
96
+ async function getPreviewedPrice(service, localStartDate, localEndDate, timeZone, numberOfParticipants) {
97
+ if (!service._id)
98
+ return null;
99
+ const lineItemInput = {
100
+ serviceId: service._id,
101
+ localStartDate,
102
+ localEndDate,
103
+ numberOfParticipants,
104
+ ...(timeZone ? { timeZone } : {}),
105
+ };
106
+ const result = await previewPrice([lineItemInput]);
107
+ return result.priceInfo?.calculatedPrice ?? null;
108
+ }
109
+ async function buildLineItemWithPreview(item, totalParticipants) {
110
+ if (item.localStartDate &&
111
+ item.localEndDate &&
112
+ getDurationRange(item.service)) {
113
+ const previewedPrice = await getPreviewedPrice(item.service, item.localStartDate, item.localEndDate, item.timeZone, totalParticipants).catch(() => null);
114
+ if (previewedPrice != null) {
115
+ const depositValue = item.service.payment?.fixed?.deposit?.value;
116
+ const hasDeposit = !!depositValue && Number(depositValue) > 0;
117
+ const deposit = hasDeposit
118
+ ? Number(depositValue) * totalParticipants
119
+ : undefined;
120
+ return {
121
+ _id: item.lineItemId,
122
+ catalogReference: {
123
+ catalogItemId: item.service._id || '',
124
+ appId: BOOKING_APP_ID,
125
+ },
126
+ price: String(previewedPrice),
127
+ ...(deposit ? { depositAmount: String(deposit) } : {}),
128
+ quantity: 1,
129
+ paymentOption: getPaymentOption(item.service, hasDeposit),
130
+ };
131
+ }
132
+ }
133
+ return buildLineItem(item.service, totalParticipants, item.lineItemId);
134
+ }
89
135
  async function calculatePaymentDetails(pricingServiceSelections, totalParticipants) {
90
- // Ensure all booking services have a lineItemId
91
136
  const data = pricingServiceSelections.map((selection) => ({
92
137
  ...selection,
93
138
  lineItemId: selection.lineItemId || generateLineItemId(),
94
139
  }));
95
- const lineItems = data.map((item) => buildLineItem(item.service, totalParticipants, item.lineItemId));
140
+ const lineItems = await Promise.all(data.map((item) => buildLineItemWithPreview(item, totalParticipants)));
96
141
  const response = await calculateTotals({
97
142
  lineItems,
98
143
  });
@@ -163,22 +208,29 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
163
208
  }
164
209
  // Mode 3: Reactive - observe BookingService signals (client-side only)
165
210
  else if (typeof window !== 'undefined' && bookingService) {
166
- // Computed signal that only changes when services change (not participants)
211
+ // Computed signal that changes when services or selected time slots change (not participants)
167
212
  const servicesKey = signalsService.computed(() => {
168
213
  const selections = bookingService?.serviceSelections.get() || [];
169
- return selections.map((s) => s.service._id).join(',');
214
+ const timezone = bookingService?.timezone.get();
215
+ return (selections
216
+ .map((s) => `${s.service._id}:${s.timeSlot?.startDate ?? ''}:${s.timeSlot?.endDate ?? ''}`)
217
+ .join(',') + `:${timezone ?? ''}`);
170
218
  });
171
219
  signalsService.effect(() => {
172
- // Subscribe to servicesKey to trigger effect only when services change
220
+ // Subscribe to servicesKey to trigger effect when service or time slot changes
173
221
  servicesKey.get();
174
222
  // Read current selections without subscribing (peek)
175
223
  const selections = bookingService?.serviceSelections.peek() || [];
224
+ const timezone = bookingService?.timezone.peek();
176
225
  // For single service mode, take the first selection
177
226
  const firstSelection = selections[0];
178
227
  const numParticipants = firstSelection?.totalParticipants || 1;
179
228
  const pricingServiceSelections = selections.map((selection) => ({
180
229
  service: selection.service,
181
230
  lineItemId: selection.instanceId,
231
+ localStartDate: selection.timeSlot?.startDate,
232
+ localEndDate: selection.timeSlot?.endDate,
233
+ timeZone: timezone,
182
234
  }));
183
235
  if (pricingServiceSelections.length) {
184
236
  serviceOptionsSignal.set(deriveServiceOptions(pricingServiceSelections));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-bookings",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -36,6 +36,7 @@
36
36
  "dependencies": {
37
37
  "@wix/auto_sdk_bookings_bookings": "^1.0.183",
38
38
  "@wix/auto_sdk_bookings_event-time-slots": "^1.0.64",
39
+ "@wix/auto_sdk_bookings_pricing": "^1.0.87",
39
40
  "@wix/auto_sdk_bookings_services": "^1.0.226",
40
41
  "@wix/auto_sdk_ecom_cart-v-2": "^1.0.86",
41
42
  "@wix/forms": "^1.0.384",
@@ -74,5 +75,5 @@
74
75
  "groupId": "com.wixpress.headless-components"
75
76
  }
76
77
  },
77
- "falconPackageHash": "c5f3384150eba7e23d903366ac5a1fc164fc30f5172f6fc4bc2d61d8"
78
+ "falconPackageHash": "60d159ffa08e430dd4c999859e909d281cd9262b93a577989e8de015"
78
79
  }