Apply account credit on same-month rebook, surface credit on student page
CI / Coding Standards (pull_request) Successful in 27s
CI / Tests (PHP 8.2) (pull_request) Successful in 28s
CI / No Debug Code (pull_request) Successful in 9s
CI / Tests (PHP 8.5) (pull_request) Successful in 35s
CI / Tests (PHP 8.1) (pull_request) Successful in 37s
CI / Tests (PHP 8.3) (pull_request) Successful in 43s
CI / Static Analysis (pull_request) Successful in 51s
CI / Build Plugin Zip (pull_request) Skipped
CI / Coding Standards (pull_request) Successful in 27s
CI / Tests (PHP 8.2) (pull_request) Successful in 28s
CI / No Debug Code (pull_request) Successful in 9s
CI / Tests (PHP 8.5) (pull_request) Successful in 35s
CI / Tests (PHP 8.1) (pull_request) Successful in 37s
CI / Tests (PHP 8.3) (pull_request) Successful in 43s
CI / Static Analysis (pull_request) Successful in 51s
CI / Build Plugin Zip (pull_request) Skipped
A monthly/weekly lesson booked into a month whose billing date has already passed is charged at booking time by LessonBooker::settle, bypassing the daily scan where account credit is otherwise applied. So rebooking a cancelled paid lesson within the same month charged the family in full while their cancellation credit sat unused — billed twice for the same slot. Apply the payer's credit to that charge-at-booking payment for scheduled offerings, mirroring the daily scan: a payment fully covered by credit settles and confirms its lesson. Add PaymentService::findPayment so settle can re-read the row after applyCredits writes to it. Also surface the student's total account credit at the top of their detail page when they hold a balance, so the studio sees it at a glance. Co-authored-by: anthropic/claude-opus-4-8
This commit is contained in:
co-authored by
anthropic/claude-opus-4-8
parent
d8d842b1ef
commit
e7de627752
@@ -57,6 +57,13 @@ class BookingEndpointTest extends TestCase
|
||||
// Crediting a cancelled paid lesson is exercised in dedicated tests; other
|
||||
// cancellation paths simply allow the call.
|
||||
$this->payments->shouldReceive('creditForCancelledLesson')->andReturn(null)->byDefault();
|
||||
// A charge-at-booking scheduled lesson (an add-on to an already-billed month)
|
||||
// applies the payer's credit before it settles. The default holds no balance
|
||||
// and re-reads the same payment; the same-month-rebook test overrides both.
|
||||
$this->payments->shouldReceive('applyCredits')->andReturn([])->byDefault();
|
||||
$this->payments->shouldReceive('findPayment')->andReturnUsing(
|
||||
static fn (int $id): ?Payment => null
|
||||
)->byDefault();
|
||||
|
||||
$this->guardians = Mockery::mock(GuardianService::class);
|
||||
// The default account books only for itself: no guardian link anywhere.
|
||||
@@ -463,6 +470,42 @@ class BookingEndpointTest extends TestCase
|
||||
self::assertNotNull($result->get_data()['payment']);
|
||||
}
|
||||
|
||||
public function testMonthlyRebookInBilledMonthAppliesAccountCredit(): void
|
||||
{
|
||||
// Rebooking a cancelled monthly lesson inside an already-billed month is
|
||||
// charged at booking. The payer holds a cancellation credit that must be
|
||||
// applied to that charge — otherwise the family is billed twice for the same
|
||||
// slot. A credit that fully covers it settles the payment and confirms the
|
||||
// lesson, so the front end runs no payment step.
|
||||
$this->availability->shouldReceive('findById')->with(10)->andReturn(
|
||||
new AvailabilitySlot(instructorId: 3, startDt: '2026-06-20 10:00:00', endDt: '2026-06-20 11:00:00', offeringId: null, id: 10)
|
||||
);
|
||||
$this->offerings->shouldReceive('findById')->with(8)->andReturn(
|
||||
new Offering(instructorId: 3, kind: Offering::KIND_PRIVATE_LESSON, title: 'Lesson', price: 45.0, billingMode: Offering::BILLING_MONTHLY, 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();
|
||||
|
||||
$pending = new Payment(5, 3, Payment::REG_LESSON, 77, 45.0, currency: 'CAD', method: Payment::METHOD_ETRANSFER, status: Payment::STATUS_PENDING, id: 12);
|
||||
$this->payments->shouldReceive('createForRegistration')
|
||||
->once()
|
||||
->with(Payment::REG_LESSON, 77, 5, 3, 45.0, 'CAD', null, null, null, 5)
|
||||
->andReturn($pending);
|
||||
|
||||
// Credit is applied against the fresh charge for the same payer.
|
||||
$this->payments->shouldReceive('applyCredits')->once()->with(5, [$pending])->andReturn([12 => 45.0]);
|
||||
// applyCredits settled the row; the re-read reflects it as paid.
|
||||
$paid = new Payment(5, 3, Payment::REG_LESSON, 77, 45.0, currency: 'CAD', method: Payment::METHOD_ETRANSFER, status: Payment::STATUS_PAID, creditApplied: 45.0, id: 12);
|
||||
$this->payments->shouldReceive('findPayment')->with(12)->andReturn($paid);
|
||||
|
||||
$result = $this->endpoint->book(new \WP_REST_Request(['slot_id' => 10, 'offering_id' => 8]));
|
||||
|
||||
self::assertInstanceOf(\WP_REST_Response::class, $result);
|
||||
self::assertSame(Lesson::STATUS_CONFIRMED, $result->get_data()['status']);
|
||||
}
|
||||
|
||||
public function testMonthlyLessonBeforeBillingDateDefersPayment(): void
|
||||
{
|
||||
// "now" is 2026-06-01; a monthly lesson for July is booked before July's 1st,
|
||||
|
||||
Reference in New Issue
Block a user