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