diff --git a/CRM/Contribute/BAO/Contribution.php b/CRM/Contribute/BAO/Contribution.php index c57b02e185ab..12536ca9013a 100644 --- a/CRM/Contribute/BAO/Contribution.php +++ b/CRM/Contribute/BAO/Contribution.php @@ -5191,11 +5191,17 @@ public static function getContributionTokenValues($id, $messageToken) { } $result = civicrm_api3('Contribution', 'get', ['id' => $id]); // lab.c.o mail#46 - show labels, not values, for custom fields with option values. + foreach ($result['values'][$id] as $fieldName => $fieldValue) { + if (strpos($fieldName, 'custom_') === 0 && array_search($fieldName, $messageToken['contribution']) !== FALSE) { + $result['values'][$id][$fieldName] = CRM_Core_BAO_CustomField::displayValue($result['values'][$id][$fieldName], $fieldName); + } + } if (!empty($messageToken['contribution'])) { - foreach ($result['values'][$id] as $fieldName => $fieldValue) { - if (strpos($fieldName, 'custom_') === 0 && array_search($fieldName, $messageToken['contribution']) !== FALSE) { - $result['values'][$id][$fieldName] = CRM_Core_BAO_CustomField::displayValue($result['values'][$id][$fieldName], $fieldName); - } + $processor = new CRM_Contribute_Tokens(); + $pseudoFields = array_keys($processor->getPseudoTokens()); + foreach ($pseudoFields as $pseudoField) { + $split = explode(':', $pseudoField); + $result['values'][$id][$pseudoField] = $processor->getPseudoValue($split[0], $split[1], $result['values'][$id][$split[0]]); } } return $result; diff --git a/CRM/Contribute/Tokens.php b/CRM/Contribute/Tokens.php index a8ddf76aafcb..6ff9a0c63731 100644 --- a/CRM/Contribute/Tokens.php +++ b/CRM/Contribute/Tokens.php @@ -25,6 +25,27 @@ */ class CRM_Contribute_Tokens extends AbstractTokenSubscriber { + /** + * @return string + */ + private function getEntityName(): string { + return 'contribution'; + } + + /** + * Get the relevant bao name. + */ + public function getBAOName(): string { + return CRM_Core_DAO_AllCoreTables::getFullName(ucfirst($this->getEntityName())); + } + + /** + * Metadata about the entity fields. + * + * @var array + */ + protected $entityFieldMetadata = []; + /** * Get a list of tokens whose name and title match the DB fields. * @return array @@ -78,12 +99,31 @@ protected function getBasicTokens(): array { return ['contribution_status_id' => ts('Contribution Status ID')]; } + /** + * Get pseudoTokens - it tokens that reflect the name or label of a pseudoconstant. + * + * @internal - this function will likely be made protected soon. + * + * @return array + */ + public function getPseudoTokens(): array { + $return = []; + foreach (array_keys($this->getBasicTokens()) as $fieldName) { + if (!empty($this->entityFieldMetadata[$fieldName]['pseudoconstant'])) { + $return[$fieldName . ':label'] = $this->entityFieldMetadata[$fieldName]['html']['label']; + $return[$fieldName . ':name'] = ts('Machine name') . ': ' . $this->entityFieldMetadata[$fieldName]['html']['label']; + } + } + return $return; + } + /** * Class constructor. */ public function __construct() { + $this->entityFieldMetadata = CRM_Contribute_DAO_Contribution::fields(); $tokens = CRM_Utils_Array::subset( - CRM_Utils_Array::collect('title', CRM_Contribute_DAO_Contribution::fields()), + CRM_Utils_Array::collect('title', $this->entityFieldMetadata), $this->getPassthruTokens() ); $tokens['id'] = ts('Contribution ID'); @@ -94,7 +134,7 @@ public function __construct() { // {contribution.contribution_status_id:label} $tokens['status'] = ts('Contribution Status'); $tokens['type'] = ts('Financial Type'); - $tokens = array_merge($tokens, CRM_Utils_Token::getCustomFieldTokens('Contribution')); + $tokens = array_merge($tokens, $this->getPseudoTokens(), CRM_Utils_Token::getCustomFieldTokens('Contribution')); parent::__construct('contribution', $tokens); } @@ -124,8 +164,12 @@ public function alterActionScheduleQuery(MailingQueryEvent $e): void { foreach ($this->getPassthruTokens() as $token) { $e->query->select("e." . $fields[$token]['name'] . " AS contrib_{$token}"); } + foreach (array_keys($this->getPseudoTokens()) as $token) { + $split = explode(':', $token); + $e->query->select("e." . $fields[$split[0]]['name'] . " AS contrib_{$split[0]}"); + } foreach ($this->getAliasTokens() as $alias => $orig) { - $e->query->select("e." . $fields[$orig]['name'] . " AS contrib_{$alias}"); + $e->query->select('e.' . $fields[$orig]['name'] . " AS contrib_{$alias}"); } } @@ -147,6 +191,10 @@ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) elseif ($cfID = \CRM_Core_BAO_CustomField::getKeyID($field)) { $row->customToken($entity, $cfID, $actionSearchResult->entity_id); } + elseif (array_key_exists($field, $this->getPseudoTokens())) { + $split = explode(':', $field); + $row->tokens($entity, $field, $this->getPseudoValue($split[0], $split[1], $actionSearchResult->{"contrib_$split[0]"} ?? NULL)); + } elseif (in_array($field, array_keys($this->getBasicTokens()))) { // For now we just ensure that the label fields do not override the // id field here. @@ -158,4 +206,26 @@ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) } } + /** + * Get the value for the relevant pseudo field. + * + * @param string $realField e.g contribution_status_id + * @param string $pseudoKey e.g name + * @param int|string $fieldValue e.g 1 + * + * @return string + * Eg. 'Completed' in the example above. + * + * @internal function will likely be protected soon. + */ + public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string { + if ($pseudoKey === 'name') { + $fieldValue = (string) CRM_Core_PseudoConstant::getName($this->getBAOName(), $realField, $fieldValue); + } + if ($pseudoKey === 'label') { + $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($this->getBAOName(), $realField, $fieldValue); + } + return (string) $fieldValue; + } + } diff --git a/CRM/Core/SelectValues.php b/CRM/Core/SelectValues.php index 327efe906908..361ed4cd6213 100644 --- a/CRM/Core/SelectValues.php +++ b/CRM/Core/SelectValues.php @@ -582,7 +582,9 @@ public static function contributionTokens() { '{contribution.amount_level}' => ts('Amount Level'), //'{contribution.contribution_recur_id}' => ts('Contribution Recurring ID'), //'{contribution.honor_contact_id}' => ts('Honor Contact ID'), - '{contribution.contribution_status_id}' => ts('Contribution Status'), + '{contribution.contribution_status_id}' => ts('Contribution Status ID'), + '{contribution.contribution_status_id:label}' => ts('Contribution Status'), + '{contribution.contribution_status_id:name}' => ts('Machine name') . ': ' . ts('Contribution Status'), //'{contribution.honor_type_id}' => ts('Honor Type ID'), //'{contribution.address_id}' => ts('Address ID'), '{contribution.check_number}' => ts('Check Number'), diff --git a/CRM/Utils/Token.php b/CRM/Utils/Token.php index ce10fce61cb5..8d065c4021f1 100644 --- a/CRM/Utils/Token.php +++ b/CRM/Utils/Token.php @@ -191,7 +191,7 @@ public static function token_replace($type, $var, $value, &$str, $escapeSmarty = * regular expression sutiable for using in preg_replace */ private static function tokenRegex($token_type) { - return '/(? [], 'financial_type' => [], 'payment_instrument' => []], - self::getCustomFieldTokens('Contribution') + self::getCustomFieldTokens('Contribution'), + $processor->getPseudoTokens() ); foreach ($tokens as $token) { if (!empty($token['name'])) { diff --git a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php index 0fe9af14f5d3..a2d1bc7c08dd 100644 --- a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php +++ b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php @@ -151,7 +151,7 @@ public function createTestCases() { * Create a contribution record for Alice with type "Member Dues". */ public function addAliceDues() { - $this->callAPISuccess('Contribution', 'create', [ + $this->ids['Contribution']['alice'] = $this->callAPISuccess('Contribution', 'create', [ 'contact_id' => $this->contacts['alice']['id'], 'receive_date' => date('Ymd', strtotime($this->targetDate)), 'total_amount' => '100', @@ -168,7 +168,7 @@ public function addAliceDues() { 'soft_credit_type_id' => 3, ], ], - ]); + ])['id']; } /** @@ -237,7 +237,19 @@ public function useHelloFirstNameStatus() { $this->schedule->body_text = 'Hello, {contact.first_name}. @{contribution.status} (via body_text)'; } - public function testTokenRendering() { + /** + * Test that reconciled tokens are rendered the same via multiple code paths. + * + * We expect that the list of tokens from the processor class === the selectValues function. + * - once this is verified to be true selectValues can call the processor function internally. + * + * We also expect that rendering action action schedules will do the same as the + * legacy processor function. Once this is true we can expose the listener on the + * token processor for contribution and call it internally from the legacy code. + * + * @throws \CiviCRM_API3_Exception + */ + public function testTokenRendering(): void { $this->targetDate = '20150201000107'; \CRM_Utils_Time::setTime('2015-02-01 00:00:00'); $this->addAliceDues(); @@ -248,15 +260,43 @@ public function testTokenRendering() { first name = {contact.first_name} receive_date = {contribution.receive_date} contribution status id = {contribution.contribution_status_id} - legacy style status = {contribution.status}'; + legacy style status = {contribution.status} + new style status = {contribution.contribution_status_id:name}'; $this->schedule->save(); $this->callAPISuccess('job', 'send_reminder', []); - $this->mut->checkMailLog([ + $expected = [ 'first name = Alice', 'receive_date = February 1st, 2015 12:00 AM', 'contribution status id = 1', + 'new style status = Completed', 'legacy style status = Completed', - ]); + ]; + $this->mut->checkMailLog($expected); + + $messageToken = CRM_Utils_Token::getTokens($this->schedule->body_text); + + $contributionDetails = CRM_Contribute_BAO_Contribution::replaceContributionTokens( + [$this->ids['Contribution']['alice']], + $this->schedule->body_text, + $messageToken, + $this->schedule->body_text, + $this->schedule->body_text, + $messageToken, + TRUE + ); + $expected = [ + 'receive_date = February 1st, 2015 12:00 AM', + 'new style status = Completed', + 'contribution status id = 1', + ]; + foreach ($expected as $string) { + $this->assertStringContainsString($string, $contributionDetails[$this->contacts['alice']['id']]['html']); + } + $tokens = ['contribution_status_id', 'contribution_status_id:name', 'contribution_status_id:label']; + $processor = new CRM_Contribute_Tokens(); + foreach ($tokens as $token) { + $this->assertEquals(CRM_Core_SelectValues::contributionTokens()['{contribution.' . $token . '}'], $processor->tokenNames[$token]); + } } } diff --git a/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php b/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php index 17686476ac24..292d7df34e45 100644 --- a/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php +++ b/tests/phpunit/CRM/Contribute/BAO/ContributionTest.php @@ -1304,9 +1304,12 @@ public function testProcessOnBehalfOrganization() { /** * Test for replaceContributionTokens. - * This function tests whether the contribution tokens are replaced with values from contribution. + * This function tests whether the contribution tokens are replaced with + * values from contribution. + * + * @throws \CiviCRM_API3_Exception */ - public function testReplaceContributionTokens() { + public function testReplaceContributionTokens(): void { $customGroup = $this->customGroupCreate(['extends' => 'Contribution', 'title' => 'contribution stuff']); $customField = $this->customFieldOptionValueCreate($customGroup, 'myCustomField'); $contactId1 = $this->individualCreate(); @@ -1339,11 +1342,12 @@ public function testReplaceContributionTokens() { $ids = [$contribution1, $contribution2]; $subject = "This is a test for contribution ID: {contribution.contribution_id}"; - $text = "Contribution Amount: {contribution.total_amount}"; + $text = 'Contribution Amount: {contribution.total_amount}'; $html = "

