Practice Sessions Booking Widget (mu-plugin)
Practice Sessions Booking Widget
Section titled “Practice Sessions Booking Widget”Custom WordPress mu-plugin implementing the returning-participant booking widget for the Practice Sessions Montreal 2026 page. Handles WP Fusion access gating, AJAX add-to-cart, and a WooCommerce sliding-scale discount.
File Locations
Section titled “File Locations”| Path | |
|---|---|
| Server | /var/www/baseworks.com/wp-content/mu-plugins/baseworks-practice-sessions.php |
| Local tracking | baseworks-changelog/sites/baseworks.com/mu-plugins/baseworks-practice-sessions.php |
| Changelog entries | baseworks-changelog/changelog-entries/20260227-143000-practice-sessions-booking-widget.md, 20260305-190000-practice-sessions-v2-complete.md |
| Shortcode | `[practice_session_booking program=”…” tier=“alumni |
| Page ID | 47154 (baseworks.com) |
Architecture
Section titled “Architecture”Six functional components in one file:
1. Shortcode [practice_session_booking]
Section titled “1. Shortcode [practice_session_booking]”Accepts program (required) and tier ("alumni" or "standard") parameters. Reads all product IDs, tag IDs, and pricing params from bw_sb_get_programs() in baseworks-session-booking.php.
tier="alumni" render states:
- Logged out — login prompt + intake form CTA
- Logged in, no tag 97 — message explaining alumni-only access + intake form CTA
- Logged in + tag 97 — live quantity selector with dynamic sliding-scale pricing and AJAX checkout
tier="standard" render states:
- Logged out — login prompt + intake form CTA
- Logged in — variable product quantity/variation selector with AJAX checkout (no tag gating)
2. Asset enqueue
Section titled “2. Asset enqueue”Inline CSS + JS registered once per page via static flag. Safe for multiple shortcode instances on the same page.
3. AJAX handler wp_ajax_bw_ps_add_to_cart
Section titled “3. AJAX handler wp_ajax_bw_ps_add_to_cart”- Verifies nonce, authentication, and WP Fusion tag 97
- Removes any existing product 47188 items from cart (idempotent — fresh per transaction)
- Adds requested quantity
- Returns
wc_get_checkout_url()— JS redirects directly to checkout
4. Discount hook woocommerce_before_calculate_totals (priority 10)
Section titled “4. Discount hook woocommerce_before_calculate_totals (priority 10)”Sliding-scale pricing applied to all alumni products found in the program registry:
discount = min( (qty - 1) × 0.035, 0.49 )per-session price = base_usd × (1 - discount)Uses get_regular_price('edit') as base — see critical note below. Hook iterates all programs from bw_sb_get_programs() to find alumni product IDs rather than relying on a hardcoded constant.
5. URL cleanup wp_footer
Section titled “5. URL cleanup wp_footer”On the checkout page, strips add-to-cart and quantity query params from the URL using history.replaceState(). WooCommerce appends these during the redirect chain even when using AJAX add-to-cart.
6. Exchange rate (display only)
Section titled “6. Exchange rate (display only)”Reads CAD rate from wcpay_multi_currency_cached_currencies WP option for live price preview in the widget. Actual currency conversion is handled by WCPay at totals calculation time — this is display-only.
Alumni Pricing Reference
Section titled “Alumni Pricing Reference”Base price read live from WooCommerce product (alumni product ID in program registry, get_regular_price('edit')). Current alumni product: 47188, $27 USD/session base (~$38 CAD at current rate). Page copy lists prices in CAD; WooCommerce products are priced in USD.
| Sessions | Discount | Per session (USD) | Per session (CAD ~$38 base) |
|---|---|---|---|
| 1 | 0% | $27.00 | ~$38.00 |
| 5 | 14% | $23.22 | ~$32.68 |
| 10 | 31.5% | $18.49 | ~$26.03 |
| 16 | 49% | $13.77 | ~$19.38 |
QC tax: GST 5% + QST 9.975% = 14.975% — applied at checkout.
Source: practice-sessions-pricing
Critical Technical Notes
Section titled “Critical Technical Notes”get_regular_price('edit') — Double-conversion prevention
Section titled “get_regular_price('edit') — Double-conversion prevention”WCPay hooks FrontendPrices::get_product_price_string() into woocommerce_product_get_regular_price at priority 99. In view context, calling get_regular_price() fires this filter and converts the USD price to CAD before we apply the discount. Then set_price() stores the CAD-discounted value. When WooCommerce reads get_price() again during totals calculation, WCPay converts again — double-conversion.
Using get_regular_price('edit') bypasses all filters via WC_Data::get_prop(), returning the raw USD value stored in the database. WCPay then converts our discounted USD price correctly once at totals time.
Currency system: WCPay, not WCML
Section titled “Currency system: WCPay, not WCML”woocommerce-multilingual (WCML) is installed but enable_multi_currency: 0. All currency conversion is handled by WooCommerce Payments.
Currency switcher block: woocommerce-payments/multi-currency-switcher (not a WCML widget).
If WC_Payments_Multi_Currency()->is_provider_connected() returns false, get_account_available_currencies() returns an empty array → only USD loads → switcher block renders nothing. Resolved by keeping WCPay account connected in WP Admin → Payments.
Discount priority vs WCML
Section titled “Discount priority vs WCML”Our discount hook runs at priority 10. WCML hooks woocommerce_before_calculate_totals at priority 100 (if ever re-enabled). Our hook runs first.
Version History
Section titled “Version History”| Version | Date | Summary |
|---|---|---|
| v1.0.0 | 2026-02-27 | Initial: shortcode + AJAX + discount hook + CSS/JS |
| v1.1.0 | 2026-02-27 | Currency investigation; switched to WCPay; get_regular_price('edit') fix; CSS fixes |
| v1.2.0 | 2026-02-27 | Gate/intro text color; URL cleanup via history.replaceState() |
| v2.0.0 | 2026-03-05 | tier parameter added to [practice_session_booking] — "alumni" (existing) and "standard" (new variable-product widget). Reads config from bw_sb_get_programs() registry; PHP constants removed. Discount hook generalised to iterate all programs. readyState guard init pattern for Perfmatters compatibility. Full detail: changelog-entries/20260305-190000-practice-sessions-v2-complete.md. |
Perfmatters Configuration
Section titled “Perfmatters Configuration”Both widget init functions (bwPsInit, bwStdInit) are in the Perfmatters delay_js_exclusions list. This is required because Perfmatters uses delay_js_behavior: "all", which marks every script as delayed and only executes them on first user interaction (or after a 30-second fallback). Without the exclusion, widget content would not render for up to 30 seconds.
If Perfmatters is reconfigured or its exclusions are reset, re-add both identifiers to delay_js_exclusions via WP Admin → Perfmatters → Assets or via WP-CLI.
Watchpoints
Section titled “Watchpoints”- WCPay must stay connected. Account disconnect causes currency switcher to go blank and CAD display to break.
- Exchange rate cache. Widget display rate comes from
wcpay_multi_currency_cached_currencies. If this option is stale or empty, the displayed CAD prices may be inaccurate. WCPay normally refreshes this automatically. - Discount is per-transaction only. No accumulation across orders. Each purchase calculates fresh from
get_regular_price('edit'). - Standard variation naming. The standard widget derives credits from
intval()of thepass-sizevariation attribute. Variations must be named"N session"or"N sessions"(e.g."5 sessions"). See naming convention in the program registry section below.
Related
Section titled “Related”- Practice Sessions Montreal 2026 — Page Draft
- Practice Sessions Pricing (source of truth)
- Practice Sessions T&Cs Addendum
- Practice Sessions FAQ Block
Session Booking System
Section titled “Session Booking System”Added 2026-02-28
A separate, second mu-plugin that handles the actual session booking flow after purchase. Distinct from the purchase/pricing widget above.
File Locations
Section titled “File Locations”| Path | |
|---|---|
| Server | /var/www/baseworks.com/wp-content/mu-plugins/baseworks-session-booking.php |
| Changelog entries | baseworks-changelog/changelog-entries/20260228-120000-session-booking-system.md, 20260305-190000-practice-sessions-v2-complete.md |
| Shortcode | [bw_booking_dashboard program="ps_spring_2026" title="..."] |
| Account page | baseworks.com/available-programs/ |
Version History
Section titled “Version History”| Version | Date | Summary |
|---|---|---|
| v1.0.0 | 2026-02-28 | Initial build — DB, program registry, credit system, WC hook, booking/cancel logic, shortcode, admin page |
| v1.1.0 | 2026-02-28 | Baseworks Blue color scheme; spots display logic; title attribute; email toggle in admin; attendee list in admin; past session handling |
| v1.2.0 | 2026-02-28 | Re-booking bug fix (UNIQUE constraint); first name in emails; booking status summary in emails; shortcode reference in admin |
| v2.0.0 | 2026-03-05 | Extended program config format (alumni_tag_id, purchase_url, pricing, updated products array). Credit-granting hook branched to qty/variation_attr/fixed modes. bw_sb_get_cad_rate() helper added. Removed redundant global wp_enqueue_scripts hook. ([bw_ps_price_table] shortcode was included in initial deploy and removed same day — unused.) |
| v2.1.0 | 2026-03-20 | Admin email notifications — new independent toggle in admin UI; bw_sb_admin_emails_enabled(), bw_sb_send_admin_booking_email(), bw_sb_send_admin_cancellation_email() added. Admin emails go to get_option('admin_email') and identify the participant by full name and email address. |
Architecture Overview
Section titled “Architecture Overview”Data flow:
- WooCommerce order →
woocommerce_order_status_completedhook → credits added tobw_credits_{program_id}user meta (credit count determined by product’screditsmode in program config) - User visits account page → shortcode renders session list with checkboxes
- User selects sessions → AJAX →
bw_sb_book_sessions()→ rows inwp_bw_bookings, credits deducted - User cancels → AJAX →
bw_sb_cancel_session()→ row updated, credit refunded
Key functions:
bw_sb_get_programs()— program registry (filterable, add new programs here)bw_sb_get_sessions( $program_id )— reads session list from WP optionbw_sb_get_credits / set_credits / add_credits— user credit managementbw_sb_book_sessions( $user_id, $program_id, $session_ids[] )— batch bookingbw_sb_cancel_session( $user_id, $program_id, $session_id )— single cancellationbw_sb_emails_enabled( $program_id )— checks WP option (admin toggle) then falls back to program configbw_sb_admin_emails_enabled( $program_id )— checksbw_sb_admin_emails_{program_id}WP option; off by defaultbw_sb_send_admin_booking_email( $user_id, $program_id, $session_ids[] )— notifies admin on booking; identifies participant by full name and emailbw_sb_send_admin_cancellation_email( $user_id, $program_id, $session_id )— notifies admin on cancellation
Database table wp_bw_bookings:
id | user_id | program_id | session_id (YYYYMMDD_HHMM) | status | created_at | cancelled_atUNIQUE KEY on (user_id, program_id, session_id) — one row per user/program/session across all time. Re-booking a cancelled session UPDATEs the row rather than INSERTing.
Session data stored in WP option bw_sb_sessions_{program_id} as a JSON array. Each session: id, week, date, day, time_start, time_end, location, room. Seeded once on plugin init via a guard option.
Credits stored in WP user meta as bw_credits_{program_id} (integer). Visible and editable in WP Admin → Users → Edit User → “Session Credits”.
Future Extensions
Section titled “Future Extensions”1. Adding New Programs (current process)
Section titled “1. Adding New Programs (current process)”a. Add a new entry to bw_sb_get_programs() in the plugin (see v2 config format below)
b. Seed sessions into WP option bw_sb_sessions_my_new_program_id (array of session objects)
c. Add shortcode to account page: [bw_booking_dashboard program="my_new_program_id" title="..."]
The admin page, user profile credit fields, and email system all pick up the new program automatically.
2. Other Future Considerations
Section titled “2. Other Future Considerations”- Admin UI — program creation form, session schedule editor, product ID fields, program archive. Makes season setup self-service without code deploys.
- Credit transfer between programs — not implemented; each credit is program-specific
- Waitlists — no waitlist logic; full sessions block booking
- Batch cancellation — currently one session at a time
- Email HTML templates — emails are plain text
- French language support — booking UI is English-only
Program Registry
Section titled “Program Registry”Program configuration lives in bw_sb_get_programs() in baseworks-session-booking.php. Both plugins read from this registry. Adding a new season is a config-only operation — no code changes needed in either plugin.
Program Config Format
Section titled “Program Config Format”'ps_spring_2026' => [
// ── existing keys (unchanged) ──────────────────────────────────────── 'name' => 'Practice Sessions — Montreal, Spring 2026', 'capacity' => 20, 'cancel_hours' => 24, 'timezone' => 'America/Toronto', 'location' => 'Proto Studio', 'emails' => false,
// ── new keys ─────────────────────────────────────────────────────────
// WP Fusion tag ID that gates alumni access (used by the booking widget // and the price table CTA gating) 'alumni_tag_id' => 97,
// Page where the alumni booking widget lives (used by price table CTA) 'purchase_url' => '/practice-sessions/',
// Products — one entry per WooCommerce product that grants credits // for this program. Replaces the old single-product products array. 'products' => [
// Alumni: simple product. Credits = quantity purchased. // Sliding scale discount applied at cart via WC hook. 47188 => [ 'tier' => 'alumni', 'credits' => 'qty', 'label' => 'Alumni', ],
// Standard: variable product. Credits derived from the variation's // 'pass-size' attribute via intval() — see naming convention below. // No custom price hook needed; WC handles variation prices natively. 47343 => [ 'tier' => 'standard', 'credits' => 'variation_attr', 'attr' => 'pass-size', 'label' => 'Standard', ],
// Intro: simple product. Always 1 credit regardless of qty. 47341 => [ 'tier' => 'intro', 'credits' => 'fixed', 'count' => 1, 'label' => 'Intro', ], ],
// Pricing — parameters for the price table shortcode and the alumni // booking widget. Decoupled from the credit-granting logic above. 'pricing' => [
'tax_rate' => 0.14975, // QC: GST 5% + QST 9.975%
'alumni' => [ 'type' => 'sliding_scale', 'product_id' => 47188, // base USD price read live from WC regular_price 'step' => 0.035, // 3.5% discount per additional session 'cap' => 0.49, // maximum discount: 49% 'max_qty' => 16, ],
'standard' => [ 'type' => 'tiers_from_variations', 'product_id' => 47343, // tiers + prices read live from WC variations 'attr' => 'pass-size', ], ],],Credit-Granting Logic — Three Modes
Section titled “Credit-Granting Logic — Three Modes”The woocommerce_order_status_completed hook in baseworks-session-booking.php checks the credits field in the program config and branches:
| Mode | Key | Logic |
|---|---|---|
qty | Alumni, legacy products | $credits = $item->get_quantity() — unchanged |
variation_attr | Standard variable product | $credits = intval( wc_get_product( $item->get_variation_id() )->get_attribute( 'pass-size' ) ) |
fixed | Intro | $credits = $program_product['count'] — ignores quantity |
The hook looks up the ordered product ID across all programs in the registry to determine which mode to use. If a product ID doesn’t appear in any program, it falls through without granting credits.
Variation Naming Convention — MUST FOLLOW
Section titled “Variation Naming Convention — MUST FOLLOW”The variation_attr mode derives credit count from intval() of the attribute value. This means the first number in the attribute value must equal the intended credit count.
Required format: "N session" or "N sessions" where N is a positive integer.
Examples: "1 session" → 1, "5 sessions" → 5, "10 sessions" → 10, "16 sessions" → 16.
Do not name variations anything else (e.g., “Pack of 5” would give 0). When creating products for a new season, copy the attribute name (
pass-size) and value format exactly from the Spring 2026 product (ID 47343).
New Season Operational Workflow
Section titled “New Season Operational Workflow”When launching a future season (e.g., ps_fall_2026):
- Create WooCommerce products — Alumni (simple), Standard (variable), Intro (simple). For Standard variations, use the naming format
"N session(s)"— see naming convention above. - Add program entry to
bw_sb_get_programs()inbaseworks-session-booking.php. Copy theps_spring_2026entry, update the program ID, product IDs, name, and any changed pricing params. - Seed session schedule into WP option
bw_sb_sessions_ps_fall_2026. - Update shortcodes on the public landing page and account page to reference the new program ID.
No other plugin changes required. Admin page, credit system, booking/cancel logic, and email system all pick up the new program automatically from the registry.
Admin Reference
Section titled “Admin Reference”- WP Admin → Session Bookings — session attendance, all bookings, credit balances, email toggles, shortcode reference
- Email notifications (participants) — off by default per program; toggle via admin page; sends one confirmation email per batch booking, one email per cancellation
- Email notifications (admin) — separate toggle; off by default; sends to the WordPress admin email (
Settings → General → Administration Email Address); booking emails include participant name, email, sessions booked, and their upcoming schedule; cancellation emails include participant name, email, cancelled session, and remaining schedule - Manual credit adjustment — WP Admin → Users → Edit User → Session Credits