Security fixes from a pen-test review (issues #31–#37):
- #31 Booking no longer trusts a client-supplied offering_id: a slot-tied
offering is authoritative and any offering used must belong to the slot's
instructor, closing a free/misrouted-payment bypass.
- #34 Availability slot creation rejects an offering the instructor does not
own (AvailabilityEndpoint now takes OfferingRepository).
- #32 Offering/question/policy listing endpoints now require book_lesson
instead of being public (no anonymous consumer exists); Offering::toArray
also omits etransfer_email from listings as defense-in-depth.
- #33 Slots are claimed atomically (UPDATE ... WHERE is_booked = 0) before a
lesson is inserted, preventing a double-booking race.
- #35 A single weekly booking is capped (MAX_WEEKLY_OCCURRENCES) and only
creates lessons for slots it actually claimed.
- #36 Stripe secret/webhook keys are write-only in the settings UI and a blank
submit keeps the stored value; secrets are never echoed back into HTML.
- #37 Pending invites expire after 14 days (Invite::isAcceptable), enforced at
registration and surfaced on the admin invites list.
Adds BookingEndpointTest plus Invite/Offering/AvailabilityRepository coverage
and minimal WP_REST_Request/WP_REST_Response stubs. composer test (200),
lint, and cs all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Availability (#2):
- us_availability gains offering_id, duration_minutes (default 60), and
recurrence_group; AvailabilitySlot carries the new fields.
- AvailabilityRepository::createWeeklySeries() generates N weekly rows
sharing a recurrence_group; findAvailable() filters by offering and
duration. Date math uses DateTimeImmutable::modify() (the no-debug CI
regex `dd\(` matches `->add(`).
- REST GET filters by offering_id/duration_minutes; POST accepts
duration_minutes, offering_id, recurrence (single|weekly) + weeks.
- Admin form adds duration, an offering picker, and one-off/weekly options
(OfferingRepository wired into AvailabilityController).
- booking.js renders an agenda calendar (slots grouped by day, with
duration). The richer booking UX lands with the booking-flow work.
Offering price in dollars:
- Switch us_offerings.price_cents (INT) to price DECIMAL(10,2); Offering
uses float $price. Admin form and REST take dollars.
- Fix a pre-existing misalignment in the Offering insert/update $wpdb
format arrays (billing_mode/capacity/is_active were mapped to the wrong
specifiers, which would corrupt values) via a single COLUMN_FORMATS list.
Also bump PHPStan to --memory-limit=1G in the lint script; 128M now
crashes analysis as the codebase has grown.
Refs #2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All classes are now organised by domain (Availability, Booking, Auth).
Each domain package contains its value object, repository, admin controller,
REST endpoint, and any shortcode pages under a matching sub-namespace.
Cross-cutting wiring (Plugin, AdminMenu, RestRegistrar, ShortcodeRegistrar,
Schema) lives at src/ root. Tests mirror the domain structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>