APIv4 - Add Event.remaining_participants calculated field
authorcolemanw <coleman@civicrm.org>
Mon, 16 Oct 2023 17:02:03 +0000 (13:02 -0400)
committercolemanw <coleman@civicrm.org>
Mon, 16 Oct 2023 17:02:03 +0000 (13:02 -0400)
ext/civi_event/Civi/Api4/Service/Spec/Provider/EventGetSpecProvider.php [new file with mode: 0644]
tests/phpunit/api/v4/Entity/ParticipantTest.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 (file)
index 0000000..3c32afc
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Query\Api4SelectQuery;
+use Civi\Api4\Service\Spec\FieldSpec;
+use Civi\Api4\Service\Spec\RequestSpec;
+
+/**
+ * @service
+ * @internal
+ */
+class EventGetSpecProvider extends \Civi\Core\Service\AutoService implements Generic\SpecProviderInterface {
+
+  /**
+   * @inheritDoc
+   */
+  public function modifySpec(RequestSpec $spec) {
+    $field = (new FieldSpec('remaining_participants', 'Event', 'Integer'))
+      ->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)";
+  }
+
+}
index 33f1c978b17d59866cb6b80b51de36a37d7ea4cb..05af6542afc0892b967cd0137fdcc54ce17ea3d4 100644 (file)
@@ -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
    *