bookings = Mockery::mock(BookingRepository::class); $this->availability = Mockery::mock(AvailabilityRepository::class); $this->enrollments = Mockery::mock(EnrollmentRepository::class); $this->payments = Mockery::mock(PaymentService::class); $this->links = Mockery::mock(GuardianRepository::class); $this->guardians = Mockery::mock(GuardianService::class); // Most accounts have nobody linked to them; the guardian tests say so. $this->links->shouldReceive('findByGuardian')->andReturn([])->byDefault(); $this->cleanup = new DeletedUserCleanup( $this->bookings, $this->availability, $this->enrollments, $this->payments, $this->links, $this->guardians ); } public function testHooksBothSingleSiteAndNetworkDeletion(): void { Actions\expectAdded('delete_user')->once(); Actions\expectAdded('wpmu_delete_user')->once(); $this->cleanup->register(); } public function testEachUpcomingLessonIsCancelledItsSlotFreedAndItsPendingPaymentVoided(): void { $this->bookings->shouldReceive('findUpcomingForStudent')->with(5)->andReturn([ new Lesson(slotId: 7, studentId: 5, instructorId: 3, status: Lesson::STATUS_PENDING, paymentId: 40, id: 12), new Lesson(slotId: 8, studentId: 5, instructorId: 3, status: Lesson::STATUS_CONFIRMED, paymentId: null, id: 13), ]); $this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([]); $this->bookings->shouldReceive('updateStatus')->once()->with(12, Lesson::STATUS_CANCELLED)->andReturn(true); $this->bookings->shouldReceive('updateStatus')->once()->with(13, Lesson::STATUS_CANCELLED)->andReturn(true); // The point of the whole exercise: the times go back on sale. $this->availability->shouldReceive('release')->once()->with(7)->andReturn(true); $this->availability->shouldReceive('release')->once()->with(8)->andReturn(true); $this->payments->shouldReceive('voidPending')->once()->with(40); $this->payments->shouldReceive('voidPending')->once()->with(null); $this->cleanup->releaseBookings(5); } /** * A paid lesson is not credited back. The credit could only ever be spent on * the account being deleted, so writing one would be book-keeping nobody can * act on — a refund is the studio's call to make and record. */ public function testNoCreditIsIssuedForAPaidLesson(): void { $this->bookings->shouldReceive('findUpcomingForStudent')->with(5)->andReturn([ new Lesson(slotId: 7, studentId: 5, instructorId: 3, status: Lesson::STATUS_CONFIRMED, paymentId: 40, id: 12), ]); $this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([]); $this->bookings->shouldReceive('updateStatus')->andReturn(true); $this->availability->shouldReceive('release')->andReturn(true); $this->payments->shouldReceive('voidPending'); $this->payments->shouldNotReceive('creditForCancelledLesson'); $this->cleanup->releaseBookings(5); } public function testActiveEnrolmentsAreCancelledAndTheirPendingPaymentsVoided(): void { $this->bookings->shouldReceive('findUpcomingForStudent')->with(5)->andReturn([]); $this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([ new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, paymentId: 41, id: 40), new Enrollment( offeringId: 9, studentId: 5, instructorId: 3, status: Enrollment::STATUS_CANCELLED, paymentId: 42, id: 41, ), ]); // Only the active one: a withdrawn enrolment is already holding nothing. $this->enrollments->shouldReceive('updateStatus')->once()->with(40, Enrollment::STATUS_CANCELLED)->andReturn(true); $this->payments->shouldReceive('voidPending')->once()->with(41); $this->cleanup->releaseBookings(5); } /** * Past lessons happened and may have been paid for, so they stay exactly as * they are — `findUpcomingForStudent` is what draws that line. */ public function testNothingHappensWhenTheAccountHasNothingBookedAhead(): void { $this->bookings->shouldReceive('findUpcomingForStudent')->with(5)->andReturn([]); $this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([]); $this->bookings->shouldNotReceive('updateStatus'); $this->availability->shouldNotReceive('release'); $this->enrollments->shouldNotReceive('updateStatus'); $this->cleanup->releaseBookings(5); } public function testAnInvalidUserIdIsIgnored(): void { $this->bookings->shouldNotReceive('findUpcomingForStudent'); $this->enrollments->shouldNotReceive('findByStudent'); $this->cleanup->releaseBookings(0); } /** * A child account is login-less and exists only so its guardian has somebody * to book for. Without the guardian nobody can reach it, book for it, or be * billed for it — so it goes too, and what it was holding goes back. */ public function testDeletingAGuardianReleasesAndDeletesEachChild(): void { $this->bookings->shouldReceive('findUpcomingForStudent')->with(5)->andReturn([]); $this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([]); $this->links->shouldReceive('findByGuardian')->with(5)->andReturn([ new GuardianLink(guardianId: 5, studentId: 42, id: 1), new GuardianLink(guardianId: 5, studentId: 43, id: 2), ]); $this->bookings->shouldReceive('findUpcomingForStudent')->with(42)->andReturn([ new Lesson(slotId: 7, studentId: 42, instructorId: 3, status: Lesson::STATUS_CONFIRMED, paymentId: 40, id: 12), ]); $this->bookings->shouldReceive('findUpcomingForStudent')->with(43)->andReturn([]); $this->enrollments->shouldReceive('findByStudent')->with(42)->andReturn([]); $this->enrollments->shouldReceive('findByStudent')->with(43)->andReturn([ new Enrollment(offeringId: 8, studentId: 43, instructorId: 3, paymentId: 41, id: 40), ]); // The child's lesson is cancelled and its time freed, exactly as the // guardian's own would have been. $this->bookings->shouldReceive('updateStatus')->once()->with(12, Lesson::STATUS_CANCELLED)->andReturn(true); $this->availability->shouldReceive('release')->once()->with(7)->andReturn(true); $this->enrollments->shouldReceive('updateStatus')->once()->with(40, Enrollment::STATUS_CANCELLED)->andReturn(true); $this->payments->shouldReceive('voidPending')->with(40)->once(); $this->payments->shouldReceive('voidPending')->with(41)->once(); // Then the link row and the account itself. $this->links->shouldReceive('delete')->once()->with(5, 42)->andReturn(true); $this->links->shouldReceive('delete')->once()->with(5, 43)->andReturn(true); $this->guardians->shouldReceive('deleteUser')->once()->with(42); $this->guardians->shouldReceive('deleteUser')->once()->with(43); $this->cleanup->releaseBookings(5); } /** * Deleting a child fires `delete_user` again, which lands back in this same * handler. It must return without redoing the release — and a self-link, * however it got into the table, must not recurse for ever. */ public function testAChildAlreadyDealtWithIsNotProcessedTwice(): void { $this->bookings->shouldReceive('findUpcomingForStudent')->with(5)->andReturn([]); $this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([]); $this->links->shouldReceive('findByGuardian')->with(5)->andReturn([ new GuardianLink(guardianId: 5, studentId: 42, id: 1), // A duplicate row, and a self-link: neither may cause a second pass. new GuardianLink(guardianId: 5, studentId: 42, id: 2), new GuardianLink(guardianId: 5, studentId: 5, id: 3), ]); $this->bookings->shouldReceive('findUpcomingForStudent')->with(42)->once()->andReturn([]); $this->enrollments->shouldReceive('findByStudent')->with(42)->once()->andReturn([]); $this->links->shouldReceive('delete')->once()->with(5, 42)->andReturn(true); $this->guardians->shouldReceive('deleteUser')->once()->with(42); $this->cleanup->releaseBookings(5); // The re-entrant call the child's own deletion triggers is a no-op. $this->cleanup->releaseBookings(42); } /** * A child's own deletion (from the family screen, say) touches nothing but * that child — they have nobody linked beneath them. */ public function testDeletingAStudentWithNoChildrenDeletesNobodyElse(): void { $this->bookings->shouldReceive('findUpcomingForStudent')->with(42)->andReturn([]); $this->enrollments->shouldReceive('findByStudent')->with(42)->andReturn([]); $this->links->shouldReceive('findByGuardian')->with(42)->andReturn([]); $this->guardians->shouldNotReceive('deleteUser'); $this->links->shouldNotReceive('delete'); $this->cleanup->releaseBookings(42); } }