From 01cd1d82af1d28030b7250fb9bf0b215452e4df6 Mon Sep 17 00:00:00 2001 From: colemanw Date: Mon, 16 Oct 2023 13:02:03 -0400 Subject: [PATCH] APIv4 - Add Event.remaining_participants calculated field --- .../Spec/Provider/EventGetSpecProvider.php | 59 ++++++++++++++++++ .../phpunit/api/v4/Entity/ParticipantTest.php | 60 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 ext/civi_event/Civi/Api4/Service/Spec/Provider/EventGetSpecProvider.php diff --git a/ext/civi_event/Civi/Api4/Service/Spec/Provider/EventGetSpecProvider.php b/ext/civi_event/Civi/Api4/Service/Spec/Provider/EventGetSpecProvider.php new file mode 100644 index 0000000000..3c32afc7a9 --- /dev/null +++ b/ext/civi_event/Civi/Api4/Service/Spec/Provider/EventGetSpecProvider.php @@ -0,0 +1,59 @@ +setTitle(ts('Remaining Participants')) + ->setDescription(ts('Maximum participants minus registered participants')) + ->setInputType('Number') + ->setColumnName('max_participants') + ->setSqlRenderer([__CLASS__, 'getRemainingParticipants']); + $spec->addFieldSpec($field); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Event' && $action === 'get'; + } + + /** + * Subtracts max_participants from number of counted (non-test, non-deleted) participants. + * + * @param array $maxField + * @param \Civi\Api4\Query\Api4SelectQuery $query + * return string + */ + public static function getRemainingParticipants(array $maxField, Api4SelectQuery $query): string { + $statuses = \CRM_Event_PseudoConstant::participantStatus(NULL, 'is_counted = 1'); + $statusIds = implode(',', array_keys($statuses)); + $idField = $query->getFieldSibling($maxField, 'id'); + return "IF($maxField[sql_name], (CAST($maxField[sql_name] AS SIGNED) - (SELECT COUNT(`p`.`id`) FROM `civicrm_participant` `p`, `civicrm_contact` `c` WHERE `p`.`event_id` = $idField[sql_name] AND `p`.`contact_id` = `c`.`id` AND `p`.`is_test` = 0 AND `c`.`is_deleted` = 0 AND `p`.status_id IN ($statusIds))), NULL)"; + } + +} diff --git a/tests/phpunit/api/v4/Entity/ParticipantTest.php b/tests/phpunit/api/v4/Entity/ParticipantTest.php index 33f1c978b1..05af6542af 100644 --- a/tests/phpunit/api/v4/Entity/ParticipantTest.php +++ b/tests/phpunit/api/v4/Entity/ParticipantTest.php @@ -19,6 +19,7 @@ namespace api\v4\Entity; +use Civi\Api4\Event; use Civi\Api4\Participant; use api\v4\Api4TestBase; use Civi\Test\TransactionalInterface; @@ -251,6 +252,65 @@ class ParticipantTest extends Api4TestBase implements TransactionalInterface { $this->assertCount(1, Participant::get()->selectRowCount()->addWhere('id', '=', $testParticipants->first()['id'])->execute()); } + public function testGetRemainingParticipants(): void { + $eventWithMax = $this->createTestRecord('Event', ['max_participants' => 3])['id']; + $eventUnlimited = $this->createTestRecord('Event', ['max_participants' => 0])['id']; + + $events = Event::get(FALSE) + ->addSelect('remaining_participants') + ->addWhere('id', 'IN', [$eventWithMax, $eventUnlimited]) + ->addOrderBy('id') + ->execute(); + $this->assertEquals(3, $events[0]['remaining_participants']); + // `remaining_participants` is always NULL for unlimited events + $this->assertNull($events[1]['remaining_participants']); + + $deleted = $this->createTestRecord('Contact', ['is_deleted' => TRUE])['id']; + + $this->saveTestRecords('Participant', [ + 'records' => [ + // 2 legit registrations for $eventWithMax + ['event_id' => $eventWithMax, 'status_id:name' => 'Registered'], + ['event_id' => $eventWithMax, 'status_id:name' => 'Attended'], + // None of these should count toward $eventWithMax participant limit + ['event_id' => $eventWithMax, 'status_id:name' => 'Registered', 'contact_id' => $deleted], + ['event_id' => $eventWithMax, 'status_id:name' => 'Cancelled'], + ['event_id' => $eventUnlimited, 'status_id:name' => 'Registered'], + ], + ]); + + $events = Event::get(FALSE) + ->addSelect('remaining_participants') + ->addWhere('id', 'IN', [$eventWithMax, $eventUnlimited]) + ->addOrderBy('id') + ->execute(); + // 1 Spot remaining + $this->assertEquals(1, $events[0]['remaining_participants']); + // `remaining_participants` is always NULL for unlimited events + $this->assertNull($events[1]['remaining_participants']); + + $this->saveTestRecords('Participant', [ + 'records' => [ + // 2 legit registrations for $eventWithMax + ['event_id' => $eventWithMax, 'status_id:name' => 'Registered'], + ['event_id' => $eventWithMax, 'status_id:name' => 'Attended'], + // None of these should count toward $eventWithMax participant limit + ['event_id' => $eventWithMax, 'status_id:name' => 'Registered', 'contact_id' => $deleted], + ['event_id' => $eventUnlimited, 'status_id:name' => 'Registered'], + ], + ]); + + $events = Event::get(FALSE) + ->addSelect('remaining_participants') + ->addWhere('id', 'IN', [$eventWithMax, $eventUnlimited]) + ->addOrderBy('id') + ->execute(); + // -1 spot remaining + $this->assertEquals(-1, $events[0]['remaining_participants']); + // `remaining_participants` is always NULL for unlimited events + $this->assertNull($events[1]['remaining_participants']); + } + /** * Quick record counter * -- 2.25.1