EPA Booking Engine releasesself-updates
Generic WordPress booking engine for Shabbat meals, festivals, workshops and events. Built on WooCommerce.
by EPA Productions · github.com/elchanan-star/epa-booking-engine · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/elchanan-star/epa-booking-engine/releases/download/v1.7.1/epa-booking-engine-1.7.1.zipShips its own WordPress updater (Plugin Update Checker), so new versions show up under Dashboard → Updates.
Readme
EPA Booking Engine
A modular WordPress plugin that powers a step-by-step booking flow. Built first for Shabbat meal reservations at a Tbilisi restaurant, architected to be reused for workshops, hotels, events and other bookable products.
File structure
epa-booking-engine/
├── epa-booking-engine.php Plugin bootstrap (headers, constants, requires)
├── assets/
│ ├── css/booking.css Front-end styling (dark luxury / gold theme)
│ ├── css/admin.css Admin settings page tab styling
│ ├── js/booking.js Booking flow logic (state, validation, Hebcal, REST submit)
│ └── js/admin.js Admin settings page tab switching
├── includes/
│ ├── class-plugin.php Singleton, settings reader, pricing, custom post type
│ ├── class-admin.php "EPA Booking" admin settings page (Settings API)
│ ├── class-assets.php Conditional CSS/JS enqueue (only on pages with the shortcode)
│ ├── class-shortcodes.php Renders [epa_booking_engine] markup
│ ├── class-rest.php REST endpoint that stores a submitted booking. Never touches WooCommerce.
│ ├── class-woocommerce.php WooCommerce order-lifecycle sync (cart pricing/display, order↔booking link, status sync)
│ └── class-checkout-controller.php The only place a booking is added to the WooCommerce cart — via a normal page load, not REST
├── assets/vendor/intl-tel-input/ Self-hosted phone input library (v18.5.3, MIT) — not loaded from a CDN
└── README.md
Both WooCommerce-facing classes are self-guarding: each no-ops entirely if WooCommerce is inactive.
Installation
- Copy the whole
epa-booking-enginefolder intowp-content/plugins/. - In WordPress admin, go to Plugins and activate EPA Booking Engine.
- Edit any page/post and add the shortcode:
[epa_booking_engine] - Publish the page. The booking form (CSS/JS) only loads on pages that contain this shortcode — it does not load site-wide.
Submitted bookings are stored under Bookings in the WordPress admin menu (a private post type, epa_booking), with meta fields for the Shabbat date, meals, guest counts, contact details and computed total.
If WooCommerce is active (and the configured product resolves — see the WooCommerce tab below), the booking is added to the WooCommerce cart with the server-calculated price and the visitor is redirected straight to WooCommerce Checkout. Payment, tax, coupons, order emails and refunds are entirely WooCommerce's — this plugin never touches any of that.
If WooCommerce is inactive, or misconfigured, nothing breaks: the booking is still saved with status pending_payment, and the visitor sees the original inline "thank you" placeholder instead of being redirected. The plugin never assumes WooCommerce exists.
Admin Settings Page
Everything below is controlled without touching code, from WordPress Admin → EPA Booking (left-hand menu, calendar icon).
The page has 5 tabs, all saved together under one option (epa_booking_engine_settings) when you click Save Settings:
| Tab | Controls |
|---|---|
| General Settings | Currency symbol, booking form title, success message, error message |
| Meals & Pricing | Per meal: enabled on/off, title, adult price, child price |
| Shabbat Settings | How many upcoming Shabbatot to show (1–10), Diaspora vs. Israel mode |
| Texts / Labels | Every step title, button label, guest/total/select labels, Step 5 field labels, summary row labels, the payment placeholder text, and all validation messages |
| WooCommerce | The WooCommerce product ID every booking is added to cart against (default 311, "Shabbat Reservation"). Shows a live status line: whether WooCommerce is active, and whether the configured product ID actually resolves |
Requires the manage_options capability (Administrator role).
How to change prices
EPA Booking → Meals & Pricing → edit Adult Price / Child Price for either meal → Save Settings. The new price is used immediately by the meal cards, the live summary total, and the server-side total recomputed on submit (the server never trusts a client-sent price).
How to enable/disable a meal
EPA Booking → Meals & Pricing → untick Enabled under that meal → Save Settings. A disabled meal disappears from the front-end meal cards entirely and is excluded from price calculation, both client- and server-side.
How to switch between Diaspora and Israel mode
EPA Booking → Shabbat Settings → Diaspora / Israel Mode → choose Diaspora or Israel → Save Settings. This flips the Hebcal i parameter (i=off for Diaspora, i=on for Israel) used when resolving Parasha names — see below.
How to change how many Shabbatot are shown
EPA Booking → Shabbat Settings → Number of Future Shabbatot (1–10) → Save Settings.
How to change texts
EPA Booking → Texts / Labels — every step title, button label, and validation message shown on the front-end lives here. No code or translation files needed for a simple wording change.
All settings fall back to sensible defaults if nothing has been saved yet (fresh install), so the plugin works out of the box.
Booking steps
The form is a 6-step flow: 1. Shabbat → 2. Branch → 3. Meals → 4. Diners → 5. Identification → 6. Summary.
- Branch (Step 2) is a simple single-select between the two configured branches (
tbilisi/batumiby default —EPA_Booking_Engine_Plugin::get_branches(), filterable viaepa_booking_engine_branches). Rendered server-side (no async fetch, unlike Step 1's Shabbat dates);booking.js'sbindBranchCards()handles the click/select interaction. - Diners (Step 4) is built dynamically by
booking.js(renderDinersStep()), not byclass-shortcodes.php— how many guest-count blocks to show depends on Step 3's selection, which doesn't exist yet at PHP render time. With one meal selected it's a single Adults/Children pair, identical to before this feature existed. With both meals selected, it renders one pair per meal (labeled with that meal's title), so a customer can bring a different number of guests to each sitting — e.g. 4 people Friday night, only 2 for Shabbat lunch. Each meal is priced against its own count.
On submit, the REST payload carries branch and a meal_guests array ([{meal, adults, children}, ...], one entry per selected meal) instead of flat adults/children fields. class-rest.php validates that every selected meal has at least one diner of its own, computes the total per meal, and — alongside the new branch and meal_guests post meta — still stores adults/children as the summed totals across meals, so everywhere else in the plugin that only ever needed "how many people total" (WooCommerce cart/order summary, the admin Guests column, the Reservation meta box's Adults/Children rows) keeps working unchanged. The Reservation meta box additionally shows a Guests per Meal breakdown row whenever a booking has more than one meal.
How the Shabbat dates are calculated
booking.js computes the next N upcoming Saturdays (N = Number of Future Shabbatot from settings, client-side, based on the visitor's browser date), then calls the public Hebcal API once per calendar year needed (with caching) to resolve the Parasha name for each date:
https://www.hebcal.com/hebcal?cfg=json&v=1&year=<YEAR>&i=<off|on>&maj=off&min=off&mod=off&nx=off&s=on
i=off is used in Diaspora mode (default, correct for Tbilisi), i=on in Israel mode — controlled by EPA Booking → Shabbat Settings. If the upcoming Shabbatot span a year boundary, both years are fetched automatically.
Phone field (intl-tel-input)
Step 5's phone field is powered by intl-tel-input v18.5.3, self-hosted under assets/vendor/intl-tel-input/ (not loaded from a CDN, so the field never depends on a third-party service being reachable). Default country is Georgia (ge); booking.js first tries a hint from the visitor's browser language (navigator.language, e.g. ka-GE → ge) with no IP lookup or external geolocation call, and falls back to Georgia whenever that isn't available. The visitor can always pick a different country from the dropdown.
On submit, state.phone is the E.164-formatted number (iti.getNumber(), e.g. +995...) and state.countryCode is the ISO 3166-1 alpha-2 code (e.g. GE), sent to the REST endpoint as phone and country_code and stored as their own post meta fields — country_code is intended for future reporting/WhatsApp/automation use. Validation (iti.isValidNumber()) blocks Step 5 from advancing with a friendly message (EPA Booking → Texts / Labels → Validation: Invalid Phone) if the number is incomplete or malformed for the selected country.
If the library script fails to load for any reason, every part of this degrades to the original plain-text-input behavior (iti stays null, checked at every call site) — the field keeps working, just without formatting/validation, and country_code is simply left empty. Existing bookings saved before this field existed have no country_code meta at all and continue to display and function normally (get_post_meta() on a missing key returns '', handled the same way everywhere it's already read).
Read the full README on GitHub →
Releases
| Tag | Published | Asset | Downloads |
|---|---|---|---|
| v1.7.1 | Aug 31, 2026 | epa-booking-engine-1.7.1.zip | 1 |
| v1.7.0 | Aug 31, 2026 | epa-booking-engine-1.7.0.zip | 1 |
| v1.6.3 | Aug 31, 2026 | epa-booking-engine-1.6.3.zip | 1 |
| v1.6.2 | Aug 31, 2026 | epa-booking-engine-1.6.2.zip | 1 |
| v1.6.1 | Aug 31, 2026 | epa-booking-engine-1.6.1.zip | 1 |
| v1.6.0 | Aug 31, 2026 | epa-booking-engine-1.6.0.zip | 0 |