LIEBEN YOGA / CLASS SCHEDULING AND RESERVATIONS Implementation and requirements / 2026-11-01 Local development work in website-devtest-1. Nothing deployed. 1. START HERE classes.php Calendar with native detail dialogs + date list. classes-calendar-v2.php Calendar with direct Book links, no dialogs. classes-2.php Chronological rectangular rows. classes-3.php Four / three / two-column class badges. classes-codex.php Recommended weekly schedule + next-class feature. booking.php?class=ID Class details and reservation form. booking-status.php Private booking / hold / check-in token / email preview. booking-return.php PayPal return; payment is not trusted from a GET. classes-admin.php Password-protected schedule, roster and email notebook. classes-guide.php Browser-friendly explanation and requirements. The top menu now has Classes. The five schedule views link to one another, use one schedule and launch one booking flow. Existing website/ is untouched. 2. WHAT WORKS NOW, WITHOUT ANY ACCOUNT CONNECTION The default mode is demo. Sample dates, venues, prices and descriptions are clearly marked. Booking creates a 15-minute seat hold; confirmation records a DEMO admission token, a private receipt link, a roster entry and a queued confirmation message together. The demo sends no email and takes no payment. The owner can choose a password once through a loopback PHP preview, sign in, add / edit a session, cancel a session, view the roster, check someone in, download a CSV, queue one reminder per reservation and preview email messages. No shared default password is included. No student registration is required. The first calendar uses ; Escape, the close button and backdrop clicks close it. A normal booking link remains available without JavaScript. The second calendar has no dialogs at all. On narrow screens the seven-day grid shows times; the full chronological list below preserves all information. Class cards use four columns from 960px, three from 760px, and two below 760px. 3. THE EASY-TO-EDIT SOURCE FILE assets/data/classes.json This is plain, indented JSON, not a database. It contains: timezone America/Los_Angeles currency USD is_demo true until sample schedule is replaced booking_policy Your actual booking / cancellation notes class_types Reusable owner-form title / description / price defaults classes A list of dated sessions Each session has id, type, title, date (YYYY-MM-DD), start_time / end_time (24-hour HH:MM), location, address, description, price (e.g. "18.00"), capacity, and status (scheduled or cancelled). Dates are local Pacific times. Each class must end after it starts on the same date. Midnight-spanning sessions are not supported. Keep the permanent ID unchanged after sharing a booking link. The PHP reader validates and sorts the sessions by date / time, then ID. The file order does not matter. Broken JSON does not silently become an empty roster or overwrite anything; the service reports that review is needed. Replace placeholder teaching descriptions with your own approved language. The owner form fills class-type defaults when requested. It detects a stale editing page using a schedule hash. Every saved edit preserves the previous schedule in private schedule-history before the atomic replacement. The same lock used by reservations protects editor saves. Manual text-file edits do not participate in that lock; use the form while bookings are active. Once reservations or active holds exist in the current mode, the editor blocks changes to title, date, time, price and venue. It allows description / capacity changes, with capacity no lower than occupied places. Create a new session for a different date. Cancel rather than delete a session; roster records remain. Cancellation queues notices but DOES NOT perform a refund. Old unsent confirmations / reminders are marked superseded. A cancelled booked session cannot silently be reopened; create a replacement session. 4. PRIVATE STORAGE AND OWNER ACCESS Default local private directory, relative to the repository root: .lieben-yoga-private/website-devtest-1/ It is ignored by Git. It is created only when a write is needed. Configure LIEBEN_BOOKING_STORAGE outside the public document root on the final host. Private files additionally contain a PHP exit guard and an Apache deny rule, so PHP development previews cannot expose their JSON through an HTTP request. Files are read as data, never executed / included by the application. The private directory holds: owner-settings.php Password hash, when set locally. reservations-demo.php Demo reservations / outbox / rate limits. reservations-paypal_sandbox.php Sandbox-only records. reservations-paypal_live.php Live-only records. write-lock.php Shared file lock. schedule-history/ Guarded, timestamped previous schedules. The first visit through PHP's built-in localhost server in demo mode offers password setup if no owner hash exists. Other IDE preview servers may not show that first-time form. If needed, run this in a PowerShell terminal once: & 'C:/tools/php85/php.exe' -S 127.0.0.1:8785 -t 'C:/LiebenYoga/website-devtest-1' Then visit http://127.0.0.1:8785/classes-admin.php, choose your owner password, and stop that temporary server with Ctrl+C. Your regular preview can then use the saved hash in the same private storage directory. Choose 12–72 bytes for the default PHP password-hashing algorithm; longer inputs are rejected. On remote hosting, configure LIEBEN_ADMIN_PASSWORD_HASH privately before using the owner page. Use password_hash() output, not plaintext. Owner sessions have a 30-minute idle limit and 8-hour absolute limit. Login attempts are rate-limited. Student links use separate random bearer tokens; private pages have no-store and no-referrer headers. Student tokens and name / email are sensitive data; the private files and their backups must stay private. All state-changing forms use POST and a session CSRF token. Owner tools do not trust browser-supplied prices or payment statuses. Roster CSV values that could be spreadsheet formulas are escaped. The roster is never a public JSON feed. 5. BOOKING STATES AND CAPACITY pending Holds one seat until hold_expires_at (15 minutes). expired Computed when an unpaid pending hold has elapsed; frees seat. capture_pending Actual capture attempt may have succeeded; keeps seat held. confirmed Verified payment / free place / explicitly marked demo. needs_review Payment or availability changed; do not automatically admit. cancelled Unpaid hold released; no paid cancellation is invented. One booking reserves one place. The form has a server-issued submission key; repeated submissions reuse the same booking. Booking state, admission token and confirmation job are written in one atomic update under flock(). Capacity is checked inside that lock, not just from the display's places-left number. An incomplete payment is never called paid. A late verified payment that can no longer fit goes to needs_review, without an admission token. A failed / uncertain capture is NOT automatically retried as a new order. The same request identifier is reused. The owner can release an uncertain unpaid hold only after explicitly verifying in PayPal that no capture occurred. If payment was received, arrange the place or refund in PayPal and reconcile the record. Automated refund issuance and an owner refund-resolution screen are not implemented. They require your cancellation policy and sandbox tests. The private link is a long, random access credential, kept out of query strings using a URL fragment and then a CSRF-protected POST. It can resume the booking from another browser without a student password. Save / copy this link before leaving checkout. The shorter admission token is for the class roster, not a public credential that exposes a student's email or receipt. 6. PAYPAL STRUCTURE ALREADY WRITTEN booking/paypal.php implements a native cURL Orders v2 adapter: Server creates order using its stored class price and booking reference. Stable create-ID prevents duplicate orders from a timeout retry. Buyer follows PayPal's HTTPS approval link. Return page shows a final reservation button; GET never proves payment. Server checks approval, records capture_pending, and captures once using a stable capture-ID. Unknown outcomes remain held for reconciliation. Server retrieves the order and validates completed order / capture, exact booking references, amount, currency, merchant and final capture. Only then does it confirm, issue the token and queue the message. booking-webhook.php is the HTTPS recovery endpoint. It rejects unconfigured requests, validates PayPal's signature through PayPal's verification API, then retrieves trusted order data for completed captures. Repeated events are idempotent. A capture-completed event cannot reset a refunded / review record. Refund / reversal / denial notifications flag matching records for owner review; they do not guess a refund amount or silently admit a student. The browser-return path captures after approval; the webhook recovers a completed capture if that browser response is lost. An approved order that was never captured is not a payment and does not receive a token. Sandbox and live host URLs are fixed in the adapter; secrets never enter JS. No PayPal SDK or shopping cart dependency is required for one class place. All remote API behavior still needs real sandbox validation. No keys were available and no PayPal request or charge was made during implementation. 7. EXACT ACTIVATION SETTINGS See booking/config.example.php for environment-variable names. It is a reference only; it is NOT loaded as configuration and must never contain real secrets. Set variables through the PHP host's private environment / server controls. LIEBEN_BOOKING_MODE demo | paypal_sandbox | paypal_live LIEBEN_BOOKING_STORAGE Absolute private directory outside docroot. LIEBEN_CLASSES_FILE Optional path override; default is classes.json. LIEBEN_BOOKING_BASE_URL Exact HTTPS site URL, including any subdirectory. LIEBEN_ADMIN_PASSWORD_HASH Owner password_hash(), or local setup instead. LIEBEN_PAYPAL_CLIENT_ID Correct sandbox / live app client ID. LIEBEN_PAYPAL_SECRET Matching private app secret. LIEBEN_PAYPAL_MERCHANT_ID Expected receiving merchant ID. LIEBEN_PAYPAL_WEBHOOK_ID ID of this app's subscribed HTTPS webhook. LIEBEN_LIVE_CHECKOUT_ENABLED Must be 1 to activate real paid bookings. LIEBEN_EMAIL_TRANSPORT queue (default) | mail (host configured). LIEBEN_EMAIL_FROM Valid sender / verified domain address. LIEBEN_OWNER_EMAIL Optional owner copies of booking / review notices. For paid sandbox checkout: configure app credentials, merchant / webhook ID, HTTPS base URL and cURL/TLS. Subscribe the app's listener to capture completed, denied, refunded and reversed events. Test with a sandbox buyer. PayPal's webhook simulator does not support the postback signature-verification method used here; use actual sandbox app transactions for end-to-end verification. For live: additionally replace sample schedule / policies, set is_demo=false, enable the explicit live switch and configure the email sender / owner password. Do not reuse the demo runtime directory or preview schedule for real students. Configure private permissions, backups, retention and HTTPS first. These settings are preparation, not a claim that a payment integration has passed its live tests. The local PHP 8.5 installation used here does not currently load cURL. Enable PHP cURL with working TLS certificate validation on the chosen environment; the adapter never disables certificate checks as a workaround. Verify PHP sessions, random_bytes(), password_hash(), JSON, file locking and atomic same-volume rename are supported by the host. No database extension is needed. 8. CONFIRMATION EMAIL AND REMINDERS The email outbox is part of the same protected store as the reservation. One confirmation job is created per booking. Demo mode never sends email. Queue mode stores / previews messages without pretending they were sent. The included delivery adapter uses PHP mail() only after explicit configuration. This relies on the host's mail server; it is not an SMTP-login client. For an authenticated SMTP / transactional API provider, replace the small delivery adapter in booking/email.php while keeping the outbox / booking rules intact. If that is your preference, provide the chosen service, sender domain and server-side credentials privately. Verify domain authentication and an actual arrival in your inbox before live booking. A mail() true result is recorded as accepted, never as proof of delivery. The owner may dispatch queued messages when the transport is active. Confirmed PayPal / free bookings also attempt a small outbox dispatch. The optional CLI entry point is booking/dispatch-emails.php. No scheduler or background job was installed. Queue one reminder manually from the roster; automated reminder timing is a future decision. Failed / interrupted messages can be returned to queue after checking whether the mail server accepted them. With PHP mail, a crash after acceptance can make a retry duplicate an email; a provider with idempotency support would improve this. The contact page currently posts to Formspree. Its existing setup is unchanged. Formspree's documentation describes autoresponse workflows, with plan / custom domain conditions. That is a contact-form mechanism, not authoritative proof of class payment. Do not use a browser posting to that endpoint to mark a class paid. A verified backend could be connected after provider / plan review. PayPal's transaction receipt is separate from your class / venue / token email. SMS is not implemented. Adding it requires a provider, account / sender, explicit student opt-in, phone-number handling and your notification policy. 9. WHAT I NEED FROM YOU TO GO LIVE Business / content: - Actual class types and descriptions approved in your own voice. - Dates, times, venue names, exact addresses, arrival / accessibility notes. - Capacity, prices, any taxes / fees, free classes and currency decisions. - Booking cutoff, cancellation / refund / no-show / transfer rules. - Whether you want a waitlist, recurring series, packs / credits or guest seats. - Privacy / data-retention choices and who may access the roster. Configuration: - Final HTTPS site URL and hosting / PHP details. - Sandbox PayPal app and buyer, then the correct live business app. - Merchant ID and the webhook subscription for this endpoint. - A privately configured owner password and storage / backup location. - An email-delivery choice, verified sender domain and test recipient. - SMS provider and consent choices only if SMS is wanted. Acceptance tests before real payment: - Complete, cancel, decline and retry real sandbox transactions. - Verify wrong amount / currency / merchant / booking references cannot confirm. - Verify duplicate returns and webhook redeliveries create one token / message. - Verify final-seat contention, expiration, capture uncertainty and late payment. - Verify refund / reversal review, owner correction and actual refund policy. - Verify lost-cookie / another-browser recovery through the private link. - Verify mail delivery, failure / retry and phone email formatting. - Test actual iPhone Safari dialogs, forms, keyboards, checkout return and links. - Review persistence / recovery on the final host and decide when to archive. 10. FLAT FILES OR A DATABASE? Flat files are adequate for a small schedule on one PHP instance with local disk. The private JSON is atomic for each reservation update. This design is not intended for several servers sharing a network filesystem or heavy concurrent checkout traffic. The public schedule and private roster are two files, so a class cancellation plus notices is not a full database transaction across both; monitor the outbox after owner changes / interrupted saves. SQLite would be the first upgrade I would consider: one file, real transactions, indexes, easier searching / reporting and less whole-file rewriting. It needs the host's SQLite PHP driver enabled. MySQL makes more sense for multiple servers, larger scale or a host that already provides managed MySQL. The storage module can be replaced without rebuilding the calendar / class pages. No database is introduced here. No framework, package manager or build step. 11. CODE MAP AND REVIEW NOTES booking/bootstrap.php Paths, configuration, sessions, CSRF and small helpers. booking/storage.php Guarded JSON, file lock, atomic writes, rate limits. booking/schedule.php Schedule validation, sorting and owner save/history. booking/reservations.php Holds, capacity, access links, admission / payment state. booking/paypal.php Order creation, capture and provider-side verification. booking/email.php Durable outbox and replaceable delivery adapter. booking/owner.php Owner password / session and roster actions. partials/classes.php Reusable calendar, rows, details, dialog / link markup. assets/css/classes.css Scoped class layouts, cards, forms and owner UI. assets/js/classes.js Dialogs, private-link import / copy and template fill. Shared head / footer load these assets only on class pages. Header adds one ordinary Classes link. Existing design demos and galleries retain their own CSS / scripts. New pages remain flat PHP / HTML; there is no CMS or page table. 12. INPUTS, LIMITS AND REFERENCES Design / engineering: existing website-devtest-1 code, the owner's brief and AGENTS.md. Sample teaching copy is generic placeholder text; no canonical curriculum was read or rewritten. Real-world venues / class offerings / prices are not asserted. Runtime tests use an isolated private directory, not a live student roster. Implementation / validation records and verified backups: generated-lieben-yoga-proprietary/website-classes-2026-11-01/ research/website-classes-2026-11-01/ Official integration references, accessed 2026-11-01: https://developer.paypal.com/api/orders/v2 https://developer.paypal.com/api/orders/v2/orders-create https://developer.paypal.com/api/rest/webhooks/rest/ https://developer.paypal.com/api/rest/reference/idempotency/ https://help.formspree.io/articles/plugins/sending-a-confirmation-or-response-email/ Local / fixture tests can verify the state machine and interface. They cannot prove PayPal account configuration, actual payment capture, webhook delivery, email arrival, real-network reliability or physical iOS behavior. Those remain integration acceptance work after the accounts and hosting are available.