Merge pull request #21706 from eileenmcnaughton/token_metadata
[civicrm-core.git] / tests / phpunit / CRM / Utils / TokenConsistencyTest.php
CommitLineData
d0ce76fd
EM
1<?php
2/*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
0f4031da 12use Civi\Token\TokenProcessor;
f399fbd8
EM
13use Civi\Api4\LocBlock;
14use Civi\Api4\Email;
15use Civi\Api4\Phone;
16use Civi\Api4\Address;
0f4031da 17
d0ce76fd
EM
18/**
19 * CRM_Utils_TokenConsistencyTest
20 *
21 * Class for ensuring tokens have internal consistency.
22 *
23 * @group Tokens
24 *
25 * @group headless
26 */
27class CRM_Utils_TokenConsistencyTest extends CiviUnitTestCase {
28
29 use CRMTraits_Custom_CustomDataTrait;
30
31 /**
32 * Created case.
33 *
34 * @var array
35 */
36 protected $case;
37
0f4031da
EM
38 /**
39 * Recurring contribution.
40 *
41 * @var array
42 */
43 protected $contributionRecur;
44
d0ce76fd
EM
45 /**
46 * Post test cleanup.
d0ce76fd
EM
47 */
48 public function tearDown(): void {
e80f2261 49 $this->quickCleanup(['civicrm_case', 'civicrm_case_type', 'civicrm_participant', 'civicrm_event'], TRUE);
d0ce76fd
EM
50 parent::tearDown();
51 }
52
53 /**
54 * Test that case tokens are consistently rendered.
55 *
d0ce76fd
EM
56 * @throws \CiviCRM_API3_Exception
57 */
58 public function testCaseTokenConsistency(): void {
59 $this->createLoggedInUser();
60 CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
61 $this->createCustomGroupWithFieldOfType(['extends' => 'Case']);
62 $tokens = CRM_Core_SelectValues::caseTokens();
63 $this->assertEquals($this->getCaseTokens(), $tokens);
64 $caseID = $this->getCaseID();
044c0ad1 65 $tokenString = $this->getTokenString(array_keys($this->getCaseTokens()));
78ffc4d7 66 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString, ['case' => $this->getCaseTokenKeys()]);
d0ce76fd
EM
67 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
68 // Now do the same without passing in 'knownTokens'
78ffc4d7 69 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, $tokenString);
d0ce76fd 70 $this->assertEquals($this->getExpectedCaseTokenOutput(), $tokenHtml);
2f5c0408
EM
71
72 // And check our deprecated tokens still work.
73 $tokenHtml = CRM_Utils_Token::replaceCaseTokens($caseID, '{case.case_type_id} {case.status_id}');
74 $this->assertEquals('Housing Support Ongoing', $tokenHtml);
78ffc4d7
EM
75 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
76 'controller' => __CLASS__,
77 'smarty' => FALSE,
78 'schema' => ['caseId'],
79 ]);
889b0617 80 $this->assertEquals(array_merge($this->getCaseTokens(), $this->getDomainTokens()), $tokenProcessor->listTokens());
78ffc4d7
EM
81 $tokenProcessor->addRow([
82 'caseId' => $this->getCaseID(),
83 ]);
84 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
85
86 $tokenProcessor->evaluate();
87 foreach ($tokenProcessor->getRows() as $row) {
88 $text = $row->render('html');
89 }
90 $this->assertEquals($this->getExpectedCaseTokenOutput(), $text);
d0ce76fd
EM
91 }
92
93 /**
94 * Get expected output from token parsing.
95 *
96 * @return string
97 */
98 protected function getExpectedCaseTokenOutput(): string {
044c0ad1
EM
99 return 'case.id :1
100case.case_type_id:label :Housing Support
101case.subject :Case Subject
102case.start_date :July 23rd, 2021
103case.end_date :July 26th, 2021
104case.details :case details
105case.status_id:label :Ongoing
106case.is_deleted:label :No
107case.created_date :' . CRM_Utils_Date::customFormat($this->case['created_date']) . '
108case.modified_date :' . CRM_Utils_Date::customFormat($this->case['modified_date']) . '
109case.custom_1 :' . '
d0ce76fd
EM
110';
111 }
112
113 /**
114 * @return int
115 */
116 protected function getContactID(): int {
117 if (!isset($this->ids['Contact'][0])) {
118 $this->ids['Contact'][0] = $this->individualCreate();
119 }
120 return $this->ids['Contact'][0];
121 }
122
123 /**
124 * Get the keys for the case tokens.
125 *
126 * @return array
127 */
128 public function getCaseTokenKeys(): array {
129 $return = [];
130 foreach (array_keys($this->getCaseTokens()) as $key) {
131 $return[] = substr($key, 6, -1);
132 }
133 return $return;
134 }
135
136 /**
137 * Get declared tokens.
138 *
139 * @return string[]
140 */
141 public function getCaseTokens(): array {
142 return [
143 '{case.id}' => 'Case ID',
2f5c0408 144 '{case.case_type_id:label}' => 'Case Type',
d0ce76fd
EM
145 '{case.subject}' => 'Case Subject',
146 '{case.start_date}' => 'Case Start Date',
147 '{case.end_date}' => 'Case End Date',
148 '{case.details}' => 'Details',
2f5c0408 149 '{case.status_id:label}' => 'Case Status',
78ffc4d7 150 '{case.is_deleted:label}' => 'Case is in the Trash',
d0ce76fd
EM
151 '{case.created_date}' => 'Created Date',
152 '{case.modified_date}' => 'Modified Date',
153 '{case.custom_1}' => 'Enter text here :: Group with field text',
154 ];
155 }
156
157 /**
158 * Get case ID.
159 *
160 * @return int
161 */
162 protected function getCaseID(): int {
163 if (!isset($this->case)) {
a0fb3068 164 $case_id = $this->callAPISuccess('Case', 'create', [
d0ce76fd
EM
165 'case_type_id' => 'housing_support',
166 'activity_subject' => 'Case Subject',
167 'client_id' => $this->getContactID(),
168 'status_id' => 1,
169 'subject' => 'Case Subject',
170 'start_date' => '2021-07-23 15:39:20',
a0fb3068 171 // Note end_date is inconsistent with status Ongoing but for the
172 // purposes of testing tokens is ok. Creating it with status Resolved
173 // then ignores our known fixed end date.
d0ce76fd
EM
174 'end_date' => '2021-07-26 18:07:20',
175 'medium_id' => 2,
176 'details' => 'case details',
177 'activity_details' => 'blah blah',
178 'sequential' => 1,
a0fb3068 179 ])['id'];
180 // Need to retrieve the case again because modified date might be updated a
181 // split-second later than the original return value because of activity
182 // triggers when the timeline is populated. The returned array from create
183 // is determined before that happens.
184 $this->case = $this->callAPISuccess('Case', 'getsingle', ['id' => $case_id]);
d0ce76fd
EM
185 }
186 return $this->case['id'];
187 }
188
0f4031da
EM
189 /**
190 * Test that contribution recur tokens are consistently rendered.
191 */
192 public function testContributionRecurTokenConsistency(): void {
193 $this->createLoggedInUser();
194 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
195 'controller' => __CLASS__,
196 'smarty' => FALSE,
197 'schema' => ['contribution_recurId'],
198 ]);
889b0617
EM
199 $expectedTokens = array_merge($this->getContributionRecurTokens(), $this->getDomainTokens());
200 $this->assertEquals(array_diff_key($expectedTokens, $this->getUnadvertisedTokens()), $tokenProcessor->listTokens());
044c0ad1 201 $tokenString = $this->getTokenString(array_keys($this->getContributionRecurTokens()));
0f4031da
EM
202
203 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
204 $tokenProcessor->addRow(['contribution_recurId' => $this->getContributionRecurID()]);
205 $tokenProcessor->evaluate();
206 $this->assertEquals($this->getExpectedContributionRecurTokenOutPut(), $tokenProcessor->getRow(0)->render('html'));
207 }
208
e9841a51
EM
209 /**
210 * Get tokens that are not advertised via listTokens.
211 *
212 * @return string[]
213 */
214 public function getUnadvertisedTokens(): array {
215 return [
216 '{membership.status_id}' => 'Status ID',
217 '{membership.membership_type_id}' => 'Membership Type ID',
218 '{membership.status_id:name}' => 'Machine name: Status',
219 '{membership.membership_type_id:name}' => 'Machine name: Membership Type',
889b0617
EM
220 '{contribution_recur.frequency_unit}' => 'Frequency Unit',
221 '{contribution_recur.contribution_status_id}' => 'Status',
222 '{contribution_recur.payment_processor_id}' => 'Payment Processor ID',
223 '{contribution_recur.financial_type_id}' => 'Financial Type ID',
224 '{contribution_recur.payment_instrument_id}' => 'Payment Method',
225 '{contribution_recur.frequency_unit:name}' => 'Machine name: Frequency Unit',
226 '{contribution_recur.payment_instrument_id:name}' => 'Machine name: Payment Method',
227 '{contribution_recur.contribution_status_id:name}' => 'Machine name: Status',
228 '{contribution_recur.payment_processor_id:name}' => 'Machine name: Payment Processor',
229 '{contribution_recur.financial_type_id:name}' => 'Machine name: Financial Type',
230 '{participant.status_id:name}' => 'Machine name: Status',
231 '{participant.role_id:name}' => 'Machine name: Participant Role',
232 '{participant.status_id}' => 'Status ID',
233 '{participant.role_id}' => 'Participant Role ID',
e9841a51
EM
234 ];
235 }
236
0f4031da
EM
237 /**
238 * Get the contribution recur tokens keyed by the token.
239 *
240 * e.g {contribution_recur.id}
241 *
242 * @return array
243 */
244 protected function getContributionRecurTokens(): array {
245 $return = [];
246 foreach ($this->getContributionRecurTokensByField() as $key => $value) {
247 $return['{contribution_recur.' . $key . '}'] = $value;
248 }
249 return $return;
250 }
251
252 protected function getContributionRecurTokensByField(): array {
253 return [
254 'id' => 'Recurring Contribution ID',
255 'amount' => 'Amount',
256 'currency' => 'Currency',
257 'frequency_unit' => 'Frequency Unit',
258 'frequency_interval' => 'Interval (number of units)',
259 'installments' => 'Number of Installments',
260 'start_date' => 'Start Date',
261 'create_date' => 'Created Date',
262 'modified_date' => 'Modified Date',
263 'cancel_date' => 'Cancel Date',
264 'cancel_reason' => 'Cancellation Reason',
265 'end_date' => 'Recurring Contribution End Date',
266 'processor_id' => 'Processor ID',
267 'payment_token_id' => 'Payment Token ID',
268 'trxn_id' => 'Transaction ID',
269 'invoice_id' => 'Invoice ID',
270 'contribution_status_id' => 'Status',
97ca72e4 271 'is_test:label' => 'Test',
0f4031da
EM
272 'cycle_day' => 'Cycle Day',
273 'next_sched_contribution_date' => 'Next Scheduled Contribution Date',
274 'failure_count' => 'Number of Failures',
275 'failure_retry_date' => 'Retry Failed Attempt Date',
97ca72e4 276 'auto_renew:label' => 'Auto Renew',
0f4031da
EM
277 'payment_processor_id' => 'Payment Processor ID',
278 'financial_type_id' => 'Financial Type ID',
279 'payment_instrument_id' => 'Payment Method',
97ca72e4 280 'is_email_receipt:label' => 'Send email Receipt?',
0f4031da
EM
281 'frequency_unit:label' => 'Frequency Unit',
282 'frequency_unit:name' => 'Machine name: Frequency Unit',
283 'contribution_status_id:label' => 'Status',
284 'contribution_status_id:name' => 'Machine name: Status',
285 'payment_processor_id:label' => 'Payment Processor',
286 'payment_processor_id:name' => 'Machine name: Payment Processor',
287 'financial_type_id:label' => 'Financial Type',
288 'financial_type_id:name' => 'Machine name: Financial Type',
289 'payment_instrument_id:label' => 'Payment Method',
290 'payment_instrument_id:name' => 'Machine name: Payment Method',
291 ];
292 }
293
294 /**
295 * Get contributionRecur ID.
296 *
297 * @return int
298 */
299 protected function getContributionRecurID(): int {
300 if (!isset($this->contributionRecur)) {
301 $paymentProcessorID = $this->processorCreate();
302 $this->contributionRecur = $this->callAPISuccess('ContributionRecur', 'create', [
303 'contact_id' => $this->getContactID(),
304 'status_id' => 1,
305 'is_email_receipt' => 1,
306 'start_date' => '2021-07-23 15:39:20',
307 'end_date' => '2021-07-26 18:07:20',
308 'cancel_date' => '2021-08-19 09:12:45',
34795e7a 309 'next_sched_contribution_date' => '2021-09-08',
0f4031da
EM
310 'cancel_reason' => 'Because',
311 'amount' => 5990.99,
312 'currency' => 'EUR',
313 'frequency_unit' => 'year',
314 'frequency_interval' => 2,
315 'installments' => 24,
316 'payment_instrument_id' => 'Check',
317 'financial_type_id' => 'Member dues',
318 'processor_id' => 'abc',
319 'payment_processor_id' => $paymentProcessorID,
320 'trxn_id' => 123,
321 'invoice_id' => 'inv123',
322 'sequential' => 1,
323 'failure_retry_date' => '2020-01-03',
324 'auto_renew' => 1,
325 'cycle_day' => '15',
326 'is_test' => TRUE,
327 'payment_token_id' => $this->callAPISuccess('PaymentToken', 'create', [
328 'contact_id' => $this->getContactID(),
329 'token' => 456,
330 'payment_processor_id' => $paymentProcessorID,
331 ])['id'],
332 ])['values'][0];
333 }
334 return $this->contributionRecur['id'];
335 }
336
337 /**
338 * Get rendered output for contribution tokens.
339 *
340 * @return string
341 */
342 protected function getExpectedContributionRecurTokenOutPut(): string {
044c0ad1
EM
343 return 'contribution_recur.id :' . $this->getContributionRecurID() . '
344contribution_recur.amount :€ 5,990.99
345contribution_recur.currency :EUR
346contribution_recur.frequency_unit :year
347contribution_recur.frequency_interval :2
348contribution_recur.installments :24
349contribution_recur.start_date :July 23rd, 2021 3:39 PM
350contribution_recur.create_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['create_date']) . '
351contribution_recur.modified_date :' . CRM_Utils_Date::customFormat($this->contributionRecur['modified_date']) . '
352contribution_recur.cancel_date :August 19th, 2021 9:12 AM
353contribution_recur.cancel_reason :Because
354contribution_recur.end_date :July 26th, 2021 6:07 PM
355contribution_recur.processor_id :abc
356contribution_recur.payment_token_id :1
357contribution_recur.trxn_id :123
358contribution_recur.invoice_id :inv123
359contribution_recur.contribution_status_id :2
360contribution_recur.is_test:label :Yes
361contribution_recur.cycle_day :15
362contribution_recur.next_sched_contribution_date :September 8th, 2021
363contribution_recur.failure_count :0
364contribution_recur.failure_retry_date :January 3rd, 2020
365contribution_recur.auto_renew:label :Yes
366contribution_recur.payment_processor_id :1
367contribution_recur.financial_type_id :2
368contribution_recur.payment_instrument_id :4
369contribution_recur.is_email_receipt:label :Yes
370contribution_recur.frequency_unit:label :year
371contribution_recur.frequency_unit:name :year
372contribution_recur.contribution_status_id:label :Pending Label**
373contribution_recur.contribution_status_id:name :Pending
374contribution_recur.payment_processor_id:label :Dummy (test)
375contribution_recur.payment_processor_id:name :Dummy (test)
376contribution_recur.financial_type_id:label :Member Dues
377contribution_recur.financial_type_id:name :Member Dues
378contribution_recur.payment_instrument_id:label :Check
379contribution_recur.payment_instrument_id:name :Check
380';
381
0f4031da
EM
382 }
383
d41a5d53
EM
384 /**
385 * Test that membership tokens are consistently rendered.
386 *
387 * @throws \API_Exception
388 */
389 public function testMembershipTokenConsistency(): void {
390 $this->createLoggedInUser();
391 $this->restoreMembershipTypes();
392 $this->createCustomGroupWithFieldOfType(['extends' => 'Membership']);
393 $tokens = CRM_Core_SelectValues::membershipTokens();
d568dbe0
EM
394 $expectedTokens = $this->getMembershipTokens();
395 $this->assertEquals($expectedTokens, $tokens);
d41a5d53
EM
396 $newStyleTokens = "\n{membership.status_id:label}\n{membership.membership_type_id:label}\n";
397 $tokenString = $newStyleTokens . implode("\n", array_keys($this->getMembershipTokens()));
b024d6a1 398
d41a5d53
EM
399 $memberships = CRM_Utils_Token::getMembershipTokenDetails([$this->getMembershipID()]);
400 $messageToken = CRM_Utils_Token::getTokens($tokenString);
401 $tokenHtml = CRM_Utils_Token::replaceEntityTokens('membership', $memberships[$this->getMembershipID()], $tokenString, $messageToken);
402 $this->assertEquals($this->getExpectedMembershipTokenOutput(), $tokenHtml);
dd2f879a 403
b024d6a1
EM
404 // Custom fields work in the processor so test it....
405 $tokenString .= "\n{membership." . $this->getCustomFieldName('text') . '}';
dd2f879a
EM
406 // Now compare with scheduled reminder
407 $mut = new CiviMailUtils($this);
408 CRM_Utils_Time::setTime('2007-01-22 15:00:00');
409 $this->callAPISuccess('action_schedule', 'create', [
410 'title' => 'job',
411 'subject' => 'job',
412 'entity_value' => 1,
413 'mapping_id' => 4,
414 'start_action_date' => 'membership_join_date',
415 'start_action_offset' => 1,
416 'start_action_condition' => 'after',
417 'start_action_unit' => 'day',
418 'body_html' => $tokenString,
419 ]);
420 $this->callAPISuccess('job', 'send_reminder', []);
d568dbe0
EM
421 $expected = $this->getExpectedMembershipTokenOutput();
422 // Unlike the legacy method custom fields are resolved by the processor.
423 $expected .= "\nmy field";
424 $mut->checkMailLog([$expected]);
425
426 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
427 'controller' => __CLASS__,
428 'smarty' => FALSE,
429 'schema' => ['membershipId'],
430 ]);
431 $tokens = $tokenProcessor->listTokens();
432 // Add in custom tokens as token processor supports these.
889b0617 433 $expectedTokens = array_merge($expectedTokens, $this->getTokensAdvertisedByTokenProcessorButNotLegacy());
3c78698e 434 $this->assertEquals(array_merge($expectedTokens, $this->getDomainTokens()), $tokens);
d568dbe0
EM
435 $tokenProcessor->addMessage('html', $tokenString, 'text/plain');
436 $tokenProcessor->addRow(['membershipId' => $this->getMembershipID()]);
437 $tokenProcessor->evaluate();
438 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
439
d41a5d53
EM
440 }
441
889b0617
EM
442 /**
443 * Get the advertised tokens the legacy function doesn't know about.
444 *
445 * @return string[]
446 */
447 public function getTokensAdvertisedByTokenProcessorButNotLegacy(): array {
448 return [
449 '{membership.custom_1}' => 'Enter text here :: Group with field text',
450 '{membership.source}' => 'Source',
451 '{membership.status_override_end_date}' => 'Status Override End Date',
452 ];
453 }
454
d41a5d53
EM
455 /**
456 * Get declared membership tokens.
457 *
458 * @return string[]
459 */
460 public function getMembershipTokens(): array {
461 return [
462 '{membership.id}' => 'Membership ID',
044c0ad1 463 '{membership.status_id:label}' => 'Status',
eac0a5bf 464 '{membership.membership_type_id:label}' => 'Membership Type',
d41a5d53 465 '{membership.start_date}' => 'Membership Start Date',
044c0ad1
EM
466 '{membership.join_date}' => 'Member Since',
467 '{membership.end_date}' => 'Membership Expiration Date',
d41a5d53
EM
468 '{membership.fee}' => 'Membership Fee',
469 ];
470 }
471
472 /**
473 * Get case ID.
474 *
475 * @return int
476 */
477 protected function getMembershipID(): int {
478 if (!isset($this->ids['Membership'][0])) {
b024d6a1
EM
479 $this->ids['Membership'][0] = $this->contactMembershipCreate([
480 'contact_id' => $this->getContactID(),
481 $this->getCustomFieldName('text') => 'my field',
482 ]);
d41a5d53
EM
483 }
484 return $this->ids['Membership'][0];
485 }
486
f399fbd8
EM
487 /**
488 * Get expected output from token parsing.
489 *
490 * @return string
491 */
e80f2261 492 protected function getExpectedParticipantTokenOutput(): string {
044c0ad1
EM
493 return 'participant.status_id :2
494participant.role_id :1
495participant.register_date :February 19th, 2007
496participant.source :Wimbeldon
497participant.fee_level :steep
498participant.fee_amount :$ 50.00
499participant.registered_by_id :
500participant.transferred_to_contact_id :
501participant.role_id:label :Attendee
502participant.balance :
503participant.custom_2 :99999
504participant.id :2
505participant.fee_currency :USD
506participant.discount_amount :
507participant.status_id:label :Attended
508participant.status_id:name :Attended
509participant.role_id:name :Attendee
510participant.is_test:label :No
511participant.must_wait :
e80f2261
EM
512';
513 }
514
515 /**
516 * Get expected output from token parsing.
517 *
518 * @return string
519 */
520 protected function getExpectedEventTokenOutput(): string {
044c0ad1
EM
521 return 'event.id :' . $this->ids['event'][0] . '
522event.title :Annual CiviCRM meet
523event.start_date :October 21st, 2008
524event.end_date :October 23rd, 2008
525event.event_type_id:label :Conference
526event.summary :If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now
527event.contact_email :event@example.com
528event.contact_phone :456 789
529event.description :event description
530event.location :15 Walton St
f399fbd8
EM
531Emerald City, Maine 90210
532
044c0ad1
EM
533event.info_url :' . CRM_Utils_System::url('civicrm/event/info', NULL, TRUE) . '&reset=1&id=1
534event.registration_url :' . CRM_Utils_System::url('civicrm/event/register', NULL, TRUE) . '&reset=1&id=1
535event.custom_1 :my field
536';
f399fbd8
EM
537 }
538
d41a5d53
EM
539 /**
540 * Get expected output from token parsing.
541 *
542 * @return string
543 */
544 protected function getExpectedMembershipTokenOutput(): string {
545 return '
546Expired
547General
5481
549Expired
550General
551January 21st, 2007
552January 21st, 2007
553December 21st, 2007
554100.00';
555 }
556
1ed50dc6
EM
557 /**
558 * Test that membership tokens are consistently rendered.
559 *
560 * @throws \API_Exception
561 */
562 public function testParticipantTokenConsistency(): void {
563 $this->createLoggedInUser();
e80f2261
EM
564 $this->setupParticipantScheduledReminder();
565
1ed50dc6 566 $tokens = CRM_Core_SelectValues::participantTokens();
889b0617 567 $this->assertEquals(array_diff_key($this->getParticipantTokens(), $this->getUnadvertisedTokens()), $tokens);
e80f2261
EM
568
569 $mut = new CiviMailUtils($this);
570
571 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
572 'controller' => __CLASS__,
573 'smarty' => FALSE,
574 'schema' => ['participantId'],
575 ]);
576 $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
577
578 $this->callAPISuccess('job', 'send_reminder', []);
579 $expected = $this->getExpectedParticipantTokenOutput();
580 $mut->checkMailLog([$expected]);
581
044c0ad1 582 $tokenProcessor->addMessage('html', $this->getTokenString(array_keys($this->getParticipantTokens())), 'text/plain');
e80f2261
EM
583 $tokenProcessor->addRow(['participantId' => $this->ids['participant'][0]]);
584 $tokenProcessor->evaluate();
585 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
586
1ed50dc6
EM
587 }
588
2a7cae66
EM
589 /**
590 * Test that membership tokens are consistently rendered.
591 *
592 * @throws \API_Exception
593 * @throws \CRM_Core_Exception
594 */
595 public function testParticipantCustomDateToken(): void {
596 $this->createEventAndParticipant();
597 $dateFieldID = $this->createDateCustomField(['custom_group_id' => $this->ids['CustomGroup']['participant_'], 'default_value' => ''])['id'];
598 $input = '{participant.custom_' . $dateFieldID . '}';
599 $input .= '{participant.' . $this->getCustomFieldName('participant_int') . '}';
600 $tokenHtml = CRM_Core_BAO_MessageTemplate::renderTemplate([
601 'messageTemplate' => ['msg_html' => $input],
602 'tokenContext' => array_merge(['participantId' => $this->ids['participant'][0]], ['schema' => ['participantId', 'eventId']]),
603 ])['html'];
604 $this->assertEquals(99999, $tokenHtml);
605 }
606
1ed50dc6 607 /**
3c78698e 608 * Get declared participant tokens.
1ed50dc6
EM
609 *
610 * @return string[]
611 */
612 public function getParticipantTokens(): array {
613 return [
b7472bd6 614 '{participant.status_id}' => 'Status ID',
e80f2261 615 '{participant.role_id}' => 'Participant Role ID',
b7472bd6
EM
616 '{participant.register_date}' => 'Register date',
617 '{participant.source}' => 'Participant Source',
618 '{participant.fee_level}' => 'Fee level',
619 '{participant.fee_amount}' => 'Fee Amount',
620 '{participant.registered_by_id}' => 'Registered By Participant ID',
1ed50dc6 621 '{participant.transferred_to_contact_id}' => 'Transferred to Contact ID',
e80f2261
EM
622 '{participant.role_id:label}' => 'Participant Role',
623 '{participant.balance}' => 'Event Balance',
624 '{participant.' . $this->getCustomFieldName('participant_int') . '}' => 'Enter integer here :: participant_Group with field int',
625 '{participant.id}' => 'Participant ID',
626 '{participant.fee_currency}' => 'Fee Currency',
627 '{participant.discount_amount}' => 'Discount Amount',
628 '{participant.status_id:label}' => 'Status',
629 '{participant.status_id:name}' => 'Machine name: Status',
630 '{participant.role_id:name}' => 'Machine name: Participant Role',
631 '{participant.is_test:label}' => 'Test',
632 '{participant.must_wait}' => 'Must Wait on List',
1ed50dc6
EM
633 ];
634 }
635
3c78698e
EM
636 /**
637 * Test that domain tokens are consistently rendered.
638 */
639 public function testDomainTokenConsistency(): void {
640 $tokens = CRM_Core_SelectValues::domainTokens();
641 $this->assertEquals($this->getDomainTokens(), $tokens);
642 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
643 'controller' => __CLASS__,
644 'smarty' => FALSE,
645 ]);
646 $tokens['{domain.id}'] = 'Domain ID';
647 $tokens['{domain.description}'] = 'Domain Description';
dfe53edd 648 $tokens['{domain.now}'] = 'Current time/date';
3c78698e
EM
649 $this->assertEquals($tokens, $tokenProcessor->listTokens());
650 }
651
defba8ff
EM
652 /**
653 * @throws \API_Exception
654 * @throws \CRM_Core_Exception
655 */
656 public function testDomainNow(): void {
657 putenv('TIME_FUNC=frozen');
5e62af3d 658 CRM_Utils_Time::setTime('2021-09-18 23:58:00');
44dd64f0
EM
659 $modifiers = [
660 'shortdate' => '09/18/2021',
f85e1a4b 661 '%B %Y' => 'September 2021',
44dd64f0
EM
662 ];
663 foreach ($modifiers as $filter => $expected) {
664 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
665 'messageTemplate' => [
f85e1a4b 666 'msg_text' => '{domain.now|crmDate:"' . $filter . '"}',
44dd64f0
EM
667 ],
668 ])['text'];
f85e1a4b 669 $this->assertEquals($expected, $resolved);
44dd64f0 670 }
defba8ff
EM
671 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
672 'messageTemplate' => [
673 'msg_text' => '{domain.now}',
674 ],
675 ])['text'];
676 $this->assertEquals('September 18th, 2021 11:58 PM', $resolved);
f85e1a4b
TO
677
678 // This example is malformed - no quotes
679 try {
680 $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
681 'messageTemplate' => [
682 'msg_text' => '{domain.now|crmDate:shortdate}',
683 ],
684 ])['text'];
e80f2261 685 $this->fail('Expected unquoted parameter to fail');
f85e1a4b
TO
686 }
687 catch (\CRM_Core_Exception $e) {
688 $this->assertRegExp(';Malformed token param;', $e->getMessage());
689 }
defba8ff
EM
690 }
691
3c78698e
EM
692 /**
693 * Get declared participant tokens.
694 *
695 * @return string[]
696 */
697 public function getDomainTokens(): array {
698 return [
699 '{domain.name}' => ts('Domain name'),
700 '{domain.address}' => ts('Domain (organization) address'),
701 '{domain.phone}' => ts('Domain (organization) phone'),
702 '{domain.email}' => 'Domain (organization) email',
703 '{domain.id}' => ts('Domain ID'),
704 '{domain.description}' => ts('Domain Description'),
defba8ff 705 '{domain.now}' => 'Current time/date',
3c78698e
EM
706 ];
707 }
708
ce971869 709 /**
e80f2261 710 * Test that event tokens are consistently rendered.
34795e7a
EM
711 *
712 * @throws \API_Exception
ce971869
EM
713 */
714 public function testEventTokenConsistency(): void {
f399fbd8
EM
715 $mut = new CiviMailUtils($this);
716 $this->setupParticipantScheduledReminder();
717
ce971869 718 $tokens = CRM_Core_SelectValues::eventTokens();
889b0617 719 $this->assertEquals(array_merge($this->getEventTokens()), $tokens);
ce971869
EM
720 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
721 'controller' => __CLASS__,
722 'smarty' => FALSE,
723 'schema' => ['eventId'],
724 ]);
725 $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
f399fbd8 726
044c0ad1 727 $expectedEventString = $this->getExpectedEventTokenOutput();
e80f2261 728 $this->callAPISuccess('job', 'send_reminder', []);
044c0ad1
EM
729 $expectedParticipantString = $this->getExpectedParticipantTokenOutput();
730 $toCheck = array_merge(explode("\n", $expectedEventString), explode("\n", $expectedParticipantString));
731 $toCheck[] = $expectedEventString;
732 $toCheck[] = $expectedParticipantString;
733 $mut->checkMailLog($toCheck);
734 $tokens = array_keys($this->getEventTokens());
735 $html = $this->getTokenString($tokens);
736 $tokenProcessor->addMessage('html', $html, 'text/plain');
e80f2261
EM
737 $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
738 $tokenProcessor->evaluate();
044c0ad1 739 $this->assertEquals($expectedEventString, $tokenProcessor->getRow(0)->render('html'));
e80f2261
EM
740 }
741
742 /**
743 * Test that event tokens work absent participant tokens.
744 *
745 * @throws \API_Exception
746 */
747 public function testEventTokenConsistencyNoParticipantTokens(): void {
748 $mut = new CiviMailUtils($this);
749 $this->setupParticipantScheduledReminder(FALSE);
750
f399fbd8
EM
751 $this->callAPISuccess('job', 'send_reminder', []);
752 $expected = $this->getExpectedEventTokenOutput();
044c0ad1
EM
753 // Checking these individually is easier to decipher discrepancies
754 // but we also want to check in entirety.
755 $toCheck = explode("\n", $expected);
756 $toCheck[] = $expected;
757 $mut->checkMailLog($toCheck);
e80f2261
EM
758
759 $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
760 'controller' => __CLASS__,
761 'smarty' => FALSE,
762 'schema' => ['eventId'],
763 ]);
044c0ad1 764 $html = $this->getTokenString(array_keys($this->getEventTokens()));
e80f2261 765
044c0ad1 766 $tokenProcessor->addMessage('html', $html, 'text/plain');
e80f2261
EM
767 $tokenProcessor->addRow(['eventId' => $this->ids['event'][0]]);
768 $tokenProcessor->evaluate();
769 $this->assertEquals($expected, $tokenProcessor->getRow(0)->render('html'));
770
f399fbd8
EM
771 }
772
773 /**
774 * Set up scheduled reminder for participants.
775 *
776 * @throws \API_Exception
777 */
e80f2261 778 public function setupParticipantScheduledReminder($includeParticipant = TRUE): void {
2a7cae66 779 $this->createEventAndParticipant();
044c0ad1 780 $tokens = array_keys($this->getEventTokens());
e80f2261 781 if ($includeParticipant) {
044c0ad1 782 $tokens = array_keys(array_merge($this->getEventTokens(), $this->getParticipantTokens()));
e80f2261 783 }
044c0ad1 784 $html = $this->getTokenString($tokens);
f399fbd8
EM
785 CRM_Utils_Time::setTime('2007-02-20 15:00:00');
786 $this->callAPISuccess('action_schedule', 'create', [
787 'title' => 'job',
788 'subject' => 'job',
789 'entity_value' => 1,
790 'mapping_id' => 2,
791 'start_action_date' => 'register_date',
792 'start_action_offset' => 1,
793 'start_action_condition' => 'after',
794 'start_action_unit' => 'day',
e80f2261 795 'body_html' => $html,
f399fbd8 796 ]);
ce971869
EM
797 }
798
799 /**
800 * Get expected event tokens.
801 *
802 * @return string[]
803 */
804 protected function getEventTokens(): array {
805 return [
e80f2261 806 '{event.id}' => 'Event ID',
ce971869
EM
807 '{event.title}' => 'Event Title',
808 '{event.start_date}' => 'Event Start Date',
809 '{event.end_date}' => 'Event End Date',
e80f2261 810 '{event.event_type_id:label}' => 'Event Type',
ce971869
EM
811 '{event.summary}' => 'Event Summary',
812 '{event.contact_email}' => 'Event Contact Email',
813 '{event.contact_phone}' => 'Event Contact Phone',
814 '{event.description}' => 'Event Description',
815 '{event.location}' => 'Event Location',
ce971869
EM
816 '{event.info_url}' => 'Event Info URL',
817 '{event.registration_url}' => 'Event Registration URL',
f399fbd8 818 '{event.' . $this->getCustomFieldName('text') . '}' => 'Enter text here :: Group with field text',
ce971869
EM
819 ];
820 }
821
044c0ad1
EM
822 /**
823 * @param array $tokens
824 *
825 * @return string
826 */
827 protected function getTokenString(array $tokens): string {
828 $html = '';
829 foreach ($tokens as $token) {
830 $html .= substr($token, 1, -1) . ' :' . $token . "\n";
831 }
832 return $html;
833 }
834
2a7cae66
EM
835 /**
836 * Create an event with a participant.
837 *
838 * @throws \API_Exception
839 */
840 protected function createEventAndParticipant(): void {
841 $this->createCustomGroupWithFieldOfType(['extends' => 'Event']);
842 $this->createCustomGroupWithFieldOfType(['extends' => 'Participant'], 'int', 'participant_');
843 $emailID = Email::create()
844 ->setValues(['email' => 'event@example.com'])
845 ->execute()
846 ->first()['id'];
847 $addressID = Address::create()->setValues([
848 'street_address' => '15 Walton St',
849 'supplemental_address_1' => 'up the road',
850 'city' => 'Emerald City',
851 'state_province_id:label' => 'Maine',
852 'postal_code' => 90210,
853 ])->execute()->first()['id'];
854 $phoneID = Phone::create()
855 ->setValues(['phone' => '456 789'])
856 ->execute()
857 ->first()['id'];
858
859 $locationBlockID = LocBlock::save(FALSE)->setRecords([
860 [
861 'email_id' => $emailID,
862 'address_id' => $addressID,
863 'phone_id' => $phoneID,
864 ],
865 ])->execute()->first()['id'];
866 $this->ids['event'][0] = $this->eventCreate([
867 'description' => 'event description',
868 $this->getCustomFieldName('text') => 'my field',
869 'loc_block_id' => $locationBlockID,
870 ])['id'];
871 // Create an unrelated participant record so that the ids don't match.
872 // this prevents things working just because the id 'happens to be valid'
873 $this->participantCreate([
874 'register_date' => '2020-01-01',
875 'event_id' => $this->ids['event'][0],
876 ]);
877 $this->ids['participant'][0] = $this->participantCreate([
878 'event_id' => $this->ids['event'][0],
879 'fee_amount' => 50,
880 'fee_level' => 'steep',
881 $this->getCustomFieldName('participant_int') => '99999',
882 ]);
883 }
884
d0ce76fd 885}