Contribution Source: {contribution.contribution_source}


Contribution Invoice ID: {contribution.invoice_id}


Contribution Receive Date: {contribution.receive_date}


-

Contribution Custom Field: {contribution.custom_{$customField['id']}}


"; +

Contribution Custom Field: {contribution.custom_{$customField['id']}}


+ {contribution.contribution_status_id:name}"; $subjectToken = CRM_Utils_Token::getTokens($subject); $messageToken = CRM_Utils_Token::getTokens($text); @@ -1359,11 +1363,12 @@ public function testReplaceContributionTokens() { TRUE ); - $this->assertEquals("Contribution Amount: € 100.00", $contributionDetails[$contactId1]['text'], "The text does not match"); - $this->assertEquals("

Contribution Source: ABC


+ $this->assertEquals('Contribution Amount: € 100.00', $contributionDetails[$contactId1]['text'], "The text does not match"); + $this->assertEquals('

Contribution Source: ABC


Contribution Invoice ID: 12345


Contribution Receive Date: May 11th, 2015 12:00 AM


-

Contribution Custom Field: Label2


", $contributionDetails[$contactId2]['html'], "The html does not match"); +

Contribution Custom Field: Label2


+ Completed', $contributionDetails[$contactId2]['html'], 'The html does not match'); } /**