From 78e5b83c1da7fa970016c090539f4e894832a468 Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Tue, 25 Mar 2025 22:02:52 +0000 Subject: [PATCH] Add first test, and fix the bugs it found! --- Civi/Api4/Action/ContactCategory/GetFlows.php | 59 +++++--- Civi/Api4/ContactCategory.php | 8 + .../Action/ContactCategory/GetFlowsTest.php | 137 ++++++++++++++++++ 3 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 tests/phpunit/Civi/Api4/Action/ContactCategory/GetFlowsTest.php diff --git a/Civi/Api4/Action/ContactCategory/GetFlows.php b/Civi/Api4/Action/ContactCategory/GetFlows.php index 82e88b8..502c630 100644 --- a/Civi/Api4/Action/ContactCategory/GetFlows.php +++ b/Civi/Api4/Action/ContactCategory/GetFlows.php @@ -19,26 +19,37 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction { /** * Start Date * - * @required * @var string + * @required + * @default NULL */ - protected string $startDate; + protected $startDate = ''; /** * End Date (optional) * - * @var string|NULL + * @var string|null * @default NULL */ protected ?string $endDate = NULL; + private string $catChangesTableName; + private int $activityTypeId; + public function _run(Result $result) { Civi::log()->debug('Begin', ['=start' => 'GetFlows', '=timed' => 1, 'from' => $this->startDate, 'to' => $this->endDate]); + $this->catChangesTableName = \Civi\Api4\CustomGroup::get(FALSE) + ->addWhere('name', '=', 'Category_changes') + ->execute()->single()['table_name']; + $this->activityTypeId = (int) \Civi\Api4\OptionValue::get(FALSE) + ->addWhere('name', '=', 'changed_contact_category') + ->addWhere('option_group_id:name', '=', 'activity_type') + ->execute()->single()['value']; // Check dates are valid and get a Ymd format of the day *following* the specified one. // This is so we can find activities *before* this date and thereby capture activities that // occurred *on* the given date. - $getValidDate = fn(?string $input) => $input ? (new DateTimeImmutable($input))->modify('+1 day')->format('Ymd'): FALSE; + $getValidDate = fn(?string $input) => $input ? (new DateTimeImmutable($input))->modify('+1 day')->format('Ymd') : FALSE; $endDate = NULL; $startDate = $getValidDate($this->startDate); @@ -54,7 +65,7 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction { throw new CRM_Core_Exception(E::ts("This version of CiviCRM does not support twisting the space time continuum. End date cannot be before start date.")); } } - else if ($startDate > date('Ymd', strtotime('tomorrow'))) { + elseif ($startDate > date('Ymd', strtotime('tomorrow'))) { throw new CRM_Core_Exception(E::ts("This version of CiviCRM does not support predicting the future. Start time cannot be in the future.")); } @@ -79,18 +90,19 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction { */ protected function windowFunctionsSupported() { $cache = \CRM_Utils_Cache::create(['type' => ['SqlGroup'], 'name' => 'contactcats']); - $supported = $cache->get('windowFunctionsSupported'); // Will return default if cached value expired. - $supported = null; + // Will return default if cached value expired. + $supported = $cache->get('windowFunctionsSupported'); + $supported = NULL; if ($supported === NULL) { // Need to test for window functions. $supported = FALSE; // Prevent Civi handling possible execption - $handler = set_exception_handler(fn() => false); + $handler = set_exception_handler(fn() => FALSE); try { \CRM_Core_DAO::executeQuery("CREATE TEMPORARY TABLE contactcats_window_check (i int unsigned not null, x int unsigned not null)"); \CRM_Core_DAO::executeQuery("INSERT INTO contactcats_window_check VALUES (1, 1), (2, 1), (3, 2);"); $rows = \CRM_Core_DAO::executeQuery("SELECT x, ROW_NUMBER() OVER ( PARTITION BY (x) ORDER BY i DESC) rn FROM contactcats_window_check ORDER BY x, rn")->fetchAll(); - $supported = ($rows === [ [ 'x' => "1", 'rn' => "1"], [ 'x' => "1", 'rn' => "2"], [ 'x' => "2", 'rn' => "1"] ]) ; + $supported = ($rows === [['x' => "1", 'rn' => "1"], ['x' => "1", 'rn' => "2"], ['x' => "2", 'rn' => "1"]]); } catch (\Exception $e) { // Silent fail. @@ -114,9 +126,8 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction { FROM civicrm_activity a INNER JOIN civicrm_activity_contact ac ON a.id = ac.activity_id AND ac.record_type_id = 3 - INNER JOIN civicrm_contact_category cc - ON cc.id = ac.contact_id - WHERE a.activity_type_id = 89 + /*INNER JOIN civicrm_contact_category cc ON cc.id = ac.contact_id */ + WHERE a.activity_type_id = $this->activityTypeId ), /* startActivity is the latest activity before the window's start date */ @@ -141,15 +152,15 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction { WHERE a2.activity_date_time BETWEEN %1 AND %2 ) - /* join startActivity and 2 to count changes between each shift */ - SELECT cat1.new_category_id from_category_id, cat2.new_category_id to_category_id, count(*) contact_count + /* join startActivity and endActivity to count changes between each shift */ + SELECT startCat.new_category_id from_category_id, endCat.new_category_id to_category_id, count(*) contact_count FROM endActivity - INNER JOIN civicrm_value_category_chan_41 cat1 - ON endActivity.activity_id = cat1.entity_id + INNER JOIN $this->catChangesTableName endCat + ON endActivity.activity_id = endCat.entity_id LEFT JOIN ( startActivity - INNER JOIN civicrm_value_category_chan_41 cat2 - ON startActivity.activity_id = cat2.entity_id + INNER JOIN $this->catChangesTableName startCat + ON startActivity.activity_id = startCat.entity_id ) ON startActivity.contact_id = endActivity.contact_id AND startActivity.rn = 1 WHERE endActivity.rn = 1 @@ -157,6 +168,8 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction { ; SQL; + // print "\n$sql\n %1 $startDateYmd, %2 $endDateYmd\n"; + if (!$endDateYmd) { $endDateYmd = date('Ymd', strtotime('tomorrow')); } @@ -168,14 +181,18 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction { // Don't use exchange array so as not to gazump non-array data(?) foreach ($data as $row) { $result[] = [ - 'from_category_id' => (int) $row['new_category_id'], + 'from_category_id' => (int) $row['from_category_id'], 'to_category_id' => (int) $row['to_category_id'], 'contact_count' => (int) $row['contact_count'], ]; } } - + protected function dump(string $msg, string $sql) { + print "\n$msg ===============================\n$sql\n"; + $data = CRM_Core_DAO::executeQuery($sql)->fetchAll(); + print_r($data); + print "\n ===============================\n"; + } } - diff --git a/Civi/Api4/ContactCategory.php b/Civi/Api4/ContactCategory.php index 46c3bde..dea52bc 100644 --- a/Civi/Api4/ContactCategory.php +++ b/Civi/Api4/ContactCategory.php @@ -1,6 +1,7 @@ installMe(__DIR__) + ->apply(); + } + + public function setUp():void { + parent::setUp(); + } + + public function tearDown():void { + parent::tearDown(); + } + + /** + * Example: Test that a version is returned. + */ + // public function testWellFormedVersion():void { + // $this->assertNotEmpty(E::SHORT_NAME); + // $this->assertMatchesRegularExpression('/^([0-9\.]|alpha|beta)*$/', \CRM_Utils_System::version()); + + /** + * } + */ + public function testGetFlows():void { + // Create groups for our cats. + [$amazingGroupID, $mehGroupID] = Group::save(FALSE) + ->setRecords([ + ['name' => 'amazing', 'frontend_title' => 'amazing'], + ['name' => 'meh', 'frontend_title' => 'meh'], + ])->execute()->column('id'); + + // Create cats. + [$amazingCatID, $mehCatID] = ContactCategoryDefinition::save(FALSE) + ->setRecords([ + [ + 'label' => 'Amazing', + 'search_type:name' => 'group', + 'search_data' => ['group_id' => $amazingGroupID], + 'presentation_order' => 1, + 'execution_order' => 1, + 'color' => '#ff0000', + ], + [ + 'label' => 'Meh', + 'search_type:name' => 'group', + 'search_data' => ['group_id' => $mehGroupID], + 'presentation_order' => 2, + 'execution_order' => 2, + 'color' => '#fff000', + ], + ]) + ->execute()->column('id'); + + // Create a contact + $ctID = Contact::create(FALSE) + ->setValues([ + 'contact_type' => 'Individual', + 'display_name' => 'Wilma', + ])->execute()->first()['id']; + // print "xxxxx created ct $ctID\n"; + + // Create two activities. + $activityIDs = Activity::save(FALSE) + ->setDefaults([ + 'activity_type_id:name' => 'changed_contact_category', + 'target_contact_id' => $ctID, + 'source_contact_id' => $ctID, + ]) + ->setRecords([ + [ + 'activity_date_time' => '2025-01-01', + 'Category_changes.new_category_id' => $amazingCatID, + ], + [ + 'activity_date_time' => '2025-02-01', + 'Category_changes.new_category_id' => $mehCatID, + ], + ])->execute()->column('id'); + + // $acts = Activity::get(FALSE)->addWhere('id', 'IN', $activityIDs)->execute()->getArrayCopy(); print_r($acts); + + // FIXME: this is returning backwards. + $flows = ContactCategory::getFlows() + ->setStartDate('2025-01-03') + ->execute()->getArrayCopy(); + // print_r($flows); + $this->assertEquals([ + ['from_category_id' => $amazingCatID, 'to_category_id' => $mehCatID, 'contact_count' => 1], + ], $flows); + } + +}