@wix/headless-bookings 1.0.17 → 1.0.19

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 (43) hide show
  1. package/cjs/dist/__mocks__/booking/booking-client-service.js +4 -1
  2. package/cjs/dist/__mocks__/services/services-mocks.d.ts +13 -0
  3. package/cjs/dist/__mocks__/services/services-mocks.js +92 -1
  4. package/cjs/dist/react/core/booking/Booking.d.ts +6 -0
  5. package/cjs/dist/react/core/booking/Booking.js +2 -0
  6. package/cjs/dist/react/core/payment/Payment.d.ts +2 -1
  7. package/cjs/dist/react/core/payment/Payment.js +11 -8
  8. package/cjs/dist/react/payment/Payment.d.ts +3 -2
  9. package/cjs/dist/react/payment/Payment.js +11 -5
  10. package/cjs/dist/services/booking/book-action/buildBookingRequest.js +13 -4
  11. package/cjs/dist/services/booking/book-action/types.d.ts +6 -0
  12. package/cjs/dist/services/booking/booking.d.ts +17 -0
  13. package/cjs/dist/services/booking/booking.js +8 -0
  14. package/cjs/dist/services/index.d.ts +1 -1
  15. package/cjs/dist/services/payment/payment.d.ts +4 -1
  16. package/cjs/dist/services/payment/payment.def.d.ts +49 -1
  17. package/cjs/dist/services/payment/payment.js +107 -69
  18. package/cjs/dist/services/servicePayment.d.ts +26 -0
  19. package/cjs/dist/services/servicePayment.js +46 -0
  20. package/cjs/dist/utils/money.d.ts +12 -2
  21. package/cjs/dist/utils/money.js +30 -18
  22. package/dist/__mocks__/booking/booking-client-service.js +4 -1
  23. package/dist/__mocks__/services/services-mocks.d.ts +13 -0
  24. package/dist/__mocks__/services/services-mocks.js +91 -0
  25. package/dist/react/core/booking/Booking.d.ts +6 -0
  26. package/dist/react/core/booking/Booking.js +2 -0
  27. package/dist/react/core/payment/Payment.d.ts +2 -1
  28. package/dist/react/core/payment/Payment.js +11 -8
  29. package/dist/react/payment/Payment.d.ts +3 -2
  30. package/dist/react/payment/Payment.js +11 -5
  31. package/dist/services/booking/book-action/buildBookingRequest.js +13 -4
  32. package/dist/services/booking/book-action/types.d.ts +6 -0
  33. package/dist/services/booking/booking.d.ts +17 -0
  34. package/dist/services/booking/booking.js +8 -0
  35. package/dist/services/index.d.ts +1 -1
  36. package/dist/services/payment/payment.d.ts +4 -1
  37. package/dist/services/payment/payment.def.d.ts +49 -1
  38. package/dist/services/payment/payment.js +107 -69
  39. package/dist/services/servicePayment.d.ts +26 -0
  40. package/dist/services/servicePayment.js +40 -0
  41. package/dist/utils/money.d.ts +12 -2
  42. package/dist/utils/money.js +30 -18
  43. package/package.json +4 -4
@@ -10,6 +10,7 @@ import { previewPrice, } from '@wix/auto_sdk_bookings_pricing';
10
10
  import { BookingServiceDefinition } from '../booking/booking.js';
11
11
  import { RateType, } from '@wix/auto_sdk_bookings_services';
12
12
  import { PaymentServiceDefinition, } from './payment.def.js';
13
+ import { getDepositValue, hasDeposit as serviceHasDeposit, isFullUpfrontPaymentAllowed, requiresDeposit, } from '../servicePayment.js';
13
14
  import { generateLineItemId } from '../../utils/guid.js';
14
15
  export { PaymentServiceDefinition, } from './payment.def.js';
15
16
  import { BOOKING_APP_ID } from '../constants.js';
@@ -20,14 +21,13 @@ const DEFAULT_SERVICE_OPTIONS = {
20
21
  isOnline: false,
21
22
  isInPerson: false,
22
23
  isDeposit: false,
24
+ canPayFullUpfront: false,
23
25
  };
