@wix/headless-bookings 1.0.17 → 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 (41) 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/payment/payment.d.ts +3 -0
  15. package/cjs/dist/services/payment/payment.def.d.ts +15 -1
  16. package/cjs/dist/services/payment/payment.js +89 -58
  17. package/cjs/dist/services/servicePayment.d.ts +26 -0
  18. package/cjs/dist/services/servicePayment.js +46 -0
  19. package/cjs/dist/utils/money.d.ts +12 -2
  20. package/cjs/dist/utils/money.js +30 -18
  21. package/dist/__mocks__/booking/booking-client-service.js +4 -1
  22. package/dist/__mocks__/services/services-mocks.d.ts +13 -0
  23. package/dist/__mocks__/services/services-mocks.js +91 -0
  24. package/dist/react/core/booking/Booking.d.ts +6 -0
  25. package/dist/react/core/booking/Booking.js +2 -0
  26. package/dist/react/core/payment/Payment.d.ts +2 -1
  27. package/dist/react/core/payment/Payment.js +11 -8
  28. package/dist/react/payment/Payment.d.ts +3 -2
  29. package/dist/react/payment/Payment.js +11 -5
  30. package/dist/services/booking/book-action/buildBookingRequest.js +13 -4
  31. package/dist/services/booking/book-action/types.d.ts +6 -0
  32. package/dist/services/booking/booking.d.ts +17 -0
  33. package/dist/services/booking/booking.js +8 -0
  34. package/dist/services/payment/payment.d.ts +3 -0
  35. package/dist/services/payment/payment.def.d.ts +15 -1
  36. package/dist/services/payment/payment.js +89 -58
  37. package/dist/services/servicePayment.d.ts +26 -0
  38. package/dist/services/servicePayment.js +40 -0
  39. package/dist/utils/money.d.ts +12 -2
  40. package/dist/utils/money.js +30 -18
  41. package/package.json +3 -3
