Completes the instructor-management half of #9: the studio admin can now
create instructor accounts and toggle each instructor's capabilities.
- InstructorController (manage_instructors): list instructors, create a
us_instructor WP user (emailing a set-password link), and a per-instructor
capability detail view.
- InstructorCapabilities: pure, unit-tested rules for which managed caps an
admin may assign and how a submitted form maps to assignments. Managed caps
are manage_offerings, manage_questions, view_own_payments, export_payments;
manage_availability and view_own_lessons are core to every instructor.
- A studio admin can never grant a capability it does not itself hold: only
held caps (checked via current_user_can, so an administrator's dynamic grant
counts) are offered, and on creation any managed cap the admin lacks is
denied on the new instructor so they never exceed their creator. The role
grants the managed caps by default; the page layers per-user overrides.
- AdminMenu: register the Instructors page in the people section.
- Tests for the capability logic; docs/features/user-roles.md updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A WordPress administrator previously inherited the studio-admin
capabilities but not `manage_availability`, so the studio owner running
as an admin had no way to reach "My Availability" or act as the
instructor — breaking single-instructor businesses.
Grant the instructor capabilities to administrators as well (via the
existing `user_has_cap` filter), and make both grants — studio-admin and
instructor — independently toggleable from a new Access admin page.
- RoleManager: extract `INSTRUCTOR_CAPS`; apply studio and instructor
cap sets to administrators, each gated on a stored toggle (default on).
- AccessSettings + templates/admin/access.php: two options
(`us_admin_grant_studio` / `us_admin_grant_instructor`), gated on the
core `manage_options` capability so disabling a grant can never lock an
administrator out of re-enabling it.
- AdminMenu: register the Access page after Studio Settings; keep the
studio sidebar separator visible for any administrator.
- Tests for the toggles and the new settings reader; docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the deferred half of payments: real credit-card processing on
top of the existing ledger/e-transfer/comp foundation.
- StripeGateway wraps stripe/stripe-php: creates idempotent PaymentIntents
(amount in cents, registration ids in metadata) and verifies webhook
signatures. Stripe calls sit behind protected seams for unit testing.
- PaymentService::createIntent resolves the client-side step for a new
registration (card → client secret; e-transfer → display data; comp →
none) with caller-ownership enforcement.
- PaymentService::handleWebhook finalises a payment exactly once on
payment_intent.succeeded (mark paid → confirm → receipt) and marks it
failed on payment_intent.payment_failed.
- PaymentEndpoint: POST /payments/intent (book_lesson) and public,
signature-verified POST /payments/webhook.
- PaymentRepository: setStripeIntentId / findByStripeIntentId.
- StudioSettings: us_stripe_webhook_secret option, with the webhook URL
and required events surfaced on the settings page.
- Front end: shared payment.js mounts Stripe Payment Elements and confirms
the card (or shows e-transfer instructions); Stripe.js enqueued only when
configured. Wired into booking and group-class flows.
Tests: new StripeGatewayTest; PaymentService card-intent + webhook cases;
repository coverage. composer test/lint/cs all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Studio Settings gains a default HST rate; the rate is frozen onto each
payment at booking and computed against the pre-tax subtotal, with the
total billed as subtotal + tax. The rate is overridable per booking on
My Lessons while unpaid (recomputing the tax amount), comped
registrations are never taxed, and receipts break out subtotal/HST/total.
Builds the payments report (roadmap #8) from us_payments: a monthly
per-instructor view with subtotal, HST collected, and grand-total
aggregation, plus a nonce-protected CSV export via admin-post. Studio
admins see all instructors and can filter; instructors are scoped to
their own rows. The Payment Report menu is gated on export_payments so
instructors (who lack manage_billing) can reach it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add sidebar separators (wp-menu-separator) that set the studio menus apart
from the core WordPress items and split them into three sections:
[sep] Studio Settings · Policies · Invites · Offerings
[sep] Students · Group Classes · Payments
[sep] Scheduler (then the instructor menus: My Availability, My Lessons)
Separators live at positions 29 / 34 / 38; each is only added when the user
can see a menu in the following section, to avoid orphaned dividers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The e-transfer destination is resolved at booking time (offering override ->
studio default) and frozen onto the payment, so each record keeps where the
student was directed. It can then be corrected per booking.
- StudioSettings: us_etransfer_email option + a Default e-transfer email field
on the Studio Settings page.
- Offering: etransfer_email column/field (instructor override) across VO, repo,
REST endpoint, admin controller, and form.
- Payment: etransfer_email column on the payment (frozen record) +
PaymentRepository::updateEtransferEmail; PaymentService freezes it from the
offering override or studio default at creation; booking/enrolment pass the
offering override.
- My Lessons: instructors edit the e-transfer email per pending lesson payment
(ownership-checked).
- Payments queue: studio admin can correct the email at confirmation (for when
a student sends it to the wrong place).
- Docs updated.
Tests: Payment/Offering rows + PaymentService freezing. composer test (148),
cs, and PHPStan level 6 all pass.
Refs #7
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the payments foundation for #7. Without Stripe credentials
everything works on e-transfer (pending payment confirmed by a studio
admin); when Stripe keys are configured the default flips to credit card.
Per-student override (card/etransfer/comp) is set on the student detail.
- Schema: us_payments (amount DECIMAL dollars, method, status, receipt,
stripe intent id).
- src/Payment/: Payment VO, PaymentRepository, StudioSettings (Stripe
options + isStripeConfigured + settings page), BillingMethodResolver
(per-student override; default card if configured else etransfer),
ReceiptMailer, PaymentService (create at registration, link payment_id,
comp->paid+confirm, markPaid->confirm+receipt), PaymentController
(e-transfer confirmation queue), PaymentEndpoint (PATCH /payments/{id}).
- Booking + enrolment create the payment from the offering price; comp
auto-confirms the lesson; setPaymentId on both repositories.
- Admin: Studio Settings + Payments menus (manage_billing); per-student
billing method on the student detail page.
- Docs: payments.md + README updated.
Deferred to a follow-up: the live Stripe card charge (PaymentIntent +
Stripe.js Elements + webhook + stripe/stripe-php). Until then a card
payment is created pending and confirmed like an e-transfer.
Tests: tests/Unit/Payment/ (VO, repository, resolver, service, mailer).
composer test (147), cs, and PHPStan level 6 all pass.
Refs #7
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements #22: a read-only Students area for studio admins.
- StudentController (manage_students): a list of us_student users with
upcoming-lesson and active-enrolment counts, each linking to a detail page
showing account info, upcoming/past lessons (offering, instructor, status),
and group-class enrolments.
- StudentSchedule::partition() — pure, unit-tested upcoming/past split.
- Repo counts: BookingRepository::countUpcomingForStudent and
EnrollmentRepository::countActiveForStudent (single-query, tested).
- Templates: templates/admin/students.php, student-detail.php.
- Students admin menu wired in AdminMenu (no Plugin change — the repos were
already available there).
- Docs: README status flipped to implemented; feature spec updated.
Payment history slots into the detail when Payments (#7) lands.
Tests: StudentScheduleTest + the two repo count tests. composer test (127),
cs, and PHPStan level 6 all pass.
Refs #22
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README.md: plugin overview, package-by-domain architecture, feature/status
table linking each docs/features spec (implemented vs planned), shortcodes,
REST namespace, roles, install via composer build, dev commands, and CI.
- docs/features/student-administration.md: spec for the planned studio-admin
Students list + per-student detail (upcoming/past lessons, group enrolments);
read-only; manage_students; no new tables. Tracked as #22.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements #4: students enrol in a group_class offering via the same
registration gate as private lessons (intake questions + booking-scoped
policy acceptance). Enrolment is capacity-enforced and prevents duplicates.
- Schema: us_group_enrollments table.
- Enrollment value object + EnrollmentRepository (countActiveForOffering,
hasActiveEnrollment, per-student/instructor/all-active queries, status).
- EnrollmentEndpoint: GET /enrollments (scoped) and POST /enrollments
(validates group_class, capacity, no-duplicate; reuses RegistrationGate;
records answers/acceptances type enrollment).
- GroupClassController + admin page (view_all_lessons): all active enrolments.
- Front-end: [us_group_classes] shortcode (GroupClassPage) + group-classes.js
enrol flow (list classes -> questions + policies -> POST /enrollments).
- Wiring in Plugin, RestRegistrar, AdminMenu, ShortcodeRegistrar.
Payment is the deferred seam (#7): enrolment lands active, payment_id null.
JS left untested for parity with the repo's no-build vanilla-JS posture.
Tests: tests/Unit/GroupClass/ (Enrollment, EnrollmentRepository).
composer test (121), cs, and PHPStan level 6 all pass.
Refs #4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements #3: students register for a private lesson by picking a slot,
answering the offering's intake questions, and accepting booking-scoped
policies. Payment is a clean seam for #7 (lessons land pending; payment_id
null; instructor confirms via PATCH /bookings/{id}/status).
- Schema: us_lessons += offering_id, recurrence, series_id, payment_id.
- Lesson: new fields + recurrence constants.
- BookingRepository::insertSeries() builds a weekly series sharing a
series_id; AvailabilityRepository::findUnbookedInGroup() reserves a group.
- RegistrationGate (src/Registration/): validate + record intake answers and
booking-scoped policy acceptances. Reused by group enrolment (#4).
- BookingEndpoint::book(): offering_id, recurrence, answers,
accepted_policy_version_ids; single or weekly; records answers/acceptances
(type lesson).
- GET /policies?scope=booking filter.
- Front-end booking.js: slot -> questions + policies -> submit.
- Wiring: RegistrationGate built in Plugin, passed via RestRegistrar.
- Test-only WP_Error stub in tests/bootstrap.php for gate testing.
Refs #3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RegistrationPage::maybeRedirectToRegistrationPage() (hooked on
template_redirect): any front-end request carrying a us_invite token is
redirected to the configured registration page (token preserved), unless
already there. Covers links shared before a page was selected; no-op when
no page is set.
- Invites button text: "Send Invite" -> "Generate Invitation Link".
- Doc updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Invitation links previously pointed at the site home page, which usually
does not host the [us_student_register] shortcode. Let the studio admin
choose the registration page (stored in the us_registration_page_id
option); invitation links now point there, falling back to the home page
when unset (with a warning notice).
- RegistrationController: OPTION_PAGE constant; set_page action; pass the
page id/url to the template.
- templates/admin/invites.php: wp_dropdown_pages selector + save; build the
invite link from the selected page.
- Doc updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements #16: invite-only student self-registration through a front-end
page, accepting signup-scoped policies at account creation.
Policy domain:
- us_policies.acceptance_scope (signup/booking/both); Policy::appliesTo();
PolicyRepository::findForScope(); scope threaded through PolicyService,
the REST create, the admin controller, and the Policies form.
- PolicyAcceptance::REG_ACCOUNT (registration_id = the new user's ID).
Auth:
- Invite value object + InviteRepository; us_invites table.
- RegistrationController + Invites admin page (manage_students): invite an
email, share the registration link, revoke.
- RegistrationPage ([us_student_register] shortcode): validates the invite
token, collects name/password, renders signup-scoped published policies
with required acceptance, creates the us_student user, records account-type
acceptances, marks the invite accepted, and logs the user in.
- RoleManager: manage_students cap added to STUDIO_ADMIN_CAPS.
Invite-only is implemented; the us_registration_mode self_approval path is a
documented future seam.
Docs: docs/features/account-registration.md; policies.md updated.
Tests: tests/Unit/Auth/ (Invite, InviteRepository) plus Policy scope
updates. composer test (104), cs, and PHPStan level 6 all pass.
Refs #16
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>
Implements #6: studio admins draft, version, and publish policies; the
public registration gate reads the current published version of each, and
acceptance is recorded against the exact version so a new version must be
re-accepted at the next booking.
- src/Policy/: Policy, PolicyVersion, PolicyAcceptance value objects;
PolicyRepository, PolicyVersionRepository, AcceptanceRepository;
PolicyService (orchestrates create/add-draft/publish across the policies
and versions tables); PolicyEndpoint (REST); PolicyController +
templates/admin/policies.php (Policies admin menu, manage_policies)
- us_policies, us_policy_versions, us_policy_acceptances tables in Schema
- REST: public GET /policies (current published versions); manage_policies
for create, add version, edit draft, and publish
- Wiring in Plugin, RestRegistrar, AdminMenu
AcceptanceRepository is built now and consumed by the booking/enrolment
gate in #3/#4.
Also bump PHPStan to --memory-limit=1G in the composer lint script; the
default 128M now crashes the analysis as the codebase has grown.
Tests: tests/Unit/Policy/ (value objects, repositories, service).
composer test (90 total), cs, and PHPStan level 6 all pass.
Refs #6
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gitea/Actions re-zips artifacts on download, so uploading the built plugin
zip produced a double-wrapped archive (a zip containing a zip). WordPress
then reported "No valid plugins were found" because the upload had no
plugin folder/header at its top level.
Unpack the built zip and upload the resulting plugin folder instead, so the
downloaded artifact's top level is unsupervised-schedular/ and installs
directly via Plugins -> Add New -> Upload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflect the in-progress, pre-release state. Bump to 1.0.0 before tagging a
release. Updates both the plugin header and USC_VERSION; the build/CI zip
artifact name tracks this automatically.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WordPress administrators (manage_options) now implicitly hold every
studio-admin capability via a user_has_cap filter, so the site owner runs
the studio without being assigned the separate us_studio_admin role. The
grant persists nothing and is removed on deactivation. The us_studio_admin
role still exists for non-administrator staff and does NOT confer any core
WordPress admin powers.
Also re-gate the studio-wide "Scheduler" dashboard off manage_options onto
a new view_all_lessons capability (added to the studio-admin cap set), so a
us_studio_admin user can see it too — previously it was administrator-only.
- RoleManager: STUDIO_ADMIN_CAPS constant, CAP_VIEW_ALL_LESSONS,
grantStudioCapsToAdministrators() user_has_cap filter
- AdminMenu + LessonController: Scheduler gated on view_all_lessons
- Docs: user-roles.md cap matrix + administrator note; lesson-booking.md
- Tests: administrators receive studio caps; non-admins do not
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- bin/build-zip.sh + `composer build`: stage runtime files only, generate a
production (no-dev) optimized autoloader, and emit
dist/<slug>-<version>.zip with a single top-level plugin folder, ready to
upload via wp-admin. Tests, tooling configs, docs, and dev dependencies
are excluded; version is read from the plugin header.
- CI `build` job: on push to main (post-merge), after lint/static-analysis/
test/no-debug pass, runs the build and uploads the zip via
actions/upload-artifact.
- Ignore build/ and dist/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements #5: studio admin / instructors author intake questions scoped
per offering; answers are stored against a lesson or group enrolment via a
polymorphic registration reference.
- src/Registration/: Question + Answer value objects, QuestionRepository
and AnswerRepository, QuestionEndpoint (REST), QuestionController +
templates/admin/questions.php (Offerings -> Questions submenu)
- us_questions and us_question_answers tables in Schema.php
- REST: public GET /offerings/{id}/questions; POST/PATCH/DELETE /questions
gated by manage_questions + offering ownership (owner or studio admin)
- Field types text/textarea/select/checkbox; select options stored as JSON
- Wiring in Plugin, RestRegistrar, AdminMenu
AnswerRepository is built now and consumed by the booking/enrolment flow
in #3/#4.
Tests: tests/Unit/Registration/ (19 tests). composer test (63 total), cs,
and PHPStan level 6 all pass.
Refs #5
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the offerings catalog (#1): private-lesson types and group
classes carrying pricing, billing mode (one_time/full_term), duration,
capacity, and term details. Adds the src/Offering/ domain (value object,
repository, REST endpoint, admin controller + template), the us_offerings
table, and an Offerings admin page.
Also lands the capability slice of #9: registers the us_studio_admin role
and the new capability strings (manage_instructors, manage_offerings,
manage_questions, manage_policies, manage_billing, view_all_payments,
view_own_payments, export_payments) so offering management gates correctly.
Tests: tests/Unit/Offering/ (value object + repository) and a studio-admin
case in RoleManagerTest. composer test, cs, and PHPStan level 6 all pass.
Refs #1#9
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update availability, lesson-booking, and user-roles docs and add specs
for offerings, group classes, registration questions, versioned policies,
Stripe payments (with e-transfer/comp overrides and receipts), and
monthly per-instructor payment reporting. Tracked in issues #1-#9.
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>
- Add phpcs.xml.dist: excludes PSR-4 file naming, camelCase naming,
short array syntax, and redundant per-method/property docblocks
- Fix wp_unslash() on all $_POST reads (LoginPage, AvailabilityController)
- Add phpcs:ignore for password field (must not be sanitized)
- Fix Yoda conditions throughout (AvailabilityRepository, AvailabilityEndpoint,
BookingEndpoint, AvailabilityController)
- Fix inline comments to end with full stops (AdminMenu)
- Replace short ternary ?: with explicit full ternary (BookingEndpoint)
- Rename $namespace param to $route_namespace (reserved keyword warning)
- Add short descriptions to doc blocks that had tag-only blocks
- Add nonce suppression comment in handleFormAction (nonce verified by caller)
- Update composer.json and CI to use phpcs.xml.dist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused AvailabilityRepository/BookingRepository from BookingPage
and ShortcodeRegistrar (template is a JS shell; no PHP data needed yet)
- Add @param array<string, string> to shortcode render() signatures
- Add @return array<string, mixed> to model toArray() methods
- Fix get_permalink() ?? '' — returns string|false not nullable, use cast
- Remove unused ignoreErrors pattern from phpstan.neon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Custom DB tables for availability slots and lesson bookings
- Instructor (wp-admin) and student (front-end) roles with custom capabilities
- REST API under us-scheduler/v1 for availability CRUD and booking
- [us_booking] and [us_student_login] shortcodes for student front end
- PHPUnit + Brain\Monkey unit test suite (29 tests)
- Gitea Actions CI: lint, PHPStan, tests on PHP 8.1/8.2/8.3, no-debug check
- Feature docs under docs/features/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>