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