Skip payment step for unpriced bookings, confirm them immediately, show students their lessons
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / PHPStan (pull_request) Successful in 2m46s
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / Coding Standards (pull_request) Successful in 47s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m35s
CI / Build Plugin Zip (pull_request) Has been skipped
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / PHPStan (pull_request) Successful in 2m46s
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / Coding Standards (pull_request) Successful in 47s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m35s
CI / Build Plugin Zip (pull_request) Has been skipped
Booking a slot with no priced offering created the lesson but no payment,
yet the front end still called POST /payments/intent, which 400ed with
"Could not start payment for this registration" — the student saw an error
while the backend held a claimed slot and a lesson stuck at pending.
- POST /bookings and POST /enrollments now return a `payment` summary
({id, method, status}) or null when nothing is owed; the JS only runs
the payment step when a payment exists.
- Bookings with nothing owed are confirmed at creation — there is no
payment step that would ever confirm them later.
- The booking page now shows the student's upcoming lessons (GET /bookings,
now scoped to upcoming non-cancelled lessons with slot start/end times)
with a pending-payment/confirmed status badge.
Fixes #53
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user