1{2 "solution": "rentals",3 "requires": [4 "shared"5 ],6 "entrypoints": [7 "js/wix/rental-flow-store.js",8 "js/wix/rentals-store.js",9 "js/wix/rentals.js"10 ],11 "files": {12 "js/wix/rental-flow-store.js": "// Declarations of rental-flow-store.ts: what this file exports, their arguments and what they return.\n
error = e instanceof Error ? e.message : String(e);
\n
throw e;
\n
}
\n
finally {
\n
renting = false;
\n
emit();
\n
}
\n
},
\n
};
\n
}
\n
"
,
13 "js/wix/rentals-core.js": "// Declarations of rentals-core.ts: what this file exports, their arguments and what they return.\n// import type { EndOption, RentalDetail, RentalFormField, RentalLocation, RentalQuote, RentalResult, RentalSummary, RentalUnit, StartOption } from \"./rentals-types.js\";\n// export type Raw = Record<string, any>;\n// export type ImgSrc = (value: any, width: number, height: number) => string;\n// export declare const RENTALS_APP_ID = \"ff5d6eb1-65e4-4f9a-8b14-64d34c12cc2e\";\n// export declare const RENTALS_FORM_ID = \"3a2ea2ce-91f4-4617-ab24-629933c0c31a\";\n// export declare const RENTALS_PAGE_SIZE = 20;\n// export declare const SLOTS_PAGE_LIMIT = 1000;\n// export declare const rawId: (raw: Raw | undefined | null) => string;\n// export declare function formatMoney(value: string | number | undefined | null, currency: string | undefined | null, locale?: string): string;\n// export declare function unitsLabel(units: number, unit: RentalUnit): string;\n// export declare function rangeLabel(minUnits: number, maxUnits: number, unit: RentalUnit): string;\n// export declare function toLocalDateString(d: Date): string;\n// export declare const startOfDay: (d: Date) => Date;\n// export declare const defaultTimeZone: () => string;\n// export declare const dayKeyOf: (local: string) => string;\n// export declare function midnightAfter(dayKey: string): string;\n// export declare function addDaysKey(dayKey: string, days: number): string;\n// export declare function timeLabel(local: string, locale?: string): string;\n// export declare function dayLabel(dayKey: string, locale?: string): string;\n// export interface DurationRange {\n// unit: RentalUnit;\n// minUnits: number;\n// maxUnits: number;\n// }\n// export declare function durationRangeOf(raw: Raw): DurationRange | null;\n// export declare const isRental: (raw: Raw) => boolean;\n// export declare function toLocation(raw: Raw): RentalLocation;\n// export declare function toSummary(raw: Raw, imgSrc: ImgSrc): RentalSummary;\n// export declare function toDetail(raw: Raw, imgSrc: ImgSrc): RentalDetail;\n// export interface StartWindow {\n// from?: Date;\n// days?: number;\n// timeZone?: string;\n// cursor?: string;\n// }\n// export declare function startsRequest(rental: Pick<RentalDetail, \"id\" | \"unit\" | \"resourceTypeId\">, { from, days, timeZone, cursor }: StartWindow): Raw;\n// export declare const nextSlotsCursor: (res: Raw | null | undefined) => string | null;\n// export declare function toStartOption(raw: Raw, unit: RentalUnit): StartOption;\n// export declare function toStartOptions(raws: Raw[], unit: RentalUnit): StartOption[];\n// export declare function endOptionsRequest(start: StartOption, timeZone: string | null | undefined, idKey?: \"_id\" | \"id\"): Raw;\n// export declare function toHourlyEndOptions(res: Raw | null | undefined, start: StartOption): EndOption[];\n// export declare function dailyEndOptions(start: StartOption, availableDayKeys: string[], range: Pick<RentalDetail, \"minUnits\" | \"maxUnits\">): EndOption[];\n// export declare function previewRequest(rental: Pick<RentalDetail, \"id\">, start: StartOption, endLocal: string, timeZone: string): Raw[];\n// export declare function toQuote(res: Raw | null | undefined, rental: Pick<RentalDetail, \"rateAmount\" | \"currency\" | \"unit\">, units: number): RentalQuote;\n// export declare const FALLBACK_FIELDS: RentalFormField[];\n// export declare function toFormFields(summary: Raw | null | undefined, form?: Raw | null): RentalFormField[];\n// export declare function bookingLocationType(type: RentalLocation[\"type\"] | undefined): \"OWNER_BUSINESS\" | \"CUSTOM\" | \"OWNER_CUSTOM\";\n// export declare function bookingRequest(rental: Pick<RentalDetail, \"id\" | \"paymentOption\">, start: StartOption, endLocal: string, timeZone: string, idKey?: \"_id\" | \"id\"): Raw;\n// export declare function bookingOptions(formValues: Record<string, unknown>): Raw;\n// export declare const bookingIdOf: (res: Raw | null | undefined) => string;\n// export declare const contactOf: (res: Raw | null | undefined) => Raw | undefined;\n// export declare function cartRequest(bookingIds: string[], contact?: Raw | null, locationId?: string | null): Raw;\n// export declare const cartIdOf: (res: Raw | null | undefined) => string;\n// export declare const cartTotal: (calc: Raw | null | undefined) => number;\n// export declare function checkoutRequired(rental: Pick<RentalDetail, \"cancellationFeeEnabled\">, calc: Raw | null | undefined): boolean;\n// export declare function checkoutRedirectRequest(cartId: string, origin: string): Raw;\n// export declare function confirmedResult(bookingId: string, order: Raw | null | undefined): RentalResult;\n// export declare function friendlyError(message: string): string;\n/** The Wix Rentals app: the service's appId, the catalog filter, the cart's catalogReference.appId. */\nexport const RENTALS_APP_ID = \"ff5d6eb1-65e4-4f9a-8b14-64d34c12cc2e\";\n/** The Rentals default booking form, provisioned by the install with the same id on every site. */\nexport const RENTALS_FORM_ID = \"3a2ea2ce-91f4-4617-ab24-629933c0c31a\";\nexport const RENTALS_PAGE_SIZE = 20;\n/** Slots per page the availability API allows; the transports follow the cursor. */\nexport const SLOTS_PAGE_LIMIT = 1000;\nexport const rawId = (raw) => raw?._id ?? raw?.id ?? \"\";\n// ---- money and labels ------------------------------------------------------------------------------\n/** \"$40\" from a Wix money-like `{ value, currency }`; \"\" when either is missing (never a guessed currency). */\nexport function formatMoney(value, currency, locale) {\n if (value == null || value === \"\" || !currency)\n return \"\";\n const n = Number(value);\n if (!Number.isFinite(n))\n return \"\";\n try {\n const opts = { style: \"currency\", currency };\n if (Number.isInteger(n))\n opts.maximumFractionDigits = 0;\n return new Intl.NumberFormat(locale, opts).format(n);\n }\n catch {\n return \"\";\n }\n}\nconst unitWord = (unit, n) => (unit === \"DAY\" ? (n === 1 ? \"day\" : \"days\") : n === 1 ? \"hour\" : \"hours\");\n/** \"2 hours\", \"1.5 hours\", \"3 days\". */\nexport function unitsLabel(units, unit) {\n const n = Math.round(units * 100) / 100;\n return `${n} ${unitWord(unit, n)}`;\n}\nexport function rangeLabel(minUnits, maxUnits, unit) {\n if (!minUnits || !maxUnits)\n return \"\";\n if (minUnits === maxUnits)\n return unitsLabel(minUnits, unit);\n return `${Math.round(minUnits * 100) / 100} to ${unitsLabel(maxUnits, unit)}`;\n}\n// ---- dates ---------------------------------------------------------------------------------------------\n/** Local wall-clock \"YYYY-MM-DDThh:mm:ss\" for a Date (no zone, no Z) — the format the availability API speaks. */\nexport function toLocalDateString(d) {\n const p = (n) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;\n}\nexport const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nexport const defaultTimeZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;\nexport const dayKeyOf = (local) => local.slice(0, 10);\n/** Midnight of the day after `dayKey`, as a local wall-clock string. */\nexport function midnightAfter(dayKey) {\n const [y, m, d] = dayKey.split(\"-\").map(Number);\n return toLocalDateString(new Date(y, m - 1, d + 1));\n}\nexport function addDaysKey(dayKey, days) {\n const [y, m, d] = dayKey.split(\"-\").map(Number);\n return toLocalDateString(new Date(y, m - 1, d + days)).slice(0, 10);\n}\nconst minutesBetween = (a, b) => Math.round((new Date(b).getTime() - new Date(a).getTime()) / 60000);\nexport function timeLabel(local, locale) {\n try {\n return new Date(local).toLocaleTimeString(locale, { hour: \"numeric\", minute: \"2-digit\" });\n }\n catch {\n return local.slice(11, 16);\n }\n}\nexport function dayLabel(dayKey, locale) {\n try {\n const [y, m, d] = dayKey.split(\"-\").map(Number);\n return new Date(y, m - 1, d).toLocaleDateString(locale, { weekday: \"short\", month: \"short\", day: \"numeric\" });\n }\n catch {\n return dayKey;\n }\n}\n/** The customer-pickable range off a service; HOUR minutes become hours. A service without one is not a rental. */\nexport function durationRangeOf(raw) {\n const range = raw.schedule?.availabilityConstraints?.durationRange;\n if (!range?.unitType)\n return null;\n if (range.unitType === \"DAY\") {\n const o = range.dayOptions ?? {};\n return { unit: \"DAY\", minUnits: Number(o.minDurationInDays ?? 0), maxUnits: Number(o.maxDurationInDays ?? 0) };\n }\n const o = range.hourOptions ?? {};\n return { unit: \"HOUR\", minUnits: Number(o.minDurationInMinutes ?? 0) / 60, maxUnits: Number(o.maxDurationInMinutes ?? 0) / 60 };\n}\n/** A service is a rental when it carries the Rentals app id AND a duration range. */\nexport const isRental = (raw) => raw?.appId === RENTALS_APP_ID && durationRangeOf(raw) !== null;\nexport function toLocation(raw) {\n const type = ([\"BUSINESS\", \"CUSTOM\", \"CUSTOMER\"].includes(raw?.type) ? raw.type : \"BUSINESS\");\n return { id: rawId(raw?.business) || null, name: raw?.business?.name ?? raw?.custom?.address?.formattedAddress ?? \"\", type };\n}\nexport function toSummary(raw, imgSrc) {\n const range = durationRangeOf(raw) ?? { unit: \"HOUR\", minUnits: 0, maxUnits: 0 };\n const payment = raw.payment ?? {};\n const price = payment.fixed?.price;\n const rateAmount = Number(price?.value ?? 0) || 0;\n const currency = price?.currency ?? \"\";\n const free = payment.rateType === \"NO_FEE\" || (payment.rateType === \"FIXED\" && rateAmount === 0);\n const ratePerUnit = free ? \"Free\" : formatMoney(price?.value, currency);\n const serviceResources = (raw.serviceResources ?? []);\n const resourceTypeId = raw.primaryResourceType ?? rawId(serviceResources[0]?.resourceType) ?? null;\n const resourceCount = serviceResources.reduce((n, sr) => n + (sr.resourceIds?.values ?? []).length, 0);\n return {\n id: rawId(raw),\n slug: raw.mainSlug?.name ?? \"\",\n name: raw.name ?? \"\",\n tagLine: raw.tagLine ?? \"\",\n unit: range.unit,\n ratePerUnit,\n rateLabel: ratePerUnit && !free ? `${ratePerUnit} / ${unitWord(range.unit, 1)}` : ratePerUnit,\n rateAmount,\n currency,\n minUnits: range.minUnits,\n maxUnits: range.maxUnits,\n rangeLabel: rangeLabel(range.minUnits, range.maxUnits, range.unit),\n imageUrl: raw.media?.mainMedia?.image ? imgSrc(raw.media.mainMedia.image, 800, 600) : \"\",\n resourceTypeId: resourceTypeId || null,\n resourceCount,\n locations: (raw.locations ?? []).map(toLocation),\n onlineBookingEnabled: raw.onlineBooking?.enabled !== false,\n requiresManualApproval: raw.onlineBooking?.requireManualApproval === true,\n free,\n };\n}\nexport function toDetail(raw, imgSrc) {\n const summary = toSummary(raw, imgSrc);\n const online = raw.payment?.options?.online === true;\n const inPerson = raw.payment?.options?.inPerson === true;\n // Derived, never hardcoded: an in-person-only (or free) rental booked ONLINE is refused by the cart.\n const paymentOption = !online && inPerson ? \"OFFLINE\" : \"ONLINE\";\n return {\n ...summary,\n description: raw.description ?? \"\",\n formId: rawId(raw.form) || null,\n paymentOption,\n cancellationFeeEnabled: raw.bookingPolicy?.cancellationFeePolicy?.enabled === true,\n scheduleId: rawId(raw.schedule) || null,\n };\n}\n/**\n * The List Availability Time Slots body for a rental. `includeResourceTypeIds` = the rental's\n * primary resource type — without it the slots come back with no resource to book. A DAILY rental\n * asks for one slot per day (the start of each available day).\n */\nexport function startsRequest(rental, { from = new Date(), days = 7, timeZone, cursor }) {\n const start = startOfDay(from);\n const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + days);\n return {\n serviceId: rental.id,\n fromLocalDate: toLocalDateString(start),\n toLocalDate: toLocalDateString(end), // exclusive: midnight after the last day, never 23:59:59\n ...(timeZone ? { timeZone } : {}),\n ...(rental.resourceTypeId ? { includeResourceTypeIds: [rental.resourceTypeId] } : {}),\n bookable: true,\n ...(rental.unit === \"DAY\" ? { timeSlotsPerDay: 1 } : {}),\n cursorPaging: { limit: SLOTS_PAGE_LIMIT, ...(cursor ? { cursor } : {}) },\n };\n}\nexport const nextSlotsCursor = (res) => res?.pagingMetadata?.cursors?.next ?? res?.cursorPagingMetadata?.cursors?.next ?? null;\nconst toSlotLocation = (raw) => {\n if (!raw || (!rawId(raw) && !raw.name))\n return null;\n const type = ([\"BUSINESS\", \"CUSTOM\", \"CUSTOMER\"].includes(raw.locationType) ? raw.locationType : \"BUSINESS\");\n return { id: rawId(raw) || null, name: raw.name ?? \"\", type };\n};\nexport function toStartOption(raw, unit) {\n const start = raw.localStartDate ?? \"\";\n const resource = (raw.availableResources ?? []).flatMap((ar) => (ar.resources ?? []))[0];\n return {\n key: `${start}|${rawId(raw.location)}`,\n startLocal: start,\n dayKey: dayKeyOf(start),\n label: unit === \"DAY\" ? dayLabel(dayKeyOf(start)) : timeLabel(start),\n bookable: raw.bookable !== false,\n location: toSlotLocation(raw.location),\n resource: resource && rawId(resource) ? { id: rawId(resource), name: resource.name ?? \"\" } : null,\n scheduleId: raw.scheduleId ?? null,\n };\n}\n/** Raw slots from one or more pages → deduped (one per start and location), ordered. */\nexport function toStartOptions(raws, unit) {\n const byKey = new Map();\n for (const raw of raws) {\n const key = `${raw.localStartDate ?? \"\"}|${rawId(raw.location)}`;\n const seen = byKey.get(key);\n if (!seen)\n byKey.set(key, raw);\n else if (!seen.availableResources?.length && raw.availableResources?.length)\n byKey.set(key, raw);\n }\n return [...byKey.values()].map((r) => toStartOption(r, unit)).sort((a, b) => a.startLocal.localeCompare(b.startLocal));\n}\n// ---- availability: lengths ----------------------------------------------------------------------------\n/**\n * HOURLY: the List Availability Time Slot End Options body for a chosen start. `location` is\n * required and is the start's own; the service's maximum caps the response, so no maxLocalEndDate.\n * The SDK takes `serviceId` as a positional first argument; REST sends it in the body. `idKey`\n * spells the location id the way the transport wants it: `_id` on the SDK, `id` on REST.\n */\nexport function endOptionsRequest(start, timeZone, idKey = \"id\") {\n return {\n localStartDate: start.startLocal,\n ...(timeZone ? { timeZone } : {}),\n location: start.location\n ? { ...(start.location.id ? { [idKey]: start.location.id } : {}), name: start.location.name, locationType: start.location.type }\n : { locationType: \"BUSINESS\" },\n };\n}\n/** HOURLY end options (the response field is `endOptions`, not `timeSlots`) → lengths, shortest first. */\nexport function toHourlyEndOptions(res, start) {\n return (res?.endOptions ?? [])\n .map((o) => o.localEndDate)\n .filter(Boolean)\n .map((endLocal) => {\n const units = minutesBetween(start.startLocal, endLocal) / 60;\n return { endLocal, units, label: `${unitsLabel(units, \"HOUR\")} · until ${timeLabel(endLocal)}` };\n })\n .filter((o) => o.units > 0)\n .sort((a, b) => a.units - b.units);\n}\n/**\n * DAILY: there is no end-options call. From the chosen start day, walk forward through the available\n * days and stop at the first gap or at the rental's maximum; each reachable length is an option whose\n * end is midnight on the day AFTER its last day. A 24/7 resource (the seed's default) takes the whole\n * range as ONE booking.\n */\nexport function dailyEndOptions(start, availableDayKeys, range) {\n const available = new Set(availableDayKeys);\n const out = [];\n let lastDay = start.dayKey;\n for (let days = 1; days <= Math.max(1, Math.floor(range.maxUnits)); days++) {\n const day = addDaysKey(start.dayKey, days - 1);\n if (!available.has(day))\n break;\n lastDay = day;\n if (days >= Math.max(1, Math.floor(range.minUnits))) {\n out.push({ endLocal: midnightAfter(lastDay), units: days, label: days === 1 ? `1 day · ${dayLabel(lastDay)}` : `${days} days · until ${dayLabel(lastDay)}` });\n }\n }\n return out;\n}\n// ---- price -------------------------------------------------------------------------------------------------\n/**\n * Preview Price line items for a rental: one line, ONE participant (a rental takes one resource; the\n * length drives the price), the resource, and BOTH local dates with the zone — without all three the\n * server silently returns a duration-blind flat rate.\n */\nexport function previewRequest(rental, start, endLocal, timeZone) {\n return [\n {\n serviceId: rental.id,\n ...(start.resource ? { resourceId: start.resource.id } : {}),\n numberOfParticipants: 1,\n localStartDate: start.startLocal,\n localEndDate: endLocal,\n timeZone,\n },\n ];\n}\n/** The server's total (`priceInfo.calculatedPrice`), or the local rate times the length when the preview fails. */\nexport function toQuote(res, rental, units) {\n const server = res?.priceInfo?.calculatedPrice;\n const totalAmount = typeof server === \"number\" && Number.isFinite(server) ? server : Math.round(rental.rateAmount * units * 100) / 100;\n return { total: formatMoney(totalAmount, rental.currency), totalAmount, units, unit: rental.unit };\n}\n// ---- the booking form ---------------------------------------------------------------------------------\nexport const FALLBACK_FIELDS = [\n { target: \"first_name\", label: \"First Name\", type: \"STRING\", required: true },\n { target: \"last_name\", label: \"Last Name\", type: \"STRING\", required: true },\n { target: \"email\", label: \"Email\", type: \"EMAIL\", required: true },\n];\nconst FIELD_TYPES = [\"STRING\", \"EMAIL\", \"PHONE\", \"NUMBER\", \"URL\"];\n/** Flat, render-ready fields from a form summary (+ the full schema for `required`); never empty. */\nexport function toFormFields(summary, form) {\n const requiredByTarget = new Map((form?.fields ?? []).filter((f) => f.target).map((f) => [f.target, f.validation?.required === true]));\n const fields = (summary?.fields ?? [])\n .filter((f) => !f.deleted && f.type && FIELD_TYPES.includes(f.type))\n .map((f) => ({\n target: f.target ?? \"\",\n label: f.label ?? f.target ?? \"\",\n type: f.type,\n ...(Array.isArray(f.options) && f.options.length ? { options: f.options } : {}),\n required: requiredByTarget.get(f.target ?? \"\") ?? true,\n }))\n .filter((f) => f.target);\n return fields.length ? fields : FALLBACK_FIELDS;\n}\n// ---- createBooking → cart → checkout-or-place -------------------------------------------------------------\n/** A start's location type → the booking's. */\nexport function bookingLocationType(type) {\n if (type === \"CUSTOMER\")\n return \"CUSTOM\";\n if (type === \"CUSTOM\")\n return \"OWNER_CUSTOM\";\n return \"OWNER_BUSINESS\";\n}\n/**\n * The Create Booking `booking` object for a rental: the start's schedule, the chosen start and END\n * (the customer's length, never a duration added to the start), the zone, the start's RESOURCE (rentals\n * are resource-driven: no ANY_RESOURCE staff fallback), and the start's location. `idKey` spells ids\n * the way the transport wants them: `_id` on the SDK, `id` on REST.\n */\nexport function bookingRequest(rental, start, endLocal, timeZone, idKey = \"id\") {\n if (!start.resource)\n throw new Error(\"This time has no resource to rent — pick another start.\");\n return {\n selectedPaymentOption: rental.paymentOption,\n totalParticipants: 1,\n bookedEntity: {\n slot: {\n serviceId: rental.id,\n ...(start.scheduleId ? { scheduleId: start.scheduleId } : {}),\n startDate: start.startLocal,\n endDate: endLocal,\n timezone: timeZone,\n // availableResources carries ids only (no names, verified live) — send the name only when known\n resource: { [idKey]: start.resource.id, ...(start.resource.name ? { name: start.resource.name } : {}) },\n location: start.location\n ? { ...(start.location.id ? { [idKey]: start.location.id } : {}), name: start.location.name, locationType: bookingLocationType(start.location.type) }\n : { locationType: \"OWNER_BUSINESS\" },\n },\n },\n };\n}\n/** The rest of the Create Booking request: the form values as the formSubmission, and Wix's own notifications. */\nexport function bookingOptions(formValues) {\n return {\n participantNotification: { notifyParticipants: true, metadata: { channels: \"EMAIL,SMS\" } },\n sendSmsReminder: true,\n formSubmission: formValues,\n };\n}\nexport const bookingIdOf = (res) => rawId(res?.booking);\nexport const contactOf = (res) => res?.booking?.contactDetails ?? undefined;\n/** The Create Cart body holding the booking: catalogItemId is the BOOKING id and appId the RENTALS app. */\nexport function cartRequest(bookingIds, contact, locationId) {\n const cart = { source: { channelType: \"WEB\" } };\n if (locationId)\n cart.businessInfo = { locationId };\n if (contact) {\n cart.customerInfo = {\n ...(contact.firstName ? { firstName: contact.firstName } : {}),\n ...(contact.lastName ? { lastName: contact.lastName } : {}),\n ...(contact.phone ? { phone: contact.phone } : {}),\n ...(contact.email ? { email: contact.email } : {}),\n };\n }\n return { catalogItems: bookingIds.map((id) => ({ quantity: 1, catalogReference: { catalogItemId: id, appId: RENTALS_APP_ID } })), cart };\n}\nexport const cartIdOf = (res) => rawId(res) || rawId(res?.cart);\nexport const cartTotal = (calc) => Number(calc?.summary?.priceSummary?.total?.amount ?? 0);\n/** Hosted checkout whenever a card is involved: a cancellation fee, or a non-zero total not paid fully offline. */\nexport function checkoutRequired(rental, calc) {\n if (rental.cancellationFeeEnabled)\n return true;\n if (cartTotal(calc) === 0)\n return false;\n return calc?.cart?.lineItems?.[0]?.paymentConfig?.paymentOption !== \"FULL_PAYMENT_OFFLINE\";\n}\nexport function checkoutRedirectRequest(cartId, origin) {\n return { ecomCheckout: { checkoutId: cartId }, callbacks: origin ? { postFlowUrl: `${origin}/` } : {} };\n}\nexport function confirmedResult(bookingId, order) {\n return { kind: \"confirmed\", bookingId, orderId: order?.orderId ?? (rawId(order?.order) || null) };\n}\n/** Wix's refusal codes a visitor sees, in plain words. */\nexport function friendlyError(message) {\n if (/SLOT_NOT_AVAILABLE/.test(message))\n return \"That time was just taken. Pick another start.\";\n if (/INVALID_DURATION/.test(message))\n return \"That length is outside what this rental allows. Pick another length.\";\n if (/PREMIUM_VALIDATION_FAILED|SITE_NOT_ACCEPTING/.test(message))\n return \"This site cannot take online bookings yet (no payment method or plan). Contact the business directly.\";\n if (/END_OPTIONS_NOT_SUPPORTED/.test(message))\n return \"This rental is booked by the day.\";\n return message;\n}\n",
14 "js/wix/rentals-store.js": "// Declarations of rentals-store.ts: what this file exports, their arguments and what they return.\n// import type { RentalSummary } from \"./rentals-types.js\";\n// export interface RentalsStoreOptions {\n// initialRentals?: RentalSummary[];\n// initialHasMore?: boolean;\n// pageSize?: number;\n// }\n// export interface RentalsState {\n// rentals: RentalSummary[] | null;\n// hasMore: boolean;\n// loadingMore: boolean;\n// error: string | null;\n// }\n// export interface RentalsStore {\n// getState(): RentalsState;\n// subscribe(listener: (state: RentalsState) => void): () => void;\n// start(): void;\n// stop(): void;\n// loadMore(): Promise<void>;\n// }\n// export declare function createRentalsStore({ initialRentals, initialHasMore, pageSize }?: RentalsStoreOptions): RentalsStore;\n// The rentals listing as a framework-free store — the logic behind useRentals, usable from React\n// (useRentals wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and\n// render), or as the specification for a port. Pages are 20 long with `hasMore`/`loadMore()`.\n// SSR-friendly: seed with `initialRentals` (+ `initialHasMore`) and nothing is fetched for the first\n// paint; without them `start()` loads the first page. One store per mounted listing.\nimport { RENTALS_PAGE_SIZE, fetchRentals } from \"./rentals.js\";\nexport function createRentalsStore({ initialRentals, initialHasMore = false, pageSize = RENTALS_PAGE_SIZE } = {}) {\n let rentals = initialRentals ?? null;\n let hasMore = initialHasMore;\n let loadingMore = false;\n let error = null;\n let started = false;\n let generation = 0;\n const listeners = new Set();\n let snapshot = null;\n const emit = () => {\n snapshot = null;\n for (const fn of listeners)\n fn();\n };\n function getState() {\n if (snapshot)\n return snapshot;\n snapshot = { rentals, hasMore, loadingMore, error };\n return snapshot;\n }\n function loadFirstPage() {\n const id = ++generation;\n rentals = null;\n hasMore = false;\n error = null;\n emit();\n fetchRentals({ limit: pageSize, offset: 0 })\n .then((page) => {\n if (!started || generation !== id)\n return; // superseded — drop it\n rentals = page.items;\n hasMore = page.hasMore;\n emit();\n })\n .catch((e) => {\n if (!started || generation !== id)\n return;\n rentals = [];\n error = e instanceof Error ? e.message : String(e);\n emit();\n });\n }\n return {\n getState,\n subscribe(fn) {\n const listener = () => { try {\n fn(getState());\n }\n catch (e) {\n console.error(\"rentals-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n if (!initialRentals)\n loadFirstPage();\n },\n stop() {\n started = false;\n generation++;\n },\n async loadMore() {\n if (!started || loadingMore || !hasMore || !rentals)\n return;\n const id = generation;\n loadingMore = true;\n emit();\n try {\n const page = await fetchRentals({ limit: pageSize, offset: rentals.length });\n if (!started || generation !== id)\n return;\n rentals = [...rentals, ...page.items];\n hasMore = page.hasMore;\n }\n catch (e) {\n if (!started || generation !== id)\n return;\n error = e instanceof Error ? e.message : String(e);\n }\n finally {\n if (generation === id)\n loadingMore = false;\n emit();\n }\n },\n };\n}\n",
15 "js/wix/rentals-types.js": "// Declarations of rentals-types.ts: what this file exports, their arguments and what they return.\n// /** How a rental is measured and priced: one of the two, never both on one rental. */\n// export type RentalUnit = \"HOUR\" | \"DAY\";\n// /** Where the rental is handed over. BUSINESS locations have an id; CUSTOM/CUSTOMER don't. */\n// export interface RentalLocation {\n// id: string | null;\n// name: string;\n// type: \"BUSINESS\" | \"CUSTOM\" | \"CUSTOMER\";\n// }\n// /** A rental as a listing tile needs it. */\n// export interface RentalSummary {\n// id: string;\n// slug: string;\n// name: string;\n// tagLine: string;\n// unit: RentalUnit;\n// /** Display-ready price per unit: \"$40\" (\"\" when unknown). */\n// ratePerUnit: string;\n// /** \"$40 / hour\" or \"$40 / day\" (\"\" when unknown). */\n// rateLabel: string;\n// /** The per-unit amount as a number with its currency — for running totals only; never render these. */\n// rateAmount: number;\n// currency: string;\n// /** Shortest and longest length the customer may pick, in the rental's unit (hours may be fractional: 0.5). */\n// minUnits: number;\n// maxUnits: number;\n// /** \"1 to 8 hours\" / \"1 to 5 days\" (\"\" when unknown). */\n// rangeLabel: string;\n// /** Resolved https URL (\"\" when the rental has no image). */\n// imageUrl: string;\n// /** The resource type the rental books from, and the concrete resources behind it. */\n// resourceTypeId: string | null;\n// resourceCount: number;\n// locations: RentalLocation[];\n// onlineBookingEnabled: boolean;\n// /** The owner approves each rental by hand: the CTA is \"Request to rent\". */\n// requiresManualApproval: boolean;\n// /** NO_FEE (or a rate of 0): rents without a checkout. */\n// free: boolean;\n// }\n// /** A rental as the booking page needs it. */\n// export interface RentalDetail extends RentalSummary {\n// /** Description text as stored. */\n// description: string;\n// /** The booking form id (the Rentals default form on a seeded site; null → the contact-basics fallback). */\n// formId: string | null;\n// /** Derived payment option the booking must send. */\n// paymentOption: \"ONLINE\" | \"OFFLINE\";\n// /** True → checkout is always required regardless of price (cancellation-fee policy). */\n// cancellationFeeEnabled: boolean;\n// scheduleId: string | null;\n// }\n// /** A start the customer can pick: a time (hourly) or a day at midnight (daily). */\n// export interface StartOption {\n// /** Identity for React keys: start|location. */\n// key: string;\n// /** Local wall-clock \"YYYY-MM-DDThh:mm:ss\" (no Z) in the response's time zone. */\n// startLocal: string;\n// /** \"YYYY-MM-DD\" — for grouping and the daily walk. */\n// dayKey: string;\n// /** \"9:00 AM\" for hourly; \"Mon, Oct 5\" for daily. */\n// label: string;\n// bookable: boolean;\n// location: RentalLocation | null;\n// /** The resource this start books (from the slot's available resources); null when the server named none. */\n// resource: {\n// id: string;\n// name: string;\n// } | null;\n// scheduleId: string | null;\n// }\n// /** A length the customer can pick for the chosen start. */\n// export interface EndOption {\n// /** Local wall-clock end; for a daily rental midnight on the day AFTER the last day. */\n// endLocal: string;\n// /** Length in the rental's unit (hours may be fractional). */\n// units: number;\n// /** \"2 hours\" / \"3 days\" / \"Until Fri, Oct 9\". */\n// label: string;\n// }\n// /** The server's price for a chosen start and length. */\n// export interface RentalQuote {\n// /** Formatted total (\"\" when the currency is unknown). */\n// total: string;\n// totalAmount: number;\n// units: number;\n// unit: RentalUnit;\n// }\n// /** A booking-form field (flat; values are keyed by `target`). */\n// export interface RentalFormField {\n// target: string;\n// label: string;\n// type: \"STRING\" | \"EMAIL\" | \"PHONE\" | \"NUMBER\" | \"URL\";\n// options?: string[];\n// required: boolean;\n// }\n// /** The outcome of rent(): either the browser is being redirected, or it's done. */\n// export type RentalResult = {\n// kind: \"redirect\";\n// url: string;\n// } | {\n// kind: \"confirmed\";\n// bookingId: string;\n// orderId: string | null;\n// };\n// Rentals DTOs — the serializable shapes every hook, component, and page consumes. Plain JSON:\n// safe as Astro island props and across server/client boundaries. Images are resolved https URLs;\n// every displayable price is a ready formatted string (\"\" when the site sent no currency — the\n// surface omits it, never guesses one).\n//\n// A rental is a Wix Bookings service with rentals-specific values (there is no Rentals API of its\n// own): the thing rented is a RESOURCE (a room, a vehicle, a piece of gear), the customer picks the\n// START and the LENGTH, and the price is per unit — per hour or per day.\nexport {};\n",
16 "js/wix/rentals.js": "// Declarations of rentals.ts: what this file exports, their arguments and what they return.\n// import { RENTALS_APP_ID, RENTALS_FORM_ID, RENTALS_PAGE_SIZE, dailyEndOptions, defaultTimeZone, type StartWindow } from \"./rentals-core.js\";\n// import type { EndOption, RentalDetail, RentalFormField, RentalQuote, RentalResult, RentalSummary, StartOption } from \"./rentals-types.js\";\n// export { RENTALS_APP_ID, RENTALS_FORM_ID, RENTALS_PAGE_SIZE, dailyEndOptions, defaultTimeZone };\n// export interface RentalsPage {\n// items: RentalSummary[];\n// hasMore: boolean;\n// }\n// export declare function fetchRentals({ limit, offset }?: {\n// limit?: number;\n// offset?: number;\n// }): Promise<RentalsPage>;\n// export declare function fetchRentalBySlug(slug: string): Promise<RentalDetail | null>;\n// export interface StartsPage {\n// options: StartOption[];\n// timeZone: string | null;\n// }\n// export declare function fetchStarts(rental: Pick<RentalDetail, \"id\" | \"unit\" | \"resourceTypeId\">, window?: StartWindow): Promise<StartsPage>;\n// export declare function fetchEndOptions(rental: Pick<RentalDetail, \"id\" | \"unit\" | \"minUnits\" | \"maxUnits\">, start: StartOption, { timeZone, availableDayKeys }?: {\n// timeZone?: string | null;\n// availableDayKeys?: string[];\n// }): Promise<EndOption[]>;\n// export declare function fetchQuote(rental: Pick<RentalDetail, \"id\" | \"rateAmount\" | \"currency\" | \"unit\">, start: StartOption, end: EndOption, timeZone?: string): Promise<RentalQuote>;\n// export declare function fetchRentalForm(formId: string | null): Promise<RentalFormField[]>;\n// export declare function rentResource(rental: RentalDetail, start: StartOption, end: EndOption, formValues: Record<string, unknown>, timeZone?: string): Promise<RentalResult>;\n// Rentals over REST — the twin of app/wix/rentals/rentals.ts. Same exports, same DTOs; every body\n// comes from rentals-core (the SAME file the SDK transport uses, deployed flat next to this one), so\n// this file is only the transport: literal paths, one fetch per step, all with the visitor token\n// (see ./client). Failures are loud where a visitor acts (renting) and silent-with-a-default where\n// they only inform (form, preview).\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/query-services.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slots.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slot-end-options.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/pricing/pricing-api/preview-price.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-writer-v2/create-booking.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md\n// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md\nimport { wixRequest } from \"./client.js\";\nimport { imgSrc } from \"./media.js\";\nimport { 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, } from \"./rentals-core.js\";\nexport { RENTALS_APP_ID, RENTALS_FORM_ID, RENTALS_PAGE_SIZE, dailyEndOptions, defaultTimeZone };\n/**\n * POST /bookings/v2/services/query { query: { filter: { appId, hidden: false }, paging: { limit, offset } } }\n */\nexport async function fetchRentals({ limit = RENTALS_PAGE_SIZE, offset = 0 } = {}) {\n const res = await wixRequest(\"/bookings/v2/services/query\", { body: { query: { filter: { appId: RENTALS_APP_ID, hidden: false }, paging: { limit, offset } } } });\n const items = (res.services ?? []).filter(isRental).map((r) => toSummary(r, imgSrc));\n const total = Number(res.pagingMetadata?.total ?? 0);\n return { items, hasMore: total ? offset + (res.services?.length ?? 0) < total : false };\n}\n/** POST /bookings/v2/services/query filtered on mainSlug.name — null when not found or not a rental. */\nexport async function fetchRentalBySlug(slug) {\n const res = await wixRequest(\"/bookings/v2/services/query\", { body: { query: { filter: { \"mainSlug.name\": slug, appId: RENTALS_APP_ID, hidden: false }, paging: { limit: 1 } } } });\n const raw = (res.services ?? [])[0];\n return raw && isRental(raw) ? toDetail(raw, imgSrc) : null;\n}\n/** POST /service-availability/v2/time-slots (cursor followed) — the starts a customer can pick. */\nexport async function fetchStarts(rental, window = {}) {\n const raws = [];\n let timeZone = null;\n let cursor;\n do {\n const res = await wixRequest(\"/service-availability/v2/time-slots\", { body: startsRequest(rental, { ...window, cursor }) });\n raws.push(...(res.timeSlots ?? []));\n timeZone ??= res.timeZone ?? null;\n cursor = nextSlotsCursor(res) ?? undefined;\n } while (cursor);\n return { options: toStartOptions(raws, rental.unit), timeZone };\n}\n/**\n * HOURLY: POST /service-availability/v2/time-slots/end-options { serviceId, localStartDate, timeZone, location }\n * (the response field is `endOptions`). DAILY: walked locally from the starts already fetched.\n */\nexport async function fetchEndOptions(rental, start, { timeZone, availableDayKeys = [] } = {}) {\n if (rental.unit === \"DAY\")\n return dailyEndOptions(start, availableDayKeys, rental);\n const res = await wixRequest(\"/service-availability/v2/time-slots/end-options\", { body: { serviceId: rental.id, ...endOptionsRequest(start, timeZone, \"id\") } });\n return toHourlyEndOptions(res, start);\n}\n/** POST /bookings/v2/pricing/preview { bookingLineItems: [...] } — falls back to rate × length. */\nexport async function fetchQuote(rental, start, end, timeZone = defaultTimeZone()) {\n try {\n const res = await wixRequest(\"/bookings/v2/pricing/preview\", { body: { bookingLineItems: previewRequest(rental, start, end.endLocal, timeZone) } });\n return toQuote(res, rental, end.units);\n }\n catch {\n return toQuote(null, rental, end.units);\n }\n}\n/** GET /form-schema-service/v4/forms/{id}/summary (+ the schema for `required`); contact basics when unreadable. */\nexport async function fetchRentalForm(formId) {\n const id = encodeURIComponent(formId ?? RENTALS_FORM_ID);\n try {\n const [summary, form] = await Promise.all([\n wixRequest(`/form-schema-service/v4/forms/${id}/summary`, { method: \"GET\" }),\n wixRequest(`/form-schema-service/v4/forms/${id}`, { method: \"GET\" }).catch(() => null),\n ]);\n return toFormFields(summary?.formSummary, form?.form);\n }\n catch {\n return FALLBACK_FIELDS;\n }\n}\n/**\n * POST /bookings/v2/bookings → POST /ecom/v2/carts → POST /ecom/v2/carts/{id}/calculate →\n * POST /headless/v1/redirect-session (paid) | POST /ecom/v2/carts/{id}/place-order (free / offline).\n */\nexport async function rentResource(rental, start, end, formValues, timeZone = defaultTimeZone()) {\n let created;\n try {\n created = await wixRequest(\"/bookings/v2/bookings\", { body: { booking: bookingRequest(rental, start, end.endLocal, timeZone, \"id\"), ...bookingOptions(formValues) } });\n }\n catch (e) {\n throw new Error(friendlyError(e instanceof Error ? e.message : String(e)));\n }\n const bookingId = bookingIdOf(created);\n if (!bookingId)\n throw new Error(\"The rental couldn't be booked — the time may have just been taken.\");\n const newCart = await wixRequest(\"/ecom/v2/carts\", { body: cartRequest([bookingId], contactOf(created), start.location?.type === \"BUSINESS\" ? start.location.id : null) });\n const cartId = cartIdOf(newCart);\n if (!cartId)\n throw new Error(\"The rental couldn't be reserved — please try again.\");\n const calc = await wixRequest(`/ecom/v2/carts/${cartId}/calculate`, { body: {} });\n if (checkoutRequired(rental, calc)) {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n const session = await wixRequest(\"/headless/v1/redirect-session\", { body: checkoutRedirectRequest(cartId, origin) });\n const url = session?.redirectSession?.fullUrl;\n if (!url)\n throw new Error(\"Checkout couldn't start — please try again.\");\n return { kind: \"redirect\", url };\n }\n const order = await wixRequest(`/ecom/v2/carts/${cartId}/place-order`, { body: {} });\n return confirmedResult(bookingId, order);\n}\n"