CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Tests (PHP 8.1) (pull_request) Successful in 54s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m54s
CI / Coding Standards (pull_request) Successful in 2m59s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m43s
CI / Build Plugin Zip (pull_request) Skipped
A weekly booking reserves a series of lessons, but the student answers the intake and ticks the policy boxes once — so BookingEndpoint records both against the anchor lesson alone. The admin detail view looked them up by whichever lesson id was being viewed, so every occurrence after the first showed no answers and no acceptances at all. LessonDetail now takes the Lesson rather than a bare id and resolves the registration to `series_id ?? id`, so each occurrence reads the anchor's records. This is the same seam PaymentService already uses to find a series lesson's payment on the anchor. Nothing was ever missing from the database, so existing bookings read correctly with no migration and no schema change. Closes #167 Co-Authored-By: Claude Opus 5 <[email protected]>
88 lines
3.2 KiB
PHP
88 lines
3.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Unsupervised\Schedular\Booking;
|
|
|
|
use Unsupervised\Schedular\Policy\AcceptanceRepository;
|
|
use Unsupervised\Schedular\Policy\PolicyAcceptance;
|
|
use Unsupervised\Schedular\Policy\PolicyRepository;
|
|
use Unsupervised\Schedular\Policy\PolicyVersionRepository;
|
|
use Unsupervised\Schedular\Registration\Answer;
|
|
use Unsupervised\Schedular\Registration\AnswerRepository;
|
|
use Unsupervised\Schedular\Registration\QuestionRepository;
|
|
|
|
/**
|
|
* Builds the display rows for the admin lesson detail view: the intake answers
|
|
* the student submitted and the policy versions they accepted when booking.
|
|
*
|
|
* Scoped to a single lesson (the `lesson` registration type), mirroring the
|
|
* per-student history in {@see \Unsupervised\Schedular\Auth\StudentHistory}.
|
|
*
|
|
* A weekly reservation is answered for and agreed to once, so its answers and
|
|
* acceptances hang off the series anchor. Every occurrence therefore reads its
|
|
* series' registration, not its own id — otherwise only the first lesson of a
|
|
* series showed the intake and the audit trail, and the rest looked as though
|
|
* nothing had been accepted.
|
|
*/
|
|
class LessonDetail {
|
|
|
|
public function __construct(
|
|
private AnswerRepository $answers,
|
|
private QuestionRepository $questions,
|
|
private AcceptanceRepository $acceptances,
|
|
private PolicyRepository $policies,
|
|
private PolicyVersionRepository $versions,
|
|
) {}
|
|
|
|
/**
|
|
* The intake-question answers recorded for this lesson, in submission order.
|
|
*
|
|
* @return list<array{question: string, answer: string}>
|
|
*/
|
|
public function answers( Lesson $lesson ): array {
|
|
return array_map(
|
|
function ( Answer $answer ): array {
|
|
$question = $this->questions->findById( $answer->questionId );
|
|
$value = $answer->answerValue ?? '';
|
|
|
|
return [
|
|
'question' => $question ? $question->label : sprintf( '#%d', $answer->questionId ),
|
|
'answer' => '' === $value ? '—' : $value,
|
|
];
|
|
},
|
|
$this->answers->findByRegistration( Answer::REG_LESSON, $this->registrationId( $lesson ) )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The policy versions the student accepted when booking this lesson, with the
|
|
* captured acceptance time and IP for the audit trail.
|
|
*
|
|
* @return list<array{policy: string, version: string, accepted_at: string, ip: string}>
|
|
*/
|
|
public function acceptances( Lesson $lesson ): array {
|
|
return array_map(
|
|
function ( PolicyAcceptance $acceptance ): array {
|
|
$version = $this->versions->findById( $acceptance->policyVersionId );
|
|
$policy = $version ? $this->policies->findById( $version->policyId ) : null;
|
|
|
|
return [
|
|
'policy' => $policy ? $policy->title : sprintf( '#%d', $acceptance->policyVersionId ),
|
|
'version' => $version ? sprintf( 'v%d', $version->versionNumber ) : '—',
|
|
'accepted_at' => $acceptance->acceptedAt ?? '',
|
|
'ip' => $acceptance->ipAddress ?? '',
|
|
];
|
|
},
|
|
$this->acceptances->findByRegistration( PolicyAcceptance::REG_LESSON, $this->registrationId( $lesson ) )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The lesson id the booking's answers and acceptances were recorded against:
|
|
* the series anchor for a weekly reservation, the lesson itself otherwise.
|
|
*/
|
|
private function registrationId( Lesson $lesson ): int {
|
|
return $lesson->seriesId ?? (int) $lesson->id;
|
|
}
|
|
}
|