← Files WixARCHIVED FILE
skills/wix-headless-templates/rentals/rest/rentals.ts
7.81 KB · Oct 8, 2026 · 12:02 UTC
// Rentals over REST — the twin of app/wix/rentals/rentals.ts. Same exports, same DTOs; every body
// comes from rentals-core (the SAME file the SDK transport uses, deployed flat next to this one), so
// this file is only the transport: literal paths, one fetch per step, all with the visitor token
// (see ./client). Failures are loud where a visitor acts (renting) and silent-with-a-default where
// they only inform (form, preview).
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/query-services.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slots.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slot-end-options.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/pricing/pricing-api/preview-price.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-writer-v2/create-booking.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
import { wixRequest } from "./client.js";
import { imgSrc } from "./media.js";
import {
FALLBACK_FIELDS,
RENTALS_APP_ID,
RENTALS_FORM_ID,
RENTALS_PAGE_SIZE,
bookingIdOf,
bookingOptions,
bookingRequest,
cartIdOf,
cartRequest,
checkoutRedirectRequest,
checkoutRequired,
confirmedResult,
contactOf,
dailyEndOptions,
defaultTimeZone,
endOptionsRequest,
friendlyError,
isRental,
nextSlotsCursor,
previewRequest,
startsRequest,
toDetail,
toFormFields,
toHourlyEndOptions,
toQuote,
toStartOptions,
toSummary,
type Raw,
type StartWindow,
} from "./rentals-core.js";
import type { EndOption, RentalDetail, RentalFormField, RentalQuote, RentalResult, RentalSummary, StartOption } from "./types.js";
export { RENTALS_APP_ID, RENTALS_FORM_ID, RENTALS_PAGE_SIZE, dailyEndOptions, defaultTimeZone };
export interface RentalsPage {
items: RentalSummary[];
hasMore: boolean;
}
/**
* POST /bookings/v2/services/query { query: { filter: { appId, hidden: false }, paging: { limit, offset } } }
*/
export async function fetchRentals({ limit = RENTALS_PAGE_SIZE, offset = 0 }: { limit?: number; offset?: number } = {}): Promise<RentalsPage> {
const res = await wixRequest<Raw>("/bookings/v2/services/query", { body: { query: { filter: { appId: RENTALS_APP_ID, hidden: false }, paging: { limit, offset } } } });
const items = ((res.services ?? []) as Raw[]).filter(isRental).map((r) => toSummary(r, imgSrc));
const total = Number(res.pagingMetadata?.total ?? 0);
return { items, hasMore: total ? offset + (res.services?.length ?? 0) < total : false };
}
/** POST /bookings/v2/services/query filtered on mainSlug.name — null when not found or not a rental. */
export async function fetchRentalBySlug(slug: string): Promise<RentalDetail | null> {
const res = await wixRequest<Raw>("/bookings/v2/services/query", { body: { query: { filter: { "mainSlug.name": slug, appId: RENTALS_APP_ID, hidden: false }, paging: { limit: 1 } } } });
const raw = (res.services ?? [])[0] as Raw | undefined;
return raw && isRental(raw) ? toDetail(raw, imgSrc) : null;
}
export interface StartsPage {
options: StartOption[];
timeZone: string | null;
}
/** POST /service-availability/v2/time-slots (cursor followed) — the starts a customer can pick. */
export async function fetchStarts(rental: Pick<RentalDetail, "id" | "unit" | "resourceTypeId">, window: StartWindow = {}): Promise<StartsPage> {
const raws: Raw[] = [];
let timeZone: string | null = null;
let cursor: string | undefined;
do {
const res = await wixRequest<Raw>("/service-availability/v2/time-slots", { body: startsRequest(rental, { ...window, cursor }) });
raws.push(...((res.timeSlots ?? []) as Raw[]));
timeZone ??= res.timeZone ?? null;
cursor = nextSlotsCursor(res) ?? undefined;
} while (cursor);
return { options: toStartOptions(raws, rental.unit), timeZone };
}
/**
* HOURLY: POST /service-availability/v2/time-slots/end-options { serviceId, localStartDate, timeZone, location }
* (the response field is `endOptions`). DAILY: walked locally from the starts already fetched.
*/
export async function fetchEndOptions(rental: Pick<RentalDetail, "id" | "unit" | "minUnits" | "maxUnits">, start: StartOption, { timeZone, availableDayKeys = [] }: { timeZone?: string | null; availableDayKeys?: string[] } = {}): Promise<EndOption[]> {
if (rental.unit === "DAY") return dailyEndOptions(start, availableDayKeys, rental);
const res = await wixRequest<Raw>("/service-availability/v2/time-slots/end-options", { body: { serviceId: rental.id, ...endOptionsRequest(start, timeZone, "id") } });
return toHourlyEndOptions(res, start);
}
/** POST /bookings/v2/pricing/preview { bookingLineItems: [...] } — falls back to rate × length. */
export async function fetchQuote(rental: Pick<RentalDetail, "id" | "rateAmount" | "currency" | "unit">, start: StartOption, end: EndOption, timeZone: string = defaultTimeZone()): Promise<RentalQuote> {
try {
const res = await wixRequest<Raw>("/bookings/v2/pricing/preview", { body: { bookingLineItems: previewRequest(rental, start, end.endLocal, timeZone) } });
return toQuote(res, rental, end.units);
} catch {
return toQuote(null, rental, end.units);
}
}
/** GET /form-schema-service/v4/forms/{id}/summary (+ the schema for `required`); contact basics when unreadable. */
export async function fetchRentalForm(formId: string | null): Promise<RentalFormField[]> {
const id = encodeURIComponent(formId ?? RENTALS_FORM_ID);
try {
const [summary, form] = await Promise.all([
wixRequest<Raw>(`/form-schema-service/v4/forms/${id}/summary`, { method: "GET" }),
wixRequest<Raw>(`/form-schema-service/v4/forms/${id}`, { method: "GET" }).catch(() => null),
]);
return toFormFields(summary?.formSummary, form?.form);
} catch {
return FALLBACK_FIELDS;
}
}
/**
* POST /bookings/v2/bookings → POST /ecom/v2/carts → POST /ecom/v2/carts/{id}/calculate →
* POST /headless/v1/redirect-session (paid) | POST /ecom/v2/carts/{id}/place-order (free / offline).
*/
export async function rentResource(rental: RentalDetail, start: StartOption, end: EndOption, formValues: Record<string, unknown>, timeZone: string = defaultTimeZone()): Promise<RentalResult> {
let created: Raw;
try {
created = await wixRequest<Raw>("/bookings/v2/bookings", { body: { booking: bookingRequest(rental, start, end.endLocal, timeZone, "id"), ...bookingOptions(formValues) } });
} catch (e) {
throw new Error(friendlyError(e instanceof Error ? e.message : String(e)));
}
const bookingId = bookingIdOf(created);
if (!bookingId) throw new Error("The rental couldn't be booked — the time may have just been taken.");
const newCart = await wixRequest<Raw>("/ecom/v2/carts", { body: cartRequest([bookingId], contactOf(created), start.location?.type === "BUSINESS" ? start.location.id : null) });
const cartId = cartIdOf(newCart);
if (!cartId) throw new Error("The rental couldn't be reserved — please try again.");
const calc = await wixRequest<Raw>(`/ecom/v2/carts/${cartId}/calculate`, { body: {} });
if (checkoutRequired(rental, calc)) {
const origin = typeof window !== "undefined" ? window.location.origin : "";
const session = await wixRequest<Raw>("/headless/v1/redirect-session", { body: checkoutRedirectRequest(cartId, origin) });
const url = session?.redirectSession?.fullUrl;
if (!url) throw new Error("Checkout couldn't start — please try again.");
return { kind: "redirect", url };
}
const order = await wixRequest<Raw>(`/ecom/v2/carts/${cartId}/place-order`, { body: {} });
return confirmedResult(bookingId, order);
}
SHA-256: 30780bc208ef699c78950a1ad93d60e584250dac8500e78b1b45247028d5ea51