Back to Arky

Scheduled Services

Book services through the same cart and checkout as products

Scheduling is part of commerce. A booking service is what is booked and sets what every booking of it is: its segments and buffers, slot interval, booking window, how late a customer may still change a booking, its reminders and its tax category. A booking resource is who or what does it, with its timezone, capacity and working hours. A booking offering says one resource provides one service, inside the resource’s hours or its own narrower ones, at a store location, and is priced like any sellable. Checkout makes one order whose booking lines are the bookings: products and bookings share one cart, one quote, one payment and one order.

See E-shop for creating services, resources and offerings.

Setup

Every storefront read and cart names its catalog:

import { initialize } from "arky-sdk/storefront";
import { createOrderToken } from "arky-sdk/utils";

const arky = initialize("arky_pk_...", {
	locale: "en",
	market: "hr",
	createVisitorIdentity: () => ({
		customer_id: crypto.randomUUID(),
		session_id: crypto.randomUUID(),
	}),
	createSessionId: () => crypto.randomUUID(),
	createId: () => crypto.randomUUID(),
	createOrderToken,
});

const { items: [catalog] } = await arky.eshop.catalog.find();
const cart = arky.eshop.cart({ buyer: { type: "customer" }, catalog_id: catalog.id });
await cart.load();

Service Flow

eshop.bookingCalendar({ buyer, catalog_id }) keeps the service, its availability, the month calendar and the selected slot together:

const { items: services } = await arky.eshop.bookingService.find({ catalog_id: catalog.id, limit: 20 });

const calendar = arky.eshop.bookingCalendar({ buyer: { type: "customer" }, catalog_id: catalog.id });
await calendar.select({ booking_service_id: services[0].id });
calendar.selectFirstAvailableDate();

const { slots } = calendar.state.get();
calendar.selectSlot({ slot: slots[0] });

await calendar.addToCart({ slot: slots[0] });
  • addToCart adds the slot as a booking line to the cart store of the calendar’s buyer and catalog, the one eshop.cart({ buyer, catalog_id }) answers, loading or creating that cart first. To book at a company’s or location’s price, open the calendar for that buyer. The line is in that cart store’s booking_items and on the server cart.
  • Each line id comes from your createId; pass id with the slot to pick it yourself.
  • A slot that spans days, such as a stay of several nights, is is_multi_day: its time_text is the date range, and once it is chosen its days are is_in_range in the calendar.

Building your own scheduler? Add the line directly:

await cart.addBooking({
	booking_offering_id: offeringId,
	requested_interval: { from, to },
	capacity_units: 1,
});
  • Availability is computed per offering in its resource’s timezone, for at most 60 days per request. Its days are "YYYY-MM-DD" strings, and each slot says how many spots are left. After a resource’s timezone changes, a slot whose time reaches a day it keeps in the earlier timezone shows no spots until that day has passed.
  • Reading availability reserves nothing; checkout makes the binding check.
  • A booking’s interval covers the service’s segments. Its buffers hold the resource before and after but aren’t part of the appointment.

Quote And Checkout

const checkout = arky.eshop.checkoutFlow({ buyer: { type: "customer" }, catalog_id: catalog.id });

await checkout.open();
checkout.setContact({ contact_email: "ana@example.com" });
const state = await checkout.place({ return_url: (orderId) => `${location.origin}/bookings/${orderId}` });
console.log(state.step, state.placed?.number);

open loads the cart the booking was added to and quotes it, and place checks the quote out under an order id and token from your factories, with the quote’s suggested payment option. See Checkout for the steps, mounting the payment and recovering a lost answer. The cart store’s checkout({ contact_email, payment }) places the order directly when you build the page yourself.

Note

A service-only order needs no shipping address or method. The quote’s ready is false while a booking is no longer available (a booking_unavailable blocker). The blocker and the line’s availability carry a code that says why: BOOKING_OFFERING.OUTSIDE_WINDOW, BOOKING_OFFERING.NOT_A_SLOT, BOOKING_OFFERING.OUTSIDE_HOURS, ORDER.BOOKING_UNAVAILABLE (no free place) or ORDER.BOOKING_TIMEZONE_CHANGED (the time reaches a day the resource keeps in its earlier timezone). Quote again after the buyer picks another slot.

A confirmed booking sends the store’s booking_confirmed email, and each of the service’s reminder_offsets_minutes sends booking_reminder before it, in the order’s language. A reminder goes only while the booking is confirmed and upcoming, for the start it was planned for.

Customer Changes

The service’s customer_changes says what a customer may do with their own booking on the storefront: not_allowed, or until_before_start with a number of minutes. Within it, the signed-in customer, or a guest holding the order’s link, can cancel or move the booking:

await arky.eshop.order.rescheduleBookingItem({
	order_id: order.id,
	line_item_id: booking.id,
	expected_updated_at: order.updated_at,
	starts_at: newSlot.from,
});

await arky.eshop.order.cancelBookingItem({
	order_id: order.id,
	line_item_id: booking.id,
	credit_id: crypto.randomUUID(),
	expected_updated_at: order.updated_at,
});
  • A move goes to another start of the same offering, which must be bookable like a new booking. The line keeps its units and money, and keeps each earlier start, up to 20 moves.
  • Cancelling names the id of the credit it creates, picked by your code: money owed back to the buyer, refunded through the payment.

Managing Bookings

Staff act on a booking line with its line id and the order’s expected_updated_at:

import { createAdmin } from "arky-sdk/admin";
import { orderBookingItems } from "arky-sdk";

const sdk = createAdmin({ apiUrl: "https://api.arky.io", secretKey: process.env.ARKY_SECRET_KEY });

const order = await sdk.eshop.order.get({ store_id: storeId, id: orderId });
const booking = orderBookingItems(order)[0];

await sdk.eshop.order.attendBookingItem({
	store_id: storeId,
	order_id: order.id,
	line_item_id: booking.id,
	expected_updated_at: order.updated_at,
});
  • attendBookingItem and markBookingItemNoShow record who came, once the booking has started (409 ORDER.BOOKING_NOT_STARTED before), on a confirmed booking whose attendance is still open.
  • rescheduleBookingItem moves a booking until it starts, and cancelBookingItem cancels it until it ends, whatever the service’s customer_changes says. Both carry notify_customer: a moved booking sends booking_confirmed again and a cancelled one booking_cancelled, unless it is false.
  • sdk.eshop.order.findBookingItems lists bookings across orders, by service, resource, time and status (pending, confirmed, attended, no_show, cancelled).
  • The server’s clock decides every boundary. Show the actions and let the server refuse when a boundary isn’t met.
Warning

Cancelling a booking frees its capacity. Money goes back only through the credit’s refund; a late payment never brings back an expired or cancelled booking.