@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.
- package/cjs/dist/__mocks__/booking/booking-client-service.js +4 -1
- package/cjs/dist/__mocks__/services/services-mocks.d.ts +13 -0
- package/cjs/dist/__mocks__/services/services-mocks.js +92 -1
- package/cjs/dist/react/core/booking/Booking.d.ts +6 -0
- package/cjs/dist/react/core/booking/Booking.js +2 -0
- package/cjs/dist/react/core/payment/Payment.d.ts +2 -1
- package/cjs/dist/react/core/payment/Payment.js +11 -8
- package/cjs/dist/react/core/service/Service.js +4 -2
- package/cjs/dist/react/payment/Payment.d.ts +3 -2
- package/cjs/dist/react/payment/Payment.js +11 -5
- package/cjs/dist/services/booking/book-action/buildBookingRequest.js +13 -4
- package/cjs/dist/services/booking/book-action/types.d.ts +6 -0
- package/cjs/dist/services/booking/booking.d.ts +17 -0
- package/cjs/dist/services/booking/booking.js +8 -0
- package/cjs/dist/services/payment/payment.d.ts +3 -0
- package/cjs/dist/services/payment/payment.def.d.ts +15 -1
- package/cjs/dist/services/payment/payment.js +89 -58
- package/cjs/dist/services/service/service.js +1 -1
- package/cjs/dist/services/servicePayment.d.ts +26 -0
- package/cjs/dist/services/servicePayment.js +46 -0
- package/cjs/dist/utils/money.d.ts +12 -2
- package/cjs/dist/utils/money.js +30 -18
- package/dist/__mocks__/booking/booking-client-service.js +4 -1
- package/dist/__mocks__/services/services-mocks.d.ts +13 -0
- package/dist/__mocks__/services/services-mocks.js +91 -0
- package/dist/react/core/booking/Booking.d.ts +6 -0
- package/dist/react/core/booking/Booking.js +2 -0
- package/dist/react/core/payment/Payment.d.ts +2 -1
- package/dist/react/core/payment/Payment.js +11 -8
- package/dist/react/core/service/Service.js +4 -2
- package/dist/react/payment/Payment.d.ts +3 -2
- package/dist/react/payment/Payment.js +11 -5
- package/dist/services/booking/book-action/buildBookingRequest.js +13 -4
- package/dist/services/booking/book-action/types.d.ts +6 -0
- package/dist/services/booking/booking.d.ts +17 -0
- package/dist/services/booking/booking.js +8 -0
- package/dist/services/payment/payment.d.ts +3 -0
- package/dist/services/payment/payment.def.d.ts +15 -1
- package/dist/services/payment/payment.js +89 -58
- package/dist/services/service/service.js +1 -1
- package/dist/services/servicePayment.d.ts +26 -0
- package/dist/services/servicePayment.js +40 -0
- package/dist/utils/money.d.ts +12 -2
- package/dist/utils/money.js +30 -18
- package/package.json +3 -3
|
@@ -45,6 +45,12 @@ export interface BookingData {
|
|
|
45
45
|
* heuristic applies. Set via `actions.setSelectedPaymentOption`.
|
|
46
46
|
*/
|
|
47
47
|
selectedPaymentOption: SelectedPaymentOption | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Deposit-vs-full-upfront choice. `true` pays only the deposit now, `false`
|
|
50
|
+
* pays the full price now, `undefined` means not chosen. Set via
|
|
51
|
+
* `actions.setDepositSelected`.
|
|
52
|
+
*/
|
|
53
|
+
depositSelected: boolean | undefined;
|
|
48
54
|
actions: BookingServiceActions;
|
|
49
55
|
}
|
|
50
56
|
/**
|
|
@@ -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
|
|
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 &&
|
|
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
|
|
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,
|
|
@@ -19,8 +19,10 @@ function RootContent({ children, }) {
|
|
|
19
19
|
const selected = serviceService.selectedSignal.get();
|
|
20
20
|
const bookable = serviceService.bookableSignal.get();
|
|
21
21
|
// In "from booking signal" mode, render null when no service is selected
|
|
22
|
-
// The signal will trigger a re-render when a service is selected
|
|
23
|
-
|
|
22
|
+
// The signal will trigger a re-render when a service is selected.
|
|
23
|
+
// While a slug/id-based fetch is in flight, keep rendering the subtree so
|
|
24
|
+
// consumers' loading skeletons (<Service.Raw>) can show instead of a blank screen.
|
|
25
|
+
if ((!service || !service._id) && !serviceService.isLoadingSignal.get()) {
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
if (typeof children === 'function') {
|
|
@@ -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: {
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
/**
|
|
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`:
|
|
28
|
-
*
|
|
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:
|
|
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
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
|
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 (
|
|
81
|
+
if (chargeDeposit) {
|
|
76
82
|
return 'DEPOSIT_ONLINE';
|
|
77
83
|
}
|
|
78
84
|
return 'FULL_PAYMENT_ONLINE';
|
|
79
85
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
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:
|
|
96
|
+
catalogItemId: serviceId || '',
|
|
96
97
|
appId: BOOKING_APP_ID,
|
|
97
98
|
},
|
|
98
99
|
price: String(price),
|
|
99
|
-
...(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
|
-
|
|
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
|
|
175
|
+
const depositValue = getDepositValue(item.service);
|
|
147
176
|
const previewedDeposit = previewedPrice.deposit;
|
|
148
|
-
const hasDeposit =
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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: {
|