Ask the account holder the studio's questions when they are a student too
CI / Tests (PHP 8.2) (pull_request) Successful in 43s
CI / Tests (PHP 8.1) (pull_request) Successful in 52s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m50s
CI / Coding Standards (pull_request) Successful in 3m19s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m47s
CI / Build Plugin Zip (pull_request) Skipped

Under "both" the questions were collected per student only, so someone
registering themselves alongside their children was never asked their own
instrument, level or anything else — despite being able to book lessons. The
account-scope questions describe a student, and under "both" the account
holder is one.

Their answers are recorded against their own user id, not shared with a
child's, and recorded after the children so a rollback that deletes the
account cannot leave answers pointing at a user that no longer exists. A
pure guardian is unchanged: they are not a student, so anything posted for
them is still ignored.

Validation became two passes rather than one so the message can say whose
answers are missing — with one pass, "both" had to blame "each student" for
the account holder's own blank field.

In the form, the two questions turn out to be independent: whether student
blocks are in play, and whether the account holder answers for themselves.
"Both" is the case that needs its own answer to each, so sync() now tracks
them separately, and step two comes back into play under "both".

Verified in a headless browser: 21 checks across all three choices, now
including that "both" enables the account holder's own question panel and
offers Next rather than the early submit.

Closes #146

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-07-29 23:15:47 -03:00
co-authored by Claude Opus 5
parent 4e5382e259
commit 69179b75c9
5 changed files with 168 additions and 33 deletions
+42 -14
View File
@@ -312,15 +312,21 @@ class RegistrationPage {
$accountQuestions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true );
// Registering as a parent/guardian turns the account-signup questions from
// "about you" into "about each child" — they describe the student
// (instrument, level, school), not the person holding the account.
// The account-signup questions describe a *student* — instrument, level,
// school — not whoever holds the account. So they are asked of each
// student being added, and of the account holder only when they are a
// student themselves. "Both" is both.
$registeringFor = $this->submittedRegisteringFor();
// "Students" and "both" both collect student blocks; only "self" does not.
// "Students" and "both" collect student blocks; only "self" does not.
$isGuardian = self::FOR_SELF !== $registeringFor;
$children = $isGuardian ? $this->submittedChildren() : [];
$answers = $isGuardian ? [] : $this->submittedAnswers();
// "Self" and "both" make the account holder a student, so they answer the
// questions in their own right. Only a pure guardian does not.
$asksSelf = self::FOR_STUDENTS !== $registeringFor;
$children = $isGuardian ? $this->submittedChildren() : [];
$answers = $asksSelf ? $this->submittedAnswers() : [];
// Everything is validated before a single user is created, so a bad child
// block never leaves a half-registered family behind.
@@ -342,16 +348,18 @@ class RegistrationPage {
}
}
foreach ( $isGuardian ? array_column( $children, 'answers' ) : [ $answers ] as $set ) {
foreach ( $accountQuestions as $question ) {
if ( $question->isRequired && '' === trim( (string) ( $set[ (int) $question->id ] ?? '' ) ) ) {
return $isGuardian
? esc_html__( 'Please answer all required registration questions for each student.', 'unsupervised-schedular' )
: esc_html__( 'Please answer all required registration questions.', 'unsupervised-schedular' );
}
// Checked as two passes rather than one so the message can say *whose*
// answers are missing — under "both" a single message could not.
foreach ( array_column( $children, 'answers' ) as $set ) {
if ( $this->hasUnansweredRequired( $accountQuestions, $set ) ) {
return esc_html__( 'Please answer all required registration questions for each student.', 'unsupervised-schedular' );
}
}
if ( $asksSelf && $this->hasUnansweredRequired( $accountQuestions, $answers ) ) {
return esc_html__( 'Please answer all required registration questions.', 'unsupervised-schedular' );
}
if ( email_exists( $email ) ) {
return esc_html__( 'An account already exists for this email.', 'unsupervised-schedular' );
}
@@ -381,7 +389,11 @@ class RegistrationPage {
if ( '' !== $failure ) {
return $failure;
}
} else {
}
// After the children, so a rollback that deletes this account cannot
// leave its answers behind pointing at a user that no longer exists.
if ( $asksSelf ) {
$this->recordAnswers( $accountQuestions, $answers, (int) $userId );
}
@@ -523,6 +535,22 @@ class RegistrationPage {
return add_query_arg( 'us_confirm', rawurlencode( $rawToken ), $base );
}
/**
* Whether any required question in `$questions` is left blank in `$answers`.
*
* @param list<Question> $questions
* @param array<int, string> $answers
*/
private function hasUnansweredRequired( array $questions, array $answers ): bool {
foreach ( $questions as $question ) {
if ( $question->isRequired && '' === trim( (string) ( $answers[ (int) $question->id ] ?? '' ) ) ) {
return true;
}
}
return false;
}
/**
* Who this signup is for: {@see FOR_SELF}, {@see FOR_STUDENTS} or
* {@see FOR_BOTH}.