@@ -106,6 +106,7 @@ export function Data(props) {
106
106
  const timezone = bookingService.timezone.get();
107
107
  const formSubmission = bookingService.formSubmission.get();
108
108
  const selectedPaymentOption = bookingService.selectedPaymentOption.get();
109
+ const depositSelected = bookingService.depositSelected.get();
109
110
  const { actions } = bookingService;
110
111
  const location = resolveLocation(bookingLocation, serviceSelections);
111
112
  return children({
@@ -114,6 +115,7 @@ export function Data(props) {
114
115
  timezone,
115
116
  formSubmission,
116
117
  selectedPaymentOption,
118
+ depositSelected,
117
119
  actions,
118
120
  });
119
121
  }
@@ -172,7 +172,8 @@ export interface ServiceOptionsProps {
172
172
  }
173
173
  /**
174
174
  * Core component that exposes service payment options via render props.
175
- * Reads isOnline, isInPerson, isDeposit from the payment service signal.
175
+ * Reads isOnline, isInPerson, isDeposit, canPayFullUpfront from the payment
176
+ * service signal.
176
177
  *
177
178
  * @component
178
179
  */
@@ -81,10 +81,12 @@ export function Data(props) {
81
81
  const additionalInfoList = paymentDetails?.additionalInfo?.lineItems || [];
82
82
  const firstLineItemInfo = additionalInfoList[0];
83
83
  const customPrice = firstLineItemInfo?.customPrice || null;
84
- // Computed flags
85
- const hasTax = tax !== null && tax.amount > 0;
86
- const hasDiscount = discount !== null && discount.amount > 0;
87
- const isDeposit = payNow !== null && total !== null && payNow.amount < total.amount;
84
+ // Computed flags (amounts may be strings — compare numerically)
85
+ const hasTax = tax !== null && Number(tax.amount) > 0;
86
+ const hasDiscount = discount !== null && Number(discount.amount) > 0;
87
+ const isDeposit = payNow !== null &&
88
+ total !== null &&
89
+ Number(payNow.amount) < Number(total.amount);
88
90
  return props.children({
89
91
  subtotal,
90
92
  total,
@@ -190,7 +192,8 @@ export function LineItemProvider(props) {
190
192
  }
191
193
  /**
192
194
  * Core component that exposes service payment options via render props.
193
- * Reads isOnline, isInPerson, isDeposit from the payment service signal.
195
+ * Reads isOnline, isInPerson, isDeposit, canPayFullUpfront from the payment
196
+ * service signal.
194
197
  *
195
198
  * @component
196
199
  */
@@ -217,9 +220,9 @@ export function ServiceOptions(props) {
217
220
  */
218
221
  export function LineItemInfo(props) {
219
222
  const { lineItem, currency } = useLineItemContext();
220
- // Computed flags
221
- const hasDiscount = lineItem.discount !== null && lineItem.discount.amount > 0;
222
- const hasDeposit = lineItem.deposit !== null && lineItem.deposit.amount > 0;
223
+ // Computed flags (amounts may be strings — compare numerically)
224
+ const hasDiscount = lineItem.discount !== null && Number(lineItem.discount.amount) > 0;
225
+ const hasDeposit = lineItem.deposit !== null && Number(lineItem.deposit.amount) > 0;
223
226
  return props.children({
224
227
  lineItem,
225
228
  currency,
@@ -210,7 +210,8 @@ export interface ServiceOptionsProps {
210
210
  *
211
211
  * Payment scenarios:
212
212
  * - `isOnline=true, isInPerson=false, isDeposit=false` → Pay now only (Due now = total)
213
- * - `isOnline=true, isInPerson=false, isDeposit=true` → Pay now + security deposit
213
+ * - `isOnline=true, isInPerson=false, isDeposit=true, canPayFullUpfront=false` → Pay now + security deposit
214
+ * - `isOnline=true, isInPerson=false, isDeposit=true, canPayFullUpfront=true` → Radio: Pay deposit now / Pay in full now
214
215
  * - `isOnline=true, isInPerson=true` → Radio: Pay Online / Pay on Arrival
215
216
  * - `isOnline=false, isInPerson=true` → Pay on arrival only
216
217
  *
@@ -223,7 +224,7 @@ export interface ServiceOptionsProps {
223
224
  * @example
224
225
  * ```tsx
225
226
  * <Payment.ServiceOptions asChild>
226
- * {({ isOnline, isInPerson, isDeposit }) => (
227
+ * {({ isOnline, isInPerson, isDeposit, canPayFullUpfront }) => (
227
228
  * // render the correct payment scenario
228
229
  * )}
229
230
  * </Payment.ServiceOptions>
@@ -144,7 +144,7 @@ Total.displayName = 'Payment.Total';
144
144
  */
145
145
  export const PayNow = React.forwardRef((props, ref) => {
146
146
  const { asChild, children, className } = props;
147
- return (_jsx(CorePayment.Data, { children: ({ payNow, isDeposit }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, customElement: children, customElementProps: { payNow, isDeposit }, "data-testid": TestIds.paymentPayNow, "data-has-deposit": isDeposit, children: isDeposit && payNow && payNow.amount > 0 && (_jsx(Money, { money: payNow })) })) }));
147
+ return (_jsx(CorePayment.Data, { children: ({ payNow, isDeposit }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, customElement: children, customElementProps: { payNow, isDeposit }, "data-testid": TestIds.paymentPayNow, "data-has-deposit": isDeposit, children: isDeposit && payNow && Number(payNow.amount) > 0 && (_jsx(Money, { money: payNow })) })) }));
148
148
  });
149
149
  PayNow.displayName = 'Payment.PayNow';
150
150
  /**
@@ -159,7 +159,7 @@ PayNow.displayName = 'Payment.PayNow';
159
159
  */
160
160
  export const PayLater = React.forwardRef((props, ref) => {
161
161
  const { asChild, children, className } = props;
162
- return (_jsx(CorePayment.Data, { children: ({ payLater, isDeposit }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, customElement: children, customElementProps: { payLater, isDeposit }, "data-testid": TestIds.paymentPayLater, "data-has-deposit": isDeposit, children: isDeposit && payLater && payLater.amount > 0 && (_jsx(Money, { money: payLater })) })) }));
162
+ return (_jsx(CorePayment.Data, { children: ({ payLater, isDeposit }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, customElement: children, customElementProps: { payLater, isDeposit }, "data-testid": TestIds.paymentPayLater, "data-has-deposit": isDeposit, children: isDeposit && payLater && Number(payLater.amount) > 0 && (_jsx(Money, { money: payLater })) })) }));
163
163
  });
164
164
  PayLater.displayName = 'Payment.PayLater';
165
165
  /**
@@ -168,7 +168,8 @@ PayLater.displayName = 'Payment.PayLater';
168
168
  *
169
169
  * Payment scenarios:
170
170
  * - `isOnline=true, isInPerson=false, isDeposit=false` → Pay now only (Due now = total)
171
- * - `isOnline=true, isInPerson=false, isDeposit=true` → Pay now + security deposit
171
+ * - `isOnline=true, isInPerson=false, isDeposit=true, canPayFullUpfront=false` → Pay now + security deposit
172
+ * - `isOnline=true, isInPerson=false, isDeposit=true, canPayFullUpfront=true` → Radio: Pay deposit now / Pay in full now
172
173
  * - `isOnline=true, isInPerson=true` → Radio: Pay Online / Pay on Arrival
173
174
  * - `isOnline=false, isInPerson=true` → Pay on arrival only
174
175
  *
@@ -181,7 +182,7 @@ PayLater.displayName = 'Payment.PayLater';
181
182
  * @example
182
183
  * ```tsx
183
184
  * <Payment.ServiceOptions asChild>
184
- * {({ isOnline, isInPerson, isDeposit }) => (
185
+ * {({ isOnline, isInPerson, isDeposit, canPayFullUpfront }) => (
185
186
  * // render the correct payment scenario
186
187
  * )}
187
188
  * </Payment.ServiceOptions>
@@ -189,7 +190,12 @@ PayLater.displayName = 'Payment.PayLater';
189
190
  */
190
191
  export const ServiceOptions = React.forwardRef((props, ref) => {
191
192
  const { asChild, children } = props;
192
- return (_jsx(CorePayment.ServiceOptions, { children: ({ isOnline, isInPerson, isDeposit }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, customElement: children, customElementProps: { isOnline, isInPerson, isDeposit }, "data-testid": TestIds.paymentServiceOptions, "data-is-online": isOnline, "data-is-in-person": isInPerson, "data-is-deposit": isDeposit, children: null })) }));
193
+ return (_jsx(CorePayment.ServiceOptions, { children: ({ isOnline, isInPerson, isDeposit, canPayFullUpfront }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, customElement: children, customElementProps: {
194
+ isOnline,
195
+ isInPerson,
196
+ isDeposit,
197
+ canPayFullUpfront,
198
+ }, "data-testid": TestIds.paymentServiceOptions, "data-is-online": isOnline, "data-is-in-person": isInPerson, "data-is-deposit": isDeposit, "data-can-pay-full-upfront": canPayFullUpfront, children: null })) }));
193
199
  });
194
200
  ServiceOptions.displayName = 'Payment.ServiceOptions';
195
201
  /**
@@ -4,6 +4,7 @@
4
4
  import { LocationType as TimeSlotLocationType } from '@wix/auto_sdk_bookings_availability-time-slots';
5
5
  import { LocationType as BookingLocationType, SelectionMethod, } from '@wix/auto_sdk_bookings_bookings';
6
6
  import { STAFF_MEMBER_RESOURCE_TYPE_ID } from '../../constants.js';
7
+ import { isFullUpfrontPaymentAllowed, requiresDeposit, } from '../../servicePayment.js';
7
8
  /**
8
9
  * Derives the selected payment option from service payment settings.
9
10
  * Maps service.payment.options to booking API's selectedPaymentOption.
@@ -53,12 +54,20 @@ export function buildBookingRequest(params) {
53
54
  const resource = item.timeSlot.resources?.[0] ?? item.resources?.[0];
54
55
  // Get location: prefer timeSlot.location, fallback to params.location
55
56
  const slotLocation = item.timeSlot.location ?? location;
57
+ // Consumer override (if set) wins over the service-config heuristic.
58
+ const selectedPaymentOption = params.selectedPaymentOption ?? deriveSelectedPaymentOption(item.service);
59
+ // The only state where the schema allows `false`. Outside it, omitting the key
60
+ // leaves the server default in place.
61
+ const depositSelectedApplies = params.depositSelected !== undefined &&
62
+ selectedPaymentOption === 'ONLINE' &&
63
+ requiresDeposit(item.service) &&
64
+ isFullUpfrontPaymentAllowed(item.service);
56
65
  return {
57
66
  booking: {
58
- // Payment option: consumer override (if set) wins over the
59
- // service-config heuristic.
60
- selectedPaymentOption: params.selectedPaymentOption ??
61
- deriveSelectedPaymentOption(item.service),
67
+ selectedPaymentOption,
68
+ ...(depositSelectedApplies && {
69
+ depositSelected: params.depositSelected,
70
+ }),
62
71
  // Participants
63
72
  totalParticipants: item.totalParticipants || 1,
64
73
  // App that created the booking (used to filter bookings by source app).
@@ -59,6 +59,12 @@ export interface BookActionParams {
59
59
  * service. When omitted, the existing service-config heuristic applies.
60
60
  */
61
61
  selectedPaymentOption?: SelectedPaymentOption;
62
+ /**
63
+ * Optional consumer choice: `true` pays only the deposit now, `false` pays the
64
+ * full price now. Dropped unless the booking is ONLINE and the service has a
65
+ * deposit with `fullUpfrontPaymentAllowed`. Omit to keep the server default.
66
+ */
67
+ depositSelected?: boolean;
62
68
  /**
63
69
  * DAY-range only. When `true`, the picked per-day slots are collapsed into a
64
70
  * single Booking spanning the whole range (`firstSlot.startDate ->
@@ -89,6 +89,12 @@ export interface BookingServiceActions {
89
89
  * is cleared and the heuristic applies again.
90
90
  */
91
91
  setSelectedPaymentOption(value: SelectedPaymentOption | null): void;
92
+ /**
93
+ * Set (or clear with `undefined`) the deposit-vs-full-upfront choice. `true`
94
+ * charges only the deposit now, `false` charges the full price now. Gate the UI
95
+ * on `Payment.ServiceOptions`' `isDeposit && canPayFullUpfront`.
96
+ */
97
+ setDepositSelected(value: boolean | undefined): void;
92
98
  /**
93
99
  * Toggle the DAY-range `allDay` flag. When `true`, `book()` collapses the
94
100
  * picked per-day slots into one range-spanning Booking created via
@@ -117,6 +123,12 @@ export interface BookingService {
117
123
  * service-config heuristic. Set to `'ONLINE'` or `'OFFLINE'` to override.
118
124
  */
119
125
  selectedPaymentOption: Signal<SelectedPaymentOption | undefined>;
126
+ /**
127
+ * Signal containing the deposit-vs-full-upfront choice. `true` pays only the
128
+ * deposit now, `false` pays the full price now. `undefined` means "not chosen"
129
+ * — `book()` omits the field and the preview keeps its deposit default.
130
+ */
131
+ depositSelected: Signal<boolean | undefined>;
120
132
  /** Actions for modifying booking state */
121
133
  actions: BookingServiceActions;
122
134
  }
@@ -146,6 +158,11 @@ export interface BookingServiceConfig {
146
158
  * service-config heuristic; set to `'ONLINE'` or `'OFFLINE'` to override.
147
159
  */
148
160
  selectedPaymentOption?: SelectedPaymentOption;
161
+ /**
162
+ * Optional initial deposit-vs-full-upfront choice. Omit to keep the server's
163
+ * default.
164
+ */
165
+ depositSelected?: boolean;
149
166
  /** Optional app ID override for the catalog reference. Defaults to the bookings app ID. */
150
167
  appId?: string;
151
168
  /**
@@ -71,6 +71,7 @@ export const BookingService = implementService.withConfig()(BookingServiceDefini
71
71
  const timezoneSignal = signalsService.signal(config.timezone ?? getClientTimezone());
72
72
  const formSubmissionSignal = signalsService.signal(config.formSubmission || null);
73
73
  const selectedPaymentOptionSignal = signalsService.signal(config.selectedPaymentOption);
74
+ const depositSelectedSignal = signalsService.signal(config.depositSelected);
74
75
  // Internal-only signal: set via `setAllDay`, read by `book()`. Not
75
76
  // exposed on the service surface — consumers derive and push the value
76
77
  // (e.g. from the picker's selection mode), so there's no read-back.
@@ -173,6 +174,10 @@ export const BookingService = implementService.withConfig()(BookingServiceDefini
173
174
  setSelectedPaymentOption(value) {
174
175
  selectedPaymentOptionSignal.set(value ?? undefined);
175
176
  },
177
+ // Deposit vs full upfront action
178
+ setDepositSelected(value) {
179
+ depositSelectedSignal.set(value);
180
+ },
176
181
  // All-day (DAY-range) action
177
182
  setAllDay(value) {
178
183
  allDaySignal.set(value);
@@ -192,6 +197,7 @@ export const BookingService = implementService.withConfig()(BookingServiceDefini
192
197
  const location = locationSignal.get();
193
198
  const timezone = timezoneSignal.get();
194
199
  const selectedPaymentOption = selectedPaymentOptionSignal.get();
200
+ const depositSelected = depositSelectedSignal.get();
195
201
  const allDay = allDaySignal.get();
196
202
  // Validation happens inside executeBookAction
197
203
  return executeBookAction({
@@ -202,6 +208,7 @@ export const BookingService = implementService.withConfig()(BookingServiceDefini
202
208
  appId: config.appId,
203
209
  sourceAppId: config.sourceAppId,
204
210
  selectedPaymentOption,
211
+ depositSelected,
205
212
  allDay,
206
213
  });
207
214
  },
@@ -212,6 +219,7 @@ export const BookingService = implementService.withConfig()(BookingServiceDefini
212
219
  timezone: timezoneSignal,
213
220
  formSubmission: formSubmissionSignal,
214
221
  selectedPaymentOption: selectedPaymentOptionSignal,
222
+ depositSelected: depositSelectedSignal,
215
223
  actions,
216
224
  };
217
225
  });
@@ -28,9 +28,12 @@ export type ErrorPaymentConfigResult = {
28
28
  * @param params.totalParticipants - Number of participants (default: 1)
29
29
  * @param params.selectedPaymentOption - Optional consumer-selected payment option;
30
30
  * overrides the service-config heuristic on each line item's `paymentOption`.
31
+ * @param params.depositSelected - Optional deposit-vs-full-upfront choice; `false`
32
+ * resolves a deposit service to full payment. Omit to keep the deposit default.
31
33
  */
32
34
  export declare function loadPaymentConfig(params: {
33
35
  pricingServiceSelections: PricingServiceSelection[];
34
36
  totalParticipants?: number;
35
37
  selectedPaymentOption?: SelectedPaymentOption;
38
+ depositSelected?: boolean;
36
39
  }): Promise<SuccessPaymentConfigResult | ErrorPaymentConfigResult>;
@@ -16,8 +16,17 @@ export interface ServicePaymentOptions {
16
16
  isOnline: boolean;
17
17
  /** Customer can pay in person (on arrival) */
18
18
  isInPerson: boolean;
19
- /** A deposit is required at booking time */
19
+ /**
20
+ * A deposit is required at booking time — including percentage deposits, which
21
+ * set no amount. For the amount use `Payment.PayNow` / `Payment.PayLater`.
22
+ */
20
23
  isDeposit: boolean;
24
+ /**
25
+ * Customers may pay the full price upfront instead of only the deposit.
26
+ * Meaningful only with `isDeposit`; together they gate the
27
+ * deposit-vs-pay-in-full toggle.
28
+ */
29
+ canPayFullUpfront: boolean;
21
30
  }
22
31
  /**
23
32
  * Payment option for line items
@@ -83,6 +92,11 @@ export interface PaymentServiceInternalConfig extends PaymentServiceConfig {
83
92
  * set, overrides the service-config heuristic in line item `paymentOption`.
84
93
  */
85
94
  selectedPaymentOption?: SelectedPaymentOption;
95
+ /**
96
+ * Deposit-vs-full-upfront choice for lazy loading. `false` resolves a deposit
97
+ * service to full payment instead of `DEPOSIT_ONLINE`.
98
+ */
99
+ depositSelected?: boolean;
86
100
  }
87
101
  export interface PaymentServiceAPI {
88
102
  paymentDetailsSignal: Signal<PaymentDetails | null>;
@@ -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.
@@ -119,7 +143,7 @@ async function getPreviewedPrice(bookingLineItems) {
119
143
  deposit: result.priceInfo?.deposit ?? null,
120
144
  };
121
145
  }
122
- async function buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption) {
146
+ async function buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption, depositSelected) {
123
147
  const slots = item.rangeTimeSlots && item.rangeTimeSlots.length > 0
124
148
  ? item.rangeTimeSlots.map((s) => ({
125
149
  localStartDate: s.startDate,
@@ -133,7 +157,12 @@ async function buildLineItemWithPreview(item, totalParticipants, selectedPayment
133
157
  },
134
158
  ]
135
159
  : [];
136
- 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 = requiresDeposit(item.service) && !serviceHasDeposit(item.service);
163
+ if (item.service._id &&
164
+ slots.length &&
165
+ (getDurationRange(item.service) || needsServerResolvedDeposit)) {
137
166
  const bookingLineItems = slots.map((slot) => ({
138
167
  serviceId: item.service._id,
139
168
  localStartDate: slot.localStartDate,
@@ -143,37 +172,34 @@ async function buildLineItemWithPreview(item, totalParticipants, selectedPayment
143
172
  }));
144
173
  const previewedPrice = await getPreviewedPrice(bookingLineItems).catch(() => null);
145
174
  if (previewedPrice != null) {
146
- const depositValue = item.service.payment?.fixed?.deposit?.value;
175
+ const depositValue = getDepositValue(item.service);
147
176
  const previewedDeposit = previewedPrice.deposit;
148
- const hasDeposit = previewedDeposit != null
149
- ? previewedDeposit > 0
150
- : !!depositValue && Number(depositValue) > 0;
177
+ const hasDeposit = requiresDeposit(item.service) &&
178
+ (previewedDeposit != null
179
+ ? previewedDeposit > 0
180
+ : serviceHasDeposit(item.service));
151
181
  const deposit = hasDeposit
152
182
  ? previewedDeposit != null
153
183
  ? previewedDeposit
154
184
  : Number(depositValue) * totalParticipants
155
185
  : undefined;
156
- 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),
166
- };
186
+ return toLineItem({
187
+ lineItemId: item.lineItemId,
188
+ serviceId: item.service._id,
189
+ price: previewedPrice.price,
190
+ deposit,
191
+ paymentOption: getPaymentOption(item.service, hasDeposit, selectedPaymentOption, depositSelected),
192
+ });
167
193
  }
168
194
  }
169
- return buildLineItem(item.service, totalParticipants, item.lineItemId, selectedPaymentOption);
195
+ return buildLineItem(item.service, totalParticipants, item.lineItemId, selectedPaymentOption, depositSelected);
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 lineItems = await Promise.all(data.map((item) => buildLineItemWithPreview(item, totalParticipants, selectedPaymentOption, depositSelected)));
177
203
  const response = await calculateTotals({
178
204
  lineItems,
179
205
  });
@@ -204,7 +230,7 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
204
230
  const errorSignal = signalsService.signal(null);
205
231
  const serviceOptionsSignal = signalsService.signal(config.serviceOptions || DEFAULT_SERVICE_OPTIONS);
206
232
  // Internal calculate function
207
- const calculate = async (pricingServiceSelections, totalParticipants, selectedPaymentOption) => {
233
+ const calculate = async (pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected) => {
208
234
  if (!pricingServiceSelections.length) {
209
235
  paymentDetailsSignal.set(null);
210
236
  return;
@@ -212,7 +238,7 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
212
238
  isLoadingSignal.set(true);
213
239
  errorSignal.set(null);
214
240
  try {
215
- const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption);
241
+ const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected);
216
242
  paymentDetailsSignal.set(paymentDetails);
217
243
  }
218
244
  catch (error) {
@@ -240,16 +266,17 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
240
266
  // Mode 2: Lazy loading - pricingServiceSelections provided via internal config
241
267
  else if (config.pricingServiceSelections?.length) {
242
268
  serviceOptionsSignal.set(deriveServiceOptions(config.pricingServiceSelections));
243
- void calculate(config.pricingServiceSelections, config.totalParticipants ?? 1, config.selectedPaymentOption);
269
+ void calculate(config.pricingServiceSelections, config.totalParticipants ?? 1, config.selectedPaymentOption, config.depositSelected);
244
270
  }
245
271
  // Mode 3: Reactive - observe BookingService signals (client-side only)
246
272
  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).
273
+ // Computed signal that changes when services, selected time slots, or the
274
+ // consumer's payment choices change (not participants).
249
275
  const servicesKey = signalsService.computed(() => {
250
276
  const selections = bookingService?.serviceSelections.get() || [];
251
277
  const timezone = bookingService?.timezone.get();
252
278
  const selectedPaymentOption = bookingService?.selectedPaymentOption.get();
279
+ const depositSelected = bookingService?.depositSelected.get();
253
280
  return JSON.stringify({
254
281
  selections: selections.map((s) => ({
255
282
  serviceId: s.service._id,
@@ -259,16 +286,18 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
259
286
  })),
260
287
  timezone,
261
288
  selectedPaymentOption,
289
+ depositSelected,
262
290
  });
263
291
  });
264
292
  signalsService.effect(() => {
265
- // Subscribe to servicesKey to trigger effect when service, time
266
- // slot, or selected payment option changes.
293
+ // Subscribe to servicesKey to trigger effect when service, time slot,
294
+ // or payment choices change.
267
295
  servicesKey.get();
268
296
  // Read current selections without subscribing (peek)
269
297
  const selections = bookingService?.serviceSelections.peek() || [];
270
298
  const timezone = bookingService?.timezone.peek();
271
299
  const selectedPaymentOption = bookingService?.selectedPaymentOption.peek();
300
+ const depositSelected = bookingService?.depositSelected.peek();
272
301
  // For single service mode, take the first selection
273
302
  const firstSelection = selections[0];
274
303
  const numParticipants = firstSelection?.totalParticipants || 1;
@@ -283,7 +312,7 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
283
312
  if (pricingServiceSelections.length) {
284
313
  serviceOptionsSignal.set(deriveServiceOptions(pricingServiceSelections));
285
314
  }
286
- void calculate(pricingServiceSelections, numParticipants, selectedPaymentOption);
315
+ void calculate(pricingServiceSelections, numParticipants, selectedPaymentOption, depositSelected);
287
316
  });
288
317
  }
289
318
  return {
@@ -301,10 +330,12 @@ export const PaymentService = implementService.withConfig()(PaymentServiceDefini
301
330
  * @param params.totalParticipants - Number of participants (default: 1)
302
331
  * @param params.selectedPaymentOption - Optional consumer-selected payment option;
303
332
  * overrides the service-config heuristic on each line item's `paymentOption`.
333
+ * @param params.depositSelected - Optional deposit-vs-full-upfront choice; `false`
334
+ * resolves a deposit service to full payment. Omit to keep the deposit default.
304
335
  */
305
336
  export async function loadPaymentConfig(params) {
306
337
  try {
307
- const { pricingServiceSelections, totalParticipants = 1, selectedPaymentOption, } = params;
338
+ const { pricingServiceSelections, totalParticipants = 1, selectedPaymentOption, depositSelected, } = params;
308
339
  if (!pricingServiceSelections.length) {
309
340
  return {
310
341
  type: 'success',
@@ -314,7 +345,7 @@ export async function loadPaymentConfig(params) {
314
345
  },
315
346
  };
316
347
  }
317
- const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption);
348
+ const paymentDetails = await calculatePaymentDetails(pricingServiceSelections, totalParticipants, selectedPaymentOption, depositSelected);
318
349
  return {
319
350
  type: 'success',
320
351
  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;