Smarty modifier - stop using isset to check taxTerm
[civicrm-core.git] / tests / phpunit / CiviTest / CiviUnitTestCase.php
1 <?php
2 /**
3 * File for the CiviUnitTestCase class
4 *
5 * (PHP 5)
6 *
7 * @copyright Copyright CiviCRM LLC (C) 2009
8 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html
9 * GNU Affero General Public License version 3
10 * @package CiviCRM
11 *
12 * This file is part of CiviCRM
13 *
14 * CiviCRM is free software; you can redistribute it and/or
15 * modify it under the terms of the GNU Affero General Public License
16 * as published by the Free Software Foundation; either version 3 of
17 * the License, or (at your option) any later version.
18 *
19 * CiviCRM is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU Affero General Public License for more details.
23 *
24 * You should have received a copy of the GNU Affero General Public
25 * License along with this program. If not, see
26 * <http://www.gnu.org/licenses/>.
27 */
28
29 use Civi\Api4\Address;
30 use Civi\Api4\Contribution;
31 use Civi\Api4\CustomField;
32 use Civi\Api4\CustomGroup;
33 use Civi\Api4\FinancialType;
34 use Civi\Api4\LineItem;
35 use Civi\Api4\MembershipType;
36 use Civi\Api4\OptionGroup;
37 use Civi\Api4\Phone;
38 use Civi\Api4\RelationshipType;
39 use Civi\Payment\System;
40 use Civi\Api4\OptionValue;
41 use Civi\Test\Api3DocTrait;
42 use League\Csv\Reader;
43
44 /**
45 * Include class definitions
46 */
47 require_once 'api/api.php';
48 define('API_LATEST_VERSION', 3);
49
50 /**
51 * Base class for CiviCRM unit tests
52 *
53 * This class supports two (mutually-exclusive) techniques for cleaning up test data. Subclasses
54 * may opt for one or neither:
55 *
56 * 1. quickCleanup() is a helper which truncates a series of tables. Call quickCleanup()
57 * as part of setUp() and/or tearDown(). quickCleanup() is thorough - but it can
58 * be cumbersome to use (b/c you must identify the tables to cleanup) and slow to execute.
59 * 2. useTransaction() executes the test inside a transaction. It's easier to use
60 * (because you don't need to identify specific tables), but it doesn't work for tests
61 * which manipulate schema or truncate data -- and could behave inconsistently
62 * for tests which specifically examine DB transactions.
63 *
64 * Common functions for unit tests
65 *
66 * @package CiviCRM
67 */
68 class CiviUnitTestCase extends PHPUnit\Framework\TestCase {
69
70 use Api3DocTrait;
71 use \Civi\Test\GenericAssertionsTrait;
72 use \Civi\Test\DbTestTrait;
73 use \Civi\Test\ContactTestTrait;
74 use \Civi\Test\MailingTestTrait;
75
76 /**
77 * Database has been initialized.
78 *
79 * @var bool
80 */
81 private static $dbInit = FALSE;
82
83 /**
84 * Database connection.
85 *
86 * @var PHPUnit_Extensions_Database_DB_IDatabaseConnection
87 */
88 protected $_dbconn;
89
90 /**
91 * The database name.
92 *
93 * @var string
94 */
95 static protected $_dbName;
96
97 /**
98 * API version in use.
99 *
100 * @var int
101 */
102 protected $_apiversion = 3;
103
104 /**
105 * Track tables we have modified during a test.
106 *
107 * @var array
108 */
109 protected $_tablesToTruncate = [];
110
111 /**
112 * @var array
113 * Array of temporary directory names
114 */
115 protected $tempDirs;
116
117 /**
118 * @var bool
119 * populateOnce allows to skip db resets in setUp
120 *
121 * WARNING! USE WITH CAUTION - IT'LL RENDER DATA DEPENDENCIES
122 * BETWEEN TESTS WHEN RUN IN SUITE. SUITABLE FOR LOCAL, LIMITED
123 * "CHECK RUNS" ONLY!
124 *
125 * IF POSSIBLE, USE $this->DBResetRequired = FALSE IN YOUR TEST CASE!
126 *
127 * @see http://forum.civicrm.org/index.php/topic,18065.0.html
128 */
129 public static $populateOnce = FALSE;
130
131 /**
132 * DBResetRequired allows skipping DB reset
133 * in specific test case. If you still need
134 * to reset single test (method) of such case, call
135 * $this->cleanDB() in the first line of this
136 * test (method).
137 * @var bool
138 */
139 public $DBResetRequired = TRUE;
140
141 /**
142 * @var CRM_Core_Transaction|null
143 */
144 private $tx = NULL;
145
146 /**
147 * Array of IDs created to support the test.
148 *
149 * e.g
150 * $this->ids = ['Contact' => ['descriptive_key' => $contactID], 'Group' => [$groupID]];
151 *
152 * @var array
153 */
154 protected $ids = [];
155
156 /**
157 * Should financials be checked after the test but before tear down.
158 *
159 * Ideally all tests (or at least all that call any financial api calls ) should do this but there
160 * are some test data issues and some real bugs currently blocking.
161 *
162 * @var bool
163 */
164 protected $isValidateFinancialsOnPostAssert = TRUE;
165
166 /**
167 * Should location types be checked to ensure primary addresses are correctly assigned after each test.
168 *
169 * @var bool
170 */
171 protected $isLocationTypesOnPostAssert = TRUE;
172
173 /**
174 * Has the test class been verified as 'getsafe'.
175 *
176 * If a class is getsafe it means that where
177 * callApiSuccess is called 'return' is specified or 'return' =>'id'
178 * can be added by that function. This is part of getting away
179 * from open-ended get calls.
180 *
181 * Eventually we want to not be doing these in our test classes & start
182 * to work to not do them in our main code base. Note they mainly
183 * cause issues for activity.get and contact.get as these are where the
184 * too many joins limit is most likely to be hit.
185 *
186 * @var bool
187 */
188 protected $isGetSafe = FALSE;
189
190 /**
191 * Class used for hooks during tests.
192 *
193 * This can be used to test hooks within tests. For example in the ACL_PermissionTrait:
194 *
195 * $this->hookClass->setHook('civicrm_aclWhereClause', [$this, 'aclWhereHookAllResults']);
196 *
197 * @var \CRM_Utils_Hook_UnitTests
198 */
199 public $hookClass;
200
201 /**
202 * @var array
203 * Common values to be re-used multiple times within a class - usually to create the relevant entity
204 */
205 protected $_params = [];
206
207 /**
208 * @var CRM_Extension_System
209 */
210 protected $origExtensionSystem;
211
212 /**
213 * Array of IDs created during test setup routine.
214 *
215 * The cleanUpSetUpIds method can be used to clear these at the end of the test.
216 *
217 * @var array
218 */
219 public $setupIDs = [];
220
221 /**
222 * Constructor.
223 *
224 * Because we are overriding the parent class constructor, we
225 * need to show the same arguments as exist in the constructor of
226 * PHPUnit_Framework_TestCase, since
227 * PHPUnit_Framework_TestSuite::createTest() creates a
228 * ReflectionClass of the Test class and checks the constructor
229 * of that class to decide how to set up the test.
230 *
231 * @param string $name
232 * @param array $data
233 * @param string $dataName
234 */
235 public function __construct($name = NULL, array $data = [], $dataName = '') {
236 parent::__construct($name, $data, $dataName);
237
238 // we need full error reporting
239 error_reporting(E_ALL & ~E_NOTICE);
240
241 self::$_dbName = self::getDBName();
242
243 // also load the class loader
244 require_once 'CRM/Core/ClassLoader.php';
245 CRM_Core_ClassLoader::singleton()->register();
246 if (function_exists('_civix_phpunit_setUp')) {
247 // FIXME: loosen coupling
248 _civix_phpunit_setUp();
249 }
250 }
251
252 /**
253 * Override to run the test and assert its state.
254 *
255 * @return mixed
256 * @throws \Exception
257 * @throws \PHPUnit_Framework_IncompleteTest
258 * @throws \PHPUnit_Framework_SkippedTest
259 */
260 protected function runTest() {
261 try {
262 return parent::runTest();
263 }
264 catch (PEAR_Exception $e) {
265 // PEAR_Exception has metadata in funny places, and PHPUnit won't log it nicely
266 throw new Exception(\CRM_Core_Error::formatTextException($e), $e->getCode());
267 }
268 }
269
270 /**
271 * @return bool
272 */
273 public function requireDBReset() {
274 return $this->DBResetRequired;
275 }
276
277 /**
278 * @return string
279 */
280 public static function getDBName() {
281 static $dbName = NULL;
282 if ($dbName === NULL) {
283 require_once "DB.php";
284 $dsn = CRM_Utils_SQL::autoSwitchDSN(CIVICRM_DSN);
285 $dsninfo = DB::parseDSN($dsn);
286 $dbName = $dsninfo['database'];
287 }
288 return $dbName;
289 }
290
291 /**
292 * Create database connection for this instance.
293 *
294 * Initialize the test database if it hasn't been initialized
295 *
296 */
297 protected function getConnection() {
298 if (!self::$dbInit) {
299 $dbName = self::getDBName();
300
301 // install test database
302 echo PHP_EOL . "Installing {$dbName} database" . PHP_EOL;
303
304 static::_populateDB(FALSE, $this);
305
306 self::$dbInit = TRUE;
307 }
308
309 }
310
311 /**
312 * Required implementation of abstract method.
313 */
314 protected function getDataSet() {
315 }
316
317 /**
318 * @param bool $perClass
319 * @param null $object
320 *
321 * @return bool
322 * TRUE if the populate logic runs; FALSE if it is skipped
323 */
324 protected static function _populateDB($perClass = FALSE, &$object = NULL) {
325 if (CIVICRM_UF !== 'UnitTests') {
326 throw new \RuntimeException("_populateDB requires CIVICRM_UF=UnitTests");
327 }
328
329 if ($perClass || $object == NULL) {
330 $dbreset = TRUE;
331 }
332 else {
333 $dbreset = $object->requireDBReset();
334 }
335
336 if (self::$populateOnce || !$dbreset) {
337 return FALSE;
338 }
339 self::$populateOnce = NULL;
340
341 Civi\Test::data()->populate();
342
343 return TRUE;
344 }
345
346 public static function setUpBeforeClass(): void {
347 static::_populateDB(TRUE);
348
349 // also set this global hack
350 $GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'] = [];
351 }
352
353 /**
354 * Common setup functions for all unit tests.
355 */
356 protected function setUp(): void {
357 parent::setUp();
358 $session = CRM_Core_Session::singleton();
359 $session->set('userID', NULL);
360
361 $this->_apiversion = 3;
362
363 // Use a temporary file for STDIN
364 $GLOBALS['stdin'] = tmpfile();
365 if ($GLOBALS['stdin'] === FALSE) {
366 echo "Couldn't open temporary file\n";
367 exit(1);
368 }
369
370 // Get and save a connection to the database
371 $this->_dbconn = $this->getConnection();
372
373 // reload database before each test
374 // $this->_populateDB();
375
376 // "initialize" CiviCRM to avoid problems when running single tests
377 // FIXME: look at it closer in second stage
378
379 $GLOBALS['civicrm_setting']['domain']['fatalErrorHandler'] = 'CiviUnitTestCase_fatalErrorHandler';
380 $GLOBALS['civicrm_setting']['domain']['backtrace'] = 1;
381
382 // disable any left-over test extensions
383 CRM_Core_DAO::executeQuery('DELETE FROM civicrm_extension WHERE full_name LIKE "test.%"');
384
385 // reset all the caches
386 CRM_Utils_System::flushCache();
387
388 // initialize the object once db is loaded
389 \Civi::$statics = [];
390 // ugh, performance
391 $config = CRM_Core_Config::singleton(TRUE, TRUE);
392
393 // when running unit tests, use mockup user framework
394 $this->hookClass = CRM_Utils_Hook::singleton();
395
396 // Make sure the DB connection is setup properly
397 $config->userSystem->setMySQLTimeZone();
398 $env = new CRM_Utils_Check_Component_Env();
399 CRM_Utils_Check::singleton()->assertValid($env->checkMysqlTime());
400
401 // clear permissions stub to not check permissions
402 $config->userPermissionClass->permissions = NULL;
403
404 //flush component settings
405 CRM_Core_Component::getEnabledComponents(TRUE);
406
407 $_REQUEST = $_GET = $_POST = [];
408 error_reporting(E_ALL);
409
410 $this->renameLabels();
411 $this->ensureMySQLMode(['IGNORE_SPACE', 'ERROR_FOR_DIVISION_BY_ZERO', 'STRICT_TRANS_TABLES']);
412 putenv('CIVICRM_SMARTY_DEFAULT_ESCAPE=1');
413 }
414
415 /**
416 * Read everything from the datasets directory and insert into the db.
417 */
418 public function loadAllFixtures(): void {
419 $fixturesDir = __DIR__ . '/../../fixtures';
420
421 CRM_Core_DAO::executeQuery("SET FOREIGN_KEY_CHECKS = 0;");
422
423 $jsonFiles = glob($fixturesDir . '/*.json');
424 foreach ($jsonFiles as $jsonFixture) {
425 $json = json_decode(file_get_contents($jsonFixture));
426 foreach ($json as $tableName => $vars) {
427 if ($tableName === 'civicrm_contact') {
428 CRM_Core_DAO::executeQuery('DELETE c FROM civicrm_contact c LEFT JOIN civicrm_domain d ON d.contact_id = c.id WHERE d.id IS NULL');
429 }
430 else {
431 CRM_Core_DAO::executeQuery("TRUNCATE $tableName");
432 }
433 foreach ($vars as $entity) {
434 $keys = $values = [];
435 foreach ($entity as $key => $value) {
436 $keys[] = $key;
437 $values[] = is_numeric($value) ? $value : "'{$value}'";
438 }
439 CRM_Core_DAO::executeQuery("
440 INSERT INTO $tableName (" . implode(',', $keys) . ') VALUES(' . implode(',', $values) . ')'
441 );
442 }
443
444 }
445 }
446
447 CRM_Core_DAO::executeQuery("SET FOREIGN_KEY_CHECKS = 1;");
448 }
449
450 /**
451 * Load the data that used to be handled by the discontinued dbunit class.
452 *
453 * This could do with further tidy up - the initial priority is simply to get rid of
454 * the dbunity package which is no longer supported.
455 *
456 * @param string $fileName
457 */
458 protected function loadXMLDataSet($fileName) {
459 CRM_Core_DAO::executeQuery('SET FOREIGN_KEY_CHECKS = 0');
460 $xml = json_decode(json_encode(simplexml_load_file($fileName)), TRUE);
461 foreach ($xml as $tableName => $element) {
462 if (!empty($element)) {
463 foreach ($element as $row) {
464 $keys = $values = [];
465 if (isset($row['@attributes'])) {
466 foreach ($row['@attributes'] as $key => $value) {
467 $keys[] = $key;
468 $values[] = is_numeric($value) ? $value : "'{$value}'";
469 }
470 }
471 elseif (!empty($row)) {
472 // cos we copied it & it is inconsistent....
473 foreach ($row as $key => $value) {
474 $keys[] = $key;
475 $values[] = is_numeric($value) ? $value : "'{$value}'";
476 }
477 }
478
479 if (!empty($values)) {
480 CRM_Core_DAO::executeQuery("
481 INSERT INTO $tableName (" . implode(',', $keys) . ') VALUES(' . implode(',', $values) . ')'
482 );
483 }
484 }
485 }
486 }
487 CRM_Core_DAO::executeQuery('SET FOREIGN_KEY_CHECKS = 1');
488 }
489
490 /**
491 * Create default domain contacts for the two domains added during test class.
492 * database population.
493 */
494 public function createDomainContacts(): void {
495 try {
496 $this->organizationCreate(['api.Email.create' => ['email' => 'fixme.domainemail@example.org']]);
497 $this->organizationCreate([
498 'organization_name' => 'Second Domain',
499 'api.Email.create' => ['email' => 'domainemail2@example.org'],
500 'api.Address.create' => [
501 'street_address' => '15 Main St',
502 'location_type_id' => 1,
503 'city' => 'Collinsville',
504 'country_id' => 1228,
505 'state_province_id' => 1003,
506 'postal_code' => 6022,
507 ],
508 ]);
509 OptionValue::replace(FALSE)->addWhere(
510 'option_group_id:name', '=', 'from_email_address'
511 )->setDefaults([
512 'is_default' => 1,
513 'name' => '"FIXME" <info@EXAMPLE.ORG>',
514 'label' => '"FIXME" <info@EXAMPLE.ORG>',
515 ])->setRecords([['domain_id' => 1], ['domain_id' => 2]])->execute();
516 }
517 catch (API_Exception $e) {
518 $this->fail('failed to re-instate domain contacts ' . $e->getMessage());
519 }
520 }
521
522 /**
523 * Common teardown functions for all unit tests.
524 */
525 protected function tearDown(): void {
526 $this->_apiversion = 3;
527 $this->resetLabels();
528
529 error_reporting(E_ALL & ~E_NOTICE);
530 CRM_Utils_Hook::singleton()->reset();
531 if ($this->hookClass) {
532 $this->hookClass->reset();
533 }
534 CRM_Core_Session::singleton()->reset(1);
535
536 if ($this->tx) {
537 $this->tx->rollback()->commit();
538 $this->tx = NULL;
539
540 CRM_Core_Transaction::forceRollbackIfEnabled();
541 \Civi\Core\Transaction\Manager::singleton(TRUE);
542 }
543 else {
544 CRM_Core_Transaction::forceRollbackIfEnabled();
545 \Civi\Core\Transaction\Manager::singleton(TRUE);
546
547 $tablesToTruncate = ['civicrm_contact', 'civicrm_uf_match', 'civicrm_email', 'civicrm_address'];
548 $this->quickCleanup($tablesToTruncate);
549 $this->createDomainContacts();
550 }
551
552 $this->cleanTempDirs();
553 $this->unsetExtensionSystem();
554 $this->assertEquals([], CRM_Core_DAO::$_nullArray);
555 $this->assertEquals(NULL, CRM_Core_DAO::$_nullObject);
556 // Ensure the destruct runs by unsetting it. Also, unsetting
557 // classes frees memory as they are not otherwise unset until the
558 // very end.
559 unset($this->mut);
560 parent::tearDown();
561 }
562
563 /**
564 * CHeck that all tests that have created payments have created them with the right financial entities.
565 *
566 * @throws \API_Exception
567 * @throws \CRM_Core_Exception
568 */
569 protected function assertPostConditions(): void {
570 // Reset to version 3 as not all (e.g payments) work on v4
571 $this->_apiversion = 3;
572 if ($this->isLocationTypesOnPostAssert) {
573 $this->assertLocationValidity();
574 }
575 $this->assertCount(1, OptionGroup::get(FALSE)
576 ->addWhere('name', '=', 'from_email_address')
577 ->execute());
578 if (!$this->isValidateFinancialsOnPostAssert) {
579 return;
580 }
581 $this->validateAllPayments();
582 $this->validateAllContributions();
583 }
584
585 /**
586 * Create a batch of external API calls which can
587 * be executed concurrently.
588 *
589 * ```
590 * $calls = $this->createExternalAPI()
591 * ->addCall('Contact', 'get', ...)
592 * ->addCall('Contact', 'get', ...)
593 * ...
594 * ->run()
595 * ->getResults();
596 * ```
597 *
598 * @return \Civi\API\ExternalBatch
599 * @throws PHPUnit_Framework_SkippedTestError
600 */
601 public function createExternalAPI() {
602 global $civicrm_root;
603 $defaultParams = [
604 'version' => $this->_apiversion,
605 'debug' => 1,
606 ];
607
608 $calls = new \Civi\API\ExternalBatch($defaultParams);
609
610 if (!$calls->isSupported()) {
611 $this->markTestSkipped('The test relies on Civi\API\ExternalBatch. This is unsupported in the local environment.');
612 }
613
614 return $calls;
615 }
616
617 /**
618 * Create required data based on $this->entity & $this->params
619 * This is just a way to set up the test data for delete & get functions
620 * so the distinction between set
621 * up & tested functions is clearer
622 *
623 * @return array
624 * api Result
625 */
626 public function createTestEntity() {
627 return $entity = $this->callAPISuccess($this->entity, 'create', $this->params);
628 }
629
630 /**
631 * @param int $contactTypeId
632 *
633 * @throws Exception
634 */
635 public function contactTypeDelete($contactTypeId) {
636 $result = CRM_Contact_BAO_ContactType::del($contactTypeId);
637 if (!$result) {
638 throw new Exception('Could not delete contact type');
639 }
640 }
641
642 /**
643 * @param array $params
644 *
645 * @return int
646 */
647 public function membershipTypeCreate($params = []) {
648 CRM_Member_PseudoConstant::flush('membershipType');
649 CRM_Core_Config::clearDBCache();
650 $this->setupIDs['contact'] = $memberOfOrganization = $this->organizationCreate();
651 $params = array_merge([
652 'name' => 'General',
653 'duration_unit' => 'year',
654 'duration_interval' => 1,
655 'period_type' => 'rolling',
656 'member_of_contact_id' => $memberOfOrganization,
657 'domain_id' => 1,
658 'financial_type_id' => 2,
659 'is_active' => 1,
660 'sequential' => 1,
661 'visibility' => 'Public',
662 ], $params);
663
664 $result = $this->callAPISuccess('MembershipType', 'Create', $params);
665
666 CRM_Member_PseudoConstant::flush('membershipType');
667 CRM_Utils_Cache::singleton()->flush();
668
669 return (int) $result['id'];
670 }
671
672 /**
673 * Create membership.
674 *
675 * @param array $params
676 *
677 * @return int
678 */
679 public function contactMembershipCreate(array $params): int {
680 $params = array_merge([
681 'join_date' => '2007-01-21',
682 'start_date' => '2007-01-21',
683 'end_date' => '2007-12-21',
684 'source' => 'Payment',
685 'membership_type_id' => 'General',
686 ], $params);
687 if (!is_numeric($params['membership_type_id'])) {
688 $membershipTypes = $this->callAPISuccess('Membership', 'getoptions', ['action' => 'create', 'field' => 'membership_type_id']);
689 if (!in_array($params['membership_type_id'], $membershipTypes['values'], TRUE)) {
690 $this->membershipTypeCreate(['name' => $params['membership_type_id']]);
691 }
692 }
693
694 $result = $this->callAPISuccess('Membership', 'create', $params);
695 return $result['id'];
696 }
697
698 /**
699 * Delete Membership Type.
700 *
701 * @param array $params
702 */
703 public function membershipTypeDelete($params) {
704 $this->callAPISuccess('MembershipType', 'Delete', $params);
705 }
706
707 /**
708 * @param int $membershipID
709 */
710 public function membershipDelete($membershipID) {
711 $deleteParams = ['id' => $membershipID];
712 $result = $this->callAPISuccess('Membership', 'Delete', $deleteParams);
713 }
714
715 /**
716 * @param string $name
717 *
718 * @return mixed
719 */
720 public function membershipStatusCreate($name = 'test member status') {
721 $params['name'] = $name;
722 $params['start_event'] = 'start_date';
723 $params['end_event'] = 'end_date';
724 $params['is_current_member'] = 1;
725 $params['is_active'] = 1;
726
727 $result = $this->callAPISuccess('MembershipStatus', 'Create', $params);
728 CRM_Member_PseudoConstant::flush('membershipStatus');
729 return (int) $result['id'];
730 }
731
732 /**
733 * Delete the given membership status, deleting any memberships of the status first.
734 *
735 * @param int $membershipStatusID
736 *
737 * @throws \CRM_Core_Exception
738 */
739 public function membershipStatusDelete(int $membershipStatusID): void {
740 $this->callAPISuccess('Membership', 'get', ['status_id' => $membershipStatusID, 'api.Membership.delete' => 1]);
741 $this->callAPISuccess('MembershipStatus', 'Delete', ['id' => $membershipStatusID]);
742 }
743
744 public function membershipRenewalDate($durationUnit, $membershipEndDate) {
745 // We only have an end_date if frequency units match, otherwise membership won't be autorenewed and dates won't be calculated.
746 $renewedMembershipEndDate = new DateTime($membershipEndDate);
747 switch ($durationUnit) {
748 case 'year':
749 $renewedMembershipEndDate->add(new DateInterval('P1Y'));
750 break;
751
752 case 'month':
753 // We have to add 1 day first in case it's the end of the month, then subtract afterwards
754 // eg. 2018-02-28 should renew to 2018-03-31, if we just added 1 month we'd get 2018-03-28
755 $renewedMembershipEndDate->add(new DateInterval('P1D'));
756 $renewedMembershipEndDate->add(new DateInterval('P1M'));
757 $renewedMembershipEndDate->sub(new DateInterval('P1D'));
758 break;
759 }
760 return $renewedMembershipEndDate->format('Y-m-d');
761 }
762
763 /**
764 * Create a relationship type.
765 *
766 * @param array $params
767 *
768 * @return int
769 *
770 * @throws \CRM_Core_Exception
771 */
772 public function relationshipTypeCreate($params = []) {
773 $params = array_merge([
774 'name_a_b' => 'Relation 1 for relationship type create',
775 'name_b_a' => 'Relation 2 for relationship type create',
776 'contact_type_a' => 'Individual',
777 'contact_type_b' => 'Organization',
778 'is_reserved' => 1,
779 'is_active' => 1,
780 ], $params);
781
782 $result = $this->callAPISuccess('relationship_type', 'create', $params);
783 CRM_Core_PseudoConstant::flush('relationshipType');
784
785 return $result['id'];
786 }
787
788 /**
789 * Delete Relatinship Type.
790 *
791 * @param int $relationshipTypeID
792 */
793 public function relationshipTypeDelete($relationshipTypeID) {
794 $params['id'] = $relationshipTypeID;
795 $check = $this->callAPISuccess('relationship_type', 'get', $params);
796 if (!empty($check['count'])) {
797 $this->callAPISuccess('relationship_type', 'delete', $params);
798 }
799 }
800
801 /**
802 * @param array $params
803 *
804 * @return mixed
805 * @throws \CRM_Core_Exception
806 */
807 public function paymentProcessorTypeCreate($params = []) {
808 $params = array_merge([
809 'name' => 'API_Test_PP',
810 'title' => 'API Test Payment Processor',
811 'class_name' => 'CRM_Core_Payment_APITest',
812 'billing_mode' => 'form',
813 'is_recur' => 0,
814 'is_reserved' => 1,
815 'is_active' => 1,
816 ], $params);
817 $result = $this->callAPISuccess('PaymentProcessorType', 'create', $params);
818
819 CRM_Core_PseudoConstant::flush('paymentProcessorType');
820
821 return $result['id'];
822 }
823
824 /**
825 * Create test Authorize.net instance.
826 *
827 * @param array $params
828 *
829 * @return mixed
830 */
831 public function paymentProcessorAuthorizeNetCreate($params = []) {
832 $params = array_merge([
833 'name' => 'Authorize',
834 'domain_id' => CRM_Core_Config::domainID(),
835 'payment_processor_type_id' => 'AuthNet',
836 'title' => 'AuthNet',
837 'is_active' => 1,
838 'is_default' => 0,
839 'is_test' => 1,
840 'is_recur' => 1,
841 'user_name' => '4y5BfuW7jm',
842 'password' => '4cAmW927n8uLf5J8',
843 'url_site' => 'https://test.authorize.net/gateway/transact.dll',
844 'url_recur' => 'https://apitest.authorize.net/xml/v1/request.api',
845 'class_name' => 'Payment_AuthorizeNet',
846 'billing_mode' => 1,
847 ], $params);
848
849 $result = $this->callAPISuccess('PaymentProcessor', 'create', $params);
850 return (int) $result['id'];
851 }
852
853 /**
854 * Create Participant.
855 *
856 * @param array $params
857 * Array of contact id and event id values.
858 *
859 * @return int
860 * $id of participant created
861 */
862 public function participantCreate(array $params = []) {
863 if (empty($params['contact_id'])) {
864 $this->ids['Contact']['participant'] = $params['contact_id'] = $this->individualCreate();
865 }
866 if (empty($params['event_id'])) {
867 $event = $this->eventCreate();
868 $params['event_id'] = $event['id'];
869 }
870 $defaults = [
871 'status_id' => 2,
872 'role_id' => 1,
873 'register_date' => 20070219,
874 'source' => 'Wimbeldon',
875 'event_level' => 'Payment',
876 'debug' => 1,
877 ];
878
879 $params = array_merge($defaults, $params);
880 $result = $this->callAPISuccess('Participant', 'create', $params);
881 return $result['id'];
882 }
883
884 /**
885 * Create Payment Processor.
886 *
887 * @return int
888 * Id Payment Processor
889 */
890 public function processorCreate($params = []) {
891 $processorParams = [
892 'domain_id' => 1,
893 'name' => 'Dummy',
894 'payment_processor_type_id' => 'Dummy',
895 'financial_account_id' => 12,
896 'is_test' => TRUE,
897 'is_active' => 1,
898 'user_name' => '',
899 'url_site' => 'http://dummy.com',
900 'url_recur' => 'http://dummy.com',
901 'billing_mode' => 1,
902 'sequential' => 1,
903 'payment_instrument_id' => 'Debit Card',
904 ];
905 $processorParams = array_merge($processorParams, $params);
906 $processor = $this->callAPISuccess('PaymentProcessor', 'create', $processorParams);
907 return $processor['id'];
908 }
909
910 /**
911 * Create Payment Processor.
912 *
913 * @param array $processorParams
914 *
915 * @return \CRM_Core_Payment_Dummy
916 * Instance of Dummy Payment Processor
917 *
918 * @throws \CiviCRM_API3_Exception
919 */
920 public function dummyProcessorCreate($processorParams = []) {
921 $paymentProcessorID = $this->processorCreate($processorParams);
922 // For the tests we don't need a live processor, but as core ALWAYS creates a processor in live mode and one in test mode we do need to create both
923 // Otherwise we are testing a scenario that only exists in tests (and some tests fail because the live processor has not been defined).
924 $processorParams['is_test'] = FALSE;
925 $this->processorCreate($processorParams);
926 return System::singleton()->getById($paymentProcessorID);
927 }
928
929 /**
930 * Create contribution page.
931 *
932 * @param array $params
933 *
934 * @return array
935 * Array of contribution page
936 */
937 public function contributionPageCreate($params = []) {
938 $this->_pageParams = array_merge([
939 'title' => 'Test Contribution Page',
940 'financial_type_id' => 1,
941 'currency' => 'USD',
942 'financial_account_id' => 1,
943 'is_active' => 1,
944 'is_allow_other_amount' => 1,
945 'min_amount' => 10,
946 'max_amount' => 1000,
947 ], $params);
948 return $this->callAPISuccess('contribution_page', 'create', $this->_pageParams);
949 }
950
951 /**
952 * Create a sample batch.
953 */
954 public function batchCreate() {
955 $params = $this->_params;
956 $params['name'] = $params['title'] = 'Batch_433397';
957 $params['status_id'] = 1;
958 $result = $this->callAPISuccess('batch', 'create', $params);
959 return $result['id'];
960 }
961
962 /**
963 * Create Tag.
964 *
965 * @param array $params
966 *
967 * @return array
968 * result of created tag
969 */
970 public function tagCreate($params = []) {
971 $defaults = [
972 'name' => 'New Tag3',
973 'description' => 'This is description for Our New Tag ',
974 'domain_id' => '1',
975 ];
976 $params = array_merge($defaults, $params);
977 $result = $this->callAPISuccess('Tag', 'create', $params);
978 return $result['values'][$result['id']];
979 }
980
981 /**
982 * Delete Tag.
983 *
984 * @param int $tagId
985 * Id of the tag to be deleted.
986 *
987 * @return int
988 */
989 public function tagDelete($tagId) {
990 require_once 'api/api.php';
991 $params = [
992 'tag_id' => $tagId,
993 ];
994 $result = $this->callAPISuccess('Tag', 'delete', $params);
995 return $result['id'];
996 }
997
998 /**
999 * Add entity(s) to the tag
1000 *
1001 * @param array $params
1002 *
1003 * @return bool
1004 */
1005 public function entityTagAdd($params) {
1006 $result = $this->callAPISuccess('entity_tag', 'create', $params);
1007 return TRUE;
1008 }
1009
1010 /**
1011 * Create pledge.
1012 *
1013 * @param array $params
1014 * Parameters.
1015 *
1016 * @return int
1017 * id of created pledge
1018 */
1019 public function pledgeCreate($params): int {
1020 $params = array_merge([
1021 'pledge_create_date' => date('Ymd'),
1022 'start_date' => date('Ymd'),
1023 'scheduled_date' => date('Ymd'),
1024 'amount' => 100.00,
1025 'pledge_status_id' => '2',
1026 'financial_type_id' => '1',
1027 'pledge_original_installment_amount' => 20,
1028 'frequency_interval' => 5,
1029 'frequency_unit' => 'year',
1030 'frequency_day' => 15,
1031 'installments' => 5,
1032 ],
1033 $params);
1034
1035 $result = $this->callAPISuccess('Pledge', 'create', $params);
1036 return $result['id'];
1037 }
1038
1039 /**
1040 * Delete contribution.
1041 *
1042 * @param int $pledgeId
1043 *
1044 * @throws \CRM_Core_Exception
1045 */
1046 public function pledgeDelete($pledgeId) {
1047 $params = [
1048 'pledge_id' => $pledgeId,
1049 ];
1050 $this->callAPISuccess('Pledge', 'delete', $params);
1051 }
1052
1053 /**
1054 * Create contribution.
1055 *
1056 * @param array $params
1057 * Array of parameters.
1058 *
1059 * @return int
1060 * id of created contribution
1061 */
1062 public function contributionCreate(array $params): int {
1063 $params = array_merge([
1064 'domain_id' => 1,
1065 'receive_date' => date('Ymd'),
1066 'total_amount' => 100.00,
1067 'fee_amount' => 5.00,
1068 'financial_type_id' => 1,
1069 'payment_instrument_id' => 1,
1070 'non_deductible_amount' => 10.00,
1071 'source' => 'SSF',
1072 'contribution_status_id' => 1,
1073 ], $params);
1074
1075 $result = $this->callAPISuccess('contribution', 'create', $params);
1076 return $result['id'];
1077 }
1078
1079 /**
1080 * Delete contribution.
1081 *
1082 * @param int $contributionId
1083 *
1084 * @return array|int
1085 * @throws \CRM_Core_Exception
1086 */
1087 public function contributionDelete($contributionId) {
1088 $params = [
1089 'contribution_id' => $contributionId,
1090 ];
1091 $result = $this->callAPISuccess('contribution', 'delete', $params);
1092 return $result;
1093 }
1094
1095 /**
1096 * Create an Event.
1097 *
1098 * @param array $params
1099 * Name-value pair for an event.
1100 *
1101 * @return array
1102 */
1103 public function eventCreate(array $params = []): array {
1104 // if no contact was passed, make up a dummy event creator
1105 if (!isset($params['contact_id'])) {
1106 $params['contact_id'] = $this->_contactCreate([
1107 'contact_type' => 'Individual',
1108 'first_name' => 'Event',
1109 'last_name' => 'Creator',
1110 ]);
1111 }
1112
1113 // set defaults for missing params
1114 $params = array_merge([
1115 'title' => 'Annual CiviCRM meet',
1116 'summary' => 'If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now',
1117 'description' => 'This event is intended to give brief idea about progress of CiviCRM and giving solutions to common user issues',
1118 'event_type_id' => 1,
1119 'is_public' => 1,
1120 'start_date' => 20081021,
1121 'end_date' => 20081023,
1122 'is_online_registration' => 1,
1123 'registration_start_date' => 20080601,
1124 'registration_end_date' => 20081015,
1125 'max_participants' => 100,
1126 'event_full_text' => 'Sorry! We are already full',
1127 'is_monetary' => 0,
1128 'is_active' => 1,
1129 'is_show_location' => 0,
1130 'is_email_confirm' => 1,
1131 ], $params);
1132
1133 $event = $this->callAPISuccess('Event', 'create', $params);
1134 $this->ids['event'][] = $event['id'];
1135 return $event;
1136 }
1137
1138 /**
1139 * Create a paid event.
1140 *
1141 * @param array $params
1142 *
1143 * @param array $options
1144 *
1145 * @param string $key
1146 * Index for storing event ID in ids array.
1147 *
1148 * @return array
1149 *
1150 * @throws \CRM_Core_Exception
1151 */
1152 protected function eventCreatePaid($params, $options = [['name' => 'hundy', 'amount' => 100]], $key = 'event') {
1153 $params['is_monetary'] = TRUE;
1154 $event = $this->eventCreate($params);
1155 $this->ids['Event'][$key] = (int) $event['id'];
1156 $this->ids['PriceSet'][$key] = $this->eventPriceSetCreate(55, 0, 'Radio', $options);
1157 CRM_Price_BAO_PriceSet::addTo('civicrm_event', $event['id'], $this->ids['PriceSet'][$key]);
1158 $priceSet = CRM_Price_BAO_PriceSet::getSetDetail($this->ids['PriceSet'][$key], TRUE, FALSE);
1159 $priceSet = $priceSet[$this->ids['PriceSet'][$key]] ?? NULL;
1160 $this->eventFeeBlock = $priceSet['fields'] ?? NULL;
1161 return $event;
1162 }
1163
1164 /**
1165 * Delete event.
1166 *
1167 * @param int $id
1168 * ID of the event.
1169 *
1170 * @return array|int
1171 */
1172 public function eventDelete($id) {
1173 $params = [
1174 'event_id' => $id,
1175 ];
1176 return $this->callAPISuccess('event', 'delete', $params);
1177 }
1178
1179 /**
1180 * Delete participant.
1181 *
1182 * @param int $participantID
1183 *
1184 * @return array|int
1185 */
1186 public function participantDelete($participantID) {
1187 $params = [
1188 'id' => $participantID,
1189 ];
1190 $check = $this->callAPISuccess('Participant', 'get', $params);
1191 if ($check['count'] > 0) {
1192 return $this->callAPISuccess('Participant', 'delete', $params);
1193 }
1194 }
1195
1196 /**
1197 * Create participant payment.
1198 *
1199 * @param int $participantID
1200 * @param int $contributionID
1201 *
1202 * @return int
1203 * $id of created payment
1204 */
1205 public function participantPaymentCreate($participantID, $contributionID = NULL) {
1206 //Create Participant Payment record With Values
1207 $params = [
1208 'participant_id' => $participantID,
1209 'contribution_id' => $contributionID,
1210 ];
1211
1212 $result = $this->callAPISuccess('participant_payment', 'create', $params);
1213 return $result['id'];
1214 }
1215
1216 /**
1217 * Delete participant payment.
1218 *
1219 * @param int $paymentID
1220 */
1221 public function participantPaymentDelete($paymentID) {
1222 $params = [
1223 'id' => $paymentID,
1224 ];
1225 $result = $this->callAPISuccess('participant_payment', 'delete', $params);
1226 }
1227
1228 /**
1229 * Add a Location.
1230 *
1231 * @param int $contactID
1232 *
1233 * @return int
1234 * location id of created location
1235 */
1236 public function locationAdd($contactID) {
1237 $address = [
1238 1 => [
1239 'location_type' => 'New Location Type',
1240 'is_primary' => 1,
1241 'name' => 'Saint Helier St',
1242 'county' => 'Marin',
1243 'country' => 'UNITED STATES',
1244 'state_province' => 'Michigan',
1245 'supplemental_address_1' => 'Hallmark Ct',
1246 'supplemental_address_2' => 'Jersey Village',
1247 'supplemental_address_3' => 'My Town',
1248 ],
1249 ];
1250
1251 $params = [
1252 'contact_id' => $contactID,
1253 'address' => $address,
1254 'location_format' => '2.0',
1255 'location_type' => 'New Location Type',
1256 ];
1257
1258 $result = $this->callAPISuccess('Location', 'create', $params);
1259 return $result;
1260 }
1261
1262 /**
1263 * Delete Locations of contact.
1264 *
1265 * @param array $params
1266 * Parameters.
1267 */
1268 public function locationDelete($params) {
1269 $this->callAPISuccess('Location', 'delete', $params);
1270 }
1271
1272 /**
1273 * Add a Location Type.
1274 *
1275 * @param array $params
1276 *
1277 * @return CRM_Core_DAO_LocationType
1278 * location id of created location
1279 */
1280 public function locationTypeCreate($params = NULL) {
1281 if ($params === NULL) {
1282 $params = [
1283 'name' => 'New Location Type',
1284 'vcard_name' => 'New Location Type',
1285 'description' => 'Location Type for Delete',
1286 'is_active' => 1,
1287 ];
1288 }
1289
1290 $locationType = new CRM_Core_DAO_LocationType();
1291 $locationType->copyValues($params);
1292 $locationType->save();
1293 // clear getfields cache
1294 CRM_Core_PseudoConstant::flush();
1295 $this->callAPISuccess('phone', 'getfields', ['version' => 3, 'cache_clear' => 1]);
1296 return $locationType->id;
1297 }
1298
1299 /**
1300 * Delete a Location Type.
1301 *
1302 * @param int $locationTypeId
1303 */
1304 public function locationTypeDelete($locationTypeId) {
1305 $locationType = new CRM_Core_DAO_LocationType();
1306 $locationType->id = $locationTypeId;
1307 $locationType->delete();
1308 }
1309
1310 /**
1311 * Add a Mapping.
1312 *
1313 * @param array $params
1314 *
1315 * @return CRM_Core_DAO_Mapping
1316 * Mapping id of created mapping
1317 */
1318 public function mappingCreate($params = NULL) {
1319 if ($params === NULL) {
1320 $params = [
1321 'name' => 'Mapping name',
1322 'description' => 'Mapping description',
1323 // 'Export Contact' mapping.
1324 'mapping_type_id' => 7,
1325 ];
1326 }
1327
1328 $mapping = new CRM_Core_DAO_Mapping();
1329 $mapping->copyValues($params);
1330 $mapping->save();
1331 // clear getfields cache
1332 CRM_Core_PseudoConstant::flush();
1333 $this->callAPISuccess('mapping', 'getfields', ['version' => 3, 'cache_clear' => 1]);
1334 return $mapping;
1335 }
1336
1337 /**
1338 * Delete a Mapping
1339 *
1340 * @param int $mappingId
1341 */
1342 public function mappingDelete($mappingId) {
1343 $mapping = new CRM_Core_DAO_Mapping();
1344 $mapping->id = $mappingId;
1345 $mapping->delete();
1346 }
1347
1348 /**
1349 * Prepare class for ACLs.
1350 */
1351 protected function prepareForACLs() {
1352 $config = CRM_Core_Config::singleton();
1353 $config->userPermissionClass->permissions = [];
1354 }
1355
1356 /**
1357 * Reset after ACLs.
1358 */
1359 protected function cleanUpAfterACLs() {
1360 CRM_Utils_Hook::singleton()->reset();
1361 $tablesToTruncate = [
1362 'civicrm_acl',
1363 'civicrm_acl_cache',
1364 'civicrm_acl_entity_role',
1365 'civicrm_acl_contact_cache',
1366 ];
1367 $this->quickCleanup($tablesToTruncate);
1368 $config = CRM_Core_Config::singleton();
1369 unset($config->userPermissionClass->permissions);
1370 }
1371
1372 /**
1373 * Create a smart group.
1374 *
1375 * By default it will be a group of households.
1376 *
1377 * @param array $smartGroupParams
1378 * @param array $groupParams
1379 * @param string $contactType
1380 *
1381 * @return int
1382 */
1383 public function smartGroupCreate($smartGroupParams = [], $groupParams = [], $contactType = 'Household') {
1384 $smartGroupParams = array_merge(['form_values' => ['contact_type' => ['IN' => [$contactType]]]], $smartGroupParams);
1385 $savedSearch = CRM_Contact_BAO_SavedSearch::create($smartGroupParams);
1386
1387 $groupParams['saved_search_id'] = $savedSearch->id;
1388 return $this->groupCreate($groupParams);
1389 }
1390
1391 /**
1392 * Create a UFField.
1393 *
1394 * @param array $params
1395 */
1396 public function uFFieldCreate($params = []) {
1397 $params = array_merge([
1398 'uf_group_id' => 1,
1399 'field_name' => 'first_name',
1400 'is_active' => 1,
1401 'is_required' => 1,
1402 'visibility' => 'Public Pages and Listings',
1403 'is_searchable' => '1',
1404 'label' => 'first_name',
1405 'field_type' => 'Individual',
1406 'weight' => 1,
1407 ], $params);
1408 $this->callAPISuccess('uf_field', 'create', $params);
1409 }
1410
1411 /**
1412 * Add a UF Join Entry.
1413 *
1414 * @param array $params
1415 *
1416 * @return int
1417 * $id of created UF Join
1418 */
1419 public function ufjoinCreate($params = NULL) {
1420 if ($params === NULL) {
1421 $params = [
1422 'is_active' => 1,
1423 'module' => 'CiviEvent',
1424 'entity_table' => 'civicrm_event',
1425 'entity_id' => 3,
1426 'weight' => 1,
1427 'uf_group_id' => 1,
1428 ];
1429 }
1430 $result = $this->callAPISuccess('uf_join', 'create', $params);
1431 return $result;
1432 }
1433
1434 /**
1435 * @param array $params
1436 * Optional parameters.
1437 * @param bool $reloadConfig
1438 * While enabling CiviCampaign component, we shouldn't always forcibly
1439 * reload config as this hinder hook call in test environment
1440 *
1441 * @return int
1442 * Campaign ID.
1443 */
1444 public function campaignCreate($params = [], $reloadConfig = TRUE) {
1445 $this->enableCiviCampaign($reloadConfig);
1446 $campaign = $this->callAPISuccess('campaign', 'create', array_merge([
1447 'name' => 'big_campaign',
1448 'title' => 'Campaign',
1449 ], $params));
1450 return $campaign['id'];
1451 }
1452
1453 /**
1454 * Create Group for a contact.
1455 *
1456 * @param int $contactId
1457 */
1458 public function contactGroupCreate($contactId) {
1459 $params = [
1460 'contact_id.1' => $contactId,
1461 'group_id' => 1,
1462 ];
1463
1464 $this->callAPISuccess('GroupContact', 'Create', $params);
1465 }
1466
1467 /**
1468 * Delete Group for a contact.
1469 *
1470 * @param int $contactId
1471 */
1472 public function contactGroupDelete($contactId) {
1473 $params = [
1474 'contact_id.1' => $contactId,
1475 'group_id' => 1,
1476 ];
1477 $this->civicrm_api('GroupContact', 'Delete', $params);
1478 }
1479
1480 /**
1481 * Create Activity.
1482 *
1483 * @param array $params
1484 *
1485 * @return array|int
1486 *
1487 * @throws \CRM_Core_Exception
1488 * @throws \CiviCRM_API3_Exception
1489 */
1490 public function activityCreate($params = []) {
1491 $params = array_merge([
1492 'subject' => 'Discussion on warm beer',
1493 'activity_date_time' => date('Ymd'),
1494 'duration' => 90,
1495 'location' => 'Baker Street',
1496 'details' => 'Lets schedule a meeting',
1497 'status_id' => 1,
1498 'activity_type_id' => 'Meeting',
1499 ], $params);
1500 if (!isset($params['source_contact_id'])) {
1501 $params['source_contact_id'] = $this->individualCreate();
1502 }
1503 if (!isset($params['target_contact_id'])) {
1504 $params['target_contact_id'] = $this->individualCreate([
1505 'first_name' => 'Julia',
1506 'last_name' => 'Anderson',
1507 'prefix' => 'Ms.',
1508 'email' => 'julia_anderson@civicrm.org',
1509 'contact_type' => 'Individual',
1510 ]);
1511 }
1512 if (!isset($params['assignee_contact_id'])) {
1513 $params['assignee_contact_id'] = $params['target_contact_id'];
1514 }
1515
1516 $result = civicrm_api3('Activity', 'create', $params);
1517
1518 $result['target_contact_id'] = $params['target_contact_id'];
1519 $result['assignee_contact_id'] = $params['assignee_contact_id'];
1520 return $result;
1521 }
1522
1523 /**
1524 * Create an activity type.
1525 *
1526 * @param array $params
1527 * Parameters.
1528 *
1529 * @return array
1530 */
1531 public function activityTypeCreate($params) {
1532 return $this->callAPISuccess('ActivityType', 'create', $params);
1533 }
1534
1535 /**
1536 * Delete activity type.
1537 *
1538 * @param int $activityTypeId
1539 * Id of the activity type.
1540 *
1541 * @return array
1542 */
1543 public function activityTypeDelete($activityTypeId) {
1544 $params['activity_type_id'] = $activityTypeId;
1545 return $this->callAPISuccess('ActivityType', 'delete', $params);
1546 }
1547
1548 /**
1549 * Create custom group.
1550 *
1551 * @param array $params
1552 *
1553 * @return array
1554 */
1555 public function customGroupCreate($params = []) {
1556 $defaults = [
1557 'title' => 'new custom group',
1558 'extends' => 'Contact',
1559 'domain_id' => 1,
1560 'style' => 'Inline',
1561 'is_active' => 1,
1562 ];
1563
1564 $params = array_merge($defaults, $params);
1565
1566 return $this->callAPISuccess('custom_group', 'create', $params);
1567 }
1568
1569 /**
1570 * Existing function doesn't allow params to be over-ridden so need a new one
1571 * this one allows you to only pass in the params you want to change
1572 *
1573 * @param array $params
1574 *
1575 * @return array|int
1576 */
1577 public function CustomGroupCreateByParams($params = []) {
1578 $defaults = [
1579 'title' => "API Custom Group",
1580 'extends' => 'Contact',
1581 'domain_id' => 1,
1582 'style' => 'Inline',
1583 'is_active' => 1,
1584 ];
1585 $params = array_merge($defaults, $params);
1586 return $this->callAPISuccess('custom_group', 'create', $params);
1587 }
1588
1589 /**
1590 * Create custom group with multi fields.
1591 *
1592 * @param array $params
1593 *
1594 * @return array|int
1595 */
1596 public function CustomGroupMultipleCreateByParams($params = []) {
1597 $defaults = [
1598 'style' => 'Tab',
1599 'is_multiple' => 1,
1600 ];
1601 $params = array_merge($defaults, $params);
1602 return $this->CustomGroupCreateByParams($params);
1603 }
1604
1605 /**
1606 * Create custom group with multi fields.
1607 *
1608 * @param array $params
1609 *
1610 * @return array
1611 */
1612 public function CustomGroupMultipleCreateWithFields($params = []) {
1613 // also need to pass on $params['custom_field'] if not set but not in place yet
1614 $ids = [];
1615 $customGroup = $this->CustomGroupMultipleCreateByParams($params);
1616 $ids['custom_group_id'] = $customGroup['id'];
1617
1618 $customField = $this->customFieldCreate([
1619 'custom_group_id' => $ids['custom_group_id'],
1620 'label' => 'field_1' . $ids['custom_group_id'],
1621 'in_selector' => 1,
1622 ]);
1623
1624 $ids['custom_field_id'][] = $customField['id'];
1625
1626 $customField = $this->customFieldCreate([
1627 'custom_group_id' => $ids['custom_group_id'],
1628 'default_value' => '',
1629 'label' => 'field_2' . $ids['custom_group_id'],
1630 'in_selector' => 1,
1631 ]);
1632 $ids['custom_field_id'][] = $customField['id'];
1633
1634 $customField = $this->customFieldCreate([
1635 'custom_group_id' => $ids['custom_group_id'],
1636 'default_value' => '',
1637 'label' => 'field_3' . $ids['custom_group_id'],
1638 'in_selector' => 1,
1639 ]);
1640 $ids['custom_field_id'][] = $customField['id'];
1641
1642 return $ids;
1643 }
1644
1645 /**
1646 * Create a custom group with a single text custom field. See
1647 * participant:testCreateWithCustom for how to use this
1648 *
1649 * @param string $function
1650 * __FUNCTION__.
1651 * @param string $filename
1652 * $file __FILE__.
1653 *
1654 * @return array
1655 * ids of created objects
1656 */
1657 public function entityCustomGroupWithSingleFieldCreate($function, $filename) {
1658 $params = ['title' => $function];
1659 $entity = substr(basename($filename), 0, strlen(basename($filename)) - 8);
1660 $params['extends'] = $entity ? $entity : 'Contact';
1661 $customGroup = $this->customGroupCreate($params);
1662 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'label' => $function]);
1663 CRM_Core_PseudoConstant::flush();
1664
1665 return ['custom_group_id' => $customGroup['id'], 'custom_field_id' => $customField['id']];
1666 }
1667
1668 /**
1669 * Create a custom group with a single text custom field, multi-select widget, with a variety of option values including upper and lower case.
1670 * See api_v3_SyntaxConformanceTest:testCustomDataGet for how to use this
1671 *
1672 * @param string $function
1673 * __FUNCTION__.
1674 * @param string $filename
1675 * $file __FILE__.
1676 *
1677 * @return array
1678 * ids of created objects
1679 */
1680 public function entityCustomGroupWithSingleStringMultiSelectFieldCreate($function, $filename) {
1681 $params = ['title' => $function];
1682 $entity = substr(basename($filename), 0, strlen(basename($filename)) - 8);
1683 $params['extends'] = $entity ? $entity : 'Contact';
1684 $customGroup = $this->customGroupCreate($params);
1685 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'label' => $function, 'html_type' => 'Multi-Select', 'default_value' => 1]);
1686 CRM_Core_PseudoConstant::flush();
1687 $options = [
1688 'defaultValue' => 'Default Value',
1689 'lowercasevalue' => 'Lowercase Value',
1690 1 => 'Integer Value',
1691 'NULL' => 'NULL',
1692 ];
1693 $custom_field_params = ['sequential' => 1, 'id' => $customField['id']];
1694 $custom_field_api_result = $this->callAPISuccess('custom_field', 'get', $custom_field_params);
1695 $this->assertNotEmpty($custom_field_api_result['values'][0]['option_group_id']);
1696 $option_group_params = ['sequential' => 1, 'id' => $custom_field_api_result['values'][0]['option_group_id']];
1697 $option_group_result = $this->callAPISuccess('OptionGroup', 'get', $option_group_params);
1698 $this->assertNotEmpty($option_group_result['values'][0]['name']);
1699 foreach ($options as $option_value => $option_label) {
1700 $option_group_params = ['option_group_id' => $option_group_result['values'][0]['name'], 'value' => $option_value, 'label' => $option_label];
1701 $option_value_result = $this->callAPISuccess('OptionValue', 'create', $option_group_params);
1702 }
1703
1704 return [
1705 'custom_group_id' => $customGroup['id'],
1706 'custom_field_id' => $customField['id'],
1707 'custom_field_option_group_id' => $custom_field_api_result['values'][0]['option_group_id'],
1708 'custom_field_group_options' => $options,
1709 ];
1710 }
1711
1712 /**
1713 * Delete custom group.
1714 *
1715 * @param int $customGroupID
1716 *
1717 * @return array|int
1718 */
1719 public function customGroupDelete($customGroupID) {
1720 $params['id'] = $customGroupID;
1721 return $this->callAPISuccess('custom_group', 'delete', $params);
1722 }
1723
1724 /**
1725 * Create custom field.
1726 *
1727 * @param array $params
1728 * (custom_group_id) is required.
1729 *
1730 * @return array
1731 */
1732 public function customFieldCreate($params) {
1733 $params = array_merge([
1734 'label' => 'Custom Field',
1735 'data_type' => 'String',
1736 'html_type' => 'Text',
1737 'is_searchable' => 1,
1738 'is_active' => 1,
1739 'default_value' => 'defaultValue',
1740 ], $params);
1741
1742 $result = $this->callAPISuccess('custom_field', 'create', $params);
1743 // these 2 functions are called with force to flush static caches
1744 CRM_Core_BAO_CustomField::getTableColumnGroup($result['id'], 1);
1745 CRM_Core_Component::getEnabledComponents(1);
1746 return $result;
1747 }
1748
1749 /**
1750 * Delete custom field.
1751 *
1752 * @param int $customFieldID
1753 *
1754 * @return array|int
1755 */
1756 public function customFieldDelete($customFieldID) {
1757
1758 $params['id'] = $customFieldID;
1759 return $this->callAPISuccess('custom_field', 'delete', $params);
1760 }
1761
1762 /**
1763 * Create note.
1764 *
1765 * @param int $cId
1766 *
1767 * @return array
1768 */
1769 public function noteCreate($cId) {
1770 $params = [
1771 'entity_table' => 'civicrm_contact',
1772 'entity_id' => $cId,
1773 'note' => 'hello I am testing Note',
1774 'contact_id' => $cId,
1775 'modified_date' => date('Ymd'),
1776 'subject' => 'Test Note',
1777 ];
1778
1779 return $this->callAPISuccess('Note', 'create', $params);
1780 }
1781
1782 /**
1783 * Enable CiviCampaign Component.
1784 */
1785 public function enableCiviCampaign(): void {
1786 CRM_Core_BAO_ConfigSetting::enableComponent('CiviCampaign');
1787 }
1788
1789 /**
1790 * Create custom field with Option Values.
1791 *
1792 * @param array $customGroup
1793 * @param string $name
1794 * Name of custom field.
1795 * @param array $extraParams
1796 * Additional parameters to pass through.
1797 *
1798 * @return array|int
1799 */
1800 public function customFieldOptionValueCreate($customGroup, $name, $extraParams = []) {
1801 $fieldParams = [
1802 'custom_group_id' => $customGroup['id'],
1803 'name' => 'test_custom_group',
1804 'label' => 'Country',
1805 'html_type' => 'Select',
1806 'data_type' => 'String',
1807 'weight' => 4,
1808 'is_required' => 1,
1809 'is_searchable' => 0,
1810 'is_active' => 1,
1811 ];
1812
1813 $optionGroup = [
1814 'domain_id' => 1,
1815 'name' => 'option_group1',
1816 'label' => 'option_group_label1',
1817 ];
1818
1819 $optionValue = [
1820 'option_label' => ['Label1', 'Label2'],
1821 'option_value' => ['value1', 'value2'],
1822 'option_name' => [$name . '_1', $name . '_2'],
1823 'option_weight' => [1, 2],
1824 'option_status' => [1, 1],
1825 ];
1826
1827 $params = array_merge($fieldParams, $optionGroup, $optionValue, $extraParams);
1828
1829 return $this->callAPISuccess('custom_field', 'create', $params);
1830 }
1831
1832 /**
1833 * @param $entities
1834 *
1835 * @return bool
1836 */
1837 public function confirmEntitiesDeleted($entities) {
1838 foreach ($entities as $entity) {
1839
1840 $result = $this->callAPISuccess($entity, 'Get', []);
1841 if ($result['error'] == 1 || $result['count'] > 0) {
1842 // > than $entity[0] to allow a value to be passed in? e.g. domain?
1843 return TRUE;
1844 }
1845 }
1846 return FALSE;
1847 }
1848
1849 /**
1850 * Quick clean by emptying tables created for the test.
1851 *
1852 * @param array $tablesToTruncate
1853 * @param bool $dropCustomValueTables
1854 */
1855 public function quickCleanup(array $tablesToTruncate, $dropCustomValueTables = FALSE): void {
1856 if ($this->tx) {
1857 $this->fail('CiviUnitTestCase: quickCleanup() is not compatible with useTransaction()');
1858 }
1859 if ($dropCustomValueTables) {
1860 $this->cleanupCustomGroups();
1861 // Reset autoincrement too.
1862 $tablesToTruncate[] = 'civicrm_custom_group';
1863 $tablesToTruncate[] = 'civicrm_custom_field';
1864 }
1865
1866 $tablesToTruncate = array_unique(array_merge($this->_tablesToTruncate, $tablesToTruncate));
1867
1868 CRM_Core_DAO::executeQuery('SET FOREIGN_KEY_CHECKS = 0;');
1869 foreach ($tablesToTruncate as $table) {
1870 $sql = "TRUNCATE TABLE $table";
1871 CRM_Core_DAO::executeQuery($sql);
1872 }
1873 CRM_Core_DAO::executeQuery('SET FOREIGN_KEY_CHECKS = 1;');
1874 }
1875
1876 /**
1877 * Clean up financial entities after financial tests (so we remember to get all the tables :-))
1878 */
1879 public function quickCleanUpFinancialEntities(): void {
1880 $tablesToTruncate = [
1881 'civicrm_activity',
1882 'civicrm_activity_contact',
1883 'civicrm_contribution',
1884 'civicrm_contribution_soft',
1885 'civicrm_contribution_product',
1886 'civicrm_financial_trxn',
1887 'civicrm_financial_item',
1888 'civicrm_contribution_recur',
1889 'civicrm_line_item',
1890 'civicrm_contribution_page',
1891 'civicrm_payment_processor',
1892 'civicrm_entity_financial_trxn',
1893 'civicrm_membership',
1894 'civicrm_membership_type',
1895 'civicrm_membership_payment',
1896 'civicrm_membership_log',
1897 'civicrm_membership_block',
1898 'civicrm_event',
1899 'civicrm_participant',
1900 'civicrm_participant_payment',
1901 'civicrm_pledge',
1902 'civicrm_pcp_block',
1903 'civicrm_pcp',
1904 'civicrm_pledge_block',
1905 'civicrm_pledge_payment',
1906 'civicrm_price_set_entity',
1907 'civicrm_price_field_value',
1908 'civicrm_price_field',
1909 ];
1910 $this->quickCleanup($tablesToTruncate);
1911 CRM_Core_DAO::executeQuery("DELETE FROM civicrm_membership_status WHERE name NOT IN('New', 'Current', 'Grace', 'Expired', 'Pending', 'Cancelled', 'Deceased')");
1912 $this->restoreDefaultPriceSetConfig();
1913 $this->disableTaxAndInvoicing();
1914 $this->setCurrencySeparators(',');
1915 try {
1916 FinancialType::delete(FALSE)->addWhere(
1917 'name', 'NOT IN', [
1918 'Donation',
1919 'Member Dues',
1920 'Campaign Contribution',
1921 'Event Fee',
1922 ]
1923 )->execute();
1924 }
1925 catch (API_Exception $e) {
1926 $this->fail('failed to cleanup financial types ' . $e->getMessage());
1927 }
1928 CRM_Core_PseudoConstant::flush('taxRates');
1929 System::singleton()->flushProcessors();
1930 // @fixme this parameter is leaking - it should not be defined as a class static
1931 // but for now we just handle in tear down.
1932 CRM_Contribute_BAO_Query::$_contribOrSoftCredit = 'only contribs';
1933 }
1934
1935 /**
1936 * Reset the price set config so results exist.
1937 */
1938 public function restoreDefaultPriceSetConfig(): void {
1939 CRM_Core_DAO::executeQuery("DELETE FROM civicrm_price_set WHERE name NOT IN('default_contribution_amount', 'default_membership_type_amount')");
1940 CRM_Core_DAO::executeQuery("UPDATE civicrm_price_set SET id = 1 WHERE name ='default_contribution_amount'");
1941 CRM_Core_DAO::executeQuery("INSERT INTO `civicrm_price_field` (`id`, `price_set_id`, `name`, `label`, `html_type`, `is_enter_qty`, `help_pre`, `help_post`, `weight`, `is_display_amounts`, `options_per_line`, `is_active`, `is_required`, `active_on`, `expire_on`, `javascript`, `visibility_id`) VALUES (1, 1, 'contribution_amount', 'Contribution Amount', 'Text', 0, NULL, NULL, 1, 1, 1, 1, 1, NULL, NULL, NULL, 1)");
1942 CRM_Core_DAO::executeQuery("INSERT INTO `civicrm_price_field_value` (`id`, `price_field_id`, `name`, `label`, `description`, `amount`, `count`, `max_value`, `weight`, `membership_type_id`, `membership_num_terms`, `is_default`, `is_active`, `financial_type_id`, `non_deductible_amount`) VALUES (1, 1, 'contribution_amount', 'Contribution Amount', NULL, '1', NULL, NULL, 1, NULL, NULL, 0, 1, 1, 0.00)");
1943 }
1944
1945 /**
1946 * Recreate default membership types.
1947 *
1948 * @throws \API_Exception
1949 */
1950 public function restoreMembershipTypes(): void {
1951 MembershipType::delete(FALSE)->addWhere('id', '>', 0)->execute();
1952 $this->quickCleanup(['civicrm_membership_type']);
1953 $this->ensureMembershipPriceSetExists();
1954
1955 MembershipType::save(FALSE)
1956 ->setRecords(
1957 [
1958 [
1959 'name' => 'General',
1960 'description' => 'Regular annual membership.',
1961 'minimum_fee' => 100,
1962 'duration_unit' => 'year',
1963 'duration_interval' => 2,
1964 'period_type' => 'rolling',
1965 'relationship_type_id' => 7,
1966 'relationship_direction' => 'b_a',
1967 'visibility' => 'Public',
1968 'is_active' => 1,
1969 'weight' => 1,
1970 ],
1971 [
1972 'name' => 'Student',
1973 'description' => 'Discount membership for full-time students.',
1974 'minimum_fee' => 50,
1975 'duration_unit' => 1,
1976 'duration_interval' => 'year',
1977 'period_type' => 'rolling',
1978 'visibility' => 'Public',
1979 ],
1980 [
1981 'name' => 'Lifetime',
1982 'description' => 'Lifetime membership.',
1983 'minimum_fee' => 1200.00,
1984 'duration_unit' => 1,
1985 'duration_interval' => 'lifetime',
1986 'period_type' => 'rolling',
1987 'relationship_type_id' => 7,
1988 'relationship_direction' => 'b_a',
1989 'visibility' => 'Admin',
1990 ],
1991 ]
1992 )
1993 ->setDefaults([
1994 'domain_id' => 1,
1995 'member_of_contact_id' => 1,
1996 'financial_type_id' => 2,
1997 ]
1998 )->execute();
1999 }
2000
2001 /*
2002 * Function does a 'Get' on the entity & compares the fields in the Params with those returned
2003 * Default behaviour is to also delete the entity
2004 * @param array $params
2005 * Params array to check against.
2006 * @param int $id
2007 * Id of the entity concerned.
2008 * @param string $entity
2009 * Name of entity concerned (e.g. membership).
2010 * @param bool $delete
2011 * Should the entity be deleted as part of this check.
2012 * @param string $errorText
2013 * Text to print on error.
2014 */
2015
2016 /**
2017 * @param array $params
2018 * @param int $id
2019 * @param $entity
2020 * @param int $delete
2021 * @param string $errorText
2022 */
2023 public function getAndCheck(array $params, int $id, $entity, int $delete = 1, string $errorText = ''): void {
2024
2025 $result = $this->callAPISuccessGetSingle($entity, [
2026 'id' => $id,
2027 'return' => array_keys($params),
2028 ]);
2029
2030 if ($delete) {
2031 $this->callAPISuccess($entity, 'Delete', [
2032 'id' => $id,
2033 ]);
2034 }
2035 $dateFields = $keys = $dateTimeFields = [];
2036 $fields = $this->callAPISuccess($entity, 'getfields', ['version' => 3, 'action' => 'get']);
2037 foreach ($fields['values'] as $field => $settings) {
2038 if (array_key_exists($field, $result)) {
2039 $keys[CRM_Utils_Array::value('name', $settings, $field)] = $field;
2040 }
2041 else {
2042 $keys[CRM_Utils_Array::value('name', $settings, $field)] = CRM_Utils_Array::value('name', $settings, $field);
2043 }
2044 $type = $settings['type'] ?? NULL;
2045 if ($type === CRM_Utils_Type::T_DATE) {
2046 $dateFields[] = $settings['name'];
2047 // we should identify both real names & unique names as dates
2048 if ($field !== $settings['name']) {
2049 $dateFields[] = $field;
2050 }
2051 }
2052 if ($type === CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME) {
2053 $dateTimeFields[] = $settings['name'];
2054 // we should identify both real names & unique names as dates
2055 if ($field !== $settings['name']) {
2056 $dateTimeFields[] = $field;
2057 }
2058 }
2059 }
2060
2061 if (strtolower($entity) === 'contribution') {
2062 $params['receive_date'] = date('Y-m-d', strtotime($params['receive_date']));
2063 // this is not returned in id format
2064 unset($params['payment_instrument_id']);
2065 $params['contribution_source'] = $params['source'];
2066 unset($params['source']);
2067 }
2068
2069 foreach ($params as $key => $value) {
2070 if ($key === 'version' || strpos($key, 'api') === 0 || (!array_key_exists($key, $keys) || !array_key_exists($keys[$key], $result))) {
2071 continue;
2072 }
2073 if (in_array($key, $dateFields, TRUE)) {
2074 $value = date('Y-m-d', strtotime($value));
2075 $result[$key] = date('Y-m-d', strtotime($result[$key]));
2076 }
2077 if (in_array($key, $dateTimeFields, TRUE)) {
2078 $value = date('Y-m-d H:i:s', strtotime($value));
2079 $result[$keys[$key]] = date('Y-m-d H:i:s', strtotime(CRM_Utils_Array::value($keys[$key], $result, CRM_Utils_Array::value($key, $result))));
2080 }
2081 $this->assertEquals($value, $result[$keys[$key]], $key . " GetandCheck function determines that for key {$key} value: $value doesn't match " . print_r($result[$keys[$key]], TRUE) . $errorText);
2082 }
2083 }
2084
2085 /**
2086 * Get formatted values in the actual and expected result.
2087 *
2088 * @param array $actual
2089 * Actual calculated values.
2090 * @param array $expected
2091 * Expected values.
2092 */
2093 public function checkArrayEquals(&$actual, &$expected) {
2094 self::unsetId($actual);
2095 self::unsetId($expected);
2096 $this->assertEquals($expected, $actual);
2097 }
2098
2099 /**
2100 * Unset the key 'id' from the array
2101 *
2102 * @param array $unformattedArray
2103 * The array from which the 'id' has to be unset.
2104 */
2105 public static function unsetId(&$unformattedArray) {
2106 $formattedArray = [];
2107 if (array_key_exists('id', $unformattedArray)) {
2108 unset($unformattedArray['id']);
2109 }
2110 if (!empty($unformattedArray['values']) && is_array($unformattedArray['values'])) {
2111 foreach ($unformattedArray['values'] as $key => $value) {
2112 if (is_array($value)) {
2113 foreach ($value as $k => $v) {
2114 if ($k == 'id') {
2115 unset($value[$k]);
2116 }
2117 }
2118 }
2119 elseif ($key == 'id') {
2120 $unformattedArray[$key];
2121 }
2122 $formattedArray = [$value];
2123 }
2124 $unformattedArray['values'] = $formattedArray;
2125 }
2126 }
2127
2128 /**
2129 * Helper to enable/disable custom directory support
2130 *
2131 * @param array $customDirs
2132 * With members:.
2133 * 'php_path' Set to TRUE to use the default, FALSE or "" to disable support, or a string path to use another path
2134 * 'template_path' Set to TRUE to use the default, FALSE or "" to disable support, or a string path to use another path
2135 */
2136 public function customDirectories($customDirs) {
2137 $config = CRM_Core_Config::singleton();
2138
2139 if (empty($customDirs['php_path']) || $customDirs['php_path'] === FALSE) {
2140 unset($config->customPHPPathDir);
2141 }
2142 elseif ($customDirs['php_path'] === TRUE) {
2143 $config->customPHPPathDir = dirname(dirname(__FILE__)) . '/custom_directories/php/';
2144 }
2145 else {
2146 $config->customPHPPathDir = $php_path;
2147 }
2148
2149 if (empty($customDirs['template_path']) || $customDirs['template_path'] === FALSE) {
2150 unset($config->customTemplateDir);
2151 }
2152 elseif ($customDirs['template_path'] === TRUE) {
2153 $config->customTemplateDir = dirname(dirname(__FILE__)) . '/custom_directories/templates/';
2154 }
2155 else {
2156 $config->customTemplateDir = $template_path;
2157 }
2158 }
2159
2160 /**
2161 * Generate a temporary folder.
2162 *
2163 * @param string $prefix
2164 *
2165 * @return string
2166 */
2167 public function createTempDir($prefix = 'test-') {
2168 $tempDir = CRM_Utils_File::tempdir($prefix);
2169 $this->tempDirs[] = $tempDir;
2170 return $tempDir;
2171 }
2172
2173 public function cleanTempDirs() {
2174 if (!is_array($this->tempDirs)) {
2175 // fix test errors where this is not set
2176 return;
2177 }
2178 foreach ($this->tempDirs as $tempDir) {
2179 if (is_dir($tempDir)) {
2180 CRM_Utils_File::cleanDir($tempDir, TRUE, FALSE);
2181 }
2182 }
2183 }
2184
2185 /**
2186 * Temporarily replace the singleton extension with a different one.
2187 *
2188 * @param \CRM_Extension_System $system
2189 */
2190 public function setExtensionSystem(CRM_Extension_System $system) {
2191 if ($this->origExtensionSystem == NULL) {
2192 $this->origExtensionSystem = CRM_Extension_System::singleton();
2193 }
2194 CRM_Extension_System::setSingleton($this->origExtensionSystem);
2195 }
2196
2197 public function unsetExtensionSystem() {
2198 if ($this->origExtensionSystem !== NULL) {
2199 CRM_Extension_System::setSingleton($this->origExtensionSystem);
2200 $this->origExtensionSystem = NULL;
2201 }
2202 }
2203
2204 /**
2205 * Temporarily alter the settings-metadata to add a mock setting.
2206 *
2207 * WARNING: The setting metadata will disappear on the next cache-clear.
2208 *
2209 * @param $extras
2210 *
2211 * @return void
2212 */
2213 public function setMockSettingsMetaData($extras) {
2214 CRM_Utils_Hook::singleton()
2215 ->setHook('civicrm_alterSettingsMetaData', function (&$metadata, $domainId, $profile) use ($extras) {
2216 $metadata = array_merge($metadata, $extras);
2217 });
2218
2219 Civi::service('settings_manager')->flush();
2220
2221 $fields = $this->callAPISuccess('setting', 'getfields', []);
2222 foreach ($extras as $key => $spec) {
2223 $this->assertNotEmpty($spec['title']);
2224 $this->assertEquals($spec['title'], $fields['values'][$key]['title']);
2225 }
2226 }
2227
2228 /**
2229 * @param string $name
2230 */
2231 public function financialAccountDelete($name) {
2232 $financialAccount = new CRM_Financial_DAO_FinancialAccount();
2233 $financialAccount->name = $name;
2234 if ($financialAccount->find(TRUE)) {
2235 $entityFinancialType = new CRM_Financial_DAO_EntityFinancialAccount();
2236 $entityFinancialType->financial_account_id = $financialAccount->id;
2237 $entityFinancialType->delete();
2238 $financialAccount->delete();
2239 }
2240 }
2241
2242 /**
2243 * Set up an acl allowing contact to see 2 specified groups
2244 * - $this->_permissionedGroup & $this->_permissionedDisabledGroup
2245 *
2246 * You need to have pre-created these groups & created the user e.g
2247 * $this->createLoggedInUser();
2248 * $this->_permissionedDisabledGroup = $this->groupCreate(array('title' => 'pick-me-disabled', 'is_active' => 0, 'name' => 'pick-me-disabled'));
2249 * $this->_permissionedGroup = $this->groupCreate(array('title' => 'pick-me-active', 'is_active' => 1, 'name' => 'pick-me-active'));
2250 *
2251 * @param bool $isProfile
2252 */
2253 public function setupACL($isProfile = FALSE) {
2254 global $_REQUEST;
2255 $_REQUEST = $this->_params;
2256
2257 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM'];
2258 $optionGroupID = $this->callAPISuccessGetValue('option_group', ['return' => 'id', 'name' => 'acl_role']);
2259 $ov = new CRM_Core_DAO_OptionValue();
2260 $ov->option_group_id = $optionGroupID;
2261 $ov->value = 55;
2262 if ($ov->find(TRUE)) {
2263 CRM_Core_DAO::executeQuery("DELETE FROM civicrm_option_value WHERE id = {$ov->id}");
2264 }
2265 $optionValue = $this->callAPISuccess('option_value', 'create', [
2266 'option_group_id' => $optionGroupID,
2267 'label' => 'pick me',
2268 'value' => 55,
2269 ]);
2270
2271 CRM_Core_DAO::executeQuery("
2272 TRUNCATE civicrm_acl_cache
2273 ");
2274
2275 CRM_Core_DAO::executeQuery("
2276 TRUNCATE civicrm_acl_contact_cache
2277 ");
2278
2279 CRM_Core_DAO::executeQuery("
2280 INSERT INTO civicrm_acl_entity_role (
2281 `acl_role_id`, `entity_table`, `entity_id`, `is_active`
2282 ) VALUES (55, 'civicrm_group', {$this->_permissionedGroup}, 1);
2283 ");
2284
2285 if ($isProfile) {
2286 CRM_Core_DAO::executeQuery("
2287 INSERT INTO civicrm_acl (
2288 `name`, `entity_table`, `entity_id`, `operation`, `object_table`, `object_id`, `is_active`
2289 )
2290 VALUES (
2291 'view picked', 'civicrm_acl_role', 55, 'Edit', 'civicrm_uf_group', 0, 1
2292 );
2293 ");
2294 }
2295 else {
2296 CRM_Core_DAO::executeQuery("
2297 INSERT INTO civicrm_acl (
2298 `name`, `entity_table`, `entity_id`, `operation`, `object_table`, `object_id`, `is_active`
2299 )
2300 VALUES (
2301 'view picked', 'civicrm_group', $this->_permissionedGroup , 'Edit', 'civicrm_saved_search', {$this->_permissionedGroup}, 1
2302 );
2303 ");
2304
2305 CRM_Core_DAO::executeQuery("
2306 INSERT INTO civicrm_acl (
2307 `name`, `entity_table`, `entity_id`, `operation`, `object_table`, `object_id`, `is_active`
2308 )
2309 VALUES (
2310 'view picked', 'civicrm_group', $this->_permissionedGroup, 'Edit', 'civicrm_saved_search', {$this->_permissionedDisabledGroup}, 1
2311 );
2312 ");
2313 }
2314
2315 $this->_loggedInUser = CRM_Core_Session::singleton()->get('userID');
2316 $this->callAPISuccess('group_contact', 'create', [
2317 'group_id' => $this->_permissionedGroup,
2318 'contact_id' => $this->_loggedInUser,
2319 ]);
2320
2321 if (!$isProfile) {
2322 CRM_ACL_BAO_Cache::resetCache();
2323 }
2324 }
2325
2326 /**
2327 * Alter default price set so that the field numbers are not all 1 (hiding errors)
2328 */
2329 public function offsetDefaultPriceSet() {
2330 $contributionPriceSet = $this->callAPISuccess('price_set', 'getsingle', ['name' => 'default_contribution_amount']);
2331 $firstID = $contributionPriceSet['id'];
2332 $this->callAPISuccess('price_set', 'create', [
2333 'id' => $contributionPriceSet['id'],
2334 'is_active' => 0,
2335 'name' => 'old',
2336 ]);
2337 unset($contributionPriceSet['id']);
2338 $newPriceSet = $this->callAPISuccess('price_set', 'create', $contributionPriceSet);
2339 $priceField = $this->callAPISuccess('price_field', 'getsingle', [
2340 'price_set_id' => $firstID,
2341 'options' => ['limit' => 1],
2342 ]);
2343 unset($priceField['id']);
2344 $priceField['price_set_id'] = $newPriceSet['id'];
2345 $newPriceField = $this->callAPISuccess('price_field', 'create', $priceField);
2346 $priceFieldValue = $this->callAPISuccess('price_field_value', 'getsingle', [
2347 'price_set_id' => $firstID,
2348 'sequential' => 1,
2349 'options' => ['limit' => 1],
2350 ]);
2351
2352 unset($priceFieldValue['id']);
2353 //create some padding to use up ids
2354 $this->callAPISuccess('price_field_value', 'create', $priceFieldValue);
2355 $this->callAPISuccess('price_field_value', 'create', $priceFieldValue);
2356 $this->callAPISuccess('price_field_value', 'create', array_merge($priceFieldValue, ['price_field_id' => $newPriceField['id']]));
2357 }
2358
2359 /**
2360 * Create an instance of the paypal processor.
2361 *
2362 * @todo this isn't a great place to put it - but really it belongs on a class that extends
2363 * this parent class & we don't have a structure for that yet
2364 * There is another function to this effect on the PaypalPro test but it appears to be silently failing
2365 * & the best protection against that is the functions this class affords
2366 *
2367 * @param array $params
2368 *
2369 * @return int $result['id'] payment processor id
2370 */
2371 public function paymentProcessorCreate($params = []) {
2372 $params = array_merge([
2373 'name' => 'demo',
2374 'domain_id' => CRM_Core_Config::domainID(),
2375 'payment_processor_type_id' => 'PayPal',
2376 'is_active' => 1,
2377 'is_default' => 0,
2378 'is_test' => 1,
2379 'user_name' => 'sunil._1183377782_biz_api1.webaccess.co.in',
2380 'password' => '1183377788',
2381 'signature' => 'APixCoQ-Zsaj-u3IH7mD5Do-7HUqA9loGnLSzsZga9Zr-aNmaJa3WGPH',
2382 'url_site' => 'https://www.sandbox.paypal.com/',
2383 'url_api' => 'https://api-3t.sandbox.paypal.com/',
2384 'url_button' => 'https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif',
2385 'class_name' => 'Payment_PayPalImpl',
2386 'billing_mode' => 3,
2387 'financial_type_id' => 1,
2388 'financial_account_id' => 12,
2389 // Credit card = 1 so can pass 'by accident'.
2390 'payment_instrument_id' => 'Debit Card',
2391 ], $params);
2392 if (!is_numeric($params['payment_processor_type_id'])) {
2393 // really the api should handle this through getoptions but it's not exactly api call so lets just sort it
2394 //here
2395 $params['payment_processor_type_id'] = $this->callAPISuccess('payment_processor_type', 'getvalue', [
2396 'name' => $params['payment_processor_type_id'],
2397 'return' => 'id',
2398 ], 'integer');
2399 }
2400 $result = $this->callAPISuccess('payment_processor', 'create', $params);
2401 return $result['id'];
2402 }
2403
2404 /**
2405 * Get the rendered contents from a form.
2406 *
2407 * @param string $formName
2408 *
2409 * @return false|string
2410 */
2411 protected function getRenderedFormContents(string $formName) {
2412 $form = $this->getFormObject($formName);
2413 $form->buildForm();
2414 ob_start();
2415 $form->controller->_actions['display']->perform($form, 'display');
2416 return ob_get_clean();
2417 }
2418
2419 /**
2420 * Set up initial recurring payment allowing subsequent IPN payments.
2421 *
2422 * @param array $recurParams (Optional)
2423 * @param array $contributionParams (Optional)
2424 */
2425 public function setupRecurringPaymentProcessorTransaction(array $recurParams = [], array $contributionParams = []): void {
2426 $this->ids['campaign'][0] = $this->callAPISuccess('Campaign', 'create', ['title' => 'get the money'])['id'];
2427 $contributionParams = array_merge([
2428 'total_amount' => '200',
2429 'invoice_id' => $this->_invoiceID,
2430 'financial_type_id' => 'Donation',
2431 'contact_id' => $this->_contactID,
2432 'contribution_page_id' => $this->_contributionPageID,
2433 'payment_processor_id' => $this->_paymentProcessorID,
2434 'receive_date' => '2019-07-25 07:34:23',
2435 'skipCleanMoney' => TRUE,
2436 'amount_level' => 'expensive',
2437 'campaign_id' => $this->ids['campaign'][0],
2438 'source' => 'Online Contribution: Page name',
2439 ], $contributionParams);
2440 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', array_merge([
2441 'contact_id' => $this->_contactID,
2442 'amount' => 1000,
2443 'sequential' => 1,
2444 'installments' => 5,
2445 'frequency_unit' => 'Month',
2446 'frequency_interval' => 1,
2447 'invoice_id' => $this->_invoiceID,
2448 'contribution_status_id' => 2,
2449 'payment_processor_id' => $this->_paymentProcessorID,
2450 // processor provided ID - use contact ID as proxy.
2451 'processor_id' => $this->_contactID,
2452 'api.Order.create' => $contributionParams,
2453 ], $recurParams))['values'][0];
2454 $this->_contributionRecurID = $contributionRecur['id'];
2455 $this->_contributionID = $contributionRecur['api.Order.create']['id'];
2456 $this->ids['Contribution'][0] = $this->_contributionID;
2457 }
2458
2459 /**
2460 * We don't have a good way to set up a recurring contribution with a membership so let's just do one then alter it
2461 *
2462 * @param array $params Optionally modify params for membership/recur (duration_unit/frequency_unit)
2463 *
2464 * @throws \API_Exception
2465 */
2466 public function setupMembershipRecurringPaymentProcessorTransaction($params = []): void {
2467 $membershipParams = $recurParams = [];
2468 if (!empty($params['duration_unit'])) {
2469 $membershipParams['duration_unit'] = $params['duration_unit'];
2470 }
2471 if (!empty($params['frequency_unit'])) {
2472 $recurParams['frequency_unit'] = $params['frequency_unit'];
2473 }
2474
2475 $this->ids['membership_type'] = $this->membershipTypeCreate($membershipParams);
2476 //create a contribution so our membership & contribution don't both have id = 1
2477 if ($this->callAPISuccess('Contribution', 'getcount', []) === 0) {
2478 $this->contributionCreate([
2479 'contact_id' => $this->_contactID,
2480 'is_test' => 1,
2481 'financial_type_id' => 1,
2482 'invoice_id' => 'abcd',
2483 'trxn_id' => 345,
2484 'receive_date' => '2019-07-25 07:34:23',
2485 ]);
2486 }
2487
2488 $this->setupRecurringPaymentProcessorTransaction($recurParams, [
2489 'line_items' => [
2490 [
2491 'line_item' => [
2492 [
2493 'label' => 'General',
2494 'qty' => 1,
2495 'unit_price' => 200,
2496 'line_total' => 200,
2497 'financial_type_id' => 1,
2498 'membership_type_id' => $this->ids['membership_type'],
2499 ],
2500 ],
2501 'params' => [
2502 'contact_id' => $this->_contactID,
2503 'membership_type_id' => $this->ids['membership_type'],
2504 'source' => 'Payment',
2505 ],
2506 ],
2507 ],
2508 ]);
2509 $this->ids['membership'] = LineItem::get()
2510 ->addWhere('contribution_id', '=', $this->ids['Contribution'][0])
2511 ->addWhere('entity_table', '=', 'civicrm_membership')
2512 ->addSelect('entity_id')
2513 ->execute()->first()['entity_id'];
2514 }
2515
2516 /**
2517 * @param $message
2518 *
2519 * @throws Exception
2520 */
2521 public function CiviUnitTestCase_fatalErrorHandler($message) {
2522 throw new Exception("{$message['message']}: {$message['code']}");
2523 }
2524
2525 /**
2526 * Wrap the entire test case in a transaction.
2527 *
2528 * Only subsequent DB statements will be wrapped in TX -- this cannot
2529 * retroactively wrap old DB statements. Therefore, it makes sense to
2530 * call this at the beginning of setUp().
2531 *
2532 * Note: Recall that TRUNCATE and ALTER will force-commit transactions, so
2533 * this option does not work with, e.g., custom-data.
2534 *
2535 * WISHLIST: Monitor SQL queries in unit-tests and generate an exception
2536 * if TRUNCATE or ALTER is called while using a transaction.
2537 *
2538 * @param bool $nest
2539 * Whether to use nesting or reference-counting.
2540 */
2541 public function useTransaction($nest = TRUE) {
2542 if (!$this->tx) {
2543 $this->tx = new CRM_Core_Transaction($nest);
2544 $this->tx->rollback();
2545 }
2546 }
2547
2548 /**
2549 * Assert the attachment exists.
2550 *
2551 * @param bool $exists
2552 * @param array $apiResult
2553 */
2554 protected function assertAttachmentExistence($exists, $apiResult) {
2555 $fileId = $apiResult['id'];
2556 $this->assertTrue(is_numeric($fileId));
2557 $this->assertEquals($exists, file_exists($apiResult['values'][$fileId]['path']));
2558 $this->assertDBQuery($exists ? 1 : 0, 'SELECT count(*) FROM civicrm_file WHERE id = %1', [
2559 1 => [$fileId, 'Int'],
2560 ]);
2561 $this->assertDBQuery($exists ? 1 : 0, 'SELECT count(*) FROM civicrm_entity_file WHERE id = %1', [
2562 1 => [$fileId, 'Int'],
2563 ]);
2564 }
2565
2566 /**
2567 * Assert 2 sql strings are the same, ignoring double spaces.
2568 *
2569 * @param string $expectedSQL
2570 * @param string $actualSQL
2571 * @param string $message
2572 */
2573 protected function assertLike($expectedSQL, $actualSQL, $message = 'different sql') {
2574 $expected = trim((preg_replace('/[ \r\n\t]+/', ' ', $expectedSQL)));
2575 $actual = trim((preg_replace('/[ \r\n\t]+/', ' ', $actualSQL)));
2576 $this->assertEquals($expected, $actual, $message);
2577 }
2578
2579 /**
2580 * Create a price set for an event.
2581 *
2582 * @param int $feeTotal
2583 * @param int $minAmt
2584 * @param string $type
2585 *
2586 * @param array $options
2587 *
2588 * @return int
2589 * Price Set ID.
2590 * @throws \CRM_Core_Exception
2591 */
2592 protected function eventPriceSetCreate($feeTotal, $minAmt = 0, $type = 'Text', $options = [['name' => 'hundy', 'amount' => 100]]) {
2593 // creating price set, price field
2594 $paramsSet['title'] = 'Price Set';
2595 $paramsSet['name'] = CRM_Utils_String::titleToVar('Price Set');
2596 $paramsSet['is_active'] = FALSE;
2597 $paramsSet['extends'] = 1;
2598 $paramsSet['min_amount'] = $minAmt;
2599
2600 $priceSet = CRM_Price_BAO_PriceSet::create($paramsSet);
2601 $this->_ids['price_set'] = $priceSet->id;
2602
2603 $paramsField = [
2604 'label' => 'Price Field',
2605 'name' => CRM_Utils_String::titleToVar('Price Field'),
2606 'html_type' => $type,
2607 'price' => $feeTotal,
2608 'option_label' => ['1' => 'Price Field'],
2609 'option_value' => ['1' => $feeTotal],
2610 'option_name' => ['1' => $feeTotal],
2611 'option_weight' => ['1' => 1],
2612 'option_amount' => ['1' => 1],
2613 'is_display_amounts' => 1,
2614 'weight' => 1,
2615 'options_per_line' => 1,
2616 'is_active' => ['1' => 1],
2617 'price_set_id' => $this->_ids['price_set'],
2618 'is_enter_qty' => 1,
2619 'financial_type_id' => $this->getFinancialTypeId('Event Fee'),
2620 ];
2621 if ($type === 'Radio') {
2622 foreach ($options as $index => $option) {
2623 $paramsField['is_enter_qty'] = 0;
2624 $optionID = $index + 2;
2625 $paramsField['option_value'][$optionID] = $paramsField['option_weight'][$optionID] = $paramsField['option_amount'][$optionID] = $option['amount'];
2626 $paramsField['option_label'][$optionID] = $paramsField['option_name'][$optionID] = $option['name'];
2627 }
2628
2629 }
2630 $this->callAPISuccess('PriceField', 'create', $paramsField);
2631 $fields = $this->callAPISuccess('PriceField', 'get', ['price_set_id' => $this->_ids['price_set']]);
2632 $this->_ids['price_field'] = array_keys($fields['values']);
2633 $fieldValues = $this->callAPISuccess('PriceFieldValue', 'get', ['price_field_id' => $this->_ids['price_field'][0]]);
2634 $this->_ids['price_field_value'] = array_keys($fieldValues['values']);
2635
2636 return $this->_ids['price_set'];
2637 }
2638
2639 /**
2640 * Add a profile to a contribution page.
2641 *
2642 * @param string $name
2643 * @param int $contributionPageID
2644 * @param string $module
2645 */
2646 protected function addProfile($name, $contributionPageID, $module = 'CiviContribute') {
2647 $params = [
2648 'uf_group_id' => $name,
2649 'module' => $module,
2650 'entity_table' => 'civicrm_contribution_page',
2651 'entity_id' => $contributionPageID,
2652 'weight' => 1,
2653 ];
2654 if ($module !== 'CiviContribute') {
2655 $params['module_data'] = [$module => []];
2656 }
2657 $this->callAPISuccess('UFJoin', 'create', $params);
2658 }
2659
2660 /**
2661 * Add participant with contribution
2662 *
2663 * @return array
2664 *
2665 * @throws \CRM_Core_Exception
2666 */
2667 protected function createPartiallyPaidParticipantOrder() {
2668 $orderParams = $this->getParticipantOrderParams();
2669 $orderParams['api.Payment.create'] = ['total_amount' => 150];
2670 return $this->callAPISuccess('Order', 'create', $orderParams);
2671 }
2672
2673 /**
2674 * Create price set that includes one price field with two option values.
2675 *
2676 * @param string $component
2677 * @param int $componentId
2678 * @param array $priceFieldOptions
2679 *
2680 * @return array - the result of API3 PriceFieldValue.get for the new PriceField
2681 */
2682 protected function createPriceSet($component = 'contribution_page', $componentId = NULL, $priceFieldOptions = []) {
2683 $paramsSet['title'] = 'Price Set' . substr(sha1(rand()), 0, 7);
2684 $paramsSet['name'] = CRM_Utils_String::titleToVar($paramsSet['title']);
2685 $paramsSet['is_active'] = TRUE;
2686 $paramsSet['financial_type_id'] = 'Event Fee';
2687 $paramsSet['extends'] = 1;
2688 $priceSet = $this->callAPISuccess('price_set', 'create', $paramsSet);
2689 if ($componentId) {
2690 CRM_Price_BAO_PriceSet::addTo('civicrm_' . $component, $componentId, $priceSet['id']);
2691 }
2692 $paramsField = array_merge([
2693 'label' => 'Price Field',
2694 'name' => CRM_Utils_String::titleToVar('Price Field'),
2695 'html_type' => 'CheckBox',
2696 'option_label' => ['1' => 'Price Field 1', '2' => 'Price Field 2'],
2697 'option_value' => ['1' => 100, '2' => 200],
2698 'option_name' => ['1' => 'Price Field 1', '2' => 'Price Field 2'],
2699 'option_weight' => ['1' => 1, '2' => 2],
2700 'option_amount' => ['1' => 100, '2' => 200],
2701 'is_display_amounts' => 1,
2702 'weight' => 1,
2703 'options_per_line' => 1,
2704 'is_active' => ['1' => 1, '2' => 1],
2705 'price_set_id' => $priceSet['id'],
2706 'is_enter_qty' => 1,
2707 'financial_type_id' => $this->getFinancialTypeId('Event Fee'),
2708 ], $priceFieldOptions);
2709
2710 $priceField = CRM_Price_BAO_PriceField::create($paramsField);
2711 return $this->callAPISuccess('PriceFieldValue', 'get', ['price_field_id' => $priceField->id]);
2712 }
2713
2714 /**
2715 * Replace the template with a test-oriented template designed to show all the variables.
2716 *
2717 * @param string $templateName
2718 * @param string $input
2719 * @param string $type
2720 */
2721 protected function swapMessageTemplateForInput(string $templateName, string $input, string $type = 'html'): void {
2722 CRM_Core_DAO::executeQuery(
2723 "UPDATE civicrm_msg_template
2724 SET msg_{$type} = %1
2725 WHERE workflow_name = '{$templateName}'
2726 AND is_default = 1", [1 => [$input, 'String']]
2727 );
2728 }
2729
2730 /**
2731 * Replace the template with a test-oriented template designed to show all the variables.
2732 *
2733 * @param string $templateName
2734 * @param string $type
2735 */
2736 protected function swapMessageTemplateForTestTemplate($templateName = 'contribution_online_receipt', $type = 'html'): void {
2737 $testTemplate = file_get_contents(__DIR__ . '/../../templates/message_templates/' . $templateName . '_' . $type . '.tpl');
2738 CRM_Core_DAO::executeQuery(
2739 "UPDATE civicrm_msg_template
2740 SET msg_{$type} = %1
2741 WHERE workflow_name = '{$templateName}'
2742 AND is_default = 1", [1 => [$testTemplate, 'String']]
2743 );
2744 }
2745
2746 /**
2747 * Reinstate the default template.
2748 *
2749 * @param string $templateName
2750 * @param string $type
2751 */
2752 protected function revertTemplateToReservedTemplate($templateName = 'contribution_online_receipt', $type = 'html') {
2753 CRM_Core_DAO::executeQuery(
2754 "UPDATE civicrm_option_group og
2755 LEFT JOIN civicrm_option_value ov ON ov.option_group_id = og.id
2756 LEFT JOIN civicrm_msg_template m ON m.workflow_id = ov.id
2757 LEFT JOIN civicrm_msg_template m2 ON m2.workflow_id = ov.id AND m2.is_reserved = 1
2758 SET m.msg_{$type} = m2.msg_{$type}
2759 WHERE og.name = 'msg_tpl_workflow_contribution'
2760 AND ov.name = '{$templateName}'
2761 AND m.is_default = 1"
2762 );
2763 }
2764
2765 /**
2766 * Flush statics relating to financial type.
2767 */
2768 protected function flushFinancialTypeStatics() {
2769 if (isset(\Civi::$statics['CRM_Financial_BAO_FinancialType'])) {
2770 unset(\Civi::$statics['CRM_Financial_BAO_FinancialType']);
2771 }
2772 if (isset(\Civi::$statics['CRM_Contribute_PseudoConstant'])) {
2773 unset(\Civi::$statics['CRM_Contribute_PseudoConstant']);
2774 }
2775 CRM_Contribute_PseudoConstant::flush('financialType');
2776 CRM_Contribute_PseudoConstant::flush('membershipType');
2777 // Pseudoconstants may be saved to the cache table.
2778 CRM_Core_DAO::executeQuery("TRUNCATE civicrm_cache");
2779 CRM_Financial_BAO_FinancialType::$_statusACLFt = [];
2780 CRM_Financial_BAO_FinancialType::$_availableFinancialTypes = NULL;
2781 }
2782
2783 /**
2784 * Set the permissions to the supplied array.
2785 *
2786 * @param array $permissions
2787 */
2788 protected function setPermissions($permissions) {
2789 CRM_Core_Config::singleton()->userPermissionClass->permissions = $permissions;
2790 $this->flushFinancialTypeStatics();
2791 }
2792
2793 /**
2794 * @param array $params
2795 * @param $context
2796 */
2797 public function _checkFinancialRecords($params, $context) {
2798 $entityParams = [
2799 'entity_id' => $params['id'],
2800 'entity_table' => 'civicrm_contribution',
2801 ];
2802 $contribution = $this->callAPISuccess('Contribution', 'getsingle', [
2803 'id' => $params['id'],
2804 'return' => ['total_amount', 'fee_amount', 'net_amount'],
2805 ]);
2806 $this->assertEquals($contribution['total_amount'] - $contribution['fee_amount'], $contribution['net_amount']);
2807 if ($context === 'pending') {
2808 $trxn = CRM_Financial_BAO_FinancialItem::retrieveEntityFinancialTrxn($entityParams);
2809 $this->assertNull($trxn, 'No Trxn to be created until IPN callback');
2810 return;
2811 }
2812 $trxn = current(CRM_Financial_BAO_FinancialItem::retrieveEntityFinancialTrxn($entityParams));
2813 $trxnParams = [
2814 'id' => $trxn['financial_trxn_id'],
2815 ];
2816 if ($context !== 'online' && $context !== 'payLater') {
2817 $compareParams = [
2818 'to_financial_account_id' => 6,
2819 'total_amount' => (float) CRM_Utils_Array::value('total_amount', $params, 100.00),
2820 'status_id' => 1,
2821 ];
2822 }
2823 if ($context === 'feeAmount') {
2824 $compareParams['fee_amount'] = 50;
2825 }
2826 elseif ($context === 'online') {
2827 $compareParams = [
2828 'to_financial_account_id' => 12,
2829 'total_amount' => (float) CRM_Utils_Array::value('total_amount', $params, 100.00),
2830 'status_id' => 1,
2831 'payment_instrument_id' => CRM_Utils_Array::value('payment_instrument_id', $params, 1),
2832 ];
2833 }
2834 elseif ($context == 'payLater') {
2835 $compareParams = [
2836 'to_financial_account_id' => 7,
2837 'total_amount' => (float) CRM_Utils_Array::value('total_amount', $params, 100.00),
2838 'status_id' => 2,
2839 ];
2840 }
2841 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialTrxn', $trxnParams, $compareParams);
2842 $entityParams = [
2843 'financial_trxn_id' => $trxn['financial_trxn_id'],
2844 'entity_table' => 'civicrm_financial_item',
2845 ];
2846 $entityTrxn = current(CRM_Financial_BAO_FinancialItem::retrieveEntityFinancialTrxn($entityParams));
2847 $fitemParams = [
2848 'id' => $entityTrxn['entity_id'],
2849 ];
2850 $compareParams = [
2851 'amount' => (float) CRM_Utils_Array::value('total_amount', $params, 100.00),
2852 'status_id' => 1,
2853 'financial_account_id' => CRM_Utils_Array::value('financial_account_id', $params, 1),
2854 ];
2855 if ($context === 'payLater') {
2856 $compareParams = [
2857 'amount' => (float) CRM_Utils_Array::value('total_amount', $params, 100.00),
2858 'status_id' => 3,
2859 'financial_account_id' => CRM_Utils_Array::value('financial_account_id', $params, 1),
2860 ];
2861 }
2862 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialItem', $fitemParams, $compareParams);
2863 if ($context == 'feeAmount') {
2864 $maxParams = [
2865 'entity_id' => $params['id'],
2866 'entity_table' => 'civicrm_contribution',
2867 ];
2868 $maxTrxn = current(CRM_Financial_BAO_FinancialItem::retrieveEntityFinancialTrxn($maxParams, TRUE));
2869 $trxnParams = [
2870 'id' => $maxTrxn['financial_trxn_id'],
2871 ];
2872 $compareParams = [
2873 'to_financial_account_id' => 5,
2874 'from_financial_account_id' => 6,
2875 'total_amount' => 50,
2876 'status_id' => 1,
2877 ];
2878 $trxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($params['id'], 'DESC');
2879 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialTrxn', $trxnParams, $compareParams);
2880 $fitemParams = [
2881 'entity_id' => $trxnId['financialTrxnId'],
2882 'entity_table' => 'civicrm_financial_trxn',
2883 ];
2884 $compareParams = [
2885 'amount' => 50.00,
2886 'status_id' => 1,
2887 'financial_account_id' => 5,
2888 ];
2889 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialItem', $fitemParams, $compareParams);
2890 }
2891 // This checks that empty Sales tax rows are not being created. If for any reason it needs to be removed the
2892 // line should be copied into all the functions that call this function & evaluated there
2893 // Be really careful not to remove or bypass this without ensuring stray rows do not re-appear
2894 // when calling completeTransaction or repeatTransaction.
2895 $this->callAPISuccessGetCount('FinancialItem', ['description' => 'Sales Tax', 'amount' => 0], 0);
2896 }
2897
2898 /**
2899 * Return financial type id on basis of name
2900 *
2901 * @param string $name Financial type m/c name
2902 *
2903 * @return int
2904 */
2905 public function getFinancialTypeId($name) {
2906 return CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_FinancialType', $name, 'id', 'name');
2907 }
2908
2909 /**
2910 * Cleanup function for contents of $this->ids.
2911 *
2912 * This is a best effort cleanup to use in tear downs etc.
2913 *
2914 * It will not fail if the data has already been removed (some tests may do
2915 * their own cleanup).
2916 */
2917 protected function cleanUpSetUpIDs() {
2918 foreach ($this->setupIDs as $entity => $id) {
2919 try {
2920 civicrm_api3($entity, 'delete', ['id' => $id, 'skip_undelete' => 1]);
2921 }
2922 catch (CiviCRM_API3_Exception $e) {
2923 // This is a best-effort cleanup function, ignore.
2924 }
2925 }
2926 }
2927
2928 /**
2929 * Create Financial Type.
2930 *
2931 * @param array $params
2932 *
2933 * @return array
2934 */
2935 protected function createFinancialType($params = []) {
2936 $params = array_merge($params,
2937 [
2938 'name' => 'Financial-Type -' . substr(sha1(rand()), 0, 7),
2939 'is_active' => 1,
2940 ]
2941 );
2942 return $this->callAPISuccess('FinancialType', 'create', $params);
2943 }
2944
2945 /**
2946 * Create Payment Instrument.
2947 *
2948 * @param array $params
2949 * @param string $financialAccountName
2950 *
2951 * @return int
2952 */
2953 protected function createPaymentInstrument($params = [], $financialAccountName = 'Donation') {
2954 $params = array_merge([
2955 'label' => 'Payment Instrument -' . substr(sha1(rand()), 0, 7),
2956 'option_group_id' => 'payment_instrument',
2957 'is_active' => 1,
2958 ], $params);
2959 $newPaymentInstrument = $this->callAPISuccess('OptionValue', 'create', $params)['id'];
2960
2961 $relationTypeID = key(CRM_Core_PseudoConstant::accountOptionValues('account_relationship', NULL, " AND v.name LIKE 'Asset Account is' "));
2962
2963 $financialAccountParams = [
2964 'entity_table' => 'civicrm_option_value',
2965 'entity_id' => $newPaymentInstrument,
2966 'account_relationship' => $relationTypeID,
2967 'financial_account_id' => $this->callAPISuccess('FinancialAccount', 'getValue', ['name' => $financialAccountName, 'return' => 'id']),
2968 ];
2969 CRM_Financial_BAO_FinancialTypeAccount::add($financialAccountParams);
2970
2971 return CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', $params['label']);
2972 }
2973
2974 /**
2975 * Enable Tax and Invoicing
2976 *
2977 * @param array $params
2978 */
2979 protected function enableTaxAndInvoicing(array $params = []): void {
2980 // Enable component contribute setting
2981 $contributeSetting = array_merge($params,
2982 [
2983 'invoicing' => 1,
2984 'invoice_prefix' => 'INV_',
2985 'invoice_due_date' => 10,
2986 'invoice_due_date_period' => 'days',
2987 'invoice_notes' => '',
2988 'invoice_is_email_pdf' => 1,
2989 'tax_term' => 'Sales Tax',
2990 'tax_display_settings' => 'Inclusive',
2991 ]
2992 );
2993 foreach ($contributeSetting as $setting => $value) {
2994 Civi::settings()->set($setting, $value);
2995 }
2996 }
2997
2998 /**
2999 * Enable Tax and Invoicing
3000 */
3001 protected function disableTaxAndInvoicing(): void {
3002 $accounts = $this->callAPISuccess('EntityFinancialAccount', 'get', ['account_relationship' => 'Sales Tax Account is'])['values'];
3003 foreach ($accounts as $account) {
3004 $this->callAPISuccess('EntityFinancialAccount', 'delete', ['id' => $account['id']]);
3005 $this->callAPISuccess('FinancialAccount', 'delete', ['id' => $account['financial_account_id']]);
3006 }
3007
3008 if (!empty(\Civi::$statics['CRM_Core_PseudoConstant']) && isset(\Civi::$statics['CRM_Core_PseudoConstant']['taxRates'])) {
3009 unset(\Civi::$statics['CRM_Core_PseudoConstant']['taxRates']);
3010 }
3011 Civi::settings()->set('invoice_is_email_pdf', FALSE);
3012 Civi::settings()->set('invoicing', FALSE);
3013 }
3014
3015 /**
3016 * Add Sales Tax Account for the financial type.
3017 *
3018 * @param int $financialTypeId
3019 *
3020 * @param array $accountParams
3021 *
3022 * @return CRM_Financial_DAO_EntityFinancialAccount
3023 * @throws \CRM_Core_Exception
3024 */
3025 protected function addTaxAccountToFinancialType(int $financialTypeId, $accountParams = []) {
3026 $params = array_merge([
3027 'name' => 'Sales tax account ' . substr(sha1(rand()), 0, 4),
3028 'financial_account_type_id' => key(CRM_Core_PseudoConstant::accountOptionValues('financial_account_type', NULL, " AND v.name LIKE 'Liability' ")),
3029 'is_deductible' => 1,
3030 'is_tax' => 1,
3031 'tax_rate' => 10,
3032 'is_active' => 1,
3033 ], $accountParams);
3034 $account = CRM_Financial_BAO_FinancialAccount::add($params);
3035 $entityParams = [
3036 'entity_table' => 'civicrm_financial_type',
3037 'entity_id' => $financialTypeId,
3038 'account_relationship' => key(CRM_Core_PseudoConstant::accountOptionValues('account_relationship', NULL, " AND v.name LIKE 'Sales Tax Account is' ")),
3039 ];
3040
3041 // set tax rate (as 10) for provided financial type ID to static variable, later used to fetch tax rates of all financial types
3042 \Civi::$statics['CRM_Core_PseudoConstant']['taxRates'][$financialTypeId] = $params['tax_rate'];
3043
3044 //CRM-20313: As per unique index added in civicrm_entity_financial_account table,
3045 // first check if there's any record on basis of unique key (entity_table, account_relationship, entity_id)
3046 $dao = new CRM_Financial_DAO_EntityFinancialAccount();
3047 $dao->copyValues($entityParams);
3048 $dao->find();
3049 if ($dao->fetch()) {
3050 $entityParams['id'] = $dao->id;
3051 }
3052 $entityParams['financial_account_id'] = $account->id;
3053
3054 return CRM_Financial_BAO_FinancialTypeAccount::add($entityParams);
3055 }
3056
3057 /**
3058 * Create price set with contribution test for test setup.
3059 *
3060 * This could be merged with 4.5 function setup in api_v3_ContributionPageTest::setUpContributionPage
3061 * on parent class at some point (fn is not in 4.4).
3062 *
3063 * @param $entity
3064 * @param array $params
3065 */
3066 public function createPriceSetWithPage($entity = NULL, $params = []) {
3067 $membershipTypeID = $this->membershipTypeCreate(['name' => 'Special']);
3068 $contributionPageResult = $this->callAPISuccess('contribution_page', 'create', [
3069 'title' => 'Test Contribution Page',
3070 'financial_type_id' => 1,
3071 'currency' => 'NZD',
3072 'goal_amount' => 50,
3073 'is_pay_later' => 1,
3074 'is_monetary' => TRUE,
3075 'is_email_receipt' => FALSE,
3076 ]);
3077 $priceSet = $this->callAPISuccess('price_set', 'create', [
3078 'is_quick_config' => 0,
3079 'extends' => 'CiviMember',
3080 'financial_type_id' => 1,
3081 'title' => 'my Page',
3082 ]);
3083 $priceSetID = $priceSet['id'];
3084
3085 CRM_Price_BAO_PriceSet::addTo('civicrm_contribution_page', $contributionPageResult['id'], $priceSetID);
3086 $priceField = $this->callAPISuccess('price_field', 'create', [
3087 'price_set_id' => $priceSetID,
3088 'label' => 'Goat Breed',
3089 'html_type' => 'Radio',
3090 ]);
3091 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
3092 'price_set_id' => $priceSetID,
3093 'price_field_id' => $priceField['id'],
3094 'label' => 'Long Haired Goat',
3095 'amount' => 20,
3096 'financial_type_id' => 'Donation',
3097 'membership_type_id' => $membershipTypeID,
3098 'membership_num_terms' => 1,
3099 ]);
3100 $this->_ids['price_field_value'] = [$priceFieldValue['id']];
3101 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
3102 'price_set_id' => $priceSetID,
3103 'price_field_id' => $priceField['id'],
3104 'label' => 'Shoe-eating Goat',
3105 'amount' => 10,
3106 'financial_type_id' => 'Donation',
3107 'membership_type_id' => $membershipTypeID,
3108 'membership_num_terms' => 2,
3109 ]);
3110 $this->_ids['price_field_value'][] = $priceFieldValue['id'];
3111
3112 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
3113 'price_set_id' => $priceSetID,
3114 'price_field_id' => $priceField['id'],
3115 'label' => 'Shoe-eating Goat',
3116 'amount' => 10,
3117 'financial_type_id' => 'Donation',
3118 ]);
3119 $this->_ids['price_field_value']['cont'] = $priceFieldValue['id'];
3120
3121 $this->_ids['price_set'] = $priceSetID;
3122 $this->_ids['contribution_page'] = $contributionPageResult['id'];
3123 $this->_ids['price_field'] = [$priceField['id']];
3124
3125 $this->_ids['membership_type'] = $membershipTypeID;
3126 }
3127
3128 /**
3129 * Only specified contact returned.
3130 *
3131 * @implements CRM_Utils_Hook::aclWhereClause
3132 *
3133 * @param $type
3134 * @param $tables
3135 * @param $whereTables
3136 * @param $contactID
3137 * @param $where
3138 */
3139 public function aclWhereMultipleContacts($type, &$tables, &$whereTables, &$contactID, &$where) {
3140 $where = " contact_a.id IN (" . implode(', ', $this->allowedContacts) . ")";
3141 }
3142
3143 /**
3144 * @implements CRM_Utils_Hook::selectWhereClause
3145 *
3146 * @param string $entity
3147 * @param array $clauses
3148 */
3149 public function selectWhereClauseHook($entity, &$clauses) {
3150 if ($entity == 'Event') {
3151 $clauses['event_type_id'][] = "IN (2, 3, 4)";
3152 }
3153 }
3154
3155 /**
3156 * An implementation of hook_civicrm_post used with all our test cases.
3157 *
3158 * @param $op
3159 * @param string $objectName
3160 * @param int $objectId
3161 * @param $objectRef
3162 */
3163 public function onPost($op, $objectName, $objectId, &$objectRef) {
3164 if ($op == 'create' && $objectName == 'Individual') {
3165 CRM_Core_DAO::executeQuery(
3166 "UPDATE civicrm_contact SET nick_name = 'munged' WHERE id = %1",
3167 [
3168 1 => [$objectId, 'Integer'],
3169 ]
3170 );
3171 }
3172
3173 if ($op == 'edit' && $objectName == 'Participant') {
3174 $params = [
3175 1 => [$objectId, 'Integer'],
3176 ];
3177 $query = "UPDATE civicrm_participant SET source = 'Post Hook Update' WHERE id = %1";
3178 CRM_Core_DAO::executeQuery($query, $params);
3179 }
3180 }
3181
3182 /**
3183 * Instantiate form object.
3184 *
3185 * We need to instantiate the form to run preprocess, which means we have to trick it about the request method.
3186 *
3187 * @param string $class
3188 * Name of form class.
3189 *
3190 * @param array $formValues
3191 *
3192 * @param string $pageName
3193 *
3194 * @param array $searchFormValues
3195 * Values for the search form if the form is a task eg.
3196 * for selected ids 6 & 8:
3197 * [
3198 * 'radio_ts' => 'ts_sel',
3199 * 'task' => CRM_Member_Task::PDF_LETTER,
3200 * 'mark_x_6' => 1,
3201 * 'mark_x_8' => 1,
3202 * ]
3203 *
3204 * @return \CRM_Core_Form
3205 */
3206 public function getFormObject($class, $formValues = [], $pageName = '', $searchFormValues = []) {
3207 $_POST = $formValues;
3208 /* @var CRM_Core_Form $form */
3209 $form = new $class();
3210 $_SERVER['REQUEST_METHOD'] = 'GET';
3211 switch ($class) {
3212 case 'CRM_Event_Cart_Form_Checkout_Payment':
3213 case 'CRM_Event_Cart_Form_Checkout_ParticipantsAndPrices':
3214 $form->controller = new CRM_Event_Cart_Controller_Checkout();
3215 break;
3216
3217 case strpos($class, '_Form_') !== FALSE:
3218 $form->controller = new CRM_Core_Controller_Simple($class, $pageName);
3219 break;
3220
3221 default:
3222 $form->controller = new CRM_Core_Controller();
3223 }
3224 if (!$pageName) {
3225 $pageName = $form->getName();
3226 }
3227 $form->controller->setStateMachine(new CRM_Core_StateMachine($form->controller));
3228 $_SESSION['_' . $form->controller->_name . '_container']['values'][$pageName] = $formValues;
3229 if ($searchFormValues) {
3230 $_SESSION['_' . $form->controller->_name . '_container']['values']['Search'] = $searchFormValues;
3231 }
3232 if (isset($formValues['_qf_button_name'])) {
3233 $_SESSION['_' . $form->controller->_name . '_container']['_qf_button_name'] = $formValues['_qf_button_name'];
3234 }
3235 return $form;
3236 }
3237
3238 /**
3239 * Get possible thousand separators.
3240 *
3241 * @return array
3242 */
3243 public function getThousandSeparators() {
3244 return [['.'], [',']];
3245 }
3246
3247 /**
3248 * Get the boolean options as a provider.
3249 *
3250 * @return array
3251 */
3252 public function getBooleanDataProvider() {
3253 return [[TRUE], [FALSE]];
3254 }
3255
3256 /**
3257 * Set the separators for thousands and decimal points.
3258 *
3259 * Note that this only covers some common scenarios.
3260 *
3261 * It does not cater for a situation where the thousand separator is a [space]
3262 * Latter is the Norwegian localization. At least some tests need to
3263 * use setMonetaryDecimalPoint and setMonetaryThousandSeparator directly
3264 * to provide broader coverage.
3265 *
3266 * @param string $thousandSeparator
3267 */
3268 protected function setCurrencySeparators($thousandSeparator) {
3269 Civi::settings()->set('monetaryThousandSeparator', $thousandSeparator);
3270 Civi::settings()->set('monetaryDecimalPoint', ($thousandSeparator === ',' ? '.' : ','));
3271 }
3272
3273 /**
3274 * Sets the thousand separator.
3275 *
3276 * If you use this function also set the decimal separator: setMonetaryDecimalSeparator
3277 *
3278 * @param $thousandSeparator
3279 */
3280 protected function setMonetaryThousandSeparator($thousandSeparator) {
3281 Civi::settings()->set('monetaryThousandSeparator', $thousandSeparator);
3282 }
3283
3284 /**
3285 * Sets the decimal separator.
3286 *
3287 * If you use this function also set the thousand separator setMonetaryDecimalPoint
3288 *
3289 * @param $decimalPoint
3290 */
3291 protected function setMonetaryDecimalPoint($decimalPoint) {
3292 Civi::settings()->set('monetaryDecimalPoint', $decimalPoint);
3293 }
3294
3295 /**
3296 * Sets the default currency.
3297 *
3298 * @param $currency
3299 */
3300 protected function setDefaultCurrency($currency) {
3301 Civi::settings()->set('defaultCurrency', $currency);
3302 }
3303
3304 /**
3305 * Format money as it would be input.
3306 *
3307 * @param string $amount
3308 *
3309 * @return string
3310 */
3311 protected function formatMoneyInput($amount) {
3312 return CRM_Utils_Money::format($amount, NULL, '%a');
3313 }
3314
3315 /**
3316 * Get the contribution object.
3317 *
3318 * @param int $contributionID
3319 *
3320 * @return \CRM_Contribute_BAO_Contribution
3321 */
3322 protected function getContributionObject($contributionID) {
3323 $contributionObj = new CRM_Contribute_BAO_Contribution();
3324 $contributionObj->id = $contributionID;
3325 $contributionObj->find(TRUE);
3326 return $contributionObj;
3327 }
3328
3329 /**
3330 * Enable multilingual.
3331 */
3332 public function enableMultilingual() {
3333 $this->callAPISuccess('Setting', 'create', [
3334 'lcMessages' => 'en_US',
3335 'languageLimit' => [
3336 'en_US' => 1,
3337 ],
3338 ]);
3339
3340 CRM_Core_I18n_Schema::makeMultilingual('en_US');
3341
3342 global $dbLocale;
3343 $dbLocale = '_en_US';
3344 }
3345
3346 /**
3347 * Setup or clean up SMS tests
3348 *
3349 * @param bool $teardown
3350 *
3351 * @throws \CiviCRM_API3_Exception
3352 */
3353 public function setupForSmsTests($teardown = FALSE) {
3354 require_once 'CiviTest/CiviTestSMSProvider.php';
3355
3356 // Option value params for CiviTestSMSProvider
3357 $groupID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', 'sms_provider_name', 'id', 'name');
3358 $params = [
3359 'option_group_id' => $groupID,
3360 'label' => 'unittestSMS',
3361 'value' => 'unit.test.sms',
3362 'name' => 'CiviTestSMSProvider',
3363 'is_default' => 1,
3364 'is_active' => 1,
3365 'version' => 3,
3366 ];
3367
3368 if ($teardown) {
3369 // Test completed, delete provider
3370 $providerOptionValueResult = civicrm_api3('option_value', 'get', $params);
3371 civicrm_api3('option_value', 'delete', ['id' => $providerOptionValueResult['id']]);
3372 return;
3373 }
3374
3375 // Create an SMS provider "CiviTestSMSProvider". Civi handles "CiviTestSMSProvider" as a special case and allows it to be instantiated
3376 // in CRM/Sms/Provider.php even though it is not an extension.
3377 return civicrm_api3('option_value', 'create', $params);
3378 }
3379
3380 /**
3381 * Start capturing browser output.
3382 *
3383 * The starts the process of browser output being captured, setting any variables needed for e-notice prevention.
3384 */
3385 protected function startCapturingOutput() {
3386 ob_start();
3387 $_SERVER['HTTP_USER_AGENT'] = 'unittest';
3388 }
3389
3390 /**
3391 * Stop capturing browser output and return as a csv.
3392 *
3393 * @param bool $isFirstRowHeaders
3394 *
3395 * @return \League\Csv\Reader
3396 *
3397 * @throws \League\Csv\Exception
3398 */
3399 protected function captureOutputToCSV($isFirstRowHeaders = TRUE) {
3400 $output = ob_get_flush();
3401 $stream = fopen('php://memory', 'r+');
3402 fwrite($stream, $output);
3403 rewind($stream);
3404 $this->assertEquals("\xEF\xBB\xBF", substr($output, 0, 3));
3405 $csv = Reader::createFromString($output);
3406 if ($isFirstRowHeaders) {
3407 $csv->setHeaderOffset(0);
3408 }
3409 ob_clean();
3410 return $csv;
3411 }
3412
3413 /**
3414 * Rename various labels to not match the names.
3415 *
3416 * Doing these mimics the fact the name != the label in international installs & triggers failures in
3417 * code that expects it to.
3418 */
3419 protected function renameLabels() {
3420 $replacements = ['Pending', 'Refunded'];
3421 foreach ($replacements as $name) {
3422 CRM_Core_DAO::executeQuery("UPDATE civicrm_option_value SET label = '{$name} Label**' where label = '{$name}' AND name = '{$name}'");
3423 }
3424 }
3425
3426 /**
3427 * Undo any label renaming.
3428 */
3429 protected function resetLabels() {
3430 CRM_Core_DAO::executeQuery("UPDATE civicrm_option_value SET label = REPLACE(name, ' Label**', '') WHERE label LIKE '% Label**'");
3431 }
3432
3433 /**
3434 * Get parameters to set up a multi-line participant order.
3435 *
3436 * @return array
3437 * @throws \CRM_Core_Exception
3438 */
3439 protected function getParticipantOrderParams(): array {
3440 $event = $this->eventCreate();
3441 $this->_eventId = $event['id'];
3442 $eventParams = [
3443 'id' => $this->_eventId,
3444 'financial_type_id' => 4,
3445 'is_monetary' => 1,
3446 ];
3447 $this->callAPISuccess('event', 'create', $eventParams);
3448 $priceFields = $this->createPriceSet('event', $this->_eventId);
3449 $orderParams = [
3450 'total_amount' => 300,
3451 'currency' => 'USD',
3452 'contact_id' => $this->individualCreate(),
3453 'financial_type_id' => 4,
3454 'contribution_status_id' => 'Pending',
3455 ];
3456 foreach ($priceFields['values'] as $key => $priceField) {
3457 $orderParams['line_items'][] = [
3458 'line_item' => [
3459 [
3460 'price_field_id' => $priceField['price_field_id'],
3461 'price_field_value_id' => $priceField['id'],
3462 'label' => $priceField['label'],
3463 'field_title' => $priceField['label'],
3464 'qty' => 1,
3465 'unit_price' => $priceField['amount'],
3466 'line_total' => $priceField['amount'],
3467 'financial_type_id' => $priceField['financial_type_id'],
3468 'entity_table' => 'civicrm_participant',
3469 ],
3470 ],
3471 'params' => [
3472 'financial_type_id' => 4,
3473 'event_id' => $this->_eventId,
3474 'role_id' => 1,
3475 'status_id' => 14,
3476 'fee_currency' => 'USD',
3477 'contact_id' => $this->individualCreate(),
3478 ],
3479 ];
3480 }
3481 return $orderParams;
3482 }
3483
3484 /**
3485 * @param $payments
3486 *
3487 * @throws \CRM_Core_Exception
3488 */
3489 protected function validatePayments($payments): void {
3490 foreach ($payments as $payment) {
3491 $balance = CRM_Contribute_BAO_Contribution::getContributionBalance($payment['contribution_id']);
3492 if ($balance < 0 && $balance + $payment['total_amount'] === 0.0) {
3493 // This is an overpayment situation. there are no financial items to allocate the overpayment.
3494 // This is a pretty rough way at guessing which payment is the overpayment - but
3495 // for the test suite it should be enough.
3496 continue;
3497 }
3498 $items = $this->callAPISuccess('EntityFinancialTrxn', 'get', [
3499 'financial_trxn_id' => $payment['id'],
3500 'entity_table' => 'civicrm_financial_item',
3501 'return' => ['amount'],
3502 ])['values'];
3503 $itemTotal = 0;
3504 foreach ($items as $item) {
3505 $itemTotal += $item['amount'];
3506 }
3507 $this->assertEquals($payment['total_amount'], $itemTotal);
3508 }
3509 }
3510
3511 /**
3512 * Validate all created payments.
3513 *
3514 * @throws \CRM_Core_Exception
3515 */
3516 protected function validateAllPayments(): void {
3517 $payments = $this->callAPISuccess('Payment', 'get', [
3518 'return' => ['total_amount', 'tax_amount'],
3519 'options' => ['limit' => 0],
3520 ])['values'];
3521 $this->validatePayments($payments);
3522 }
3523
3524 /**
3525 * Validate all created contributions.
3526 *
3527 * @throws \API_Exception
3528 */
3529 protected function validateAllContributions(): void {
3530 $contributions = Contribution::get(FALSE)->setSelect(['total_amount', 'tax_amount'])->execute();
3531 foreach ($contributions as $contribution) {
3532 $lineItems = $this->callAPISuccess('LineItem', 'get', [
3533 'contribution_id' => $contribution['id'],
3534 'return' => ['tax_amount', 'line_total', 'entity_table', 'entity_id', 'qty'],
3535 ])['values'];
3536 $total = 0;
3537 $taxTotal = 0;
3538 $memberships = [];
3539 $participants = [];
3540 foreach ($lineItems as $lineItem) {
3541 $total += $lineItem['line_total'];
3542 $taxTotal += (float) ($lineItem['tax_amount'] ?? 0);
3543 if ($lineItem['entity_table'] === 'civicrm_membership') {
3544 $memberships[] = $lineItem['entity_id'];
3545 }
3546 if ($lineItem['entity_table'] === 'civicrm_participant' && $lineItem['qty'] > 0) {
3547 $participants[$lineItem['entity_id']] = $lineItem['entity_id'];
3548 }
3549 }
3550 $membershipPayments = $this->callAPISuccess('MembershipPayment', 'get', ['contribution_id' => $contribution['id'], 'return' => 'membership_id'])['values'];
3551 $participantPayments = $this->callAPISuccess('ParticipantPayment', 'get', ['contribution_id' => $contribution['id'], 'return' => 'participant_id'])['values'];
3552 $this->assertCount(count($memberships), $membershipPayments);
3553 $this->assertCount(count($participants), $participantPayments);
3554 foreach ($membershipPayments as $payment) {
3555 $this->assertContains($payment['membership_id'], $memberships);
3556 }
3557 foreach ($participantPayments as $payment) {
3558 $this->assertContains($payment['participant_id'], $participants);
3559 }
3560 $this->assertEquals($taxTotal, (float) ($contribution['tax_amount'] ?? 0));
3561 $this->assertEquals($total + $taxTotal, $contribution['total_amount']);
3562 }
3563 }
3564
3565 /**
3566 * @return array|int
3567 */
3568 protected function createRuleGroup() {
3569 return $this->callAPISuccess('RuleGroup', 'create', [
3570 'contact_type' => 'Individual',
3571 'threshold' => 8,
3572 'used' => 'General',
3573 'name' => 'TestRule',
3574 'title' => 'TestRule',
3575 'is_reserved' => 0,
3576 ]);
3577 }
3578
3579 /**
3580 * Generic create test.
3581 *
3582 * @param int $version
3583 *
3584 * @throws \CRM_Core_Exception
3585 */
3586 protected function basicCreateTest(int $version): void {
3587 $this->_apiversion = $version;
3588 $result = $this->callAPIAndDocument($this->_entity, 'create', $this->params, __FUNCTION__, __FILE__);
3589 $this->assertEquals(1, $result['count']);
3590 $this->assertNotNull($result['values'][$result['id']]['id']);
3591 $this->getAndCheck($this->params, $result['id'], $this->_entity);
3592 }
3593
3594 /**
3595 * Generic delete test.
3596 *
3597 * @param int $version
3598 *
3599 * @throws \CRM_Core_Exception
3600 */
3601 protected function basicDeleteTest(int $version): void {
3602 $this->_apiversion = $version;
3603 $result = $this->callAPISuccess($this->_entity, 'create', $this->params);
3604 $deleteParams = ['id' => $result['id']];
3605 $this->callAPIAndDocument($this->_entity, 'delete', $deleteParams, __FUNCTION__, __FILE__);
3606 $checkDeleted = $this->callAPISuccess($this->_entity, 'get', []);
3607 $this->assertEquals(0, $checkDeleted['count']);
3608 }
3609
3610 /**
3611 * Create and return a case object for the given Client ID.
3612 *
3613 * @param int $clientId
3614 * @param int $loggedInUser
3615 * Omit or pass NULL to use the same as clientId
3616 * @param array $extra
3617 * Optional specific parameters such as start_date
3618 *
3619 * @return CRM_Case_BAO_Case
3620 */
3621 public function createCase($clientId, $loggedInUser = NULL, $extra = []) {
3622 if (empty($loggedInUser)) {
3623 // backwards compatibility - but it's more typical that the creator is a different person than the client
3624 $loggedInUser = $clientId;
3625 }
3626 $caseParams = array_merge([
3627 'activity_subject' => 'Case Subject',
3628 'client_id' => $clientId,
3629 'case_type_id' => 1,
3630 'status_id' => 1,
3631 'case_type' => 'housing_support',
3632 'subject' => 'Case Subject',
3633 'start_date' => date('Y-m-d'),
3634 'start_date_time' => date('YmdHis'),
3635 'medium_id' => 2,
3636 'activity_details' => '',
3637 ], $extra);
3638 $form = new CRM_Case_Form_Case();
3639 return $form->testSubmit($caseParams, 'OpenCase', $loggedInUser, 'standalone');
3640 }
3641
3642 /**
3643 * Validate that all location entities have exactly one primary.
3644 *
3645 * This query takes about 2 minutes on a DB with 10s of millions of contacts.
3646 */
3647 public function assertLocationValidity(): void {
3648 $this->assertEquals(0, CRM_Core_DAO::singleValueQuery('SELECT COUNT(*) FROM
3649
3650 (SELECT a1.contact_id
3651 FROM civicrm_address a1
3652 LEFT JOIN civicrm_address a2 ON a1.id <> a2.id AND a2.is_primary = 1
3653 AND a1.contact_id = a2.contact_id
3654 WHERE
3655 a1.is_primary = 1
3656 AND a2.id IS NOT NULL
3657 AND a1.contact_id IS NOT NULL
3658 UNION
3659 SELECT a1.contact_id
3660 FROM civicrm_address a1
3661 LEFT JOIN civicrm_address a2 ON a1.id <> a2.id AND a2.is_primary = 1
3662 AND a1.contact_id = a2.contact_id
3663 WHERE a1.is_primary = 0
3664 AND a2.id IS NULL
3665 AND a1.contact_id IS NOT NULL
3666
3667 UNION
3668
3669 SELECT a1.contact_id
3670 FROM civicrm_email a1
3671 LEFT JOIN civicrm_email a2 ON a1.id <> a2.id AND a2.is_primary = 1
3672 AND a1.contact_id = a2.contact_id
3673 WHERE
3674 a1.is_primary = 1
3675 AND a2.id IS NOT NULL
3676 AND a1.contact_id IS NOT NULL
3677 UNION
3678 SELECT a1.contact_id
3679 FROM civicrm_email a1
3680 LEFT JOIN civicrm_email a2 ON a1.id <> a2.id AND a2.is_primary = 1
3681 AND a1.contact_id = a2.contact_id
3682 WHERE a1.is_primary = 0
3683 AND a2.id IS NULL
3684 AND a1.contact_id IS NOT NULL
3685
3686 UNION
3687
3688 SELECT a1.contact_id
3689 FROM civicrm_phone a1
3690 LEFT JOIN civicrm_phone a2 ON a1.id <> a2.id AND a2.is_primary = 1
3691 AND a1.contact_id = a2.contact_id
3692 WHERE
3693 a1.is_primary = 1
3694 AND a2.id IS NOT NULL
3695 AND a1.contact_id IS NOT NULL
3696 UNION
3697 SELECT a1.contact_id
3698 FROM civicrm_phone a1
3699 LEFT JOIN civicrm_phone a2 ON a1.id <> a2.id AND a2.is_primary = 1
3700 AND a1.contact_id = a2.contact_id
3701 WHERE a1.is_primary = 0
3702 AND a2.id IS NULL
3703 AND a1.contact_id IS NOT NULL
3704
3705 UNION
3706
3707 SELECT a1.contact_id
3708 FROM civicrm_im a1
3709 LEFT JOIN civicrm_im a2 ON a1.id <> a2.id AND a2.is_primary = 1
3710 AND a1.contact_id = a2.contact_id
3711 WHERE
3712 a1.is_primary = 1
3713 AND a2.id IS NOT NULL
3714 AND a1.contact_id IS NOT NULL
3715 UNION
3716 SELECT a1.contact_id
3717 FROM civicrm_im a1
3718 LEFT JOIN civicrm_im a2 ON a1.id <> a2.id AND a2.is_primary = 1
3719 AND a1.contact_id = a2.contact_id
3720 WHERE a1.is_primary = 0
3721 AND a2.id IS NULL
3722 AND a1.contact_id IS NOT NULL
3723
3724 UNION
3725
3726 SELECT a1.contact_id
3727 FROM civicrm_openid a1
3728 LEFT JOIN civicrm_openid a2 ON a1.id <> a2.id AND a2.is_primary = 1
3729 AND a1.contact_id = a2.contact_id
3730 WHERE (a1.is_primary = 1 AND a2.id IS NOT NULL)
3731 UNION
3732
3733 SELECT a1.contact_id
3734 FROM civicrm_openid a1
3735 LEFT JOIN civicrm_openid a2 ON a1.id <> a2.id AND a2.is_primary = 1
3736 AND a1.contact_id = a2.contact_id
3737 WHERE
3738 a1.is_primary = 1
3739 AND a2.id IS NOT NULL
3740 AND a1.contact_id IS NOT NULL
3741 UNION
3742 SELECT a1.contact_id
3743 FROM civicrm_openid a1
3744 LEFT JOIN civicrm_openid a2 ON a1.id <> a2.id AND a2.is_primary = 1
3745 AND a1.contact_id = a2.contact_id
3746 WHERE a1.is_primary = 0
3747 AND a2.id IS NULL
3748 AND a1.contact_id IS NOT NULL) as primary_descrepancies
3749 '));
3750 }
3751
3752 /**
3753 * Ensure the specified mysql mode/s are activated.
3754 *
3755 * @param array $modes
3756 */
3757 protected function ensureMySQLMode(array $modes): void {
3758 $currentModes = array_fill_keys(CRM_Utils_SQL::getSqlModes(), 1);
3759 $currentModes = array_merge($currentModes, array_fill_keys($modes, 1));
3760 CRM_Core_DAO::executeQuery("SET GLOBAL sql_mode = '" . implode(',', array_keys($currentModes)) . "'");
3761 CRM_Core_DAO::executeQuery("SET sql_mode = '" . implode(',', array_keys($currentModes)) . "'");
3762 }
3763
3764 /**
3765 * Delete any extraneous relationship types.
3766 *
3767 * @throws \API_Exception
3768 * @throws \Civi\API\Exception\UnauthorizedException
3769 */
3770 protected function deleteNonDefaultRelationshipTypes(): void {
3771 RelationshipType::delete(FALSE)->addWhere('name_a_b', 'NOT IN', [
3772 'Child of',
3773 'Spouse of',
3774 'Partner of',
3775 'Sibling of',
3776 'Employee of',
3777 'Volunteer for',
3778 'Head of Household for',
3779 'Household Member of',
3780 'Case Coordinator is',
3781 'Supervised by',
3782 ])->execute();
3783 }
3784
3785 /**
3786 * Delete any existing custom data groups.
3787 */
3788 protected function cleanupCustomGroups(): void {
3789 try {
3790 CustomField::get(FALSE)->setSelect(['option_group_id', 'custom_group_id'])
3791 ->addChain('delete_options', OptionGroup::delete()
3792 ->addWhere('id', '=', '$option_group_id')
3793 )
3794 ->addChain('delete_fields', CustomField::delete()
3795 ->addWhere('id', '=', '$id')
3796 )->execute();
3797
3798 CustomGroup::delete(FALSE)->addWhere('id', '>', 0)->execute();
3799 }
3800 catch (API_Exception $e) {
3801 $this->fail('failed to cleanup custom groups ' . $e->getMessage());
3802 }
3803 }
3804
3805 /**
3806 * Ensure the default price set & field exist for memberships.
3807 */
3808 protected function ensureMembershipPriceSetExists(): void {
3809 CRM_Core_DAO::executeQuery("INSERT INTO civicrm_price_set (`id`, `name`, `title`, `extends`)
3810 VALUES (2, 'default_membership_type_amount', 'Membership Amount', 3)
3811 ON DUPLICATE KEY UPDATE `name` = 'default_membership_type_amount', title = 'Membership Amount';
3812 ");
3813 CRM_Core_DAO::executeQuery("INSERT INTO civicrm_price_field
3814 (`id`, `name`, `price_set_id`, `label`, `html_type`)
3815 VALUES (2, 1, 2, 'Membership Amount', 'Radio')
3816 ON DUPLICATE KEY UPDATE `name` = '1', price_set_id = 1, label = 'Membership Amount', html_type = 'Radio'
3817 ");
3818 }
3819
3820 /**
3821 * Add an address block to the current domain.
3822 *
3823 * @noinspection PhpUnhandledExceptionInspection
3824 */
3825 protected function addLocationBlockToDomain(): void {
3826 $contactID = CRM_Core_BAO_Domain::getDomain()->contact_id;
3827 Phone::create()
3828 ->setValues(['phone' => 123, 'contact_id' => $contactID])
3829 ->execute()
3830 ->first()['id'];
3831 Address::create()->setValues([
3832 'street_address' => '10 Downing Street',
3833 'city' => 'London',
3834 'contact_id' => $contactID,
3835 ])->execute()->first();
3836 }
3837
3838 }