4 Commits
Author SHA1 Message Date
thatguygriffandClaude Opus 5 8122c158cf Keep the guardian service within the PHP the plugin supports
CI / Tests (PHP 8.2) (pull_request) Successful in 51s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m54s
CI / PHPStan (pull_request) Successful in 2m55s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m43s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
`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]>
2026-07-29 16:07:52 -03:00
thatguygriffandClaude Opus 5 b772e1811e Let parents register once and book for their children
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]>
2026-07-29 16:07:52 -03:00
thatguygriff c25260a367 Merge pull request 'Bump version to 1.2.5' (#138) from release/bump-1.2.5 into main
CI / Tests (PHP 8.1) (push) Successful in 47s
CI / No Debug Code (push) Successful in 2s
CI / Coding Standards (push) Successful in 2m51s
CI / PHPStan (push) Successful in 3m0s
CI / Tests (PHP 8.2) (push) Successful in 47s
CI / Tests (PHP 8.3) (push) Successful in 2m42s
CI / Build Plugin Zip (push) Successful in 2m49s
Reviewed-on: #138
2026-07-29 02:29:57 +00:00
Release Bot 96aaeff79c Bump version to 1.2.5 and open changelog section 2026-07-29 02:29:47 +00:00
5 changed files with 23 additions and 12 deletions
+7
View File
@@ -3,6 +3,13 @@ includes:
parameters:
level: 10
# Analyse against the whole supported range, not whatever PHP happens to be
# running. Without this, syntax newer than the `Requires PHP: 8.1` header
# promises passes lint on a modern local PHP and only fails in the 8.1 test
# job — which is how a PHP 8.2 `true` return type once reached CI.
phpVersion:
min: 80100
max: 80300
paths:
- src
bootstrapFiles:
+4 -4
View File
@@ -144,18 +144,18 @@ class FamilyPage {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller.
$childId = absint( Val::int( $_POST['child_id'] ?? 0 ) );
$result = $this->guardians->updateChild( $guardianId, $childId, $this->postString( 'child_name' ), $this->postString( 'child_dob' ) );
$error = $this->guardians->updateChild( $guardianId, $childId, $this->postString( 'child_name' ), $this->postString( 'child_dob' ) );
return $result instanceof \WP_Error ? $result : self::RESULT_UPDATED;
return $error ?? self::RESULT_UPDATED;
}
private function handleRemove( int $guardianId ): string|\WP_Error {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller.
$childId = absint( Val::int( $_POST['child_id'] ?? 0 ) );
$result = $this->guardians->removeChild( $guardianId, $childId );
$error = $this->guardians->removeChild( $guardianId, $childId );
return $result instanceof \WP_Error ? $result : self::RESULT_REMOVED;
return $error ?? self::RESULT_REMOVED;
}
/**
+8 -4
View File
@@ -99,8 +99,10 @@ class GuardianService {
* Rename a child and update their date of birth. Refuses a student the caller
* is not the guardian of, so the family screen cannot be turned into an
* arbitrary user editor by posting someone else's id.
*
* Returns null on success, mirroring {@see \Unsupervised\Schedular\Registration\RegistrationGate::validate()}.
*/
public function updateChild( int $guardianId, int $studentId, string $name, string $dateOfBirth = '' ): true|\WP_Error {
public function updateChild( int $guardianId, int $studentId, string $name, string $dateOfBirth = '' ): ?\WP_Error {
if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) {
return new \WP_Error( 'forbidden', __( 'That is not one of your children.', 'unsupervised-schedular' ) );
}
@@ -124,7 +126,7 @@ class GuardianService {
$this->setDateOfBirth( $studentId, $dateOfBirth );
return true;
return null;
}
/**
@@ -132,8 +134,10 @@ class GuardianService {
* lesson or enrolment history: their id is referenced by lessons, payments and
* credits, and deleting the user would orphan all of it. A studio admin
* handles those cases by hand.
*
* Returns null on success.
*/
public function removeChild( int $guardianId, int $studentId ): true|\WP_Error {
public function removeChild( int $guardianId, int $studentId ): ?\WP_Error {
if ( ! $this->guardians->isGuardianOf( $guardianId, $studentId ) ) {
return new \WP_Error( 'forbidden', __( 'That is not one of your children.', 'unsupervised-schedular' ) );
}
@@ -148,7 +152,7 @@ class GuardianService {
$this->guardians->delete( $guardianId, $studentId );
$this->deleteUser( $studentId );
return true;
return null;
}
/**
+2 -2
View File
@@ -182,7 +182,7 @@ class FamilyPageTest extends TestCase
'child_dob' => '2015-04-02',
];
$this->guardians->shouldReceive('updateChild')->once()->with(5, 42, 'Ada L', '2015-04-02')->andReturn(true);
$this->guardians->shouldReceive('updateChild')->once()->with(5, 42, 'Ada L', '2015-04-02')->andReturn(null);
$captured = null;
$this->capturingPage($captured)->maybeHandleSubmit();
@@ -194,7 +194,7 @@ class FamilyPageTest extends TestCase
{
$_POST = ['us_family_action' => 'remove', 'child_id' => '42'];
$this->guardians->shouldReceive('removeChild')->once()->with(5, 42)->andReturn(true);
$this->guardians->shouldReceive('removeChild')->once()->with(5, 42)->andReturn(null);
$captured = null;
$this->capturingPage($captured)->maybeHandleSubmit();
+2 -2
View File
@@ -236,7 +236,7 @@ class GuardianServiceTest extends TestCase
->with(['ID' => 42, 'display_name' => 'Ada L', 'nickname' => 'Ada L'])
->andReturn(42);
self::assertTrue($this->service->updateChild(5, 42, 'Ada L', '2015-04-02'));
self::assertNull($this->service->updateChild(5, 42, 'Ada L', '2015-04-02'));
self::assertSame('2015-04-02', $this->meta[42][GuardianService::META_DOB]);
}
@@ -249,7 +249,7 @@ class GuardianServiceTest extends TestCase
Functions\expect('wp_delete_user')->once()->with(42);
self::assertTrue($this->service->removeChild(5, 42));
self::assertNull($this->service->removeChild(5, 42));
}
/**