<?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.
*/
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.
*
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'],
+ ];
+ }
}
}
}
* @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')]);
}
/**
* @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]);
}
/**
* @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;
+ }
+
}
<?php
+use Civi\Api4\Participant;
+
/**
* Trait for participant workflow classes.
*/
trait CRM_Event_WorkflowMessage_ParticipantTrait {
+ use CRM_Contribute_WorkflowMessage_ContributionTrait;
/**
* @var int
*
*/
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
*
*/
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];
+ }
+
}