Show booked lesson info on upcoming lists and add admin booking detail
CI / Tests (PHP 8.1) (pull_request) Successful in 46s
CI / Coding Standards (pull_request) Successful in 2m53s
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / No Debug Code (pull_request) Successful in 3s
CI / PHPStan (pull_request) Successful in 3m12s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m36s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 46s
CI / Coding Standards (pull_request) Successful in 2m53s
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / No Debug Code (pull_request) Successful in 3s
CI / PHPStan (pull_request) Successful in 3m12s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m36s
CI / Build Plugin Zip (pull_request) Skipped
Front end: the student "upcoming lessons" panel now shows each booked offering's name and length next to the time, and renders only the soonest five lessons with a "Show all" reveal. GET /bookings returns offering_title and duration_minutes so the list needs no extra request. Admin: the Scheduler and My Lessons week/list views now show the booked offering, and each lesson links to a detail view showing the policy versions the student accepted (with acceptance time and IP) and their intake answers. On My Lessons an instructor may only open their own lessons; the studio Scheduler may open any. composer test / composer lint / composer cs all pass. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -10,6 +10,8 @@ use Unsupervised\Schedular\Availability\AvailabilitySlot;
|
||||
use Unsupervised\Schedular\Booking\BookingRepository;
|
||||
use Unsupervised\Schedular\Booking\Lesson;
|
||||
use Unsupervised\Schedular\Booking\LessonController;
|
||||
use Unsupervised\Schedular\Booking\LessonDetail;
|
||||
use Unsupervised\Schedular\Offering\OfferingRepository;
|
||||
use Unsupervised\Schedular\Payment\PaymentRepository;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
@@ -18,6 +20,8 @@ class LessonControllerTest extends TestCase
|
||||
private BookingRepository&Mockery\MockInterface $bookings;
|
||||
private PaymentRepository&Mockery\MockInterface $payments;
|
||||
private AvailabilityRepository&Mockery\MockInterface $availability;
|
||||
private OfferingRepository&Mockery\MockInterface $offerings;
|
||||
private LessonDetail&Mockery\MockInterface $detail;
|
||||
private LessonController $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -27,7 +31,9 @@ class LessonControllerTest extends TestCase
|
||||
$this->bookings = Mockery::mock(BookingRepository::class);
|
||||
$this->payments = Mockery::mock(PaymentRepository::class);
|
||||
$this->availability = Mockery::mock(AvailabilityRepository::class);
|
||||
$this->controller = new LessonController($this->bookings, $this->payments, $this->availability);
|
||||
$this->offerings = Mockery::mock(OfferingRepository::class);
|
||||
$this->detail = Mockery::mock(LessonDetail::class);
|
||||
$this->controller = new LessonController($this->bookings, $this->payments, $this->availability, $this->offerings, $this->detail);
|
||||
|
||||
$_POST = [];
|
||||
$_GET = [];
|
||||
@@ -176,6 +182,96 @@ class LessonControllerTest extends TestCase
|
||||
self::assertStringNotContainsString('9:00 AM', $html);
|
||||
}
|
||||
|
||||
public function testListViewShowsBookedOfferingName(): void
|
||||
{
|
||||
$_GET['usc_view'] = 'list';
|
||||
|
||||
$lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, offeringId: 8, id: 1);
|
||||
$slot = new AvailabilitySlot(
|
||||
instructorId: 3,
|
||||
startDt: '2026-07-06 09:00:00',
|
||||
endDt: '2026-07-06 10:00:00',
|
||||
id: 10
|
||||
);
|
||||
$offering = new \Unsupervised\Schedular\Offering\Offering(
|
||||
instructorId: 3,
|
||||
kind: 'private_lesson',
|
||||
title: 'Piano Lesson',
|
||||
durationMinutes: 60,
|
||||
id: 8
|
||||
);
|
||||
|
||||
$this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]);
|
||||
$this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot);
|
||||
$this->offerings->shouldReceive('findById')->once()->with(8)->andReturn($offering);
|
||||
|
||||
$html = $this->render();
|
||||
|
||||
self::assertStringContainsString('Piano Lesson', $html);
|
||||
self::assertStringContainsString('lesson_id=1', $html);
|
||||
}
|
||||
|
||||
public function testLessonIdRoutesToDetailWithAnswersAndPolicies(): void
|
||||
{
|
||||
$_GET['lesson_id'] = '1';
|
||||
|
||||
$lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, offeringId: 8, id: 1);
|
||||
$slot = new AvailabilitySlot(
|
||||
instructorId: 3,
|
||||
startDt: '2026-07-06 09:00:00',
|
||||
endDt: '2026-07-06 10:00:00',
|
||||
id: 10
|
||||
);
|
||||
$offering = new \Unsupervised\Schedular\Offering\Offering(
|
||||
instructorId: 3,
|
||||
kind: 'private_lesson',
|
||||
title: 'Piano Lesson',
|
||||
durationMinutes: 60,
|
||||
id: 8
|
||||
);
|
||||
|
||||
$this->bookings->shouldReceive('findById')->once()->with(1)->andReturn($lesson);
|
||||
$this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot);
|
||||
$this->offerings->shouldReceive('findById')->once()->with(8)->andReturn($offering);
|
||||
$this->detail->shouldReceive('answers')->once()->with(1)->andReturn([
|
||||
['question' => 'Skill level', 'answer' => 'Beginner'],
|
||||
]);
|
||||
$this->detail->shouldReceive('acceptances')->once()->with(1)->andReturn([
|
||||
['policy' => 'Cancellation', 'version' => 'v2', 'accepted_at' => '2026-07-01 10:00:00', 'ip' => '1.2.3.4'],
|
||||
]);
|
||||
|
||||
// The list of lessons must never be queried when routing to a detail view.
|
||||
$this->bookings->shouldNotReceive('findAllUpcoming');
|
||||
|
||||
$html = $this->render();
|
||||
|
||||
self::assertStringContainsString('Lesson details', $html);
|
||||
self::assertStringContainsString('Piano Lesson', $html);
|
||||
self::assertStringContainsString('Skill level', $html);
|
||||
self::assertStringContainsString('Beginner', $html);
|
||||
self::assertStringContainsString('Cancellation', $html);
|
||||
}
|
||||
|
||||
public function testInstructorCannotOpenAnotherInstructorsLessonDetail(): void
|
||||
{
|
||||
$_GET['lesson_id'] = '1';
|
||||
Functions\when('get_current_user_id')->justReturn(99);
|
||||
|
||||
// The lesson belongs to instructor 3, not the current user (99).
|
||||
$lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, offeringId: 8, id: 1);
|
||||
|
||||
$this->bookings->shouldReceive('findById')->once()->with(1)->andReturn($lesson);
|
||||
$this->detail->shouldNotReceive('answers');
|
||||
$this->detail->shouldNotReceive('acceptances');
|
||||
|
||||
ob_start();
|
||||
$this->controller->renderInstructorLessons();
|
||||
$html = (string) ob_get_clean();
|
||||
|
||||
self::assertStringContainsString('could not be found', $html);
|
||||
self::assertStringNotContainsString('Skill level', $html);
|
||||
}
|
||||
|
||||
private function render(): string
|
||||
{
|
||||
ob_start();
|
||||
|
||||
Reference in New Issue
Block a user