A parent registers once and manages lessons for one or more children, who need no login of their own.
Design decisions
The issue asked for these to be settled first; they were, and the reasoning is in docs/features/parent-guardian-accounts.md.
Question
Decision
How is a child represented?
(A) a real wp_users row with the student role and no usable login, linked via a new us_guardians table. student_id keeps meaning "a WordPress user" on all seven tables, so booking, credits, enrolments, policies and answers work unchanged.
How do children get in?
A checkbox at signup revealing a block per child, plus a Family screen for adding them later.
Who holds payments and credits?
Guardian is payer, child is subject — payer_id on us_payments and us_credits; student_id keeps naming the child.
v1 scope
Guardian can also be a student; guardian accepts policies per child (accepted_by); account questions asked per child. Two guardians sharing a child is out, but the schema does not design it out.
The authorisation boundary
POST /bookings and POST /enrollments take an optional student_id, honoured only when GuardianService::canActFor() confirms the caller is that student's guardian — anything else is a 403, resolved before a slot is claimed or a charge raised. Without it any signed-in student could book, and bill, against any user id they cared to send. Same check on cancel and withdraw. Child accounts additionally cannot authenticate at all (ChildLoginGate).
Children first, no parent default
Per feedback during the build: the "Who is this for?" picker lists children first and the account holder last, so the default selection is never the parent. Booking for the wrong child is a correctable inconvenience; silently billing a parent's account for a lesson meant for their kid is the error worth designing out. An account with only itself on the list sees no picker at all.
Schema
New us_guardians; payer_id on us_payments / us_credits; accepted_by on us_policy_acceptances. All three new columns default to 0, read back as "same as student_id", so an existing row keeps its meaning whatever happens. The installer also backfills them, because the balance lookups key on payer_id directly and an indexed WHERE payer_id = 5 would not see a legacy 0. Version: and USC_VERSION bumped to 1.3.0 so dbDelta actually runs.
What else moved
Registration — per-child questions and policy acceptances; the whole family is rolled back if any child fails, so a signup never half-works.
Billing — the billing-method override, receipts, the Stripe payment step and the daily scan all resolve the payer. A guardian gets one notice covering every child, each line naming whose lesson it is. Family credit balance.
Family screen — [us_family] / the Family block. Removing a child is refused once they have lessons or enrolments, rather than orphaning that history.
Admin — Family column on the students list, Family panel on the student screen, and the credit balance labelled with whose account holds it.
Docs: new parent-guardian-accounts.md, cross-links added to the nine related feature docs, changelog entry.
Checks
composer test (762 tests, 2189 assertions), composer lint, composer cs all green. 78 new tests, including the 403 paths on both endpoints and the signup rollback.
Not in this slice
Family discounts, two guardians per child, and creating a child from wp-admin — all noted in the doc, none designed out.
Closes #132.
A parent registers **once** and manages lessons for one or more children, who need no login of their own.
## Design decisions
The issue asked for these to be settled first; they were, and the reasoning is in `docs/features/parent-guardian-accounts.md`.
| Question | Decision |
|---|---|
| How is a child represented? | **(A) a real `wp_users` row** with the student role and no usable login, linked via a new `us_guardians` table. `student_id` keeps meaning "a WordPress user" on all seven tables, so booking, credits, enrolments, policies and answers work unchanged. |
| How do children get in? | A **checkbox at signup** revealing a block per child, plus a **Family** screen for adding them later. |
| Who holds payments and credits? | **Guardian is payer, child is subject** — `payer_id` on `us_payments` and `us_credits`; `student_id` keeps naming the child. |
| v1 scope | Guardian can also be a student; guardian accepts policies per child (`accepted_by`); account questions asked per child. Two guardians sharing a child is **out**, but the schema does not design it out. |
## The authorisation boundary
`POST /bookings` and `POST /enrollments` take an optional `student_id`, honoured **only** when `GuardianService::canActFor()` confirms the caller is that student's guardian — anything else is a `403`, resolved before a slot is claimed or a charge raised. Without it any signed-in student could book, and bill, against any user id they cared to send. Same check on cancel and withdraw. Child accounts additionally cannot authenticate at all (`ChildLoginGate`).
## Children first, no parent default
Per feedback during the build: the "Who is this for?" picker lists **children first and the account holder last**, so the default selection is never the parent. Booking for the wrong child is a correctable inconvenience; silently billing a parent's account for a lesson meant for their kid is the error worth designing out. An account with only itself on the list sees no picker at all.
## Schema
New `us_guardians`; `payer_id` on `us_payments` / `us_credits`; `accepted_by` on `us_policy_acceptances`. All three new columns default to `0`, read back as "same as `student_id`", so **an existing row keeps its meaning whatever happens**. The installer also backfills them, because the balance lookups key on `payer_id` directly and an indexed `WHERE payer_id = 5` would not see a legacy `0`. `Version:` and `USC_VERSION` bumped to **1.3.0** so `dbDelta` actually runs.
## What else moved
- **Registration** — per-child questions and policy acceptances; the whole family is rolled back if any child fails, so a signup never half-works.
- **Billing** — the billing-method override, receipts, the Stripe payment step and the daily scan all resolve the payer. A guardian gets **one** notice covering every child, each line naming whose lesson it is. Family credit balance.
- **Family screen** — `[us_family]` / the **Family** block. Removing a child is refused once they have lessons or enrolments, rather than orphaning that history.
- **Admin** — Family column on the students list, Family panel on the student screen, and the credit balance labelled with whose account holds it.
- Docs: new `parent-guardian-accounts.md`, cross-links added to the nine related feature docs, changelog entry.
## Checks
`composer test` (762 tests, 2189 assertions), `composer lint`, `composer cs` all green. 78 new tests, including the 403 paths on both endpoints and the signup rollback.
## Not in this slice
Family discounts, two guardians per child, and creating a child from wp-admin — all noted in the doc, none designed out.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
A parent registers once and manages lessons for one or more children, who
need no login of their own. A child is a real wp_users row with the student
role but no usable login — so student_id keeps meaning "a WordPress user"
on every table, and booking, credits, policies and enrolments work unchanged.
A us_guardians link table maps guardian to child.
The signup form gains a parent/guardian tick that reveals a block per child,
with the account-signup questions asked per child rather than per guardian
— they describe the student, not the account holder. Signup policies are
recorded once per child with the guardian as the acceptor, which is the
record that actually means something. A family that half-creates is rolled
back entirely rather than leaving a guardian who cannot re-register.
The booking and enrolment forms gain a "Who is this for?" picker listing
children first, so the default selection is never the parent — booking for
the wrong child is correctable, quietly billing a parent for their kid's
lesson is not. POST /bookings and POST /enrollments take an optional
student_id honoured only for that child's guardian; anything else is a 403.
That check is the authorisation boundary of the feature.
Payments and credits gain a payer: the charge names the child it was for and
the guardian who owes it, so per-child reporting is unchanged while notices,
receipts and the payment step reach the parent. Credit is held by the payer,
so one child's cancellation can settle a sibling's charge, and the daily
billing scan sends a guardian one notice covering every child.
Closes#132
Co-Authored-By: Claude Opus 5 <[email protected]>
`true` as a return type is PHP 8.2, but the plugin advertises 8.1, so the
family screen's two service calls fataled on the 8.1 test job while every
other job passed. They now return `?\WP_Error` — null on success — which
matches RegistrationGate::validate() and works on 8.1.
PHPStan was analysing against whatever PHP happened to be running (8.3 in
CI, newer locally), so `composer lint` was green on syntax the plugin
promises not to use. It is now pinned to the supported 8.1-8.3 range, which
reproduces this failure at lint time instead of three jobs later.
Co-Authored-By: Claude Opus 5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #132.
A parent registers once and manages lessons for one or more children, who need no login of their own.
Design decisions
The issue asked for these to be settled first; they were, and the reasoning is in
docs/features/parent-guardian-accounts.md.wp_usersrow with the student role and no usable login, linked via a newus_guardianstable.student_idkeeps meaning "a WordPress user" on all seven tables, so booking, credits, enrolments, policies and answers work unchanged.payer_idonus_paymentsandus_credits;student_idkeeps naming the child.accepted_by); account questions asked per child. Two guardians sharing a child is out, but the schema does not design it out.The authorisation boundary
POST /bookingsandPOST /enrollmentstake an optionalstudent_id, honoured only whenGuardianService::canActFor()confirms the caller is that student's guardian — anything else is a403, resolved before a slot is claimed or a charge raised. Without it any signed-in student could book, and bill, against any user id they cared to send. Same check on cancel and withdraw. Child accounts additionally cannot authenticate at all (ChildLoginGate).Children first, no parent default
Per feedback during the build: the "Who is this for?" picker lists children first and the account holder last, so the default selection is never the parent. Booking for the wrong child is a correctable inconvenience; silently billing a parent's account for a lesson meant for their kid is the error worth designing out. An account with only itself on the list sees no picker at all.
Schema
New
us_guardians;payer_idonus_payments/us_credits;accepted_byonus_policy_acceptances. All three new columns default to0, read back as "same asstudent_id", so an existing row keeps its meaning whatever happens. The installer also backfills them, because the balance lookups key onpayer_iddirectly and an indexedWHERE payer_id = 5would not see a legacy0.Version:andUSC_VERSIONbumped to 1.3.0 sodbDeltaactually runs.What else moved
[us_family]/ the Family block. Removing a child is refused once they have lessons or enrolments, rather than orphaning that history.parent-guardian-accounts.md, cross-links added to the nine related feature docs, changelog entry.Checks
composer test(762 tests, 2189 assertions),composer lint,composer csall green. 78 new tests, including the 403 paths on both endpoints and the signup rollback.Not in this slice
Family discounts, two guardians per child, and creating a child from wp-admin — all noted in the doc, none designed out.
🤖 Generated with Claude Code
4a41ba96fbto8122c158cf