@wix/headless-bookings 1.0.16 → 1.0.18

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 (45) 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/core/service/Service.js +4 -2
  9. package/cjs/dist/react/payment/Payment.d.ts +3 -2
  10. package/cjs/dist/react/payment/Payment.js +11 -5
  11. package/cjs/dist/services/booking/book-action/buildBookingRequest.js +13 -4
  12. package/cjs/dist/services/booking/book-action/types.d.ts +6 -0
  13. package/cjs/dist/services/booking/booking.d.ts +17 -0
  14. package/cjs/dist/services/booking/booking.js +8 -0
  15. package/cjs/dist/services/payment/payment.d.ts +3 -0
  16. package/cjs/dist/services/payment/payment.def.d.ts +15 -1
  17. package/cjs/dist/services/payment/payment.js +89 -58
  18. package/cjs/dist/services/service/service.js +1 -1
  19. package/cjs/dist/services/servicePayment.d.ts +26 -0
  20. package/cjs/dist/services/servicePayment.js +46 -0
  21. package/cjs/dist/utils/money.d.ts +12 -2
  22. package/cjs/dist/utils/money.js +30 -18
  23. package/dist/__mocks__/booking/booking-client-service.js +4 -1
  24. package/dist/__mocks__/services/services-mocks.d.ts +13 -0
  25. package/dist/__mocks__/services/services-mocks.js +91 -0
  26. package/dist/react/core/booking/Booking.d.ts +6 -0
  27. package/dist/react/core/booking/Booking.js +2 -0
  28. package/dist/react/core/payment/Payment.d.ts +2 -1
  29. package/dist/react/core/payment/Payment.js +11 -8
  30. package/dist/react/core/service/Service.js +4 -2
  31. package/dist/react/payment/Payment.d.ts +3 -2
  32. package/dist/react/payment/Payment.js +11 -5
  33. package/dist/services/booking/book-action/buildBookingRequest.js +13 -4
  34. package/dist/services/booking/book-action/types.d.ts +6 -0
  35. package/dist/services/booking/booking.d.ts +17 -0
  36. package/dist/services/booking/booking.js +8 -0
  37. package/dist/services/payment/payment.d.ts +3 -0
  38. package/dist/services/payment/payment.def.d.ts +15 -1
  39. package/dist/services/payment/payment.js +89 -58
  40. package/dist/services/service/service.js +1 -1
  41. package/dist/services/servicePayment.d.ts +26 -0
  42. package/dist/services/servicePayment.js +40 -0
  43. package/dist/utils/money.d.ts +12 -2
  44. package/dist/utils/money.js +30 -18
  45. package/package.json +3 -3
@@ -14,6 +14,7 @@ const auto_sdk_bookings_pricing_1 = require("@wix/auto_sdk_bookings_pricing");
14
14
  const booking_js_1 = require("../booking/booking.js");
15
15
  const auto_sdk_bookings_services_1 = require("@wix/auto_sdk_bookings_services");
16
16
  const payment_def_js_1 = require("./payment.def.js");
17
+ const servicePayment_js_1 = require("../servicePayment.js");
17
18
  const guid_js_1 = require("../../utils/guid.js");
18
19
  var payment_def_js_2 = require("./payment.def.js");
19
20
  Object.defineProperty(exports, "PaymentServiceDefinition", { enumerable: true, get: function () { return payment_def_js_2.PaymentServiceDefinition; } });
@@ -25,14 +26,13 @@ const DEFAULT_SERVICE_OPTIONS = {
25
26
  isOnline: false,
26
27
  isInPerson: false,
27
28
  isDeposit: false,
29
+ canPayFullUpfront: false,
28
30
  };
