[Test] Add test cover for contribution tokens in pdf letter
[civicrm-core.git] / tests / phpunit / CRM / Contribute / Form / Task / PDFLetterCommonTest.php
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
12 /**
13 * Test APIv3 civicrm_contribute_* functions
14 *
15 * @package CiviCRM_APIv3
16 * @subpackage API_Contribution
17 * @group headless
18 */
19 class CRM_Contribute_Form_Task_PDFLetterCommonTest extends CiviUnitTestCase {
20
21 use CRMTraits_Custom_CustomDataTrait;
22
23 protected $_individualId;
24
25 protected $_docTypes;
26
27 protected $_contactIds;
28
29 /**
30 * Count how many times the hookTokens is called.
31 *
32 * This only needs to be called once, check refactoring doesn't change this.
33 *
34 * @var int
35 */
36 protected $hookTokensCalled = 0;
37
38 protected function setUp(): void {
39 parent::setUp();
40 $this->_individualId = $this->individualCreate(['first_name' => 'Anthony', 'last_name' => 'Collins']);
41 $this->_docTypes = CRM_Core_SelectValues::documentApplicationType();
42 }
43
44 /**
45 * Clean up after each test.
46 *
47 * @throws \API_Exception
48 * @throws \CRM_Core_Exception
49 * @throws \CiviCRM_API3_Exception
50 */
51 public function tearDown(): void {
52 $this->quickCleanUpFinancialEntities();
53 $this->quickCleanup(['civicrm_uf_match', 'civicrm_campaign'], TRUE);
54 CRM_Utils_Hook::singleton()->reset();
55 parent::tearDown();
56 }
57
58 /**
59 * Test thank you send with grouping.
60 *
61 * @throws \CRM_Core_Exception
62 * @throws \CiviCRM_API3_Exception
63 */
64 public function testGroupedThankYous(): void {
65 $this->ids['Contact'][0] = $this->individualCreate();
66 $this->createLoggedInUser();
67 $contribution1ID = $this->callAPISuccess('Contribution', 'create', [
68 'contact_id' => $this->ids['Contact'][0],
69 'total_amount' => '60',
70 'financial_type_id' => 'Donation',
71 'currency' => 'USD',
72 'receive_date' => '2021-01-01 13:21',
73 ])['id'];
74 $contribution2ID = $this->callAPISuccess('Contribution', 'create', [
75 'contact_id' => $this->ids['Contact'][0],
76 'total_amount' => '70',
77 'financial_type_id' => 'Donation',
78 'receive_date' => '2021-02-01 2:21',
79 'currency' => 'USD',
80 ])['id'];
81 $form = $this->getFormObject('CRM_Contribute_Form_Task_PDFLetter', [
82 'campaign_id' => '',
83 'subject' => '',
84 'format_id' => '',
85 'paper_size' => 'letter',
86 'orientation' => 'portrait',
87 'metric' => 'in',
88 'margin_left' => '0.75',
89 'margin_right' => '0.75',
90 'margin_top' => '0.75',
91 'margin_bottom' => '0.75',
92 'document_type' => 'pdf',
93 'html_message' => '{contribution.currency} * {contribution.total_amount} * {contribution.receive_date}',
94 'template' => '',
95 'saveTemplateName' => '',
96 'from_email_address' => '185',
97 'thankyou_update' => '1',
98 'group_by' => 'contact_id',
99 'group_by_separator' => 'comma',
100 'email_options' => '',
101 ]);
102 $this->setSearchSelection([$contribution1ID, $contribution2ID], $form);
103 $form->preProcess();
104 try {
105 $form->postProcess();
106 }
107 catch (CRM_Core_Exception_PrematureExitException $e) {
108 $this->assertContains('USD, USD * $ 60.00, $ 70.00 * January 1st, 2021 1:21 PM, February 1st, 2021 2:21 AM', $e->errorData['html']);
109 }
110 }
111
112 /**
113 * Test the buildContributionArray function.
114 *
115 * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
116 */
117 public function testBuildContributionArray(): void {
118 $this->_individualId = $this->individualCreate();
119
120 $customGroup = $this->callAPISuccess('CustomGroup', 'create', [
121 'title' => 'Test Custom Set for Contribution',
122 'extends' => 'Contribution',
123 'is_active' => TRUE,
124 ]);
125 $params = [
126 'custom_group_id' => $customGroup['id'],
127 'label' => 'Text field',
128 'html_type' => 'Text',
129 'data_type' => 'String',
130 'weight' => 1,
131 'is_active' => 1,
132 ];
133 $customField = $this->callAPISuccess('CustomField', 'create', $params);
134 $customFieldKey = 'custom_' . $customField['id'];
135 $campaignTitle = 'Test Campaign ';
136
137 $params = [
138 'contact_id' => $this->_individualId,
139 'total_amount' => 6,
140 'campaign_id' => $this->campaignCreate(['title' => $campaignTitle], FALSE),
141 'financial_type_id' => 'Donation',
142 $customFieldKey => 'Text_' . substr(sha1(rand()), 0, 7),
143 ];
144 $contributionIDs = $returnProperties = [];
145 $result = $this->callAPISuccess('Contribution', 'create', $params);
146 $contributionIDs[] = $result['id'];
147 $this->hookClass->setHook('civicrm_tokenValues', [$this, 'hookTokenValues']);
148
149 // assume that there are two token {contribution.financial_type} and
150 // {contribution.custom_N} in message content
151 $messageToken = [
152 'contribution' => [
153 'financial_type',
154 'payment_instrument',
155 'campaign',
156 $customFieldKey,
157 ],
158 ];
159
160 $form = $this->getFormObject('CRM_Contribute_Form_Task_PDFLetter');
161 [$contributions, $contacts] = $form->buildContributionArray('contact_id', $contributionIDs, $returnProperties, TRUE, TRUE, $messageToken, 'test', '**', FALSE);
162
163 $this->assertEquals('Anthony', $contacts[$this->_individualId]['first_name']);
164 $this->assertEquals('emo', $contacts[$this->_individualId]['favourite_emoticon']);
165 $this->assertEquals('Donation', $contributions[$result['id']]['financial_type']);
166 $this->assertEquals($campaignTitle, $contributions[$result['id']]['campaign']);
167 $this->assertEquals('Check', $contributions[$result['id']]['payment_instrument']);
168 // CRM-20359: assert that contribution custom field token is rightfully replaced by its value
169 $this->assertEquals($params[$customFieldKey], $contributions[$result['id']][$customFieldKey]);
170
171 $this->customFieldDelete($customField['id']);
172 $this->customGroupDelete($customGroup['id']);
173 }
174
175 /**
176 * Implement token values hook.
177 *
178 * @param array $details
179 * @param array $contactIDs
180 * @param int $jobID
181 * @param array $tokens
182 * @param string $className
183 */
184 public function hookTokenValues(&$details, $contactIDs, $jobID, $tokens, $className): void {
185 foreach ($details as $index => $detail) {
186 $details[$index]['favourite_emoticon'] = 'emo';
187 }
188 }
189
190 /**
191 * Test contribution token replacement in
192 * html returned by postProcess function.
193 *
194 * @throws \CiviCRM_API3_Exception
195 * @throws \CRM_Core_Exception
196 */
197 public function testPostProcess(): void {
198 $this->createLoggedInUser();;
199 foreach (['docx', 'odt'] as $docType) {
200 $formValues = [
201 'is_unit_test' => TRUE,
202 'group_by' => NULL,
203 'document_file' => [
204 'name' => __DIR__ . "/sample_documents/Template.$docType",
205 'type' => $this->_docTypes[$docType],
206 ],
207 ];
208
209 $contributionId = $this->createContribution();
210 /* @var $form CRM_Contribute_Form_Task_PDFLetter */
211 $form = $this->getFormObject('CRM_Contribute_Form_Task_PDFLetter', $formValues);
212 $form->setContributionIds([$contributionId]);
213 $format = Civi::settings()->get('dateformatFull');
214 $date = CRM_Utils_Date::getToday();
215 $displayDate = CRM_Utils_Date::customFormat($date, $format);
216
217 $html = $form->postProcess();
218 $expectedValues = [
219 'Hello Anthony',
220 '$ 100.00',
221 $displayDate,
222 'Donation',
223 'Domain Name - Default Domain Name',
224 ];
225
226 foreach ($expectedValues as $val) {
227 $this->assertTrue(strpos($html[$contributionId], $val) !== 0);
228 }
229 }
230 }
231
232 /**
233 * Test that no notice or errors occur if no contribution tokens are requested.
234 *
235 * @throws \CRM_Core_Exception
236 * @throws \CiviCRM_API3_Exception
237 */
238 public function testNoContributionTokens(): void {
239 $this->createLoggedInUser();
240 $formValues = [
241 'html_message' => '{contact.display_name}',
242 'document_type' => 'pdf',
243 ];
244 /* @var $form CRM_Contribute_Form_Task_PDFLetter */
245 $form = $this->getFormObject('CRM_Contribute_Form_Task_PDFLetter', $formValues);
246 $form->setContributionIds([$this->createContribution()]);
247 try {
248 $form->postProcess();
249 }
250 catch (CRM_Core_Exception_PrematureExitException $e) {
251 $html = $e->errorData['html'];
252 }
253 $this->assertStringContainsString('Mr. Anthony Anderson II', $html);
254 }
255
256 /**
257 * Test all contribution tokens.
258 *
259 * @throws \API_Exception
260 * @throws \CRM_Core_Exception
261 * @throws \CiviCRM_API3_Exception
262 * @throws \Civi\API\Exception\UnauthorizedException
263 */
264 public function testAllContributionTokens(): void {
265 $this->createLoggedInUser();
266 $this->createCustomGroupWithFieldsOfAllTypes(['extends' => 'Contribution']);
267 $this->campaignCreate(['name' => 'Big one', 'title' => 'Big one']);
268 $tokens = $this->getAllContributionTokens();
269 $formValues = [
270 'document_type' => 'pdf',
271 'html_message' => '',
272 ];
273 foreach (array_keys($this->getAllContributionTokens()) as $token) {
274 $formValues['html_message'] .= "$token : {contribution.$token}\n";
275 }
276 /* @var $form CRM_Contribute_Form_Task_PDFLetter */
277 $form = $this->getFormObject('CRM_Contribute_Form_Task_PDFLetter', $formValues);
278 $form->setContributionIds([$this->createContribution(array_merge(['campaign_id' => $tokens['campaign']], $tokens))]);
279 try {
280 $form->postProcess();
281 }
282 catch (CRM_Core_Exception_PrematureExitException $e) {
283 $html = $e->errorData['html'];
284 }
285 $this->assertEquals('
286 <html>
287 <head>
288 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
289 <style>@page { margin: 0.75in 0.75in 0.75in 0.75in; }</style>
290 <style type="text/css">@import url(' . CRM_Core_Config::singleton()->userFrameworkResourceURL . 'css/print.css);</style>
291 ' . " \n" . ' </head>
292 <body>
293 <div id="crm-container">
294 contribution_id : 1
295 total_amount : € 9,999.99
296 fee_amount : € 1,111.11
297 net_amount : € 7,777.78
298 non_deductible_amount : € 2,222.22
299 receive_date : July 20th, 2018 12:00 AM
300 payment_instrument : Check
301 trxn_id : 1234
302 invoice_id : 568
303 currency : EUR
304 cancel_date : 2019-12-30 00:00:00
305 cancel_reason : Contribution Cancel Reason
306 receipt_date : October 30th, 2019 12:00 AM
307 thankyou_date : 2019-11-30 00:00:00
308 contribution_source : Contribution Source
309 amount_level : Amount Level
310 contribution_status_id : 2
311 check_number : 6789
312 campaign : Big one
313 ' . $this->getCustomFieldName('text') . ' : Bobsled
314 ' . $this->getCustomFieldName('select_string') . ' : Red
315 ' . $this->getCustomFieldName('select_date') . ' : 01/20/2021 12:00AM
316 ' . $this->getCustomFieldName('int') . ' : 999
317 ' . $this->getCustomFieldName('link') . ' : <a href="http://civicrm.org" target="_blank">http://civicrm.org</a>
318 ' . $this->getCustomFieldName('country') . ' : New Zealand
319 ' . $this->getCustomFieldName('multi_country') . ' : France, Canada
320 ' . $this->getCustomFieldName('contact_reference') . ' : Mr. Spider Man II
321 ' . $this->getCustomFieldName('state') . ' : Queensland
322 ' . $this->getCustomFieldName('multi_state') . ' : Victoria, New South Wales
323 ' . $this->getCustomFieldName('boolean') . ' : Yes
324 ' . $this->getCustomFieldName('checkbox') . ' : Purple
325 </div>
326 </body>
327 </html>', $html);
328 }
329
330 /**
331 * Get all the tokens available to contributions.
332 *
333 * @return array
334 * @throws \CiviCRM_API3_Exception
335 */
336 public function getAllContributionTokens(): array {
337 return [
338 'contribution_id' => '',
339 'total_amount' => '9999.99',
340 'fee_amount' => '1111.11',
341 'net_amount' => '7777.78',
342 'non_deductible_amount' => '2222.22',
343 'receive_date' => '2018-07-20',
344 'payment_instrument' => 'Check',
345 'trxn_id' => '1234',
346 'invoice_id' => '568',
347 'currency' => 'EUR',
348 'cancel_date' => '2019-12-30',
349 'cancel_reason' => 'Contribution Cancel Reason',
350 'receipt_date' => '2019-10-30',
351 'thankyou_date' => '2019-11-30',
352 'contribution_source' => 'Contribution Source',
353 'amount_level' => 'Amount Level',
354 'contribution_status_id' => 'Pending',
355 'check_number' => '6789',
356 'campaign' => 'Big one',
357 $this->getCustomFieldName('text') => 'Bobsled',
358 $this->getCustomFieldName('select_string') => 'R',
359 $this->getCustomFieldName('select_date') => '2021-01-20',
360 $this->getCustomFieldName('int') => 999,
361 $this->getCustomFieldName('link') => 'http://civicrm.org',
362 $this->getCustomFieldName('country') => 'New Zealand',
363 $this->getCustomFieldName('multi_country') => ['France', 'Canada'],
364 $this->getCustomFieldName('contact_reference') => $this->individualCreate(['first_name' => 'Spider', 'last_name' => 'Man']),
365 $this->getCustomFieldName('state') => 'Queensland',
366 $this->getCustomFieldName('multi_state') => ['Victoria', 'New South Wales'],
367 $this->getCustomFieldName('boolean') => TRUE,
368 $this->getCustomFieldName('checkbox') => 'P',
369 $this->getCustomFieldName('contact_reference') => $this->individualCreate(['first_name' => 'Spider', 'last_name' => 'Man']),
370
371 ];
372 }
373
374 /**
375 * Test assignment of variables when using the group by function.
376 *
377 * We are looking to see that the contribution aggregate and contributions
378 * arrays reflect the most recent contact rather than a total aggregate,
379 * since we are using group by.
380 *
381 * @throws \CiviCRM_API3_Exception
382 * @throws \CRM_Core_Exception
383 */
384 public function testPostProcessGroupByContact(): void {
385 $this->createLoggedInUser();
386 $this->hookClass->setHook('civicrm_tokenValues', [$this, 'hook_aggregateTokenValues']);
387 $this->hookClass->setHook('civicrm_tokens', [$this, 'hook_tokens']);
388 $this->mut = new CiviMailUtils($this, TRUE);
389 $this->_individualId = $this->individualCreate();
390 $this->_individualId2 = $this->individualCreate();
391 $htmlMessage = "{aggregate.rendered_token}";
392 $formValues = [
393 'is_unit_test' => TRUE,
394 'group_by' => 'contact_id',
395 'html_message' => $htmlMessage,
396 'email_options' => 'both',
397 'subject' => 'Testy test test',
398 'from' => 'info@example.com',
399 ];
400
401 $contributionIDs = [];
402 $contribution = $this->callAPISuccess('Contribution', 'create', [
403 'contact_id' => $this->_individualId,
404 'total_amount' => 100,
405 'financial_type_id' => 'Donation',
406 'receive_date' => '2016-12-25',
407 ]);
408 $contributionIDs[] = $contribution['id'];
409 $contribution = $this->callAPISuccess('Contribution', 'create', [
410 'contact_id' => $this->_individualId2,
411 'total_amount' => 10,
412 'financial_type_id' => 'Donation',
413 'receive_date' => '2016-12-25',
414 ]);
415 $contributionIDs[] = $contribution['id'];
416
417 $contribution = $this->callAPISuccess('Contribution', 'create', [
418 'contact_id' => $this->_individualId2,
419 'total_amount' => 1,
420 'financial_type_id' => 'Donation',
421 'receive_date' => '2016-12-25',
422 ]);
423 $contributionIDs[] = $contribution['id'];
424
425 /* @var \CRM_Contribute_Form_Task_PDFLetter $form */
426 $form = $this->getFormObject('CRM_Contribute_Form_Task_PDFLetter', $formValues);
427 $form->setContributionIds($contributionIDs);
428
429 $html = $form->postProcess();
430 $this->assertEquals("<table border='1' cellpadding='2' cellspacing='0' class='table'>
431 <tbody>
432 <tr>
433 <th>Date</th>
434 <th>Amount</th>
435 <th>Financial Type</th>
436 <th>Source</th>
437 </tr>
438 <!--
439 -->
440 <tr>
441 <td>25 December 2016</td>
442 <td>$ 100.00</td>
443 <td>Donation</td>
444 <td></td>
445 </tr>
446 <!--
447 -->
448 <tr>
449 <td><strong>Total</strong></td>
450 <td><strong>$ 100.00</strong></td>
451 <td></td>
452 <td></td>
453 </tr>
454 </tbody>
455 </table>", $html[1]);
456 $this->assertEquals("<table border='1' cellpadding='2' cellspacing='0' class='table'>
457 <tbody>
458 <tr>
459 <th>Date</th>
460 <th>Amount</th>
461 <th>Financial Type</th>
462 <th>Source</th>
463 </tr>
464 <!--
465 -->
466 <tr>
467 <td>25 December 2016</td>
468 <td>$ 10.00</td>
469 <td>Donation</td>
470 <td></td>
471 </tr>
472 <!--
473 -->
474 <tr>
475 <td>25 December 2016</td>
476 <td>$ 1.00</td>
477 <td>Donation</td>
478 <td></td>
479 </tr>
480 <!--
481 -->
482 <tr>
483 <td><strong>Total</strong></td>
484 <td><strong>$ 11.00</strong></td>
485 <td></td>
486 <td></td>
487 </tr>
488 </tbody>
489 </table>", $html[2]);
490
491 $activities = $this->callAPISuccess('Activity', 'get', ['activity_type_id' => 'Print PDF Letter', 'sequential' => 1]);
492 $this->assertEquals(2, $activities['count']);
493 $this->assertEquals($html[1], $activities['values'][0]['details']);
494 $this->assertEquals($html[2], $activities['values'][1]['details']);
495 // Checking it is not called multiple times.
496 // once for each contact create + once for the activities.
497 // & once for the token processor, for now.
498 // By calling the cached function we can get this down to 1
499 $this->assertEquals(4, $this->hookTokensCalled);
500 $this->mut->checkAllMailLog($html);
501
502 }
503
504 /**
505 * Implements civicrm_tokens().
506 */
507 public function hook_tokens(&$tokens) {
508 $this->hookTokensCalled++;
509 $tokens['aggregate'] = ['rendered_token' => 'rendered_token'];
510 }
511
512 /**
513 * Get the html message.
514 *
515 * @return string
516 */
517 public function getHtmlMessage() {
518 return '{assign var=\'contact_aggregate\' value=0}
519 <table border=\'1\' cellpadding=\'2\' cellspacing=\'0\' class=\'table\'>
520 <tbody>
521 <tr>
522 <th>Date</th>
523 <th>Amount</th>
524 <th>Financial Type</th>
525 <th>Source</th>
526 </tr>
527 <!--
528 {foreach from=$contributions item=contribution}
529 {if $contribution.contact_id == $messageContactID}
530 {assign var=\'date\' value=$contribution.receive_date|date_format:\'%d %B %Y\'}
531 {assign var=contact_aggregate
532 value=$contact_aggregate+$contribution.total_amount}
533 -->
534 <tr>
535 <td>{$date}</td>
536 <td>{$contribution.total_amount|crmMoney}</td>
537 <td>{$contribution.financial_type}</td>
538 <td></td>
539 </tr>
540 <!--
541 {/if}
542 {/foreach}
543 -->
544 <tr>
545 <td><strong>Total</strong></td>
546 <td><strong>{$contact_aggregate|crmMoney}</strong></td>
547 <td></td>
548 <td></td>
549 </tr>
550 </tbody>
551 </table>';
552 }
553
554 /**
555 * Implements CiviCRM hook.
556 *
557 * @param array $values
558 * @param array $contactIDs
559 * @param null $job
560 * @param array $tokens
561 * @param null $context
562 */
563 public function hook_aggregateTokenValues(&$values, $contactIDs, $job = NULL, $tokens = [], $context = NULL) {
564 foreach ($contactIDs as $contactID) {
565 CRM_Core_Smarty::singleton()->assign('messageContactID', $contactID);
566 $values[$contactID]['aggregate.rendered_token'] = CRM_Core_Smarty::singleton()
567 ->fetch('string:' . $this->getHtmlMessage());
568 }
569 }
570
571 /**
572 * @param string $token
573 * @param string $entity
574 * @param string $textToSearch
575 * @param bool $expected
576 *
577 * @dataProvider isHtmlTokenInTableCellProvider
578 */
579 public function testIsHtmlTokenInTableCell($token, $entity, $textToSearch, $expected) {
580 $this->assertEquals($expected,
581 CRM_Contribute_Form_Task_PDFLetter::isHtmlTokenInTableCell($token, $entity, $textToSearch)
582 );
583 }
584
585 public function isHtmlTokenInTableCellProvider() {
586 return [
587
588 'simplest TRUE' => [
589 'token',
590 'entity',
591 '<td>{entity.token}</td>',
592 TRUE,
593 ],
594
595 'simplest FALSE' => [
596 'token',
597 'entity',
598 '{entity.token}',
599 FALSE,
600 ],
601
602 'token between two tables' => [
603 'token',
604 'entity',
605 ' <table><tr><td>Top</td></tr></table>
606 {entity.token}
607 <table><tr><td>Bottom</td></tr></table>',
608 FALSE,
609 ],
610
611 'token in two tables' => [
612 'token',
613 'entity',
614 ' <table><tr><td>{entity.token}</td></tr><tr><td>foo</td></tr></table>
615 <table><tr><td>{entity.token}</td></tr><tr><td>foo</td></tr></table>',
616 TRUE,
617 ],
618
619 'token outside of table and inside of table' => [
620 'token',
621 'entity',
622 ' {entity.token}
623 <table><tr><td>{entity.token}</td></tr><tr><td>foo</td></tr></table>',
624 FALSE,
625 ],
626
627 'token inside more complicated table' => [
628 'token',
629 'entity',
630 ' <table><tr><td class="foo"><em>{entity.token}</em></td></tr></table>',
631 TRUE,
632 ],
633
634 'token inside something that looks like table cell' => [
635 'token',
636 'entity',
637 ' <tdata>{entity.token}</tdata>
638 <table><tr><td>Bottom</td></tr></table>',
639 FALSE,
640 ],
641
642 ];
643 }
644
645 /**
646 * @param array $entities
647 * @param \CRM_Core_Form $form
648 */
649 protected function setSearchSelection(array $entities, CRM_Core_Form $form): void {
650 $_SESSION['_' . $form->controller->_name . '_container']['values']['Search'] = [
651 'radio_ts' => 'ts_sel',
652 ];
653 foreach ($entities as $entityID) {
654 $_SESSION['_' . $form->controller->_name . '_container']['values']['Search']['mark_x_' . $entityID] = TRUE;
655 }
656 }
657
658 /**
659 * @return mixed
660 * @throws \CRM_Core_Exception
661 * @throws \CiviCRM_API3_Exception
662 */
663 protected function createContribution($contributionParams = []) {
664 $contributionParams = array_merge([
665 'contact_id' => $this->individualCreate(),
666 'total_amount' => 100,
667 'financial_type_id' => 'Donation',
668 ], $contributionParams);
669 return $this->callAPISuccess('Contribution', 'create', $contributionParams)['id'];
670 }
671
672 }