Charge weekly reservations for every claimed occurrence and confirm the whole series
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
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
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]>
This commit is contained in:
@@ -304,6 +304,76 @@ class BookingEndpointTest extends TestCase
|
||||
self::assertSame(Payment::METHOD_COMP, $result->get_data()['payment']['method']);
|
||||
}
|
||||
|
||||
public function testWeeklyBookingChargesPerLessonPriceTimesClaimedOccurrences(): void
|
||||
{
|
||||
$this->availability->shouldReceive('findById')->with(10)->andReturn($this->slot(10, 3, null, false, 7));
|
||||
$this->offerings->shouldReceive('findById')->with(8)->andReturn(
|
||||
new Offering(instructorId: 3, kind: Offering::KIND_PRIVATE_LESSON, title: 'Lesson', price: 50.0, allowWeekly: true, id: 8)
|
||||
);
|
||||
$this->gate->shouldReceive('validate')->andReturn(null);
|
||||
|
||||
$this->availability->shouldReceive('findUnbookedInGroup')->with(7)->andReturn([
|
||||
$this->slot(10, 3, null, false, 7),
|
||||
$this->slot(11, 3, null, false, 7),
|
||||
$this->slot(12, 3, null, false, 7),
|
||||
]);
|
||||
$this->availability->shouldReceive('claim')->times(3)->andReturn(true);
|
||||
$this->bookings->shouldReceive('insertSeries')->once()->andReturn([77, 78, 79]);
|
||||
$this->gate->shouldReceive('record')->once();
|
||||
|
||||
// Three claimed occurrences at a per-lesson (one_time) price of 50 → 150.
|
||||
$this->payments->shouldReceive('createForRegistration')
|
||||
->once()
|
||||
->with(Payment::REG_LESSON, 77, 5, 3, 150.0, 'CAD', null)
|
||||
->andReturn(new Payment(5, 3, Payment::REG_LESSON, 77, 150.0, 'CAD', Payment::METHOD_ETRANSFER, Payment::STATUS_PENDING, id: 12));
|
||||
$this->bookings->shouldNotReceive('updateStatus');
|
||||
|
||||
$request = new \WP_REST_Request(['slot_id' => 10, 'offering_id' => 8, 'recurrence' => 'weekly']);
|
||||
$result = $this->endpoint->book($request);
|
||||
|
||||
self::assertInstanceOf(\WP_REST_Response::class, $result);
|
||||
self::assertSame([77, 78, 79], $result->get_data()['ids']);
|
||||
self::assertSame(Lesson::STATUS_PENDING, $result->get_data()['status']);
|
||||
}
|
||||
|
||||
public function testWeeklyBookingChargesFullTermPriceOnce(): void
|
||||
{
|
||||
$this->availability->shouldReceive('findById')->with(10)->andReturn($this->slot(10, 3, null, false, 7));
|
||||
$this->offerings->shouldReceive('findById')->with(8)->andReturn(
|
||||
new Offering(
|
||||
instructorId: 3,
|
||||
kind: Offering::KIND_PRIVATE_LESSON,
|
||||
title: 'Term',
|
||||
price: 400.0,
|
||||
billingMode: Offering::BILLING_FULL_TERM,
|
||||
allowWeekly: true,
|
||||
id: 8
|
||||
)
|
||||
);
|
||||
$this->gate->shouldReceive('validate')->andReturn(null);
|
||||
|
||||
$this->availability->shouldReceive('findUnbookedInGroup')->with(7)->andReturn([
|
||||
$this->slot(10, 3, null, false, 7),
|
||||
$this->slot(11, 3, null, false, 7),
|
||||
]);
|
||||
$this->availability->shouldReceive('claim')->times(2)->andReturn(true);
|
||||
$this->bookings->shouldReceive('insertSeries')->once()->andReturn([77, 78]);
|
||||
$this->gate->shouldReceive('record')->once();
|
||||
|
||||
// A full_term price already covers the whole reservation.
|
||||
$this->payments->shouldReceive('createForRegistration')
|
||||
->once()
|
||||
->with(Payment::REG_LESSON, 77, 5, 3, 400.0, 'CAD', null)
|
||||
->andReturn(new Payment(5, 3, Payment::REG_LESSON, 77, 400.0, 'CAD', Payment::METHOD_ETRANSFER, Payment::STATUS_PENDING, id: 12));
|
||||
$this->bookings->shouldNotReceive('updateStatus');
|
||||
|
||||
$request = new \WP_REST_Request(['slot_id' => 10, 'offering_id' => 8, 'recurrence' => 'weekly']);
|
||||
$result = $this->endpoint->book($request);
|
||||
|
||||
self::assertInstanceOf(\WP_REST_Response::class, $result);
|
||||
self::assertSame(Lesson::STATUS_PENDING, $result->get_data()['status']);
|
||||
}
|
||||
|
||||
public function testCancelByOwnerCancelsReleasesSlotAndVoidsPayment(): void
|
||||
{
|
||||
$lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, status: Lesson::STATUS_PENDING, paymentId: 12, id: 77);
|
||||
|
||||
@@ -132,6 +132,29 @@ class BookingRepositoryTest extends TestCase
|
||||
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');
|
||||
|
||||
@@ -41,6 +41,9 @@ class PaymentServiceTest extends TestCase
|
||||
$this->stripe = Mockery::mock(StripeGateway::class);
|
||||
$this->settings->shouldReceive('etransferEmail')->andReturn('');
|
||||
$this->settings->shouldReceive('hstRate')->andReturn(0.0)->byDefault();
|
||||
// Confirming a lesson looks it up to detect a weekly series; single
|
||||
// lessons (or a lookup miss) fall back to the per-lesson update.
|
||||
$this->bookings->shouldReceive('findById')->andReturn(null)->byDefault();
|
||||
|
||||
$this->service = new PaymentService(
|
||||
$this->payments,
|
||||
@@ -180,6 +183,23 @@ class PaymentServiceTest extends TestCase
|
||||
self::assertTrue($this->service->markPaid(70));
|
||||
}
|
||||
|
||||
public function testMarkPaidConfirmsEveryLessonInAWeeklySeries(): void
|
||||
{
|
||||
$this->payments->shouldReceive('findById')->with(70)->andReturn($this->payment(Payment::METHOD_ETRANSFER, Payment::STATUS_PENDING, 70));
|
||||
$this->payments->shouldReceive('markPaid')->once()->with(70, 'USC-70')->andReturn(true);
|
||||
|
||||
// The anchor lesson (registration_id 12) belongs to series 12: the whole
|
||||
// series is confirmed, not just the anchor row.
|
||||
$this->bookings->shouldReceive('findById')->with(12)->andReturn(
|
||||
new Lesson(slotId: 10, studentId: 5, instructorId: 3, recurrence: Lesson::RECURRENCE_WEEKLY, seriesId: 12, id: 12)
|
||||
);
|
||||
$this->bookings->shouldReceive('updateStatusForSeries')->once()->with(12, Lesson::STATUS_CONFIRMED)->andReturn(true);
|
||||
$this->bookings->shouldNotReceive('updateStatus');
|
||||
$this->mailer->shouldReceive('send')->andReturn(false);
|
||||
|
||||
self::assertTrue($this->service->markPaid(70));
|
||||
}
|
||||
|
||||
public function testMarkPaidReturnsFalseWhenMissing(): void
|
||||
{
|
||||
$this->payments->shouldReceive('findById')->with(99)->andReturn(null);
|
||||
|
||||
Reference in New Issue
Block a user