24
26
  /**
25
27
  * Derives static service payment options from the first service's configuration.
26
28
  *
27
- * Note on `isDeposit`: this reflects whether the service is *configured* with a
28
- * deposit (i.e. `payment.fixed.deposit.value > 0`), consistent with how
29
- * `buildLineItem` determines `hasDeposit`. Only `FIXED` rate type is checked;
30
- * `VARIED` pricing is not currently supported for deposit detection here.
29
+ * Note on `isDeposit`: reflects whether the service *takes* a deposit, not
30
+ * whether an amount is configured — see `requiresDeposit`.
31
31
  *
32
32
  * This is distinct from the runtime `isDeposit` flag in `CorePayment.Data`, which
33
33
  * is computed dynamically from the ecom API result (`payNow.amount < total.amount`).
@@ -37,12 +37,11 @@ const DEFAULT_SERVICE_OPTIONS = {
37
37
  function deriveServiceOptions(pricingServiceSelections) {
38
38
  const service = pricingServiceSelections[0]?.service;
39
39
  const options = service?.payment?.options;
40
- const depositValue = service?.payment?.fixed?.deposit?.value;
41
- const hasDeposit = !!depositValue && Number(depositValue) > 0;
42
40
  return {
43
41
  isOnline: !!options?.online,
44
42
  isInPerson: !!options?.inPerson,
45
- isDeposit: hasDeposit,
43
+ isDeposit: requiresDeposit(service),
44
+ canPayFullUpfront: isFullUpfrontPaymentAllowed(service),
46
45
  };
47
46
  }
48
47
  function getAdditionalInfo(service, lineItemId) {
@@ -55,52 +54,77 @@ function getAdditionalInfo(service, lineItemId) {
55
54
  /**
56
55
  * Resolves the line item's `paymentOption`.
57
56
  *
58
- * Precedence: explicit consumer choice (`selectedPaymentOption`) wins over the
59
- * service-config heuristic so the preview always agrees with what `book()`
60
- * will send. When unset, the existing heuristic is preserved for backward
61
- * compat.
57
+ * Precedence: an explicit `selectedPaymentOption` wins over the service-config
58
+ * heuristic. `depositSelected` then picks deposit-vs-full within the online
59
+ * branches only — the offline branches ignore it.
60
+ *
61
+ * Note: `online && inPerson` resolves OFFLINE here but ONLINE in
62
+ * `buildBookingRequest`'s heuristic. Pre-existing; the two aren't derived from a
63
+ * shared rule.
62
64
  */
