WP Manifestindependent plugin directory
manifest / events / epa-booking-engine

EPA Booking Engine WP Registry grade A. No security issues foundWP RegistryNo security issues foundOpen the reportA

Generic WordPress booking engine for Shabbat meals, festivals, workshops and events. Built on WooCommerce.

★ 0stars
8release downloads
v1.7.1latest · 1mo ago

Install

Download epa-booking-engine-1.7.1.zip From the v1.7.1 release on GitHub, Aug 31, 2026

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/elchanan-star/epa-booking-engine/releases/download/v1.7.1/epa-booking-engine-1.7.1.zip
Updates itself. It ships its own updater (Plugin Update Checker), so new versions appear under Dashboard → Updates.

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

  1. Copy the whole epa-booking-engine folder into wp-content/plugins/.
  2. In WordPress admin, go to Plugins and activate EPA Booking Engine.
  3. Edit any page/post and add the shortcode:
    [epa_booking_engine]
  4. 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 / batumi by default — EPA_Booking_Engine_Plugin::get_branches(), filterable via epa_booking_engine_branches). Rendered server-side (no async fetch, unlike Step 1's Shabbat dates); booking.js's bindBranchCards() handles the click/select interaction.
  • Diners (Step 4) is built dynamically by booking.js (renderDinersStep()), not by class-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).

WooCommerce integration

Two classes, two very different jobs. Neither is known to class-rest.php by name — it only ever asks the epa_booking_engine_checkout_url filter for a URL, and whoever currently answers that filter decides what happens next.

  • includes/class-checkout-controller.php — the only place a booking is ever added to the WooCommerce cart. Deliberately does not do this during the REST call. WordPress REST API requests never get a working WC()->cart — WooCommerce's own is_rest_api_request() check (matching any /wp-json/... URL, not just its own) skips cart/session bootstrap for all of them, by design. Fighting that with a manual session/cart bootstrap would mean depending on WooCommerce internals it explicitly chose not to expose here. Instead, this controller answers the checkout-URL filter with a signed link back to the same site, and only adds the item to the cart when the visitor's browser actually follows that link as a normal page load — the one context WooCommerce guarantees a working cart in.
  • includes/class-woocommerce.php — everything from the moment the item is already in the cart onward: pricing it correctly, displaying the booking summary, and keeping the booking's own status in sync with the WooCommerce order once one exists.

Both self-guard identically: each registers its hooks only if function_exists( 'WC' ) is true at the moment it's instantiated (on plugins_loaded, by which point every active plugin — WooCommerce included — has already fully loaded). If WooCommerce is deactivated, both are entirely inert.

