Let students cancel their own lessons from the booking page
CI / No Debug Code (push) Successful in 3s
CI / Coding Standards (push) Successful in 46s
CI / Tests (PHP 8.1) (push) Successful in 45s
CI / Tests (PHP 8.2) (push) Successful in 45s
CI / PHPStan (push) Successful in 1m11s
CI / Tests (PHP 8.3) (push) Successful in 1m0s
CI / Build Plugin Zip (push) Successful in 1m10s
CI / No Debug Code (push) Successful in 3s
CI / Coding Standards (push) Successful in 46s
CI / Tests (PHP 8.1) (push) Successful in 45s
CI / Tests (PHP 8.2) (push) Successful in 45s
CI / PHPStan (push) Successful in 1m11s
CI / Tests (PHP 8.3) (push) Successful in 1m0s
CI / Build Plugin Zip (push) Successful in 1m10s
Adds POST /bookings/{id}/cancel (owner-only, idempotent): marks the lesson
cancelled, releases the availability slot for rebooking, and voids a
still-pending payment so it leaves the admin confirmation queue. Paid
payments are untouched — refunds stay a manual admin decision.
The instructor PATCH /bookings/{id}/status path now does the same slot
release and payment voiding on cancellation (previously cancelled lessons
left their slot permanently booked), and reinstating a cancelled lesson
re-claims the slot, rejecting with 409 if the freed time was rebooked.
The "Your upcoming lessons" panel gets a Cancel button with a confirm
prompt; on success both the lesson list and the slot calendar refresh.
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -221,6 +221,100 @@ class BookingEndpointTest extends TestCase
|
||||
self::assertSame(Payment::METHOD_COMP, $result->get_data()['payment']['method']);
|
||||
}
|
||||
|
||||
public function testCancelByOwnerCancelsReleasesSlotAndVoidsPayment(): void
|
||||
{
|
||||
$lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, status: Lesson::STATUS_PENDING, paymentId: 12, id: 77);
|
||||
$this->bookings->shouldReceive('findById')->with(77)->andReturn($lesson);
|
||||
$this->bookings->shouldReceive('updateStatus')->with(77, Lesson::STATUS_CANCELLED)->once()->andReturn(true);
|
||||
$this->availability->shouldReceive('release')->with(10)->once()->andReturn(true);
|
||||
$this->payments->shouldReceive('voidPending')->with(12)->once();
|
||||
|
||||
$result = $this->endpoint->cancel(new \WP_REST_Request(['id' => 77]));
|
||||
|
||||
self::assertInstanceOf(\WP_REST_Response::class, $result);
|
||||
self::assertSame(Lesson::STATUS_CANCELLED, $result->get_data()['status']);
|
||||
}
|
||||
|
||||
public function testCancelByAnotherStudentIsForbidden(): void
|
||||
{
|
||||
// Lesson belongs to student 9; current user is 5.
|
||||
$lesson = new Lesson(slotId: 10, studentId: 9, instructorId: 3, status: Lesson::STATUS_PENDING, id: 77);
|
||||
$this->bookings->shouldReceive('findById')->with(77)->andReturn($lesson);
|
||||
$this->bookings->shouldNotReceive('updateStatus');
|
||||
$this->availability->shouldNotReceive('release');
|
||||
|
||||
$result = $this->endpoint->cancel(new \WP_REST_Request(['id' => 77]));
|
||||
|
||||
self::assertInstanceOf(\WP_Error::class, $result);
|
||||
self::assertSame('forbidden', $result->get_error_code());
|
||||
}
|
||||
|
||||
public function testCancelUnknownLessonReturns404(): void
|
||||
{
|
||||
$this->bookings->shouldReceive('findById')->with(99)->andReturn(null);
|
||||
|
||||
$result = $this->endpoint->cancel(new \WP_REST_Request(['id' => 99]));
|
||||
|
||||
self::assertInstanceOf(\WP_Error::class, $result);
|
||||
self::assertSame('not_found', $result->get_error_code());
|
||||
}
|
||||
|
||||
public function testCancelAlreadyCancelledLessonIsIdempotent(): void
|
||||
{
|
||||
$lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, status: Lesson::STATUS_CANCELLED, id: 77);
|
||||
$this->bookings->shouldReceive('findById')->with(77)->andReturn($lesson);
|
||||
$this->bookings->shouldNotReceive('updateStatus');
|
||||
$this->availability->shouldNotReceive('release');
|
||||
$this->payments->shouldNotReceive('voidPending');
|
||||
|
||||
$result = $this->endpoint->cancel(new \WP_REST_Request(['id' => 77]));
|
||||
|
||||
self::assertInstanceOf(\WP_REST_Response::class, $result);
|
||||
self::assertSame(Lesson::STATUS_CANCELLED, $result->get_data()['status']);
|
||||
}
|
||||
|
||||
public function testUpdateStatusToCancelledReleasesSlotAndVoidsPayment(): void
|
||||
{
|
||||
// Current user 5 is the lesson's instructor.
|
||||
$lesson = new Lesson(slotId: 10, studentId: 9, instructorId: 5, status: Lesson::STATUS_PENDING, paymentId: 12, id: 77);
|
||||
$this->bookings->shouldReceive('findById')->with(77)->andReturn($lesson);
|
||||
$this->availability->shouldReceive('release')->with(10)->once()->andReturn(true);
|
||||
$this->payments->shouldReceive('voidPending')->with(12)->once();
|
||||
$this->bookings->shouldReceive('updateStatus')->with(77, Lesson::STATUS_CANCELLED)->once()->andReturn(true);
|
||||
|
||||
$result = $this->endpoint->updateStatus(new \WP_REST_Request(['id' => 77, 'status' => Lesson::STATUS_CANCELLED]));
|
||||
|
||||
self::assertInstanceOf(\WP_REST_Response::class, $result);
|
||||
self::assertSame(Lesson::STATUS_CANCELLED, $result->get_data()['status']);
|
||||
}
|
||||
|
||||
public function testUpdateStatusReinstatingCancelledLessonReclaimsSlot(): void
|
||||
{
|
||||
$lesson = new Lesson(slotId: 10, studentId: 9, instructorId: 5, status: Lesson::STATUS_CANCELLED, id: 77);
|
||||
$this->bookings->shouldReceive('findById')->with(77)->andReturn($lesson);
|
||||
$this->availability->shouldReceive('claim')->with(10)->once()->andReturn(true);
|
||||
$this->bookings->shouldReceive('updateStatus')->with(77, Lesson::STATUS_CONFIRMED)->once()->andReturn(true);
|
||||
|
||||
$result = $this->endpoint->updateStatus(new \WP_REST_Request(['id' => 77, 'status' => Lesson::STATUS_CONFIRMED]));
|
||||
|
||||
self::assertInstanceOf(\WP_REST_Response::class, $result);
|
||||
self::assertSame(Lesson::STATUS_CONFIRMED, $result->get_data()['status']);
|
||||
}
|
||||
|
||||
public function testUpdateStatusReinstatingFailsWhenSlotRebooked(): void
|
||||
{
|
||||
$lesson = new Lesson(slotId: 10, studentId: 9, instructorId: 5, status: Lesson::STATUS_CANCELLED, id: 77);
|
||||
$this->bookings->shouldReceive('findById')->with(77)->andReturn($lesson);
|
||||
// Someone booked the freed time in the meantime.
|
||||
$this->availability->shouldReceive('claim')->with(10)->once()->andReturn(false);
|
||||
$this->bookings->shouldNotReceive('updateStatus');
|
||||
|
||||
$result = $this->endpoint->updateStatus(new \WP_REST_Request(['id' => 77, 'status' => Lesson::STATUS_CONFIRMED]));
|
||||
|
||||
self::assertInstanceOf(\WP_Error::class, $result);
|
||||
self::assertSame('slot_taken', $result->get_error_code());
|
||||
}
|
||||
|
||||
public function testMyLessonsForStudentIncludesSlotTimes(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
Reference in New Issue
Block a user