29
31
  /**
30
32
  * Derives static service payment options from the first service's configuration.
31
33
  *
32
- * Note on `isDeposit`: this reflects whether the service is *configured* with a
33
- * deposit (i.e. `payment.fixed.deposit.value > 0`), consistent with how
34
- * `buildLineItem` determines `hasDeposit`. Only `FIXED` rate type is checked;
35
- * `VARIED` pricing is not currently supported for deposit detection here.
34
+ * Note on `isDeposit`: reflects whether the service *takes* a deposit, not
35
+ * whether an amount is configured — see `requiresDeposit`.
36
36
  *
37
37
  * This is distinct from the runtime `isDeposit` flag in `CorePayment.Data`, which
38
38
  * is computed dynamically from the ecom API result (`payNow.amount < total.amount`).
@@ -42,12 +42,11 @@ const DEFAULT_SERVICE_OPTIONS = {
42
42
  function deriveServiceOptions(pricingServiceSelections) {
43
43
  const service = pricingServiceSelections[0]?.service;
44
44
  const options = service?.payment?.options;
45
- const depositValue = service?.payment?.fixed?.deposit?.value;
46
- const hasDeposit = !!depositValue && Number(depositValue) > 0;
47
45
  return {
48
46
  isOnline: !!options?.online,
49
47
  isInPerson: !!options?.inPerson,
50
- isDeposit: hasDeposit,
48
+ isDeposit: (0, servicePayment_js_1.requiresDeposit)(service),
49
+ canPayFullUpfront: (0, servicePayment_js_1.isFullUpfrontPaymentAllowed)(service),
51
50
  };
52
51
  }
53
52
  function getAdditionalInfo(service, lineItemId) {
@@ -60,52 +59,77 @@ function getAdditionalInfo(service, lineItemId) {
60
59
  /**
61
60
  * Resolves the line item's `paymentOption`.
62
61
  *
63
- * Precedence: explicit consumer choice (`selectedPaymentOption`) wins over the
64
- * service-config heuristic so the preview always agrees with what `book()`
65
- * will send. When unset, the existing heuristic is preserved for backward
66
- * compat.
62
+ * Precedence: an explicit `selectedPaymentOption` wins over the service-config
63
+ * heuristic. `depositSelected` then picks deposit-vs-full within the online
64
+ * branches only — the offline branches ignore it.
65
+ *
66
+ * Note: `online && inPerson` resolves OFFLINE here but ONLINE in
67
+ * `buildBookingRequest`'s heuristic. Pre-existing; the two aren't derived from a
68
+ * shared rule.
67
69
  */