Flow, end to end:

  1. Step 6 submit → class-rest.php saves the epa_booking post and computes the total server-side, exactly as before. It fires epa_booking_engine_booking_saved as a general "a booking exists now" signal (nothing currently listens for cart purposes), then asks epa_booking_engine_checkout_url for a URL.
  2. class-checkout-controller.php answers that filter — but only checks that the configured product exists; it does not touch the cart yet. It returns a signed URL back to the same site: home_url('/') + ?epa_booking={id}&epa_booking_token={nonce}.
  3. booking.js redirects the browser to that URL instead of showing the inline confirmation (this part of the front-end is unchanged — it just follows whatever checkout_url it's given).
  4. That request is a completely normal WordPress page load — not REST, not AJAX. class-checkout-controller.php intercepts it on template_redirect, verifies the nonce, loads the booking back from its own post meta (the original REST request's data is long out of scope by now), and calls WC()->cart->add_to_cart(). This is guaranteed to work here, because a normal front-end page load is exactly the context WooCommerce's cart/session system is built for.
  5. The controller immediately wp_safe_redirect()s to wc_get_checkout_url() and exits. The visitor never sees this intermediate page render.
  6. woocommerce_before_calculate_totals (in class-woocommerce.php) overrides the cart line item's runtime price to the server-computed total. The product's own catalog price in the database is never touched.
  7. Customer completes WooCommerce's own checkout form and payment. WooCommerce creates the Order natively — this plugin never calls wc_create_order().
  8. woocommerce_checkout_create_order_line_item copies the booking ID/reference onto the order line item (hidden meta) and the human-readable summary (Shabbat, meals, diners) as visible order item meta, so staff and the customer both see it on the order screen / order email.
  9. woocommerce_checkout_order_created (fires for both classic and block-based Checkout) reads the booking ID back off the order's own line items and stamps the bidirectional link: _epa_booking_id / _epa_booking_reference on the Order, _wc_order_id on the epa_booking post. Booking status → order_created.
  10. From here on, WooCommerce's own order status transitions drive the booking status:
WooCommerce order status Booking status Hook
pending, on-hold order_created woocommerce_order_status_changed
processing, completed confirmed woocommerce_order_status_changed
cancelled cancelled woocommerce_order_status_changed
refunded refunded woocommerce_order_status_changed
failed payment_failed woocommerce_order_status_changed

Separately, payment_status post meta (unpaid → paid / refunded) is driven by woocommerce_payment_complete and woocommerce_order_refunded specifically — these fire at the exact moment money moves, which is a more precise signal than the general order-status change and lets you query "confirmed but not yet refunded" independently of the booking's lifecycle stage.

Every booking status change also fires epa_booking_engine_booking_status_changed( $booking_id, $old_status, $new_status ), regardless of what caused it — this is the hook for anything that should react to a booking being confirmed/cancelled (e.g. a future notifications feature) without needing to know WooCommerce exists.

HPOS: this plugin only reads/writes orders through wc_get_order() and WC_Order CRUD methods (get_meta(), update_meta_data(), get_items()) — never direct $wpdb queries against the posts table — and declares compatibility with WooCommerce's High-Performance Order Storage via before_woocommerce_init in epa-booking-engine.php.

For developers: where settings live in code

  • EPA_Booking_Engine_Plugin::get_settings() — the single source of truth; always returns a complete settings array (saved values merged over defaults via array_replace_recursive).
  • EPA_Booking_Engine_Plugin::get_settings_defaults() — hard-coded defaults, used to seed a fresh install and to fill in any keys missing from an older saved option.
  • EPA_Booking_Engine_Plugin::get_pricing() — derives the {currency, meals} shape from settings, already filtered to enabled meals only. Still runs through the epa_booking_engine_pricing filter for backward compatibility with any code hooking into it directly.
  • EPA_Booking_Engine_Plugin::get_upcoming_shabbatot_count() / get_shabbat_mode() — read the Shabbat tab.
  • includes/class-admin.php — registers everything via the WordPress Settings API (register_setting, add_settings_section, add_settings_field), sanitizes all input, and renders the tabbed UI.
  • The front-end (class-shortcodes.php, class-assets.php, booking.js) reads only through the methods above — nothing is hardcoded, so a future multi-site reuse of this plugin only ever needs the admin settings page, not code changes.

Known assumptions and limitations

  • The cart is never touched during the REST call. This was a real bug in an earlier build of this integration (WC()->cart is null inside any /wp-json/... request — WooCommerce skips cart/session bootstrap there on purpose) and is why class-checkout-controller.php exists: it defers the actual add_to_cart() call to the handoff page, a normal page load, where WooCommerce guarantees the cart works.
  • The handoff link is signed and single-use in intent. ?epa_booking={id}&epa_booking_token={nonce} — the nonce is scoped to that specific booking ID, so it can't be reused for a different booking. If the same link is opened again after checkout already happened (e.g. back button), the controller detects the booking already has a _wc_order_id and redirects straight to checkout without re-adding anything.
  • One booking per checkout. The handoff empties any existing cart contents before adding the booking. Deliberate v1 design choice matching the "1 booking = 1 order = 1 cart item" model — if multi-booking carts are ever needed, add_booking_to_cart() in class-checkout-controller.php is the one place that changes.
  • Product 311 is never modified. Its catalog price, stock settings, etc. are left exactly as configured in WooCommerce. Only the cart line item's runtime price is overridden per booking (in class-woocommerce.php). Worth manually verifying the product is set to Virtual (no shipping) in WooCommerce, since this plugin doesn't check or enforce that.
  • epa_booking_engine_checkout_url only fires if WooCommerce was active at plugin bootstrap (function_exists( 'WC' ) on plugins_loaded). If WooCommerce is inactive, or the configured product ID doesn't resolve to a real product, the filter is never registered / declines, and the front-end transparently falls back to the original inline "thank you" placeholder — nothing errors, nothing breaks.
  • Not yet built (still just architecture, same as before): coupons/discounts, seat availability limits, per-date pricing, translations. All hang off existing filters (epa_booking_engine_pricing, epa_booking_engine_booking_status_changed) without touching this integration.

Releases

6 releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
v1.7.1 latest
Aug 31, 2026 1mo ago
epa-booking-engine-1.7.1.zip
4
Aug 31, 2026 1mo ago
epa-booking-engine-1.7.0.zip
1
Aug 31, 2026 1mo ago
epa-booking-engine-1.6.3.zip
1
Aug 31, 2026 1mo ago
epa-booking-engine-1.6.2.zip
1
Aug 31, 2026 1mo ago
epa-booking-engine-1.6.1.zip
1
Aug 31, 2026 1mo ago
epa-booking-engine-1.6.0.zip
0
A grade

Security

EPA Booking Engine 1.7.1 · audited by WP Registry

No security issues found.

Audited release
1.7.1
Findings
none
Worst severity
—
Content hash
0bde85c1ea3afbefec741070…

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.