diff --git a/CRM/Contribute/Tokens.php b/CRM/Contribute/Tokens.php index 6569c6a72ee3..7436b0b2390f 100644 --- a/CRM/Contribute/Tokens.php +++ b/CRM/Contribute/Tokens.php @@ -10,6 +10,8 @@ +--------------------------------------------------------------------+ */ +use Civi\Api4\ContributionRecur; + /** * Class CRM_Contribute_Tokens * @@ -46,4 +48,26 @@ public function getCurrencyFieldName() { return ['currency']; } + /** + * Get Related Entity tokens. + * + * @return array[] + */ + protected function getRelatedTokens(): array { + $tokens = []; + $hiddenTokens = ['modified_date', 'create_date', 'trxn_id', 'invoice_id', 'is_test', 'payment_token_id', 'payment_processor_id', 'payment_instrument_id', 'cycle_day', 'installments', 'processor_id', 'next_sched_contribution_date', 'failure_count', 'failure_retry_date', 'auto_renew', 'is_email_receipt', 'contribution_status_id']; + $contributionRecurFields = ContributionRecur::getFields(FALSE)->setLoadOptions(TRUE)->execute(); + foreach ($contributionRecurFields as $contributionRecurField) { + $tokens['contribution_recur_id.' . $contributionRecurField['name']] = [ + 'title' => $contributionRecurField['title'], + 'name' => 'contribution_recur_id.' . $contributionRecurField['name'], + 'type' => 'mapped', + 'options' => $contributionRecurField['options'] ?? NULL, + 'data_type' => $contributionRecurField['data_type'], + 'audience' => in_array($contributionRecurField['name'], $hiddenTokens) ? 'hidden' : 'user', + ]; + } + return $tokens; + } + } diff --git a/CRM/Core/EntityTokens.php b/CRM/Core/EntityTokens.php index 9d33cf13599b..061f964c8a0f 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -78,6 +78,7 @@ protected function getTokenMetadata(): array { $cacheKey = $this->getCacheKey(); if (!Civi::cache('metadata')->has($cacheKey)) { $tokensMetadata = $this->getBespokeTokens(); + $tokensMetadata = array_merge($tokensMetadata, $this->getRelatedTokens()); foreach ($this->getFieldMetadata() as $field) { $this->addFieldToTokenMetadata($tokensMetadata, $field, $this->getExposedFields()); } @@ -275,6 +276,13 @@ protected function getBespokeTokens(): array { return []; } + /** + * Get related entity tokens. + */ + protected function getRelatedTokens(): array { + return []; + } + /** * Get the value for the relevant pseudo field. * diff --git a/CRM/Member/Tokens.php b/CRM/Member/Tokens.php index 0b6916f38339..6f78449b48f9 100644 --- a/CRM/Member/Tokens.php +++ b/CRM/Member/Tokens.php @@ -10,6 +10,8 @@ +--------------------------------------------------------------------+ */ +use Civi\Api4\ContributionRecur; + /** * Class CRM_Member_Tokens * @@ -122,4 +124,24 @@ protected function getBespokeTokens(): array { ]; } + /** + * Get related tokens related to membership e.g. recurring contribution tokens + */ + protected function getRelatedTokens(): array { + $tokens = []; + $hiddenTokens = ['modified_date', 'create_date', 'trxn_id', 'invoice_id', 'is_test', 'payment_token_id', 'payment_processor_id', 'payment_instrument_id', 'cycle_day', 'installments', 'processor_id', 'next_sched_contribution_date', 'failure_count', 'failure_retry_date', 'auto_renew', 'is_email_receipt', 'contribution_status_id']; + $contributionRecurFields = ContributionRecur::getFields(FALSE)->setLoadOptions(TRUE)->execute(); + foreach ($contributionRecurFields as $contributionRecurField) { + $tokens['contribution_recur_id.' . $contributionRecurField['name']] = [ + 'title' => $contributionRecurField['title'], + 'name' => 'contribution_recur_id.' . $contributionRecurField['name'], + 'type' => 'mapped', + 'options' => $contributionRecurField['options'] ?? NULL, + 'data_type' => $contributionRecurField['data_type'], + 'audience' => in_array($contributionRecurField['name'], $hiddenTokens) ? 'hidden' : 'user', + ]; + } + return $tokens; + } + } diff --git a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php index b978f6459479..08d751b1887a 100644 --- a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php +++ b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php @@ -426,6 +426,18 @@ public function testTokenRendering(): void { 'paid_amount' => 'Amount Paid', 'balance_amount' => 'Balance', 'tax_exclusive_amount' => 'Tax Exclusive Amount', + 'contribution_recur_id.id' => 'Recurring Contribution ID', + 'contribution_recur_id.contact_id' => 'Contact ID', + 'contribution_recur_id.amount' => 'Amount', + 'contribution_recur_id.currency' => 'Currency', + 'contribution_recur_id.frequency_unit' => 'Frequency Unit', + 'contribution_recur_id.frequency_interval' => 'Interval (number of units)', + 'contribution_recur_id.start_date' => 'Start Date', + 'contribution_recur_id.cancel_date' => 'Cancel Date', + 'contribution_recur_id.cancel_reason' => 'Cancellation Reason', + 'contribution_recur_id.end_date' => 'Recurring Contribution End Date', + 'contribution_recur_id.financial_type_id' => 'Financial Type ID', + 'contribution_recur_id.campaign_id' => 'Campaign ID', ], $comparison); } diff --git a/tests/phpunit/CRM/Utils/TokenConsistencyTest.php b/tests/phpunit/CRM/Utils/TokenConsistencyTest.php index c87775d79df9..b6514403dc3d 100644 --- a/tests/phpunit/CRM/Utils/TokenConsistencyTest.php +++ b/tests/phpunit/CRM/Utils/TokenConsistencyTest.php @@ -520,7 +520,7 @@ public function testMembershipTokenConsistency(): void { $tokens = $tokenProcessor->listTokens(); // Add in custom tokens as token processor supports these. $expectedTokens = array_merge($expectedTokens, $this->getTokensAdvertisedByTokenProcessorButNotLegacy()); - $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokens); + $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens(), $this->getRecurEntityTokens('membership')), $tokens); $tokenProcessor->addMessage('html', $tokenString, 'text/plain'); $tokenProcessor->addRow(['membershipId' => $this->getMembershipID()]); $tokenProcessor->evaluate(); @@ -957,6 +957,27 @@ protected function getEventTokens(): array { ]; } + /** + * @param string $entity + * + * @return string[] + */ + protected function getRecurEntityTokens($entity): array { + return [ + '{' . $entity . '.contribution_recur_id.id}' => 'Recurring Contribution ID', + '{' . $entity . '.contribution_recur_id.contact_id}' => 'Contact ID', + '{' . $entity . '.contribution_recur_id.amount}' => 'Amount', + '{' . $entity . '.contribution_recur_id.currency}' => 'Currency', + '{' . $entity . '.contribution_recur_id.frequency_unit}' => 'Frequency Unit', + '{' . $entity . '.contribution_recur_id.frequency_interval}' => 'Interval (number of units)', + '{' . $entity . '.contribution_recur_id.start_date}' => 'Start Date', + '{' . $entity . '.contribution_recur_id.cancel_date}' => 'Cancel Date', + '{' . $entity . '.contribution_recur_id.cancel_reason}' => 'Cancellation Reason', + '{' . $entity . '.contribution_recur_id.end_date}' => 'Recurring Contribution End Date', + '{' . $entity . '.contribution_recur_id.financial_type_id}' => 'Financial Type ID', + ]; + } + /** * @param array $tokens * diff --git a/tests/phpunit/Civi/Token/TokenProcessorTest.php b/tests/phpunit/Civi/Token/TokenProcessorTest.php index f6a4317f4f6f..07c93dc70119 100644 --- a/tests/phpunit/Civi/Token/TokenProcessorTest.php +++ b/tests/phpunit/Civi/Token/TokenProcessorTest.php @@ -311,6 +311,84 @@ protected function renderUrlMessage(int $contactID): TokenRow { return $tokenProcessor->evaluate()->getRow(0); } + /** + * Check that we can render contribution and contribution_recur tokens when passing a contribution ID. + * This checks Bestspoke tokens + * + * @return void + * @throws \API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + public function testRenderContributionRecurTokenFromContribution(): void { + $cid = $this->individualCreate(); + $crid = \Civi\Api4\ContributionRecur::create(FALSE) + ->addValue('contact_id', $cid) + ->addValue('amount', 5) + ->execute() + ->first()['id']; + $coid = $this->contributionCreate(['contact_id' => $cid, 'contribution_recur_id' => $crid]); + + $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [ + 'controller' => __CLASS__, + 'schema' => ['contactId', 'contributionId'], + 'smarty' => FALSE, + ]); + $tokenProcessor->addMessage('text', '!!{contribution.id}{contribution.contribution_recur_id.id}{contribution.contribution_recur_id.amount}!!', 'text/plain'); + $tokenProcessor->addRow()->context(['contactId' => $cid, 'contributionId' => $coid]); + + $expectText = [ + "!!{$coid}{$crid}$5.00!!", + ]; + + $rowCount = 0; + foreach ($tokenProcessor->evaluate()->getRows() as $key => $row) { + /** @var TokenRow */ + $this->assertTrue($row instanceof TokenRow); + $this->assertEquals($expectText[$key], $row->render('text')); + $rowCount++; + } + $this->assertEquals(1, $rowCount); + } + + /** + * Check that we can render membership and contribution_recur tokens when passing a membership ID. + * This checks Bestspoke Tokens work correctly + * + * @return void + * @throws \API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + public function testRenderContributionRecurTokenFromMembership(): void { + $cid = $this->individualCreate(); + $crid = \Civi\Api4\ContributionRecur::create(FALSE) + ->addValue('contact_id', $cid) + ->addValue('amount', 5) + ->execute() + ->first()['id']; + $mid = $this->contactMembershipCreate(['contribution_recur_id' => $crid, 'contact_id' => $cid]); + + $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [ + 'controller' => __CLASS__, + 'schema' => ['contactId', 'membershipId'], + 'smarty' => FALSE, + ]); + $tokenProcessor->addMessage('text', '!!{membership.id}{membership.contribution_recur_id.id}{membership.contribution_recur_id.amount}!!', 'text/plain'); + $tokenProcessor->addRow()->context(['contactId' => $cid, 'membershipId' => $mid]); + + $expectText = [ + "!!{$mid}{$crid}$5.00!!", + ]; + + $rowCount = 0; + foreach ($tokenProcessor->evaluate()->getRows() as $key => $row) { + /** @var TokenRow */ + $this->assertTrue($row instanceof TokenRow); + $this->assertEquals($expectText[$key], $row->render('text')); + $rowCount++; + } + $this->assertEquals(1, $rowCount); + } + public function testGetMessageTokens(): void { $tokenProcessor = $this->getTokenProcessor(); $tokenProcessor->addMessage('greeting_html', 'Good morning,
{contact.display_name}
. {custom.foobar}!', 'text/html');