63
- function getPaymentOption(service, hasDeposit, selectedPaymentOption) {
65
+ function getPaymentOption(service, hasDeposit, selectedPaymentOption, depositSelected) {
66
+ // Only an explicit `false` (pay in full) cancels the deposit split. `undefined`
67
+ // means the visitor never chose, and must keep charging the deposit — that is
68
+ // what every deposit service did before this field existed.
69
+ const chargeDeposit = hasDeposit && depositSelected !== false;
64
70
  if (selectedPaymentOption === 'OFFLINE') {
65
71
  return 'FULL_PAYMENT_OFFLINE';
66
72
  }
67
73
  if (selectedPaymentOption === 'ONLINE') {
68
- return hasDeposit ? 'DEPOSIT_ONLINE' : 'FULL_PAYMENT_ONLINE';
74
+ return chargeDeposit ? 'DEPOSIT_ONLINE' : 'FULL_PAYMENT_ONLINE';
69
75
  }
70
76
  const options = service.payment?.options;
71
77
  if (options?.inPerson) {
72
78
  return 'FULL_PAYMENT_OFFLINE';
73
79
  }
74
80
  // If deposit is configured, use DEPOSIT_ONLINE to get correct payNow/payLater split
75
- if (hasDeposit) {
81
+ if (chargeDeposit) {
76
82
  return 'DEPOSIT_ONLINE';
77
83
  }
78
84
  return 'FULL_PAYMENT_ONLINE';
79
85
  }
80
- function buildLineItem(service, totalParticipants, lineItemId, selectedPaymentOption) {
81
- // Get fixed price from service
82
- const servicePrice = service.payment?.fixed?.price?.value;
83
- const price = Number(servicePrice || 0) * totalParticipants;
84
- // Get deposit if configured
85
- const depositValue = service.payment?.fixed?.deposit?.value;
86
- const hasDeposit = !!depositValue && Number(depositValue) > 0;
87
- const deposit = hasDeposit
88
- ? Number(depositValue) * totalParticipants
89
- : undefined;
90
- // Get payment option from service configuration (pass hasDeposit for correct API behavior)
91
- const paymentOption = getPaymentOption(service, hasDeposit, selectedPaymentOption);
86
+ /**
87
+ * Assembles the eCom line item. Both pricing paths go through here so the
88
+ * `depositAmount` rule — eCom rejects it unless the option is `DEPOSIT_ONLINE` —
89
+ * lives in one place.
90
+ */
91
+ function toLineItem(args) {
92
+ const { lineItemId, serviceId, price, deposit, paymentOption } = args;
92
93
  return {
93
94
  _id: lineItemId,
94
95
  catalogReference: {
95
- catalogItemId: service._id || '',
96
+ catalogItemId: serviceId || '',
96
97
  appId: BOOKING_APP_ID,
97
98
  },
98
99
  price: String(price),
99
- ...(deposit ? { depositAmount: String(deposit) } : {}),
100
+ ...(deposit && paymentOption === 'DEPOSIT_ONLINE'
101
+ ? { depositAmount: String(deposit) }
102
+ : {}),
100
103
  quantity: 1,
101
104
  paymentOption,
102
105
  };
103
106
  }
107
+ function buildLineItem(service, totalParticipants, lineItemId, selectedPaymentOption, depositSelected) {
108
+ // VARIED-rate services carry price and deposit under `varied`, not `fixed`.
109
+ const servicePrice = service.payment?.fixed?.price?.value ??
110
+ service.payment?.varied?.defaultPrice?.value;
111
+ const price = Number(servicePrice || 0) * totalParticipants;
112
+ // Charge a deposit only if the service takes one and we know the amount.
113
+ const depositValue = getDepositValue(service);
114
+ const hasDeposit = requiresDeposit(service) && serviceHasDeposit(service);
115
+ const deposit = hasDeposit
116
+ ? Number(depositValue) * totalParticipants
117
+ : undefined;
118
+ // Get payment option from service configuration (pass hasDeposit for correct API behavior)
119
+ const paymentOption = getPaymentOption(service, hasDeposit, selectedPaymentOption, depositSelected);
120
+ return toLineItem({
121
+ lineItemId,
122
+ serviceId: service._id,
123
+ price,
124
+ deposit,
125
+ paymentOption,
126
+ });
127
+ }
104
128
  // durationRange is an @internal field on AvailabilityConstraints (bookings services SDK v1.0.226+).
105
129
  // It signals that the service uses flexible duration pricing (hourly/daily rentals),
106
130
  // where the correct price must be fetched via previewPrice() rather than the fixed unit price.
@@ -110,16 +134,11 @@ function getDurationRange(service) {
110
134
  async function getPreviewedPrice(bookingLineItems) {
111
135
  if (!bookingLineItems.length)
112
136
  return null;
113
- const result = await previewPrice(bookingLineItems);
114
- const price = result.priceInfo?.calculatedPrice;
115
- if (price == null)
116
- return null;
117
- return {
118
- price,
119
- deposit: result.priceInfo?.deposit ?? null,
120
- };
137
+ const { priceInfo } = await previewPrice(bookingLineItems);
138
+ const calculatedPrice = priceInfo?.calculatedPrice;
139
+ return calculatedPrice != null ? { ...priceInfo, calculatedPrice } : null;
121
140
  }
122
- async function buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption) {
141
+ async function buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption, depositSelected) {
123
142
  const slots = item.rangeTimeSlots && item.rangeTimeSlots.length > 0
124
143
  ? item.rangeTimeSlots.map((s) => ({
125
144
  localStartDate: s.startDate,
@@ -133,7 +152,12 @@ async function buildLineItemWithPreview(item, totalParticipants, selectedPayment
133
152
  },
134
153
  ]
135
154
  : [];
136
- if (item.service._id && slots.length && getDurationRange(item.service)) {
155
+ // A deposit with no configured amount is a percentage — only the server knows
156
+ // the figure, and guessing it risks charging something else.
157
+ const needsServerResolvedDeposit = requiresDeposit(item.service) && !serviceHasDeposit(item.service);
158
+ if (item.service._id &&
159
+ slots.length &&
160
+ (getDurationRange(item.service) || needsServerResolvedDeposit)) {
137
161
  const bookingLineItems = slots.map((slot) => ({
138
162
  serviceId: item.service._id,
139
163
  localStartDate: slot.localStartDate,
@@ -141,43 +165,52 @@ async function buildLineItemWithPreview(item, totalParticipants, selectedPayment
141
165
  numberOfParticipants: totalParticipants,
142
166
  ...(item.timeZone ? { timeZone: item.timeZone } : {}),
143
167
  }));
144
- const previewedPrice = await getPreviewedPrice(bookingLineItems).catch(() => null);
145
- if (previewedPrice != null) {
146
- const depositValue = item.service.payment?.fixed?.deposit?.value;
147
- const previewedDeposit = previewedPrice.deposit;
148
- const hasDeposit = previewedDeposit != null
149
- ? previewedDeposit > 0
150
- : !!depositValue && Number(depositValue) > 0;
168
+ const priceInfo = await getPreviewedPrice(bookingLineItems).catch(() => null);
169
+ if (priceInfo != null) {
170
+ const depositValue = getDepositValue(item.service);
171
+ const previewedDeposit = priceInfo.deposit ?? null;
172
+ const hasDeposit = requiresDeposit(item.service) &&
173
+ (previewedDeposit != null
174
+ ? previewedDeposit > 0
175
+ : serviceHasDeposit(item.service));
151
176
  const deposit = hasDeposit
152
177
  ? previewedDeposit != null
153
178
  ? previewedDeposit
154
179
  : Number(depositValue) * totalParticipants
155
180
  : undefined;
156
181
  return {
157
- _id: item.lineItemId,
158
- catalogReference: {
159
- catalogItemId: item.service._id || '',
160
- appId: BOOKING_APP_ID,
161
- },
162
- price: String(previewedPrice.price),
163
- ...(deposit ? { depositAmount: String(deposit) } : {}),
164
- quantity: 1,
165
- paymentOption: getPaymentOption(item.service, hasDeposit, selectedPaymentOption),
182
+ lineItem: toLineItem({
183
+ lineItemId: item.lineItemId,
184
+ serviceId: item.service._id,
185
+ price: priceInfo.calculatedPrice,
186
+ deposit,
187
+ paymentOption: getPaymentOption(item.service, hasDeposit, selectedPaymentOption, depositSelected),
188
+ }),
189
+ priceInfo,
166
190
  };
167
191
  }
168
192
  }
169
- return buildLineItem(item.service, totalParticipants, item.lineItemId, selectedPaymentOption);
193
+ return {
194
+ lineItem: buildLineItem(item.service, totalParticipants, item.lineItemId, selectedPaymentOption, depositSelected),
195
+ };
170
196
  }
171
- async function calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption) {
197
+ async function calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected) {
172
198
  const data = pricingServiceSelections.map((selection) => ({
173
199
  ...selection,
174
200
  lineItemId: selection.lineItemId || generateLineItemId(),
175
201
  }));
176
- const lineItems = await Promise.all(data.map((item) => buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption)));
202
+ const builtLineItems = await Promise.all(data.map(async (item) => {
203
+ const { lineItem, priceInfo } = await buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption, depositSelected);
204
+ const additionalInfo = {
205
+ ...getAdditionalInfo(item.service, item.lineItemId),
206
+ ...(priceInfo ? { priceInfo } : {}),
207
+ };
208
+ return { lineItem, additionalInfo };
209
+ }));
177
210
  const response = await calculateTotals({
178
- lineItems,
211
+ lineItems: builtLineItems.map(({ lineItem }) => lineItem),
179
212
  });
180
- const additionalInfoList = data.map((item) => getAdditionalInfo(item.service, item.lineItemId));
213
+ const additionalInfoList = builtLineItems.map(({ additionalInfo }) => additionalInfo);
181
214
  return {
182
215
  response,
183
216
  additionalInfo: {
@@ -204,7 +237,7 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
204
237
  const errorSignal = signalsService.signal(null);
205
238
  const serviceOptionsSignal = signalsService.signal(config.serviceOptions || DEFAULT_SERVICE_OPTIONS);
206
239
  // Internal calculate function
207
- const calculate = async (pricingServiceSelections, totalParticipants, selectedPaymentOption) => {
240
+ const calculate = async (pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected) => {
208
241
  if (!pricingServiceSelections.length) {
209
242
  paymentDetailsSignal.set(null);
210
243
  return;
@@ -212,7 +245,7 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
212
245
  isLoadingSignal.set(true);
213
246
  errorSignal.set(null);
214
247
  try {
215
- const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption);
248
+ const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected);
216
249
  paymentDetailsSignal.set(paymentDetails);
217
250
  }
218
251
  catch (error) {
@@ -240,16 +273,17 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
240
273
  // Mode 2: Lazy loading - pricingServiceSelections provided via internal config
241
274
  else if (config.pricingServiceSelections?.length) {
242
275
  serviceOptionsSignal.set(deriveServiceOptions(config.pricingServiceSelections));
243
- void calculate(config.pricingServiceSelections, config.totalParticipants ?? 1, config.selectedPaymentOption);
276
+ void calculate(config.pricingServiceSelections, config.totalParticipants ?? 1, config.selectedPaymentOption, config.depositSelected);
244
277
  }
245
278
  // Mode 3: Reactive - observe BookingService signals (client-side only)
246
279
  else if (typeof window !== 'undefined' && bookingService) {
247
- // Computed signal that changes when services, selected time slots, or
248
- // the consumer-selected payment option change (not participants).
280
+ // Computed signal that changes when services, selected time slots, or the
281
+ // consumer's payment choices change (not participants).
249
282
  const servicesKey = signalsService.computed(() => {
250
283
  const selections = bookingService?.serviceSelections.get() || [];
251
284
  const timezone = bookingService?.timezone.get();
252
285
  const selectedPaymentOption = bookingService?.selectedPaymentOption.get();
286
+ const depositSelected = bookingService?.depositSelected.get();
253
287
  return JSON.stringify({
254
288
  selections: selections.map((s) => ({
255
289
  serviceId: s.service._id,
@@ -259,16 +293,18 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
259
293
  })),
260
294
  timezone,
261
295
  selectedPaymentOption,
296
+ depositSelected,
262
297
  });
263
298
  });
264
299
  signalsService.effect(() => {
265
- // Subscribe to servicesKey to trigger effect when service, time
266
- // slot, or selected payment option changes.
300
+ // Subscribe to servicesKey to trigger effect when service, time slot,
301
+ // or payment choices change.
267
302
  servicesKey.get();
268
303
  // Read current selections without subscribing (peek)
269
304
  const selections = bookingService?.serviceSelections.peek() || [];
270
305
  const timezone = bookingService?.timezone.peek();
271
306
  const selectedPaymentOption = bookingService?.selectedPaymentOption.peek();
307
+ const depositSelected = bookingService?.depositSelected.peek();
272
308
  // For single service mode, take the first selection
273
309
  const firstSelection = selections[0];
274
310
  const numParticipants = firstSelection?.totalParticipants || 1;
@@ -283,7 +319,7 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
283
319
  if (pricingServiceSelections.length) {
284
320
  serviceOptionsSignal.set(deriveServiceOptions(pricingServiceSelections));
285
321
  }
286
- void calculate(pricingServiceSelections, numParticipants, selectedPaymentOption);
322
+ void calculate(pricingServiceSelections, numParticipants, selectedPaymentOption, depositSelected);
287
323
  });
288
324
  }
289
325
  return {
@@ -301,10 +337,12 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
301
337
  * @param params.totalParticipants - Number of participants (default: 1)
302
338
  * @param params.selectedPaymentOption - Optional consumer-selected payment option;
303
339
  * overrides the service-config heuristic on each line item's `paymentOption`.
340
+ * @param params.depositSelected - Optional deposit-vs-full-upfront choice; `false`
341
+ * resolves a deposit service to full payment. Omit to keep the deposit default.
304
342
  */
305
343
  export async function loadPaymentConfig(params) {
306
344
  try {
307
- const { pricingServiceSelections, totalParticipants = 1, selectedPaymentOption, } = params;
345
+ const { pricingServiceSelections, totalParticipants = 1, selectedPaymentOption, depositSelected, } = params;
308
346
  if (!pricingServiceSelections.length) {
309
347
  return {
310
348
  type: 'success',
@@ -314,7 +352,7 @@ export async function loadPaymentConfig(params) {
314
352
  },
315
353
  };
316
354
  }
317
- const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption);
355
+ const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected);
318
356
  return {
319
357
  type: 'success',
320
358
  config: {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared helpers for reading payment configuration off a Service entity.
3
+ *
4
+ * `payment.fixed` / `varied` / `custom` are a `@oneof` in the services SDK, so
5
+ * only one is ever populated — reading `fixed` then `varied` covers both FIXED
6
+ * and VARIED rate types.
7
+ *
8
+ * A percentage deposit stores no amount anywhere on the entity; the server
9
+ * resolves it when the customer books. Hence the split between
10
+ * {@link requiresDeposit} ("takes a deposit?") and {@link hasDeposit} ("has an
11
+ * amount?") — a percentage deposit answers `true` and `false` respectively.
12
+ *
13
+ * @module Services/ServicePayment
14
+ */
15
+ import type { Service } from '@wix/auto_sdk_bookings_services';
16
+ /**
17
+ * Whether the service takes a deposit. An explicit `options.deposit` wins; when
18
+ * the flag is absent a configured amount still counts, so a service that sets one
19
+ * without the flag keeps reporting a deposit.
20
+ */
21
+ export declare function requiresDeposit(service?: Service): boolean;
22
+ /** The deposit amount, if one is configured as a fixed amount. */
23
+ export declare function getDepositValue(service?: Service): string | undefined;
24
+ export declare function hasDeposit(service?: Service): boolean;
25
+ /** Meaningful only alongside `hasDeposit`. */
26
+ export declare function isFullUpfrontPaymentAllowed(service?: Service): boolean;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared helpers for reading payment configuration off a Service entity.
3
+ *
4
+ * `payment.fixed` / `varied` / `custom` are a `@oneof` in the services SDK, so
5
+ * only one is ever populated — reading `fixed` then `varied` covers both FIXED
6
+ * and VARIED rate types.
7
+ *
8
+ * A percentage deposit stores no amount anywhere on the entity; the server
9
+ * resolves it when the customer books. Hence the split between
10
+ * {@link requiresDeposit} ("takes a deposit?") and {@link hasDeposit} ("has an
11
+ * amount?") — a percentage deposit answers `true` and `false` respectively.
12
+ *
13
+ * @module Services/ServicePayment
14
+ */
15
+ /**
16
+ * Whether the service takes a deposit. An explicit `options.deposit` wins; when
17
+ * the flag is absent a configured amount still counts, so a service that sets one
18
+ * without the flag keeps reporting a deposit.
19
+ */
20
+ export function requiresDeposit(service) {
21
+ return service?.payment?.options?.deposit ?? hasDeposit(service);
22
+ }
23
+ /** The deposit amount, if one is configured as a fixed amount. */
24
+ export function getDepositValue(service) {
25
+ const payment = service?.payment;
26
+ return (payment?.fixed?.deposit?.value ??
27
+ payment?.varied?.deposit?.value ??
28
+ payment?.fixed?.depositDetails?.amount?.value ??
29
+ payment?.varied?.depositDetails?.amount?.value);
30
+ }
31
+ export function hasDeposit(service) {
32
+ const value = getDepositValue(service);
33
+ return !!value && Number(value) > 0;
34
+ }
35
+ /** Meaningful only alongside `hasDeposit`. */
36
+ export function isFullUpfrontPaymentAllowed(service) {
37
+ const payment = service?.payment;
38
+ return !!(payment?.fixed?.fullUpfrontPaymentAllowed ??
39
+ payment?.varied?.fullUpfrontPaymentAllowed);
40
+ }
@@ -5,13 +5,19 @@ import type { MoneyData } from '@wix/headless-components/react';
5
5
  /**
6
6
  * Converts SDK Money format to MoneyData format
7
7
  *
8
+ * The original SDK value is validated as a whole (values that are not fully
9
+ * numeric and finite, like `"12.34xyz"`, return `null`) but kept as-is, so
10
+ * string amounts reach `Money` with full decimal precision instead of being
11
+ * routed through floating point. Compare amounts numerically with
12
+ * `Number(money.amount)`.
13
+ *
8
14
  * @param sdkMoney - The SDK money object with value, currency, and optional formattedValue
9
15
  * @returns MoneyData object with amount and currency, or null if conversion fails
10
16
  *
11
17
  * @example
12
18
  * ```ts
13
19
  * const money = convertToMoneyData({ value: "25.50", currency: "USD" });
14
- * // Returns: { amount: 25.50, currency: "USD" }
20
+ * // Returns: { amount: "25.50", currency: "USD" }
15
21
  * ```
16
22
  */
17
23
  export declare function convertToMoneyData(sdkMoney: {
@@ -21,7 +27,11 @@ export declare function convertToMoneyData(sdkMoney: {
21
27
  } | undefined): MoneyData | null;
22
28
  /**
23
29
  * Converts ecom MultiCurrencyPrice format to MoneyData format
24
- * MultiCurrencyPrice uses 'amount' as string, while MoneyData uses 'amount' as number
30
+ *
31
+ * The string amount is validated as a whole (values that are not fully
32
+ * numeric and finite, like `"12.34xyz"`, return `null`) but kept as-is, so
33
+ * it reaches `Money` with full decimal precision. Compare amounts
34
+ * numerically with `Number(money.amount)`.
25
35
  *
26
36
  * @param price - The MultiCurrencyPrice object with amount as string
27
37
  * @param currency - The currency code (from response level)
@@ -1,16 +1,31 @@
1
1
  /**
2
2
  * Money utility functions for converting between SDK Money types and MoneyData
3
3
  */
4
+ // Number() (unlike parseFloat) rejects strings with trailing garbage like
5
+ // "12.34xyz", but treats empty/whitespace-only strings as 0 — reject those
6
+ // separately so invalid values return null instead of reaching the formatter.
7
+ function toNumericAmount(value) {
8
+ if (typeof value === 'string') {
9
+ return value.trim() === '' ? NaN : Number(value);
10
+ }
11
+ return value;
12
+ }
4
13
  /**
5
14
  * Converts SDK Money format to MoneyData format
6
15
  *
16
+ * The original SDK value is validated as a whole (values that are not fully
17
+ * numeric and finite, like `"12.34xyz"`, return `null`) but kept as-is, so
18
+ * string amounts reach `Money` with full decimal precision instead of being
19
+ * routed through floating point. Compare amounts numerically with
20
+ * `Number(money.amount)`.
21
+ *
7
22
  * @param sdkMoney - The SDK money object with value, currency, and optional formattedValue
8
23
  * @returns MoneyData object with amount and currency, or null if conversion fails
9
24
  *
10
25
  * @example
11
26
  * ```ts
12
27
  * const money = convertToMoneyData({ value: "25.50", currency: "USD" });
13
- * // Returns: { amount: 25.50, currency: "USD" }
28
+ * // Returns: { amount: "25.50", currency: "USD" }
14
29
  * ```
15
30
  */
16
31
  export function convertToMoneyData(sdkMoney) {
@@ -18,23 +33,20 @@ export function convertToMoneyData(sdkMoney) {
18
33
  return null;
19
34
  const priceValue = sdkMoney.value;
20
35
  const currency = sdkMoney.currency || 'USD';
21
- // Convert price value to number
22
- let amount = undefined;
23
- if (typeof priceValue === 'string') {
24
- const parsed = parseFloat(priceValue);
25
- amount = !isNaN(parsed) ? parsed : undefined;
26
- }
27
- else if (typeof priceValue === 'number') {
28
- amount = priceValue;
29
- }
30
- if (amount !== undefined && !isNaN(amount) && amount >= 0) {
31
- return { amount, currency };
32
- }
33
- return null;
36
+ if (priceValue === undefined)
37
+ return null;
38
+ const parsed = toNumericAmount(priceValue);
39
+ if (!Number.isFinite(parsed) || parsed < 0)
40
+ return null;
41
+ return { amount: priceValue, currency };
34
42
  }
35
43
  /**
36
44
  * Converts ecom MultiCurrencyPrice format to MoneyData format
37
- * MultiCurrencyPrice uses 'amount' as string, while MoneyData uses 'amount' as number
45
+ *
46
+ * The string amount is validated as a whole (values that are not fully
47
+ * numeric and finite, like `"12.34xyz"`, return `null`) but kept as-is, so
48
+ * it reaches `Money` with full decimal precision. Compare amounts
49
+ * numerically with `Number(money.amount)`.
38
50
  *
39
51
  * @param price - The MultiCurrencyPrice object with amount as string
40
52
  * @param currency - The currency code (from response level)
@@ -43,8 +55,8 @@ export function convertToMoneyData(sdkMoney) {
43
55
  export function convertMultiCurrencyPriceToMoneyData(price, currency) {
44
56
  if (!price?.amount)
45
57
  return null;
46
- const parsed = parseFloat(price.amount);
47
- if (isNaN(parsed) || parsed < 0)
58
+ const parsed = toNumericAmount(price.amount);
59
+ if (!Number.isFinite(parsed) || parsed < 0)
48
60
  return null;
49
- return { amount: parsed, currency };
61
+ return { amount: price.amount, currency };
50
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-bookings",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -41,7 +41,7 @@
41
41
  "@wix/auto_sdk_bookings_catalog-search": "^1.0.2",
42
42
  "@wix/auto_sdk_bookings_event-time-slots": "^1.0.112",
43
43
  "@wix/auto_sdk_bookings_pricing": "^1.0.87",
44
- "@wix/auto_sdk_bookings_services": "^1.0.244",
44
+ "@wix/auto_sdk_bookings_services": "^1.0.265",
45
45
  "@wix/auto_sdk_ecom_cart-v-2": "^1.0.86",
46
46
  "@wix/forms": "^1.0.384",
47
47
  "@wix/headless-media": "0.0.25",
@@ -78,7 +78,7 @@
78
78
  "vitest": "^3.1.4"
79
79
  },
80
80
  "peerDependencies": {
81
- "@wix/headless-components": "^0.0.0",
81
+ "@wix/headless-components": "~0.0.47",
82
82
  "react": ">=16.3.0"
83
83
  },
84
84
  "publishConfig": {
@@ -91,5 +91,5 @@
91
91
  "groupId": "com.wixpress.headless-components"
92
92
  }
93
93
  },
94
- "falconPackageHash": "e0eb894796285c23383284399c90c831b1dc3584451932add86c9b21"
94
+ "falconPackageHash": "025aaf3ecf64a7ef23fcc40e8cad3993079c8c4a92038520589a77a2"
95
95
  }