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