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