68
- function getPaymentOption(service, hasDeposit, selectedPaymentOption) {
70
+ function getPaymentOption(service, hasDeposit, selectedPaymentOption, depositSelected) {
71
+ // Only an explicit `false` (pay in full) cancels the deposit split. `undefined`
72
+ // means the visitor never chose, and must keep charging the deposit — that is
73
+ // what every deposit service did before this field existed.
74
+ const chargeDeposit = hasDeposit && depositSelected !== false;
69
75
  if (selectedPaymentOption === 'OFFLINE') {
70
76
  return 'FULL_PAYMENT_OFFLINE';
71
77
  }
72
78
  if (selectedPaymentOption === 'ONLINE') {
73
- return hasDeposit ? 'DEPOSIT_ONLINE' : 'FULL_PAYMENT_ONLINE';
79
+ return chargeDeposit ? 'DEPOSIT_ONLINE' : 'FULL_PAYMENT_ONLINE';
74
80
  }
75
81
  const options = service.payment?.options;
76
82
  if (options?.inPerson) {
77
83
  return 'FULL_PAYMENT_OFFLINE';
78
84
  }
79
85
  // If deposit is configured, use DEPOSIT_ONLINE to get correct payNow/payLater split
80
- if (hasDeposit) {
86
+ if (chargeDeposit) {
81
87
  return 'DEPOSIT_ONLINE';
82
88
  }
83
89
  return 'FULL_PAYMENT_ONLINE';
84
90
  }
85
- function buildLineItem(service, totalParticipants, lineItemId, selectedPaymentOption) {
86
- // Get fixed price from service
87
- const servicePrice = service.payment?.fixed?.price?.value;
88
- const price = Number(servicePrice || 0) * totalParticipants;
89
- // Get deposit if configured
90
- const depositValue = service.payment?.fixed?.deposit?.value;
91
- const hasDeposit = !!depositValue && Number(depositValue) > 0;
92
- const deposit = hasDeposit
93
- ? Number(depositValue) * totalParticipants
94
- : undefined;
95
- // Get payment option from service configuration (pass hasDeposit for correct API behavior)
96
- const paymentOption = getPaymentOption(service, hasDeposit, selectedPaymentOption);
91
+ /**
92
+ * Assembles the eCom line item. Both pricing paths go through here so the
93
+ * `depositAmount` rule — eCom rejects it unless the option is `DEPOSIT_ONLINE` —
94
+ * lives in one place.
95
+ */
96
+ function toLineItem(args) {
97
+ const { lineItemId, serviceId, price, deposit, paymentOption } = args;
97
98
  return {
98
99
  _id: lineItemId,
99
100
  catalogReference: {
100
- catalogItemId: service._id || '',
101
+ catalogItemId: serviceId || '',
101
102
  appId: constants_js_1.BOOKING_APP_ID,
102
103
  },
103
104
  price: String(price),
104
- ...(deposit ? { depositAmount: String(deposit) } : {}),
105
+ ...(deposit && paymentOption === 'DEPOSIT_ONLINE'
106
+ ? { depositAmount: String(deposit) }
107
+ : {}),
105
108
  quantity: 1,
106
109
  paymentOption,
107
110
  };
108
111
  }
112
+ function buildLineItem(service, totalParticipants, lineItemId, selectedPaymentOption, depositSelected) {
113
+ // VARIED-rate services carry price and deposit under `varied`, not `fixed`.
114
+ const servicePrice = service.payment?.fixed?.price?.value ??
115
+ service.payment?.varied?.defaultPrice?.value;
116
+ const price = Number(servicePrice || 0) * totalParticipants;
117
+ // Charge a deposit only if the service takes one and we know the amount.
118
+ const depositValue = (0, servicePayment_js_1.getDepositValue)(service);
119
+ const hasDeposit = (0, servicePayment_js_1.requiresDeposit)(service) && (0, servicePayment_js_1.hasDeposit)(service);
120
+ const deposit = hasDeposit
121
+ ? Number(depositValue) * totalParticipants
122
+ : undefined;
123
+ // Get payment option from service configuration (pass hasDeposit for correct API behavior)
124
+ const paymentOption = getPaymentOption(service, hasDeposit, selectedPaymentOption, depositSelected);
125
+ return toLineItem({
126
+ lineItemId,
127
+ serviceId: service._id,
128
+ price,
129
+ deposit,
130
+ paymentOption,
131
+ });
132
+ }
109
133
  // durationRange is an @internal field on AvailabilityConstraints (bookings services SDK v1.0.226+).
110
134
  // It signals that the service uses flexible duration pricing (hourly/daily rentals),
111
135
  // where the correct price must be fetched via previewPrice() rather than the fixed unit price.
@@ -124,7 +148,7 @@ async function getPreviewedPrice(bookingLineItems) {
124
148
  deposit: result.priceInfo?.deposit ?? null,
125
149
  };
126
150
  }
127
- async function buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption) {
151
+ async function buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption, depositSelected) {
128
152
  const slots = item.rangeTimeSlots && item.rangeTimeSlots.length > 0
129
153
  ? item.rangeTimeSlots.map((s) => ({
130
154
  localStartDate: s.startDate,
@@ -138,7 +162,12 @@ async function buildLineItemWithPreview(item, totalParticipants, selectedPayment
138
162
  },
139
163
  ]
140
164
  : [];
141
- if (item.service._id && slots.length && getDurationRange(item.service)) {
165
+ // A deposit with no configured amount is a percentage — only the server knows
166
+ // the figure, and guessing it risks charging something else.
167
+ const needsServerResolvedDeposit = (0, servicePayment_js_1.requiresDeposit)(item.service) && !(0, servicePayment_js_1.hasDeposit)(item.service);
168
+ if (item.service._id &&
169
+ slots.length &&
170
+ (getDurationRange(item.service) || needsServerResolvedDeposit)) {
142
171
  const bookingLineItems = slots.map((slot) => ({
143
172
  serviceId: item.service._id,
144
173
  localStartDate: slot.localStartDate,
@@ -148,37 +177,34 @@ async function buildLineItemWithPreview(item, totalParticipants, selectedPayment
148
177
  }));
149
178
  const previewedPrice = await getPreviewedPrice(bookingLineItems).catch(() => null);
150
179
  if (previewedPrice != null) {
151
- const depositValue = item.service.payment?.fixed?.deposit?.value;
180
+ const depositValue = (0, servicePayment_js_1.getDepositValue)(item.service);
152
181
  const previewedDeposit = previewedPrice.deposit;
153
- const hasDeposit = previewedDeposit != null
154
- ? previewedDeposit > 0
155
- : !!depositValue && Number(depositValue) > 0;
182
+ const hasDeposit = (0, servicePayment_js_1.requiresDeposit)(item.service) &&
183
+ (previewedDeposit != null
184
+ ? previewedDeposit > 0
185
+ : (0, servicePayment_js_1.hasDeposit)(item.service));
156
186
  const deposit = hasDeposit
157
187
  ? previewedDeposit != null
158
188
  ? previewedDeposit
159
189
  : Number(depositValue) * totalParticipants
160
190
  : undefined;
161
- return {
162
- _id: item.lineItemId,
163
- catalogReference: {
164
- catalogItemId: item.service._id || '',
165
- appId: constants_js_1.BOOKING_APP_ID,
166
- },
167
- price: String(previewedPrice.price),
168
- ...(deposit ? { depositAmount: String(deposit) } : {}),
169
- quantity: 1,
170
- paymentOption: getPaymentOption(item.service, hasDeposit, selectedPaymentOption),
171
- };
191
+ return toLineItem({
192
+ lineItemId: item.lineItemId,
193
+ serviceId: item.service._id,
194
+ price: previewedPrice.price,
195
+ deposit,
196
+ paymentOption: getPaymentOption(item.service, hasDeposit, selectedPaymentOption, depositSelected),
197
+ });
172
198
  }
173
199
  }
174
- return buildLineItem(item.service, totalParticipants, item.lineItemId, selectedPaymentOption);
200
+ return buildLineItem(item.service, totalParticipants, item.lineItemId, selectedPaymentOption, depositSelected);
175
201
  }
176
- async function calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption) {
202
+ async function calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected) {
177
203
  const data = pricingServiceSelections.map((selection) => ({
178
204
  ...selection,
179
205
  lineItemId: selection.lineItemId || (0, guid_js_1.generateLineItemId)(),
180
206
  }));
181
- const lineItems = await Promise.all(data.map((item) => buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption)));
207
+ const lineItems = await Promise.all(data.map((item) => buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption, depositSelected)));
182
208
  const response = await (0, auto_sdk_ecom_totals_calculator_1.calculateTotals)({
183
209
  lineItems,
184
210
  });
@@ -209,7 +235,7 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
209
235
  const errorSignal = signalsService.signal(null);
210
236
  const serviceOptionsSignal = signalsService.signal(config.serviceOptions || DEFAULT_SERVICE_OPTIONS);
211
237
  // Internal calculate function
212
- const calculate = async (pricingServiceSelections, totalParticipants, selectedPaymentOption) => {
238
+ const calculate = async (pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected) => {
213
239
  if (!pricingServiceSelections.length) {
214
240
  paymentDetailsSignal.set(null);
215
241
  return;
@@ -217,7 +243,7 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
217
243
  isLoadingSignal.set(true);
218
244
  errorSignal.set(null);
219
245
  try {
220
- const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption);
246
+ const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected);
221
247
  paymentDetailsSignal.set(paymentDetails);
222
248
  }
223
249
  catch (error) {
@@ -245,16 +271,17 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
245
271
  // Mode 2: Lazy loading - pricingServiceSelections provided via internal config
246
272
  else if (config.pricingServiceSelections?.length) {
247
273
  serviceOptionsSignal.set(deriveServiceOptions(config.pricingServiceSelections));
248
- void calculate(config.pricingServiceSelections, config.totalParticipants ?? 1, config.selectedPaymentOption);
274
+ void calculate(config.pricingServiceSelections, config.totalParticipants ?? 1, config.selectedPaymentOption, config.depositSelected);
249
275
  }
250
276
  // Mode 3: Reactive - observe BookingService signals (client-side only)
251
277
  else if (typeof window !== 'undefined' && bookingService) {
252
- // Computed signal that changes when services, selected time slots, or
253
- // the consumer-selected payment option change (not participants).
278
+ // Computed signal that changes when services, selected time slots, or the
279
+ // consumer's payment choices change (not participants).
254
280
  const servicesKey = signalsService.computed(() => {
255
281
  const selections = bookingService?.serviceSelections.get() || [];
256
282
  const timezone = bookingService?.timezone.get();
257
283
  const selectedPaymentOption = bookingService?.selectedPaymentOption.get();
284
+ const depositSelected = bookingService?.depositSelected.get();
258
285
  return JSON.stringify({
259
286
  selections: selections.map((s) => ({
260
287
  serviceId: s.service._id,
@@ -264,16 +291,18 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
264
291
  })),
265
292
  timezone,
266
293
  selectedPaymentOption,
294
+ depositSelected,
267
295
  });
268
296
  });
269
297
  signalsService.effect(() => {
270
- // Subscribe to servicesKey to trigger effect when service, time
271
- // slot, or selected payment option changes.
298
+ // Subscribe to servicesKey to trigger effect when service, time slot,
299
+ // or payment choices change.
272
300
  servicesKey.get();
273
301
  // Read current selections without subscribing (peek)
274
302
  const selections = bookingService?.serviceSelections.peek() || [];
275
303
  const timezone = bookingService?.timezone.peek();
276
304
  const selectedPaymentOption = bookingService?.selectedPaymentOption.peek();
305
+ const depositSelected = bookingService?.depositSelected.peek();
277
306
  // For single service mode, take the first selection
278
307
  const firstSelection = selections[0];
279
308
  const numParticipants = firstSelection?.totalParticipants || 1;
@@ -288,7 +317,7 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
288
317
  if (pricingServiceSelections.length) {
289
318
  serviceOptionsSignal.set(deriveServiceOptions(pricingServiceSelections));
290
319
  }
291
- void calculate(pricingServiceSelections, numParticipants, selectedPaymentOption);
320
+ void calculate(pricingServiceSelections, numParticipants, selectedPaymentOption, depositSelected);
292
321
  });
293
322
  }
294
323
  return {
@@ -306,10 +335,12 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
306
335
  * @param params.totalParticipants - Number of participants (default: 1)
307
336
  * @param params.selectedPaymentOption - Optional consumer-selected payment option;
308
337
  * overrides the service-config heuristic on each line item's `paymentOption`.
338
+ * @param params.depositSelected - Optional deposit-vs-full-upfront choice; `false`
339
+ * resolves a deposit service to full payment. Omit to keep the deposit default.
309
340
  */
310
341
  async function loadPaymentConfig(params) {
311
342
  try {
312
- const { pricingServiceSelections, totalParticipants = 1, selectedPaymentOption, } = params;
343
+ const { pricingServiceSelections, totalParticipants = 1, selectedPaymentOption, depositSelected, } = params;
313
344
  if (!pricingServiceSelections.length) {
314
345
  return {
315
346
  type: 'success',
@@ -319,7 +350,7 @@ async function loadPaymentConfig(params) {
319
350
  },
320
351
  };
321
352
  }
322
- const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption);
353
+ const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected);
323
354
  return {
324
355
  type: 'success',
325
356
  config: {
@@ -90,7 +90,7 @@ exports.ServiceService = services_definitions_1.implementService.withConfig()(ex
90
90
  const initialService = config.service || {};
91
91
  serviceSignal = signalsService.signal(initialService);
92
92
  }
93
- const isLoadingSignal = signalsService.signal(!!config.serviceId);
93
+ const isLoadingSignal = signalsService.signal(!config.service && (!!config.serviceId || !!config.serviceSlug));
94
94
  const errorSignal = signalsService.signal(null);
95
95
  // Default appId to BOOKING_APP_ID if not provided
96
96
  const appId = config.appId ?? constants_js_1.BOOKING_APP_ID;
@@ -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,46 @@
1
+ "use strict";
2
+ /**
3
+ * Shared helpers for reading payment configuration off a Service entity.
4
+ *
5
+ * `payment.fixed` / `varied` / `custom` are a `@oneof` in the services SDK, so
6
+ * only one is ever populated — reading `fixed` then `varied` covers both FIXED
7
+ * and VARIED rate types.
8
+ *
9
+ * A percentage deposit stores no amount anywhere on the entity; the server
10
+ * resolves it when the customer books. Hence the split between
11
+ * {@link requiresDeposit} ("takes a deposit?") and {@link hasDeposit} ("has an
12
+ * amount?") — a percentage deposit answers `true` and `false` respectively.
13
+ *
14
+ * @module Services/ServicePayment
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.requiresDeposit = requiresDeposit;
18
+ exports.getDepositValue = getDepositValue;
19
+ exports.hasDeposit = hasDeposit;
20
+ exports.isFullUpfrontPaymentAllowed = isFullUpfrontPaymentAllowed;
21
+ /**
22
+ * Whether the service takes a deposit. An explicit `options.deposit` wins; when
23
+ * the flag is absent a configured amount still counts, so a service that sets one
24
+ * without the flag keeps reporting a deposit.
25
+ */
26
+ function requiresDeposit(service) {
27
+ return service?.payment?.options?.deposit ?? hasDeposit(service);
28
+ }
29
+ /** The deposit amount, if one is configured as a fixed amount. */
30
+ function getDepositValue(service) {
31
+ const payment = service?.payment;
32
+ return (payment?.fixed?.deposit?.value ??
33
+ payment?.varied?.deposit?.value ??
34
+ payment?.fixed?.depositDetails?.amount?.value ??
35
+ payment?.varied?.depositDetails?.amount?.value);
36
+ }
37
+ function hasDeposit(service) {
38
+ const value = getDepositValue(service);
39
+ return !!value && Number(value) > 0;
40
+ }
41
+ /** Meaningful only alongside `hasDeposit`. */
42
+ function isFullUpfrontPaymentAllowed(service) {
43
+ const payment = service?.payment;
44
+ return !!(payment?.fixed?.fullUpfrontPaymentAllowed ??
45
+ payment?.varied?.fullUpfrontPaymentAllowed);
46
+ }
@@ -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)
@@ -5,16 +5,31 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.convertToMoneyData = convertToMoneyData;
7
7
  exports.convertMultiCurrencyPriceToMoneyData = convertMultiCurrencyPriceToMoneyData;
8
+ // Number() (unlike parseFloat) rejects strings with trailing garbage like
9
+ // "12.34xyz", but treats empty/whitespace-only strings as 0 — reject those
10
+ // separately so invalid values return null instead of reaching the formatter.
11
+ function toNumericAmount(value) {
12
+ if (typeof value === 'string') {
13
+ return value.trim() === '' ? NaN : Number(value);
14
+ }
15
+ return value;
16
+ }
8
17
  /**
9
18
  * Converts SDK Money format to MoneyData format
10
19
  *
20
+ * The original SDK value is validated as a whole (values that are not fully
21
+ * numeric and finite, like `"12.34xyz"`, return `null`) but kept as-is, so
22
+ * string amounts reach `Money` with full decimal precision instead of being
23
+ * routed through floating point. Compare amounts numerically with
24
+ * `Number(money.amount)`.
25
+ *
11
26
  * @param sdkMoney - The SDK money object with value, currency, and optional formattedValue
12
27
  * @returns MoneyData object with amount and currency, or null if conversion fails
13
28
  *
14
29
  * @example
15
30
  * ```ts
16
31
  * const money = convertToMoneyData({ value: "25.50", currency: "USD" });
17
- * // Returns: { amount: 25.50, currency: "USD" }
32
+ * // Returns: { amount: "25.50", currency: "USD" }
18
33
  * ```
19
34
  */
20
35
  function convertToMoneyData(sdkMoney) {
@@ -22,23 +37,20 @@ function convertToMoneyData(sdkMoney) {
22
37
  return null;
23
38
  const priceValue = sdkMoney.value;
24
39
  const currency = sdkMoney.currency || 'USD';
25
- // Convert price value to number
26
- let amount = undefined;
27
- if (typeof priceValue === 'string') {
28
- const parsed = parseFloat(priceValue);
29
- amount = !isNaN(parsed) ? parsed : undefined;
30
- }
31
- else if (typeof priceValue === 'number') {
32
- amount = priceValue;
33
- }
34
- if (amount !== undefined && !isNaN(amount) && amount >= 0) {
35
- return { amount, currency };
36
- }
37
- return null;
40
+ if (priceValue === undefined)
41
+ return null;
42
+ const parsed = toNumericAmount(priceValue);
43
+ if (!Number.isFinite(parsed) || parsed < 0)
44
+ return null;
45
+ return { amount: priceValue, currency };
38
46
  }
39
47
  /**
40
48
  * Converts ecom MultiCurrencyPrice format to MoneyData format
41
- * MultiCurrencyPrice uses 'amount' as string, while MoneyData uses 'amount' as number
49
+ *
50
+ * The string amount is validated as a whole (values that are not fully
51
+ * numeric and finite, like `"12.34xyz"`, return `null`) but kept as-is, so
52
+ * it reaches `Money` with full decimal precision. Compare amounts
53
+ * numerically with `Number(money.amount)`.
42
54
  *
43
55
  * @param price - The MultiCurrencyPrice object with amount as string
44
56
  * @param currency - The currency code (from response level)
@@ -47,8 +59,8 @@ function convertToMoneyData(sdkMoney) {
47
59
  function convertMultiCurrencyPriceToMoneyData(price, currency) {
48
60
  if (!price?.amount)
49
61
  return null;
50
- const parsed = parseFloat(price.amount);
51
- if (isNaN(parsed) || parsed < 0)
62
+ const parsed = toNumericAmount(price.amount);
63
+ if (!Number.isFinite(parsed) || parsed < 0)
52
64
  return null;
53
- return { amount: parsed, currency };
65
+ return { amount: price.amount, currency };
54
66
  }
@@ -24,18 +24,20 @@ export function createMockSignalsService() {
24
24
  };
25
25
  }
26
26
  export function createMockBookingService(config = {}) {
27
- const { serviceSelections = [], location = null, timezone = 'UTC', formSubmission = null, selectedPaymentOption, } = config;
27
+ const { serviceSelections = [], location = null, timezone = 'UTC', formSubmission = null, selectedPaymentOption, depositSelected, } = config;
28
28
  const serviceSelectionsSignal = mockSignal(serviceSelections);
29
29
  const locationSignal = mockSignal(location);
30
30
  const timezoneSignal = mockSignal(timezone);
31
31
  const formSubmissionSignal = mockSignal(formSubmission);
32
32
  const selectedPaymentOptionSignal = mockSignal(selectedPaymentOption);
33
+ const depositSelectedSignal = mockSignal(depositSelected);
33
34
  return {
34
35
  serviceSelections: serviceSelectionsSignal,
35
36
  location: locationSignal,
36
37
  timezone: timezoneSignal,
37
38
  formSubmission: formSubmissionSignal,
38
39
  selectedPaymentOption: selectedPaymentOptionSignal,
40
+ depositSelected: depositSelectedSignal,
39
41
  actions: {
40
42
  updateItem: vi.fn(),
41
43
  setItem: vi.fn(),
@@ -51,6 +53,7 @@ export function createMockBookingService(config = {}) {
51
53
  setFormSubmission: vi.fn(),
52
54
  clearFormSubmission: vi.fn(),
53
55
  setSelectedPaymentOption: vi.fn(),
56
+ setDepositSelected: vi.fn(),
54
57
  setAllDay: vi.fn(),
55
58
  clearAll: vi.fn(),
56
59
  book: vi.fn(),
@@ -158,6 +158,19 @@ export declare function createLargeStaffMemberList(count: number): StaffMember[]
158
158
  * Mock service with deposit configured
159
159
  */
160
160
  export declare const mockServiceWithDeposit: Service;
161
+ /** Deposit set as a fixed amount under `depositDetails`, the newer field. */
162
+ export declare const mockServiceDepositDetailsAmount: Service;
163
+ /** Percentage deposit: `options.deposit` set, no amount anywhere on the entity. */
164
+ export declare const mockServicePercentageDeposit: Service;
165
+ /**
166
+ * Mock service with a deposit that may also be paid in full upfront — the only
167
+ * configuration where `booking.depositSelected` may be `false`.
168
+ */
169
+ export declare const mockServiceWithDepositFullUpfront: Service;
170
+ /**
171
+ * Mock VARIED-rate service with a deposit (both under `payment.varied`).
172
+ */
173
+ export declare const mockServiceVariedWithDeposit: Service;
161
174
  /**
162
175
  * Mock service with in-person payment option
163
176
  */
@@ -535,6 +535,97 @@ export const mockServiceWithDeposit = {
535
535
  },
536
536
  },
537
537
  };
538
+ /** Deposit set as a fixed amount under `depositDetails`, the newer field. */
539
+ export const mockServiceDepositDetailsAmount = {
540
+ ...mockService,
541
+ _id: 'test-service-deposit-details-amount',
542
+ name: 'Deposit Details Amount Service',
543
+ payment: {
544
+ rateType: RateType.FIXED,
545
+ fixed: {
546
+ price: {
547
+ value: '100.00',
548
+ currency: 'USD',
549
+ },
550
+ depositDetails: {
551
+ amount: {
552
+ value: '25.00',
553
+ currency: 'USD',
554
+ },
555
+ },
556
+ },
557
+ options: {
558
+ online: true,
559
+ deposit: true,
560
+ },
561
+ },
562
+ };
563
+ /** Percentage deposit: `options.deposit` set, no amount anywhere on the entity. */
564
+ export const mockServicePercentageDeposit = {
565
+ ...mockService,
566
+ _id: 'test-service-percentage-deposit',
567
+ name: 'Percentage Deposit Service',
568
+ payment: {
569
+ rateType: RateType.FIXED,
570
+ fixed: {
571
+ price: {
572
+ value: '10',
573
+ currency: 'USD',
574
+ },
575
+ fullUpfrontPaymentAllowed: true,
576
+ },
577
+ options: {
578
+ online: true,
579
+ inPerson: false,
580
+ deposit: true,
581
+ },
582
+ },
583
+ };
584
+ /**
585
+ * Mock service with a deposit that may also be paid in full upfront — the only
586
+ * configuration where `booking.depositSelected` may be `false`.
587
+ */
588
+ export const mockServiceWithDepositFullUpfront = {
589
+ ...mockService,
590
+ _id: 'test-service-deposit-full-upfront',
591
+ name: 'Service with Deposit and Full Upfront Allowed',
592
+ payment: {
593
+ rateType: RateType.FIXED,
594
+ fixed: {
595
+ price: {
596
+ value: '100.00',
597
+ currency: 'USD',
598
+ },
599
+ deposit: {
600
+ value: '30.00',
601
+ currency: 'USD',
602
+ },
603
+ fullUpfrontPaymentAllowed: true,
604
+ },
605
+ },
606
+ };
607
+ /**
608
+ * Mock VARIED-rate service with a deposit (both under `payment.varied`).
609
+ */
610
+ export const mockServiceVariedWithDeposit = {
611
+ ...mockService,
612
+ _id: 'test-service-varied-deposit',
613
+ name: 'Varied Service with Deposit',
614
+ payment: {
615
+ rateType: RateType.VARIED,
616
+ varied: {
617
+ defaultPrice: {
618
+ value: '80.00',
619
+ currency: 'USD',
620
+ },
621
+ deposit: {
622
+ value: '20.00',
623
+ currency: 'USD',
624
+ },
625
+ fullUpfrontPaymentAllowed: true,
626
+ },
627
+ },
628
+ };
538
629
  /**
539
630
  * Mock service with in-person payment option
540
631
  */