Enhance examples to cover additional participants
authorEileen McNaughton <emcnaughton@wikimedia.org>
Mon, 19 Jun 2023 01:25:27 +0000 (13:25 +1200)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Mon, 19 Jun 2023 11:01:37 +0000 (23:01 +1200)
CRM/Event/WorkflowMessage/EventExamples.php
CRM/Event/WorkflowMessage/ParticipantTrait.php
xml/templates/message_templates/event_online_receipt_html.tpl

index 556bb42b26fcffd39cf17fa2e2f8cb2223905544..dbbdb6f589c38702a73d54d1b7507c1c2256d680 100644 (file)
@@ -1,9 +1,12 @@
 <?php
 
+use Civi\Api4\Event;
 use Civi\Api4\PriceSetEntity;
 use Civi\Api4\WorkflowMessage;
 use Civi\WorkflowMessage\GenericWorkflowMessage;
 use Civi\WorkflowMessage\WorkflowMessageExample;
+use Civi\Api4\PriceField;
+use Civi\Api4\PriceFieldValue;
 
 /**
  * Basic contribution example for contribution templates.
@@ -12,6 +15,15 @@ use Civi\WorkflowMessage\WorkflowMessageExample;
  */
 class CRM_Event_WorkflowMessage_EventExamples extends WorkflowMessageExample {
 
+  /**
+   * IDs of events permitting multiple participants.
+   *
+   * We prefer these for more nuanced examples.
+   *
+   * @var array
+   */
+  private $multipleRegistrationEventIDs;
+
   /**
    * Get the examples this class is able to deliver.
    *
@@ -24,12 +36,28 @@ class CRM_Event_WorkflowMessage_EventExamples extends WorkflowMessageExample {
       foreach ($priceSets as $priceSet) {
         yield [
           'name' => 'workflow/' . $workflow . '/' . 'price_set_' . $priceSet['name'],
-          'title' => ts('Completed Registration') . ' : ' . $priceSet['title'],
+          'title' => ts('Completed Registration') . ($priceSet['is_multiple_registrations'] ? ' ' . ts('primary participant') : '') . ' : ' . $priceSet['title'],
           'tags' => ['preview'],
           'workflow' => $workflow,
           'is_show_line_items' => !$priceSet['is_quick_config'],
           'event_id' => $priceSet['event_id'],
+          'is_multiple_registrations' => $priceSet['is_multiple_registrations'],
+          'is_primary' => TRUE,
+          'price_set_id' => $priceSet['id'],
         ];
+        if ($priceSet['is_multiple_registrations']) {
+          yield [
+            'name' => 'workflow/' . $workflow . '/' . 'price_set_' . $priceSet['name'] . '/' . 'additional',
+            'title' => ts('Completed Registration') . ' ' . ts('additional participant') . ' : ' . $priceSet['title'],
+            'tags' => ['preview'],
+            'workflow' => $workflow,
+            'is_show_line_items' => !$priceSet['is_quick_config'],
+            'event_id' => $priceSet['event_id'],
+            'is_multiple_registrations' => $priceSet['is_multiple_registrations'],
+            'is_primary' => FALSE,
+            'price_set_id' => $priceSet['id'],
+          ];
+        }
       }
     }
   }
@@ -58,10 +86,36 @@ class CRM_Event_WorkflowMessage_EventExamples extends WorkflowMessageExample {
    * @throws \CRM_Core_Exception
    * @throws \Civi\API\Exception\UnauthorizedException
    */
-  private function addExampleData(GenericWorkflowMessage $messageTemplate, $example): void {
+  private function addExampleData(GenericWorkflowMessage $messageTemplate, array $example): void {
     $messageTemplate->setContact(\Civi\Test::example('entity/Contact/Barb'));
     $messageTemplate->setEventID($example['event_id']);
-    $messageTemplate->setContribution(['total_amount' => 50, 'balance_amount' => 20, 'currency' => 'USD']);
+    $isPrimary = $example['is_primary'];
+    $primaryParticipantID = 60;
+    $otherParticipantID = 70;
+    $isMultipleRegistrations = $example['is_multiple_registrations'];
+    $participantContacts = [$primaryParticipantID => ['display_name' => 'Cindy Taylor']];
+    if ($isMultipleRegistrations) {
+      $participantContacts[$otherParticipantID] = ['display_name' => 'Melanie Mulder'];
+    }
+
+    $mockOrder = new CRM_Financial_BAO_Order();
+    $mockOrder->setTemplateContributionID(50);
+    $mockOrder->setPriceSetID($example['price_set_id']);
+
+    foreach (PriceField::get(FALSE)->addWhere('price_set_id', '=', $mockOrder->getPriceSetID())->execute() as $index => $priceField) {
+      $priceFieldValue = PriceFieldValue::get()->addWhere('price_field_id', '=', $priceField['id'])->execute();
+      $this->setLineItem($mockOrder, $priceField, $priceFieldValue->first(), $index, $primaryParticipantID);
+      if ($isMultipleRegistrations) {
+        $this->setLineItem($mockOrder, $priceField, $priceFieldValue->last(), $index . '-' . $otherParticipantID, $otherParticipantID);
+      }
+    }
+    $contribution['total_amount'] = $mockOrder->getTotalAmount();
+    $contribution['tax_amount'] = $mockOrder->getTotalTaxAmount() ? round($mockOrder->getTotalTaxAmount(), 2) : 0;
+    $contribution['tax_exclusive_amount'] = $contribution['total_amount'] - $contribution['tax_amount'];
+    $messageTemplate->setContribution($contribution);
+    $messageTemplate->setOrder($mockOrder);
+    $messageTemplate->setParticipantContacts($participantContacts);
+    $messageTemplate->setParticipant(['id' => $isPrimary ? $primaryParticipantID : $otherParticipantID, 'registered_by_id' => $isPrimary ? NULL : $primaryParticipantID, 'register_date' => date('Y-m-d')]);
   }
 
   /**
@@ -72,10 +126,10 @@ class CRM_Event_WorkflowMessage_EventExamples extends WorkflowMessageExample {
    * @throws \CRM_Core_Exception
    */
   private function getPriceSets(): ?array {
-    $quickConfigPriceSet = $this->getPriceSet(TRUE);
     $nonQuickConfigPriceSet = $this->getPriceSet(FALSE);
+    $quickConfigPriceSet = $this->getPriceSet(TRUE);
 
-    return array_filter([$quickConfigPriceSet, $nonQuickConfigPriceSet]);
+    return array_filter([$nonQuickConfigPriceSet, $quickConfigPriceSet]);
   }
 
   /**
@@ -87,20 +141,78 @@ class CRM_Event_WorkflowMessage_EventExamples extends WorkflowMessageExample {
    * @throws \CRM_Core_Exception
    */
   private function getPriceSet(bool $isQuickConfig): ?array {
+    // Try to find an event configured for multiple registrations
     $priceSetEntity = PriceSetEntity::get(FALSE)
       ->addWhere('entity_table', '=', 'civicrm_event')
       ->addSelect('price_set_id.id', 'entity_id', 'price_set_id.is_quick_config', 'price_set_id.name', 'price_set_id.title')
       ->setLimit(1)
       ->addWhere('price_set_id.is_quick_config', '=', $isQuickConfig)
+      ->addWhere('entity_id', 'IN', $this->getMultipleRegistrationEventIDs())
       ->execute()->first();
+    if (empty($priceSetEntity)) {
+      // Try again without limiting to multiple registrations.
+      $priceSetEntity = PriceSetEntity::get(FALSE)
+        ->addWhere('entity_table', '=', 'civicrm_event')
+        ->addSelect('price_set_id.id', 'entity_id', 'price_set_id.is_quick_config', 'price_set_id.name', 'price_set_id.title')
+        ->setLimit(1)
+        ->addWhere('price_set_id.is_quick_config', '=', $isQuickConfig)
+        ->execute()->first();
+    }
 
     return empty($priceSetEntity) ? NULL : [
-      'id' => $priceSetEntity['price_set_id'],
+      'id' => $priceSetEntity['price_set_id.id'],
       'name' => $priceSetEntity['price_set_id.name'],
       'title' => $priceSetEntity['price_set_id.title'],
       'event_id' => $priceSetEntity['entity_id'],
       'is_quick_config' => $priceSetEntity['price_set_id.is_quick_config'],
+      'is_multiple_registrations' => in_array($priceSetEntity['entity_id'], $this->getMultipleRegistrationEventIDs(), TRUE),
     ];
   }
 
+  /**
+   * @param \CRM_Financial_BAO_Order $mockOrder
+   * @param $priceField
+   * @param array|null $priceFieldValue
+   * @param int $participantID
+   * @param $index
+   *
+   * @throws \CRM_Core_Exception
+   */
+  private function setLineItem(CRM_Financial_BAO_Order $mockOrder, $priceField, ?array $priceFieldValue, $index, $participantID): void {
+    $mockOrder->setLineItem([
+      'price_field_id' => $priceField['id'],
+      'price_field_id.label' => $priceField['label'],
+      'price_field_value_id' => $priceFieldValue['id'],
+      'qty' => $priceField['is_enter_qty'] ? 2 : 1,
+      'unit_price' => $priceFieldValue['amount'],
+      'line_total' => $priceField['is_enter_qty'] ? ($priceFieldValue['amount'] * 2) : $priceFieldValue['amount'],
+      'label' => $priceFieldValue['label'],
+      'financial_type_id' => $priceFieldValue['financial_type_id'],
+      'non_deductible_amount' => $priceFieldValue['non_deductible_amount'],
+      'entity_table' => 'civicrm_participant',
+      'entity_id' => $participantID,
+    ], $index);
+  }
+
+  /**
+   * Get the ids of (up to 25) recent multiple registration events.
+   *
+   * @return array
+   * @throws \CRM_Core_Exception
+   */
+  private function getMultipleRegistrationEventIDs(): array {
+    if ($this->multipleRegistrationEventIDs === NULL) {
+      $this->multipleRegistrationEventIDs = array_keys((array) Event::get(FALSE)
+        ->addWhere('is_multiple_registrations', '=', TRUE)
+        ->addWhere('max_additional_participants', '>', 0)
+        ->addSelect('id')
+        ->addOrderBy('start_date', 'DESC')
+        ->setLimit(25)
+        ->execute()
+        ->indexBy('id'));
+
+    }
+    return $this->multipleRegistrationEventIDs;
+  }
+
 }
index 6d9df1d4fe85ae846517880e6d8aff0127dd8d43..00b4e20dcf8a31a1c48ce5a9999de9120064349c 100644 (file)
@@ -1,10 +1,13 @@
 <?php
 
+use Civi\Api4\Participant;
+
 /**
  * Trait for participant workflow classes.
  */
 trait CRM_Event_WorkflowMessage_ParticipantTrait {
 
+  use CRM_Contribute_WorkflowMessage_ContributionTrait;
   /**
    * @var int
    *
@@ -12,6 +15,24 @@ trait CRM_Event_WorkflowMessage_ParticipantTrait {
    */
   public $participantID;
 
+  /**
+   * The participant record.
+   *
+   * @var array|null
+   *
+   * @scope tokenContext as participant
+   */
+  public $participant;
+
+  /**
+   * Is this the primary participant.
+   *
+   * @var bool
+   *
+   * @scope tplParams as isPrimary
+   */
+  public $isPrimary;
+
   /**
    * @var int
    *
@@ -19,14 +40,185 @@ trait CRM_Event_WorkflowMessage_ParticipantTrait {
    */
   public $eventID;
 
+  /**
+   * Line items indexed by the participant.
+   *
+   * The format is otherwise the same as lineItems which is also available on the
+   * template. The by-participant re-keying permits only including the current
+   * participant for non-primary participants and
+   * creating a by-participant table for the primary participant.
+   *
+   * @var array
+   *
+   * @scope tplParams as participants
+   */
+  public $participants;
+
+  /**
+   * Details of the participant contacts.
+   *
+   * This would normally be loaded but exists to allow the example to set them.
+   *
+   * @var array
+   */
+  protected $participantContacts;
+
+  /**
+   * @param array $participantContacts
+   *
+   * @return CRM_Event_WorkflowMessage_ParticipantTrait
+   */
+  public function setParticipantContacts(array $participantContacts): self {
+    $this->participantContacts = $participantContacts;
+    return $this;
+  }
+
   /**
    * @param int $eventID
    *
    * @return CRM_Event_WorkflowMessage_ParticipantTrait
    */
-  public function setEventID(int $eventID) {
+  public function setEventID(int $eventID): self {
     $this->eventID = $eventID;
     return $this;
   }
 
+  /**
+   * Is the participant the primary participant.
+   *
+   * @return bool
+   * @throws \CRM_Core_Exception
+   */
+  public function getIsPrimary(): bool {
+    return !$this->getParticipant()['registered_by_id'];
+  }
+
+  /**
+   * @return int
+   */
+  public function getPrimaryParticipantID(): int {
+    return $this->participant['registered_by_id'] ?: $this->participantID;
+  }
+
+  /**
+   * Set contribution object.
+   *
+   * @param array $participant
+   *
+   * @return $this
+   */
+  public function setParticipant(array $participant): self {
+    $this->participant = $participant;
+    if (!empty($participant['id'])) {
+      $this->participantID = $participant['id'];
+    }
+    if (!empty($participant['event_id'])) {
+      $this->eventID = $participant['event_id'];
+    }
+    return $this;
+  }
+
+  /**
+   * Get the participant record.
+   *
+   * @return array
+   * @throws \CRM_Core_Exception
+   */
+  public function getParticipant(): array {
+    if (!$this->participant) {
+      $this->participant = Participant::get(FALSE)
+        ->addWhere('id', '=', $this->participantID)
+        ->addSelect('registered_by_id')->execute()->first();
+    }
+    return $this->participant;
+  }
+
+  /**
+   * Get the line items and tax information indexed by participant.
+   *
+   * We will likely add profile data to this too. This is so we can iterate through
+   * participants as the primary participant needs to show them all (and the others
+   * need to be able to filter).
+   *
+   * @return array
+   * @throws \CRM_Core_Exception
+   */
+  public function getParticipants(): array {
+    if (!$this->participants) {
+      if (!$this->getLineItems()) {
+        return [];
+      }
+      // Initiate with the current participant to ensure they are first.
+      $participants = [$this->participantID => ['id' => $this->participantID]];
+      foreach ($this->getLineItems() as $lineItem) {
+        if ($lineItem['entity_table'] === 'civicrm_participant') {
+          $participantID = $lineItem['entity_id'];
+        }
+        else {
+          // It is not clear if this could ever be true - testing the CiviCRM event
+          // form shows all line items assigned to participants but we should
+          // assign to primary if this can occur.
+          $participantID = $this->getPrimaryParticipantID();
+        }
+        $participants[$participantID]['line_items'][] = $lineItem;
+        if (!isset($participants[$participantID]['totals'])) {
+          $participants[$participantID]['totals'] = ['total_amount_exclusive' => 0, 'tax_amount' => 0, 'total_amount_inclusive' => 0];
+        }
+        $participants[$participantID]['totals']['total_amount_exclusive'] += $lineItem['line_total'];
+        $participants[$participantID]['totals']['tax_amount'] += $lineItem['tax_amount'];
+        $participants[$participantID]['totals']['total_amount_inclusive'] += ($lineItem['line_total'] + $lineItem['tax_amount']);
+        if (!isset($participants[$participantID]['tax_rate_breakdown'][$lineItem['tax_rate']])) {
+          $participants[$participantID]['tax_rate_breakdown'][$lineItem['tax_rate']] = [
+            'amount' => 0,
+            'rate' => $lineItem['tax_rate'],
+            'percentage' => sprintf('%.2f', $lineItem['tax_rate']),
+          ];
+        }
+        $participants[$participantID]['tax_rate_breakdown'][$lineItem['tax_rate']]['amount'] += $lineItem['tax_amount'];
+      }
+
+      $count = 1;
+      foreach ($participants as $participantID => &$participant) {
+        $participant['id'] = $participantID;
+        $participant['index'] = $count;
+        $participant['contact'] = $this->getParticipantContact($participantID);
+        foreach ($participant['tax_rate_breakdown'] as $rate => $details) {
+          if ($details['amount'] === 0.0) {
+            unset($participant['tax_rate_breakdown'][$rate]);
+          }
+        }
+        if (array_keys($participant['tax_rate_breakdown']) === [0]) {
+          // If the only tax rate charged is 0% then no tax breakdown is returned.
+          $participant['tax_rate_breakdown'] = [];
+        }
+        $count++;
+      }
+      $this->participants = $participants;
+    }
+    return $this->participants;
+  }
+
+  /**
+   * @param $participantID
+   *
+   * @return mixed
+   * @throws \CRM_Core_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  private function getParticipantContact($participantID = NULL) {
+    if (!$participantID) {
+      $participantID = $this->participantID;
+    }
+    if (empty($this->participantContacts[$participantID])) {
+      $participantContact = Participant::get(FALSE)
+        ->addWhere('id', '=', $participantID ?: $this->participantID)
+        ->addSelect('contact_id.display_name', 'contact_id')
+        ->execute()
+        ->first();
+      $this->participantContacts[$participantID] = ['id' => $participantContact['contact_id'], 'display_name' => $participantContact['contact_id.display_name']];
+    }
+
+    return $this->participantContacts[$participantID];
+  }
+
 }
index 4ee53b192fe3fe8a73876e7e8d35bb66fb9c5c2f..0b3046e5502000d8e1844d53144652753dfa9829 100644 (file)
     <p>
     {if !empty($isOnWaitlist)}
      <p>{ts}You have been added to the WAIT LIST for this event.{/ts}</p>
-     {if !empty($isPrimary)}
+     {if $isPrimary}
        <p>{ts}If space becomes available you will receive an email with a link to a web page where you can complete your registration.{/ts}</p>
      {/if}
     {elseif !empty($isRequireApproval)}
      <p>{ts}Your registration has been submitted.{/ts}</p>
-     {if !empty($isPrimary)}
+     {if $isPrimary}
       <p>{ts}Once your registration has been reviewed, you will receive an email with a link to a web page where you can complete the registration process.{/ts}</p>
      {/if}
     {elseif !empty($is_pay_later) && empty($isAmountzero) && empty($isAdditionalParticipant)}
         </td>
        </tr>
       {/if}
-      {if !empty($isPrimary)}
+      {if $isPrimary}
        <tr>
         <td {$labelStyle}>
          {ts}Total Amount{/ts}
      <tr>
       <td colspan="2" {$valueStyle}>
         {ts 1=$selfcancelxfer_time 2=$selfservice_preposition}You may transfer your registration to another participant or cancel your registration up to %1 hours %2 the event.{/ts} {if !empty($totalAmount)}{ts}Cancellations are not refundable.{/ts}{/if}<br />
-        {capture assign=selfService}{crmURL p='civicrm/event/selfsvcupdate' q="reset=1&pid=`$participant.id`&{contact.checksum}"  h=0 a=1 fe=1}{/capture}
+        {capture assign=selfService}{crmURL p='civicrm/event/selfsvcupdate' q="reset=1&pid=`$participantID`&{contact.checksum}"  h=0 a=1 fe=1}{/capture}
         <a href="{$selfService}">{ts}Click here to transfer or cancel your registration.{/ts}</a>
       </td>
      </tr>