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