Files
unsupervised-scheduler/src/Booking/BookingRepository.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

251 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Booking;
class BookingRepository {
private string $table;
public function __construct( private \wpdb $db ) {
$this->table = $db->prefix . 'us_lessons';
}
public function insert( Lesson $lesson ): int {
$this->db->insert(
$this->table,
[
'slot_id' => $lesson->slotId,
'offering_id' => $lesson->offeringId,
'student_id' => $lesson->studentId,
'instructor_id' => $lesson->instructorId,
'recurrence' => $lesson->recurrence,
'series_id' => $lesson->seriesId,
'status' => $lesson->status,
'payment_id' => $lesson->paymentId,
'notes' => $lesson->notes,
'created_at' => current_time( 'mysql' ),
],
[ '%d', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%s', '%s' ]
);
return $this->db->insert_id;
}
/**
* Create a weekly lesson series — one lesson per slot, all sharing a
* `series_id` (the id of the first lesson row).
*
* @param list<int> $slotIds Availability slot IDs to reserve, in order.
* @return list<int> Inserted lesson IDs.
*/
public function insertSeries( Lesson $template, array $slotIds ): array {
$ids = [];
$seriesId = 0;
foreach ( $slotIds as $slotId ) {
$id = $this->insert(
new Lesson(
slotId: $slotId,
studentId: $template->studentId,
instructorId: $template->instructorId,
offeringId: $template->offeringId,
recurrence: Lesson::RECURRENCE_WEEKLY,
seriesId: $seriesId > 0 ? $seriesId : null,
status: $template->status,
notes: $template->notes,
)
);
if ( 0 === $seriesId ) {
$seriesId = $id;
$this->setSeriesId( $id, $seriesId );
}
$ids[] = $id;
}
return $ids;
}
private function setSeriesId( int $id, int $seriesId ): void {
$this->db->update(
$this->table,
[ 'series_id' => $seriesId ],
[ 'id' => $id ],
[ '%d' ],
[ '%d' ]
);
}
public function findById( int $id ): ?Lesson {
$row = $this->db->get_row(
$this->db->prepare( 'SELECT * FROM %i WHERE id = %d', $this->table, $id )
);
return $row ? Lesson::fromRow( $row ) : null;
}
/**
* Upcoming lessons for an instructor (status != cancelled, slot in the future).
*
* @return list<Lesson>
*/
public function findUpcomingForInstructor( int $instructorId ): array {
$avTable = str_replace( 'us_lessons', 'us_availability', $this->table );
$rows = $this->db->get_results(
$this->db->prepare(
'SELECT l.* FROM %i l
JOIN %i a ON a.id = l.slot_id
WHERE l.instructor_id = %d
AND l.status != %s
AND a.start_dt >= %s
ORDER BY a.start_dt ASC',
$this->table,
$avTable,
$instructorId,
Lesson::STATUS_CANCELLED,
current_time( 'mysql' )
)
);
return array_map( Lesson::fromRow( ... ), $rows ?? [] );
}
/**
* Upcoming lessons for a student (status != cancelled, slot in the future).
*
* @return list<Lesson>
*/
public function findUpcomingForStudent( int $studentId ): array {
$avTable = str_replace( 'us_lessons', 'us_availability', $this->table );
$rows = $this->db->get_results(
$this->db->prepare(
'SELECT l.* FROM %i l
JOIN %i a ON a.id = l.slot_id
WHERE l.student_id = %d
AND l.status != %s
AND a.start_dt >= %s
ORDER BY a.start_dt ASC',
$this->table,
$avTable,
$studentId,
Lesson::STATUS_CANCELLED,
current_time( 'mysql' )
)
);
return array_map( Lesson::fromRow( ... ), $rows ?? [] );
}
/**
* Count a student's upcoming, non-cancelled lessons (slot in the future).
*/
public function countUpcomingForStudent( int $studentId ): int {
$avTable = str_replace( 'us_lessons', 'us_availability', $this->table );
return (int) $this->db->get_var(
$this->db->prepare(
'SELECT COUNT(*) FROM %i l
JOIN %i a ON a.id = l.slot_id
WHERE l.student_id = %d
AND l.status != %s
AND a.start_dt >= %s',
$this->table,
$avTable,
$studentId,
Lesson::STATUS_CANCELLED,
current_time( 'mysql' )
)
);
}
/**
* All lessons for a student.
*
* @return list<Lesson>
*/
public function findByStudent( int $studentId ): array {
$rows = $this->db->get_results(
$this->db->prepare(
'SELECT * FROM %i WHERE student_id = %d ORDER BY created_at DESC',
$this->table,
$studentId
)
);
return array_map( Lesson::fromRow( ... ), $rows ?? [] );
}
/**
* All upcoming lessons across all instructors (admin view).
*
* @return list<Lesson>
*/
public function findAllUpcoming(): array {
$avTable = str_replace( 'us_lessons', 'us_availability', $this->table );
$rows = $this->db->get_results(
$this->db->prepare(
'SELECT l.* FROM %i l
JOIN %i a ON a.id = l.slot_id
WHERE l.status != %s
AND a.start_dt >= %s
ORDER BY a.start_dt ASC',
$this->table,
$avTable,
Lesson::STATUS_CANCELLED,
current_time( 'mysql' )
)
);
return array_map( Lesson::fromRow( ... ), $rows ?? [] );
}
public function setPaymentId( int $id, int $paymentId ): bool {
return false !== $this->db->update(
$this->table,
[ 'payment_id' => $paymentId ],
[ 'id' => $id ],
[ '%d' ],
[ '%d' ]
);
}
/**
* Update every non-cancelled lesson in a weekly series at once — e.g.
* confirming the whole reservation when its single upfront payment settles.
*/
public function updateStatusForSeries( int $seriesId, string $status ): bool {
if ( ! in_array( $status, Lesson::VALID_STATUSES, true ) ) {
return false;
}
$sql = $this->db->prepare(
'UPDATE %i SET status = %s WHERE series_id = %d AND status != %s',
$this->table,
$status,
$seriesId,
Lesson::STATUS_CANCELLED
);
return null !== $sql && false !== $this->db->query( $sql );
}
public function updateStatus( int $id, string $status ): bool {
if ( ! in_array( $status, Lesson::VALID_STATUSES, true ) ) {
return false;
}
return (bool) $this->db->update(
$this->table,
[ 'status' => $status ],
[ 'id' => $id ],
[ '%s' ],
[ '%d' ]
);
}
}