alias(static fn ($v): int => abs((int) $v)); Functions\when('wp_unslash')->returnArg(); Functions\when('sanitize_text_field')->returnArg(); Functions\when('get_current_user_id')->justReturn(5); Functions\when('current_time')->justReturn('2026-07-24'); $this->enrollments = Mockery::mock(EnrollmentRepository::class); $this->offerings = Mockery::mock(OfferingRepository::class); $this->gate = Mockery::mock(RegistrationGate::class); $this->payments = Mockery::mock(PaymentService::class); $this->access = Mockery::mock(GroupAccessRepository::class); $this->endpoint = new EnrollmentEndpoint( $this->enrollments, $this->offerings, $this->gate, $this->payments, $this->access, ); } private function offering(float $price): Offering { return new Offering(instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Choir', price: $price, id: 8); } private function inviteOnlyOffering(): Offering { return new Offering(instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Private Choir', accessMode: Offering::ACCESS_INVITE_ONLY, id: 8); } private function expectSuccessfulEnrollment(): void { $this->enrollments->shouldReceive('hasActiveEnrollment')->with(8, 5)->andReturn(false); $this->enrollments->shouldReceive('countActiveForOffering')->never(); $this->gate->shouldReceive('validate')->andReturn(null); $this->enrollments->shouldReceive('insert')->once()->andReturn(44); $this->gate->shouldReceive('record')->once(); } public function testEnrollInFreeClassReturnsNullPayment(): void { $this->offerings->shouldReceive('findById')->with(8)->andReturn($this->offering(0.0)); $this->expectSuccessfulEnrollment(); $this->payments->shouldNotReceive('createForRegistration'); $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8])); self::assertInstanceOf(\WP_REST_Response::class, $result); self::assertSame(201, $result->get_status()); self::assertSame(44, $result->get_data()['id']); self::assertNull($result->get_data()['payment']); } public function testEnrollInPricedClassReturnsPaymentSummary(): void { $this->offerings->shouldReceive('findById')->with(8)->andReturn($this->offering(120.0)); $this->expectSuccessfulEnrollment(); $this->payments->shouldReceive('createForRegistration') ->once() ->with(Payment::REG_ENROLLMENT, 44, 5, 3, 120.0, 'CAD', null) ->andReturn(new Payment( studentId: 5, instructorId: 3, registrationType: Payment::REG_ENROLLMENT, registrationId: 44, amount: 120.0, method: Payment::METHOD_ETRANSFER, status: Payment::STATUS_PENDING, id: 12, )); $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8])); self::assertInstanceOf(\WP_REST_Response::class, $result); self::assertSame( ['id' => 12, 'method' => Payment::METHOD_ETRANSFER, 'status' => Payment::STATUS_PENDING], $result->get_data()['payment'] ); } public function testScheduledBillingEnrollmentDefersPayment(): void { // A monthly group class is billed later by the daily scan, so enrolment // succeeds with no payment created now. $offering = new Offering(instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Choir', price: 120.0, billingMode: Offering::BILLING_MONTHLY, id: 8); $this->offerings->shouldReceive('findById')->with(8)->andReturn($offering); $this->expectSuccessfulEnrollment(); $this->payments->shouldNotReceive('createForRegistration'); $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8])); self::assertInstanceOf(\WP_REST_Response::class, $result); self::assertSame(201, $result->get_status()); self::assertNull($result->get_data()['payment']); } public function testRejectsEnrollmentAfterExplicitDeadline(): void { // current_time is stubbed to 2026-07-24, past the 2026-07-10 deadline. $offering = new Offering(instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Choir', termStart: '2026-07-01', enrollmentDeadline: '2026-07-10', id: 8); $this->offerings->shouldReceive('findById')->with(8)->andReturn($offering); $this->enrollments->shouldReceive('hasActiveEnrollment')->with(8, 5)->andReturn(false); $this->enrollments->shouldReceive('insert')->never(); $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8])); self::assertInstanceOf(\WP_Error::class, $result); self::assertSame('enrollment_closed', $result->get_error_code()); self::assertSame(403, $result->error_data['enrollment_closed']['status']); } public function testRejectsEnrollmentAfterDefaultDeadlineOfFirstClassDay(): void { // No explicit deadline, so it defaults to term_start (the first class day), // which is in the past relative to the stubbed 2026-07-24 "today". $offering = new Offering(instructorId: 3, kind: Offering::KIND_GROUP_CLASS, title: 'Choir', termStart: '2026-07-20', id: 8); $this->offerings->shouldReceive('findById')->with(8)->andReturn($offering); $this->enrollments->shouldReceive('hasActiveEnrollment')->with(8, 5)->andReturn(false); $this->enrollments->shouldReceive('insert')->never(); $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8])); self::assertInstanceOf(\WP_Error::class, $result); self::assertSame('enrollment_closed', $result->get_error_code()); } public function testInviteOnlyClassRejectsStudentWithoutGrant(): void { $this->offerings->shouldReceive('findById')->with(8)->andReturn($this->inviteOnlyOffering()); $this->enrollments->shouldReceive('hasActiveEnrollment')->with(8, 5)->andReturn(false); $this->access->shouldReceive('hasGrant')->with(8, 5)->andReturn(false); $this->enrollments->shouldReceive('insert')->never(); $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8])); self::assertInstanceOf(\WP_Error::class, $result); self::assertSame('invite_required', $result->get_error_code()); self::assertSame(403, $result->error_data['invite_required']['status']); } public function testInviteOnlyClassAllowsGrantedStudentAndMarksEnrolled(): void { $this->offerings->shouldReceive('findById')->with(8)->andReturn($this->inviteOnlyOffering()); $this->access->shouldReceive('hasGrant')->with(8, 5)->andReturn(true); $this->expectSuccessfulEnrollment(); $this->access->shouldReceive('markEnrolled')->once()->with(8, 5); $result = $this->endpoint->enroll(new \WP_REST_Request(['offering_id' => 8])); self::assertInstanceOf(\WP_REST_Response::class, $result); self::assertSame(201, $result->get_status()); } }