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