+
diff --git a/tests/Unit/Booking/BookingEndpointTest.php b/tests/Unit/Booking/BookingEndpointTest.php
index f794dc5..9169599 100644
--- a/tests/Unit/Booking/BookingEndpointTest.php
+++ b/tests/Unit/Booking/BookingEndpointTest.php
@@ -9,8 +9,10 @@ use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Availability\AvailabilitySlot;
use Unsupervised\Schedular\Booking\BookingEndpoint;
use Unsupervised\Schedular\Booking\BookingRepository;
+use Unsupervised\Schedular\Booking\Lesson;
use Unsupervised\Schedular\Offering\Offering;
use Unsupervised\Schedular\Offering\OfferingRepository;
+use Unsupervised\Schedular\Payment\Payment;
use Unsupervised\Schedular\Payment\PaymentService;
use Unsupervised\Schedular\Registration\RegistrationGate;
use Unsupervised\Schedular\Tests\Unit\TestCase;
@@ -119,8 +121,9 @@ class BookingEndpointTest extends TestCase
$this->availability->shouldReceive('claim')->with(10)->once()->andReturn(true);
$this->bookings->shouldReceive('insert')->once()->andReturn(77);
$this->gate->shouldReceive('record')->once();
- // Free offering → no payment.
+ // Free offering → no payment, so the lesson is confirmed immediately.
$this->payments->shouldNotReceive('createForRegistration');
+ $this->bookings->shouldReceive('updateStatus')->with(77, Lesson::STATUS_CONFIRMED)->once()->andReturn(true);
$request = new \WP_REST_Request(['slot_id' => 10, 'offering_id' => 8]);
$result = $this->endpoint->book($request);
@@ -128,5 +131,110 @@ class BookingEndpointTest extends TestCase
self::assertInstanceOf(\WP_REST_Response::class, $result);
self::assertSame(201, $result->get_status());
self::assertSame([77], $result->get_data()['ids']);
+ self::assertSame(Lesson::STATUS_CONFIRMED, $result->get_data()['status']);
+ self::assertNull($result->get_data()['payment']);
+ }
+
+ public function testBookWithoutOfferingConfirmsImmediatelyWithNoPayment(): void
+ {
+ $this->availability->shouldReceive('findById')->with(10)->andReturn($this->slot(10, 3, null));
+ $this->gate->shouldReceive('validate')->andReturn(null);
+ $this->availability->shouldReceive('claim')->with(10)->once()->andReturn(true);
+ $this->bookings->shouldReceive('insert')->once()->andReturn(77);
+ $this->gate->shouldReceive('record')->once();
+ $this->payments->shouldNotReceive('createForRegistration');
+ $this->bookings->shouldReceive('updateStatus')->with(77, Lesson::STATUS_CONFIRMED)->once()->andReturn(true);
+
+ $request = new \WP_REST_Request(['slot_id' => 10]);
+ $result = $this->endpoint->book($request);
+
+ self::assertInstanceOf(\WP_REST_Response::class, $result);
+ self::assertSame(Lesson::STATUS_CONFIRMED, $result->get_data()['status']);
+ self::assertNull($result->get_data()['payment']);
+ }
+
+ public function testBookWithPricedOfferingStaysPendingAndReturnsPaymentSummary(): void
+ {
+ $this->availability->shouldReceive('findById')->with(10)->andReturn($this->slot(10, 3, null));
+ $this->offerings->shouldReceive('findById')->with(8)->andReturn(
+ new Offering(instructorId: 3, kind: Offering::KIND_PRIVATE_LESSON, title: 'Lesson', price: 50.0, id: 8)
+ );
+ $this->gate->shouldReceive('validate')->andReturn(null);
+ $this->availability->shouldReceive('claim')->with(10)->once()->andReturn(true);
+ $this->bookings->shouldReceive('insert')->once()->andReturn(77);
+ $this->gate->shouldReceive('record')->once();
+ $this->payments->shouldReceive('createForRegistration')
+ ->once()
+ ->with(Payment::REG_LESSON, 77, 5, 3, 50.0, 'CAD', null)
+ ->andReturn(new Payment(
+ studentId: 5,
+ instructorId: 3,
+ registrationType: Payment::REG_LESSON,
+ registrationId: 77,
+ amount: 50.0,
+ method: Payment::METHOD_ETRANSFER,
+ status: Payment::STATUS_PENDING,
+ id: 12,
+ ));
+ // Awaiting payment: the lesson must not be confirmed yet.
+ $this->bookings->shouldNotReceive('updateStatus');
+
+ $request = new \WP_REST_Request(['slot_id' => 10, 'offering_id' => 8]);
+ $result = $this->endpoint->book($request);
+
+ self::assertInstanceOf(\WP_REST_Response::class, $result);
+ self::assertSame(Lesson::STATUS_PENDING, $result->get_data()['status']);
+ self::assertSame(
+ ['id' => 12, 'method' => Payment::METHOD_ETRANSFER, 'status' => Payment::STATUS_PENDING],
+ $result->get_data()['payment']
+ );
+ }
+
+ public function testBookWithCompedPaymentReturnsConfirmedStatus(): void
+ {
+ $this->availability->shouldReceive('findById')->with(10)->andReturn($this->slot(10, 3, null));
+ $this->offerings->shouldReceive('findById')->with(8)->andReturn(
+ new Offering(instructorId: 3, kind: Offering::KIND_PRIVATE_LESSON, title: 'Lesson', price: 50.0, id: 8)
+ );
+ $this->gate->shouldReceive('validate')->andReturn(null);
+ $this->availability->shouldReceive('claim')->with(10)->once()->andReturn(true);
+ $this->bookings->shouldReceive('insert')->once()->andReturn(77);
+ $this->gate->shouldReceive('record')->once();
+ // Comped students are paid on creation (PaymentService confirms the lesson itself).
+ $this->payments->shouldReceive('createForRegistration')->once()->andReturn(new Payment(
+ studentId: 5,
+ instructorId: 3,
+ registrationType: Payment::REG_LESSON,
+ registrationId: 77,
+ amount: 50.0,
+ method: Payment::METHOD_COMP,
+ status: Payment::STATUS_PAID,
+ id: 12,
+ ));
+ $this->bookings->shouldNotReceive('updateStatus');
+
+ $request = new \WP_REST_Request(['slot_id' => 10, 'offering_id' => 8]);
+ $result = $this->endpoint->book($request);
+
+ self::assertInstanceOf(\WP_REST_Response::class, $result);
+ self::assertSame(Lesson::STATUS_CONFIRMED, $result->get_data()['status']);
+ self::assertSame(Payment::METHOD_COMP, $result->get_data()['payment']['method']);
+ }
+
+ public function testMyLessonsForStudentIncludesSlotTimes(): void
+ {
+ Functions\when('current_user_can')->justReturn(false);
+
+ $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, status: Lesson::STATUS_PENDING, id: 77);
+ $this->bookings->shouldReceive('findUpcomingForStudent')->with(5)->once()->andReturn([$lesson]);
+ $this->availability->shouldReceive('findById')->with(10)->andReturn($this->slot(10, 3, null));
+
+ $result = $this->endpoint->myLessons(new \WP_REST_Request([]));
+
+ $data = $result->get_data();
+ self::assertCount(1, $data);
+ self::assertSame(77, $data[0]['id']);
+ self::assertSame('2026-07-01 10:00:00', $data[0]['start_dt']);
+ self::assertSame('2026-07-01 11:00:00', $data[0]['end_dt']);
}
}
diff --git a/tests/Unit/Booking/BookingRepositoryTest.php b/tests/Unit/Booking/BookingRepositoryTest.php
index 2b0da4b..98242cf 100644
--- a/tests/Unit/Booking/BookingRepositoryTest.php
+++ b/tests/Unit/Booking/BookingRepositoryTest.php
@@ -132,6 +132,35 @@ class BookingRepositoryTest extends TestCase
self::assertFalse($this->repo->updateStatus(1, Lesson::STATUS_CONFIRMED));
}
+ public function testFindUpcomingForStudentJoinsSlotAndExcludesCancelled(): void
+ {
+ Functions\when('current_time')->justReturn('2026-06-08 12:00:00');
+
+ $this->db->shouldReceive('prepare')
+ ->once()
+ ->with(Mockery::pattern('/SELECT l\.\*.*l.student_id = %d.*l.status != %s.*a.start_dt >= %s.*ORDER BY a.start_dt ASC/s'), 'wp_us_lessons', 'wp_us_availability', 5, Lesson::STATUS_CANCELLED, '2026-06-08 12:00:00')
+ ->andReturn('SELECT ...');
+
+ $row = (object) [
+ 'id' => '15',
+ 'slot_id' => '10',
+ 'offering_id' => null,
+ 'student_id' => '5',
+ 'instructor_id' => '3',
+ 'recurrence' => Lesson::RECURRENCE_SINGLE,
+ 'series_id' => null,
+ 'status' => 'confirmed',
+ 'payment_id' => null,
+ 'notes' => null,
+ ];
+ $this->db->shouldReceive('get_results')->andReturn([$row]);
+
+ $lessons = $this->repo->findUpcomingForStudent(5);
+
+ self::assertCount(1, $lessons);
+ self::assertSame(15, $lessons[0]->id);
+ }
+
public function testCountUpcomingForStudent(): void
{
Functions\when('current_time')->justReturn('2026-06-08 12:00:00');
diff --git a/tests/Unit/GroupClass/EnrollmentEndpointTest.php b/tests/Unit/GroupClass/EnrollmentEndpointTest.php
new file mode 100644
index 0000000..95c9408
--- /dev/null
+++ b/tests/Unit/GroupClass/EnrollmentEndpointTest.php
@@ -0,0 +1,101 @@
+alias(static fn ($v): int => abs((int) $v));
+ Functions\when('wp_unslash')->returnArg();
+ Functions\when('sanitize_text_field')->returnArg();
+ Functions\when('get_current_user_id')->justReturn(5);
+
+ $this->enrollments = Mockery::mock(EnrollmentRepository::class);
+ $this->offerings = Mockery::mock(OfferingRepository::class);
+ $this->gate = Mockery::mock(RegistrationGate::class);
+ $this->payments = Mockery::mock(PaymentService::class);
+
+ $this->endpoint = new EnrollmentEndpoint(
+ $this->enrollments,
+ $this->offerings,
+ $this->gate,
+ $this->payments,
+ );
+ }
+
+ private function offering(float $price): Offering
+ {
+ return new Offering(instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Choir', price: $price, id: 8);
+ }
+
+ private function expectSuccessfulEnrollment(): void
+ {
+ $this->enrollments->shouldReceive('hasActiveEnrollment')->with(8, 5)->andReturn(false);
+ $this->enrollments->shouldReceive('countActiveForOffering')->never();
+ $this->gate->shouldReceive('validate')->andReturn(null);
+ $this->enrollments->shouldReceive('insert')->once()->andReturn(44);
+ $this->gate->shouldReceive('record')->once();
+ }
+
+ public function testEnrollInFreeClassReturnsNullPayment(): void
+ {
+ $this->offerings->shouldReceive('findById')->with(8)->andReturn($this->offering(0.0));
+ $this->expectSuccessfulEnrollment();
+ $this->payments->shouldNotReceive('createForRegistration');
+
+ $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8]));
+
+ self::assertInstanceOf(\WP_REST_Response::class, $result);
+ self::assertSame(201, $result->get_status());
+ self::assertSame(44, $result->get_data()['id']);
+ self::assertNull($result->get_data()['payment']);
+ }
+
+ public function testEnrollInPricedClassReturnsPaymentSummary(): void
+ {
+ $this->offerings->shouldReceive('findById')->with(8)->andReturn($this->offering(120.0));
+ $this->expectSuccessfulEnrollment();
+ $this->payments->shouldReceive('createForRegistration')
+ ->once()
+ ->with(Payment::REG_ENROLLMENT, 44, 5, 3, 120.0, 'CAD', null)
+ ->andReturn(new Payment(
+ studentId: 5,
+ instructorId: 3,
+ registrationType: Payment::REG_ENROLLMENT,
+ registrationId: 44,
+ amount: 120.0,
+ method: Payment::METHOD_ETRANSFER,
+ status: Payment::STATUS_PENDING,
+ id: 12,
+ ));
+
+ $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8]));
+
+ self::assertInstanceOf(\WP_REST_Response::class, $result);
+ self::assertSame(
+ ['id' => 12, 'method' => Payment::METHOD_ETRANSFER, 'status' => Payment::STATUS_PENDING],
+ $result->get_data()['payment']
+ );
+ }
+}
diff --git a/tests/Unit/Payment/PaymentTest.php b/tests/Unit/Payment/PaymentTest.php
index cec1221..af56050 100644
--- a/tests/Unit/Payment/PaymentTest.php
+++ b/tests/Unit/Payment/PaymentTest.php
@@ -71,6 +71,13 @@ class PaymentTest extends TestCase
self::assertSame(100.00, $payment->total());
}
+ public function testToSummaryArrayContainsOnlyClientFacingFields(): void
+ {
+ $summary = (new Payment(5, 3, Payment::REG_LESSON, 12, 35.00, id: 7))->toSummaryArray();
+
+ self::assertSame(['id' => 7, 'method' => Payment::METHOD_ETRANSFER, 'status' => Payment::STATUS_PENDING], $summary);
+ }
+
public function testToArrayContainsExpectedKeys(): void
{
$arr = (new Payment(5, 3, Payment::REG_LESSON, 12, 35.00, id: 7))->toArray();