@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
@@ -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.
@@ -115,16 +139,11 @@ function getDurationRange(service) {
115
139
  async function getPreviewedPrice(bookingLineItems) {
116
140
  if (!bookingLineItems.length)
117
141
  return null;
118
- const result = await (0, auto_sdk_bookings_pricing_1.previewPrice)(bookingLineItems);
119
- const price = result.priceInfo?.calculatedPrice;
120
- if (price == null)
121
- return null;
122
- return {
123
- price,
124
- deposit: result.priceInfo?.deposit ?? null,
125
- };
142
+ const { priceInfo } = await (0, auto_sdk_bookings_pricing_1.previewPrice)(bookingLineItems);
143
+ const calculatedPrice = priceInfo?.calculatedPrice;
144
+ return calculatedPrice != null ? { ...priceInfo, calculatedPrice } : null;
126
145
  }
127
- async function buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption) {
146
+ async function buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption, depositSelected) {
128
147
  const slots = item.rangeTimeSlots && item.rangeTimeSlots.length > 0
129
148
  ? item.rangeTimeSlots.map((s) => ({
130
149
  localStartDate: s.startDate,
@@ -138,7 +157,12 @@ async function buildLineItemWithPreview(item, totalParticipants, selectedPayment
138
157
  },
139
158
  ]
140
159
  : [];
141
- if (item.service._id && slots.length && getDurationRange(item.service)) {
160
+ // A deposit with no configured amount is a percentage — only the server knows
161
+ // the figure, and guessing it risks charging something else.
162
+ const needsServerResolvedDeposit = (0, servicePayment_js_1.requiresDeposit)(item.service) && !(0, servicePayment_js_1.hasDeposit)(item.service);
163
+ if (item.service._id &&
164
+ slots.length &&
165
+ (getDurationRange(item.service) || needsServerResolvedDeposit)) {
142
166
  const bookingLineItems = slots.map((slot) => ({
143
167
  serviceId: item.service._id,
144
168
  localStartDate: slot.localStartDate,
@@ -146,43 +170,52 @@ async function buildLineItemWithPreview(item, totalParticipants, selectedPayment
146
170
  numberOfParticipants: totalParticipants,
147
171
  ...(item.timeZone ? { timeZone: item.timeZone } : {}),
148
172
  }));
149
- const previewedPrice = await getPreviewedPrice(bookingLineItems).catch(() => null);
150
- if (previewedPrice != null) {
151
- const depositValue = item.service.payment?.fixed?.deposit?.value;
152
- const previewedDeposit = previewedPrice.deposit;
153
- const hasDeposit = previewedDeposit != null
154
- ? previewedDeposit > 0
155
- : !!depositValue && Number(depositValue) > 0;
173
+ const priceInfo = await getPreviewedPrice(bookingLineItems).catch(() => null);
174
+ if (priceInfo != null) {
175
+ const depositValue = (0, servicePayment_js_1.getDepositValue)(item.service);
176
+ const previewedDeposit = priceInfo.deposit ?? null;
177
+ const hasDeposit = (0, servicePayment_js_1.requiresDeposit)(item.service) &&
178
+ (previewedDeposit != null
179
+ ? previewedDeposit > 0
180
+ : (0, servicePayment_js_1.hasDeposit)(item.service));
156
181
  const deposit = hasDeposit
157
182
  ? previewedDeposit != null
158
183
  ? previewedDeposit
159
184
  : Number(depositValue) * totalParticipants
160
185
  : undefined;
161
186
  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),
187
+ lineItem: toLineItem({
188
+ lineItemId: item.lineItemId,
189
+ serviceId: item.service._id,
190
+ price: priceInfo.calculatedPrice,
191
+ deposit,
192
+ paymentOption: getPaymentOption(item.service, hasDeposit, selectedPaymentOption, depositSelected),
193
+ }),
194
+ priceInfo,
171
195
  };
172
196
  }
173
197
  }
174
- return buildLineItem(item.service, totalParticipants, item.lineItemId, selectedPaymentOption);
198
+ return {
199
+ lineItem: buildLineItem(item.service, totalParticipants, item.lineItemId, selectedPaymentOption, depositSelected),
200
+ };
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 builtLineItems = await Promise.all(data.map(async (item) => {
208
+ const { lineItem, priceInfo } = await buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption, depositSelected);
209
+ const additionalInfo = {
210
+ ...getAdditionalInfo(item.service, item.lineItemId),
211
+ ...(priceInfo ? { priceInfo } : {}),
212
+ };
213
+ return { lineItem, additionalInfo };
214
+ }));
182
215
  const response = await (0, auto_sdk_ecom_totals_calculator_1.calculateTotals)({
183
- lineItems,
216
+ lineItems: builtLineItems.map(({ lineItem }) => lineItem),
184
217
  });
185
- const additionalInfoList = data.map((item) => getAdditionalInfo(item.service, item.lineItemId));
218
+ const additionalInfoList = builtLineItems.map(({ additionalInfo }) => additionalInfo);
186
219
  return {
187
220
  response,
188
221
  additionalInfo: {
@@ -209,7 +242,7 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
209
242
  const errorSignal = signalsService.signal(null);
210
243
  const serviceOptionsSignal = signalsService.signal(config.serviceOptions || DEFAULT_SERVICE_OPTIONS);
211
244
  // Internal calculate function
212
- const calculate = async (pricingServiceSelections, totalParticipants, selectedPaymentOption) => {
245
+ const calculate = async (pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected) => {
213
246
  if (!pricingServiceSelections.length) {
214
247
  paymentDetailsSignal.set(null);
215
248
  return;
@@ -217,7 +250,7 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
217
250
  isLoadingSignal.set(true);
218
251
  errorSignal.set(null);
219
252
  try {
220
- const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption);
253
+ const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected);
221
254
  paymentDetailsSignal.set(paymentDetails);
222
255
  }
223
256
  catch (error) {
@@ -245,16 +278,17 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
245
278
  // Mode 2: Lazy loading - pricingServiceSelections provided via internal config
246
279
  else if (config.pricingServiceSelections?.length) {
247
280
  serviceOptionsSignal.set(deriveServiceOptions(config.pricingServiceSelections));
248
- void calculate(config.pricingServiceSelections, config.totalParticipants ?? 1, config.selectedPaymentOption);
281
+ void calculate(config.pricingServiceSelections, config.totalParticipants ?? 1, config.selectedPaymentOption, config.depositSelected);
249
282
  }
250
283
  // Mode 3: Reactive - observe BookingService signals (client-side only)
251
284
  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).
285
+ // Computed signal that changes when services, selected time slots, or the
286
+ // consumer's payment choices change (not participants).
254
287
  const servicesKey = signalsService.computed(() => {
255
288
  const selections = bookingService?.serviceSelections.get() || [];
256
289
  const timezone = bookingService?.timezone.get();
257
290
  const selectedPaymentOption = bookingService?.selectedPaymentOption.get();
291
+ const depositSelected = bookingService?.depositSelected.get();
258
292
  return JSON.stringify({
259
293
  selections: selections.map((s) => ({
260
294
  serviceId: s.service._id,
@@ -264,16 +298,18 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
264
298
  })),
265
299
  timezone,
266
300
  selectedPaymentOption,
301
+ depositSelected,
267
302
  });
268
303
  });
269
304
  signalsService.effect(() => {
270
- // Subscribe to servicesKey to trigger effect when service, time
271
- // slot, or selected payment option changes.
305
+ // Subscribe to servicesKey to trigger effect when service, time slot,
306
+ // or payment choices change.
272
307
  servicesKey.get();
273
308
  // Read current selections without subscribing (peek)
274
309
  const selections = bookingService?.serviceSelections.peek() || [];
275
310
  const timezone = bookingService?.timezone.peek();
276
311
  const selectedPaymentOption = bookingService?.selectedPaymentOption.peek();
312
+ const depositSelected = bookingService?.depositSelected.peek();
277
313
  // For single service mode, take the first selection
278
314
  const firstSelection = selections[0];
279
315
  const numParticipants = firstSelection?.totalParticipants || 1;
@@ -288,7 +324,7 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
288
324
  if (pricingServiceSelections.length) {
289
325
  serviceOptionsSignal.set(deriveServiceOptions(pricingServiceSelections));
290
326
  }
291
- void calculate(pricingServiceSelections, numParticipants, selectedPaymentOption);
327
+ void calculate(pricingServiceSelections, numParticipants, selectedPaymentOption, depositSelected);
292
328
  });
293
329
  }
294
330
  return {
@@ -306,10 +342,12 @@ exports.PaymentService = services_definitions_1.implementService.withConfig()(pa
306
342
  * @param params.totalParticipants - Number of participants (default: 1)
307
343
  * @param params.selectedPaymentOption - Optional consumer-selected payment option;
308
344
  * overrides the service-config heuristic on each line item's `paymentOption`.
345
+ * @param params.depositSelected - Optional deposit-vs-full-upfront choice; `false`
346
+ * resolves a deposit service to full payment. Omit to keep the deposit default.
309
347
  */
310
348
  async function loadPaymentConfig(params) {
311
349
  try {
312
- const { pricingServiceSelections, totalParticipants = 1, selectedPaymentOption, } = params;
350
+ const { pricingServiceSelections, totalParticipants = 1, selectedPaymentOption, depositSelected, } = params;
313
351
  if (!pricingServiceSelections.length) {
314
352
  return {
315
353
  type: 'success',
@@ -319,7 +357,7 @@ async function loadPaymentConfig(params) {
319
357
  },
320
358
  };
321
359
  }
322
- const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption);
360
+ const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected);
323
361
  return {
324
362
  type: 'success',
325
363
  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,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
  */