Files
unsupervised-scheduler/tests/Unit/Booking/BookingRepositoryTest.php
T
thatguygriffandClaude Fable 5 ff059909a5
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m50s
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / Tests (PHP 8.2) (pull_request) Successful in 39s
CI / PHPStan (pull_request) Successful in 2m49s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m38s
CI / Build Plugin Zip (pull_request) Skipped
Charge weekly reservations for every claimed occurrence and confirm the whole series
A weekly booking on a per-lesson (one_time) priced offering was creating its
single upfront payment for one week's price while reserving up to 12 weeks,
and settling that payment confirmed only the anchor lesson, leaving the rest
of the series pending forever.

- BookingEndpoint now charges price x claimed occurrences for one_time
  billing; a full_term price is still charged once since it covers the term.
- PaymentService::confirmRegistration resolves the anchor lesson's series and
  confirms every non-cancelled row via the new
  BookingRepository::updateStatusForSeries().

Closes #79

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 10:19:53 -03:00

225 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\Booking;
use Brain\Monkey\Functions;
use Mockery;
use Unsupervised\Schedular\Booking\BookingRepository;
use Unsupervised\Schedular\Booking\Lesson;
use Unsupervised\Schedular\Tests\Unit\TestCase;
class BookingRepositoryTest extends TestCase
{
private \wpdb $db;
private BookingRepository $repo;
protected function setUp(): void
{
parent::setUp();
$this->db = Mockery::mock(\wpdb::class);
$this->db->prefix = 'wp_';
$this->repo = new BookingRepository($this->db);
}
public function testInsertCallsWpdbInsertAndReturnsId(): void
{
Functions\expect('current_time')->with('mysql')->andReturn('2026-04-01 12:00:00');
$this->db->shouldReceive('insert')
->once()
->with(
'wp_us_lessons',
Mockery::on(static function (array $data): bool {
return $data['slot_id'] === 10
&& $data['student_id'] === 5
&& $data['offering_id'] === 7
&& $data['recurrence'] === Lesson::RECURRENCE_SINGLE
&& $data['status'] === Lesson::STATUS_PENDING;
}),
['%d', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%s', '%s']
);
$this->db->insert_id = 77;
$lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, offeringId: 7);
$result = $this->repo->insert($lesson);
self::assertSame(77, $result);
}
public function testInsertSeriesSharesSeriesIdAcrossSlots(): void
{
Functions\when('current_time')->justReturn('2026-04-01 12:00:00');
$ids = [40, 41, 42];
$this->db->shouldReceive('insert')
->times(3)
->andReturnUsing(function () use (&$ids): void {
$this->db->insert_id = array_shift($ids);
});
// The first lesson is back-filled with its own id as the series id.
$this->db->shouldReceive('update')
->once()
->with('wp_us_lessons', ['series_id' => 40], ['id' => 40], ['%d'], ['%d']);
$template = new Lesson(slotId: 0, studentId: 5, instructorId: 3, offeringId: 7);
$result = $this->repo->insertSeries($template, [100, 101, 102]);
self::assertSame([40, 41, 42], $result);
}
public function testFindByIdReturnsNullWhenNotFound(): void
{
$this->db->shouldReceive('prepare')->andReturn('SELECT ...');
$this->db->shouldReceive('get_row')->andReturn(null);
self::assertNull($this->repo->findById(99));
}
public function testFindByIdReturnsLesson(): void
{
$row = (object) [
'id' => '15',
'slot_id' => '10',
'offering_id' => null,
'student_id' => '5',
'instructor_id' => '3',
'recurrence' => Lesson::RECURRENCE_SINGLE,
'series_id' => null,
'status' => 'pending',
'payment_id' => null,
'notes' => null,
];
$this->db->shouldReceive('prepare')->andReturn('SELECT ...');
$this->db->shouldReceive('get_row')->andReturn($row);
$lesson = $this->repo->findById(15);
self::assertInstanceOf(Lesson::class, $lesson);
self::assertSame(15, $lesson->id);
}
public function testUpdateStatusReturnsFalseForInvalidStatus(): void
{
$result = $this->repo->updateStatus(1, 'invalid');
self::assertFalse($result);
}
public function testUpdateStatusCallsWpdbUpdate(): void
{
$this->db->shouldReceive('update')
->once()
->with(
'wp_us_lessons',
['status' => Lesson::STATUS_CONFIRMED],
['id' => 1],
['%s'],
['%d']
)
->andReturn(1);
self::assertTrue($this->repo->updateStatus(1, Lesson::STATUS_CONFIRMED));
}
public function testUpdateStatusReturnsFalseWhenDbFails(): void
{
$this->db->shouldReceive('update')->andReturn(0);
self::assertFalse($this->repo->updateStatus(1, Lesson::STATUS_CONFIRMED));
}
public function testUpdateStatusForSeriesReturnsFalseForInvalidStatus(): void
{
self::assertFalse($this->repo->updateStatusForSeries(12, 'invalid'));
}
public function testUpdateStatusForSeriesUpdatesNonCancelledRows(): void
{
$this->db->shouldReceive('prepare')
->once()
->with(
Mockery::on(static fn (string $sql): bool =>
str_contains($sql, 'series_id = %d') && str_contains($sql, 'status != %s')),
'wp_us_lessons',
Lesson::STATUS_CONFIRMED,
12,
Lesson::STATUS_CANCELLED
)
->andReturn('UPDATE ...');
$this->db->shouldReceive('query')->once()->with('UPDATE ...')->andReturn(3);
self::assertTrue($this->repo->updateStatusForSeries(12, 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');
$this->db->shouldReceive('prepare')
->once()
->with(Mockery::pattern('/COUNT\(\*\).*l.student_id = %d.*a.start_dt >= %s/s'), 'wp_us_lessons', 'wp_us_availability', 5, Lesson::STATUS_CANCELLED, '2026-06-08 12:00:00')
->andReturn('SELECT ...');
$this->db->shouldReceive('get_var')->andReturn('3');
self::assertSame(3, $this->repo->countUpcomingForStudent(5));
}
public function testFindByStudentReturnsLessons(): void
{
$row = (object) [
'id' => '1',
'slot_id' => '2',
'offering_id' => null,
'student_id' => '5',
'instructor_id' => '3',
'recurrence' => Lesson::RECURRENCE_SINGLE,
'series_id' => null,
'status' => 'pending',
'payment_id' => null,
'notes' => null,
];
$this->db->shouldReceive('prepare')->andReturn('SELECT ...');
$this->db->shouldReceive('get_results')->andReturn([$row]);
$lessons = $this->repo->findByStudent(5);
self::assertCount(1, $lessons);
self::assertInstanceOf(Lesson::class, $lessons[0]);
}
}