Merge pull request 'Charge weekly reservations for every claimed occurrence and confirm the whole series' (#81) from fix/recurring-payment-amount into main
CI / Tests (PHP 8.1) (push) Successful in 38s
CI / Coding Standards (push) Successful in 2m47s
CI / Tests (PHP 8.2) (push) Successful in 43s
CI / No Debug Code (push) Successful in 3s
CI / Tests (PHP 8.3) (push) Successful in 2m39s
CI / PHPStan (push) Successful in 2m51s
CI / Build Plugin Zip (push) Successful in 2m43s
CI / Tests (PHP 8.1) (push) Successful in 38s
CI / Coding Standards (push) Successful in 2m47s
CI / Tests (PHP 8.2) (push) Successful in 43s
CI / No Debug Code (push) Successful in 3s
CI / Tests (PHP 8.3) (push) Successful in 2m39s
CI / PHPStan (push) Successful in 2m51s
CI / Build Plugin Zip (push) Successful in 2m43s
Reviewed-on: #81
This commit was merged in pull request #81.
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