From b4e0bdc6edd6f4274e47e8a99ce38d139c394a01 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:47:51 +0100 Subject: [PATCH 1/6] Clean up related candidate records on delete --- lib/Candidates.php | 74 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/lib/Candidates.php b/lib/Candidates.php index 5020326aa..f4a23a9b4 100755 --- a/lib/Candidates.php +++ b/lib/Candidates.php @@ -369,10 +369,10 @@ public function update($candidateID, $isActive, $firstName, $middleName, $lastNa */ public function delete($candidateID) { - /* Delete the candidate from candidate. */ + /* Delete pipeline entries from candidate_joborder. */ $sql = sprintf( "DELETE FROM - candidate + candidate_joborder WHERE candidate_id = %s AND @@ -382,13 +382,55 @@ public function delete($candidateID) ); $this->_db->query($sql); - $history = new History($this->_siteID); - $history->storeHistoryDeleted(DATA_ITEM_CANDIDATE, $candidateID); + /* Delete pipeline history from candidate_joborder_status_history. */ + $sql = sprintf( + "DELETE FROM + candidate_joborder_status_history + WHERE + candidate_id = %s + AND + site_id = %s", + $this->_db->makeQueryInteger($candidateID), + $this->_siteID + ); + $this->_db->query($sql); - /* Delete pipeline entries from candidate_joborder. */ + /* Delete candidate activity entries. */ $sql = sprintf( "DELETE FROM - candidate_joborder + activity + WHERE + data_item_type = %s + AND + data_item_id = %s + AND + site_id = %s", + DATA_ITEM_CANDIDATE, + $this->_db->makeQueryInteger($candidateID), + $this->_siteID + ); + $this->_db->query($sql); + + /* Delete candidate calendar events. */ + $sql = sprintf( + "DELETE FROM + calendar_event + WHERE + data_item_type = %s + AND + data_item_id = %s + AND + site_id = %s", + DATA_ITEM_CANDIDATE, + $this->_db->makeQueryInteger($candidateID), + $this->_siteID + ); + $this->_db->query($sql); + + /* Delete candidate tags. */ + $sql = sprintf( + "DELETE FROM + candidate_tag WHERE candidate_id = %s AND @@ -398,10 +440,10 @@ public function delete($candidateID) ); $this->_db->query($sql); - /* Delete pipeline history from candidate_joborder_status_history. */ + /* Delete candidate questionnaire history. */ $sql = sprintf( "DELETE FROM - candidate_joborder_status_history + career_portal_questionnaire_history WHERE candidate_id = %s AND @@ -453,6 +495,22 @@ public function delete($candidateID) /* Delete extra fields. */ $this->extraFields->deleteValueByDataItemID($candidateID); + + /* Delete the candidate from candidate. */ + $sql = sprintf( + "DELETE FROM + candidate + WHERE + candidate_id = %s + AND + site_id = %s", + $this->_db->makeQueryInteger($candidateID), + $this->_siteID + ); + $this->_db->query($sql); + + $history = new History($this->_siteID); + $history->storeHistoryDeleted(DATA_ITEM_CANDIDATE, $candidateID); } /** From 5de65dea6a6e1adc0d14a699d46a30b83ef82710 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:50:28 +0100 Subject: [PATCH 2/6] Clean up related company records on delete --- lib/Companies.php | 77 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/lib/Companies.php b/lib/Companies.php index 6f4c3330a..bc0a9bdc0 100755 --- a/lib/Companies.php +++ b/lib/Companies.php @@ -218,22 +218,6 @@ public function update($companyID, $name, $address, $address2, $city, $state, */ public function delete($companyID) { - /* Delete the company. */ - $sql = sprintf( - "DELETE FROM - company - WHERE - company_id = %s - AND - site_id = %s", - $companyID, - $this->_siteID - ); - $this->_db->query($sql); - - $history = new History($this->_siteID); - $history->storeHistoryDeleted(DATA_ITEM_COMPANY, $companyID); - /* Find associated contacts. */ $sql = sprintf( "SELECT @@ -290,6 +274,51 @@ public function delete($companyID) $attachments->delete($row['attachmentID']); } + /* Delete company activity entries. */ + $sql = sprintf( + "DELETE FROM + activity + WHERE + data_item_type = %s + AND + data_item_id = %s + AND + site_id = %s", + DATA_ITEM_COMPANY, + $this->_db->makeQueryInteger($companyID), + $this->_siteID + ); + $this->_db->query($sql); + + /* Delete company calendar events. */ + $sql = sprintf( + "DELETE FROM + calendar_event + WHERE + data_item_type = %s + AND + data_item_id = %s + AND + site_id = %s", + DATA_ITEM_COMPANY, + $this->_db->makeQueryInteger($companyID), + $this->_siteID + ); + $this->_db->query($sql); + + /* Delete company departments. */ + $sql = sprintf( + "DELETE FROM + company_department + WHERE + company_id = %s + AND + site_id = %s", + $this->_db->makeQueryInteger($companyID), + $this->_siteID + ); + $this->_db->query($sql); + /* Delete from saved lists. */ $sql = sprintf( "DELETE FROM @@ -308,6 +337,22 @@ public function delete($companyID) /* Delete extra fields. */ $this->extraFields->deleteValueByDataItemID($companyID); + + /* Delete the company. */ + $sql = sprintf( + "DELETE FROM + company + WHERE + company_id = %s + AND + site_id = %s", + $companyID, + $this->_siteID + ); + $this->_db->query($sql); + + $history = new History($this->_siteID); + $history->storeHistoryDeleted(DATA_ITEM_COMPANY, $companyID); } /** From 8c2ae01c0a19c9b9b96f9992451d843c3d25d747 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:52:14 +0100 Subject: [PATCH 3/6] Clean up related contact records on delete --- lib/Contacts.php | 79 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/lib/Contacts.php b/lib/Contacts.php index e2670d541..428eaf110 100755 --- a/lib/Contacts.php +++ b/lib/Contacts.php @@ -357,8 +357,24 @@ public function updateByCompany($companyID, $address, $address2, $city, public function delete($contactID) { $sql = sprintf( - "DELETE FROM + "UPDATE contact + SET + reports_to = -1 + WHERE + reports_to = %s + AND + site_id = %s", + $this->_db->makeQueryInteger($contactID), + $this->_siteID + ); + $this->_db->query($sql); + + $sql = sprintf( + "UPDATE + joborder + SET + contact_id = -1 WHERE contact_id = %s AND @@ -370,18 +386,61 @@ public function delete($contactID) $sql = sprintf( "UPDATE - contact + company SET - reports_to = -1 + billing_contact = -1 WHERE - reports_to = %s + billing_contact = %s + AND + site_id = %s", + $this->_db->makeQueryInteger($contactID), + $this->_siteID + ); + $this->_db->query($sql); + + /* Delete contact activity entries. */ + $sql = sprintf( + "DELETE FROM + activity + WHERE + data_item_type = %s + AND + data_item_id = %s + AND + site_id = %s", + DATA_ITEM_CONTACT, + $this->_db->makeQueryInteger($contactID), + $this->_siteID + ); + $this->_db->query($sql); + + /* Delete contact calendar events. */ + $sql = sprintf( + "DELETE FROM + calendar_event + WHERE + data_item_type = %s + AND + data_item_id = %s AND site_id = %s", + DATA_ITEM_CONTACT, $this->_db->makeQueryInteger($contactID), $this->_siteID ); $this->_db->query($sql); + /* Delete attachments. */ + $attachments = new Attachments($this->_siteID); + $attachmentsRS = $attachments->getAll( + DATA_ITEM_CONTACT, $contactID + ); + + foreach ($attachmentsRS as $row) + { + $attachments->delete($row['attachmentID']); + } + /* Delete from saved lists. */ $sql = sprintf( "DELETE FROM @@ -401,6 +460,18 @@ public function delete($contactID) /* Delete extra fields. */ $this->extraFields->deleteValueByDataItemID($contactID); + $sql = sprintf( + "DELETE FROM + contact + WHERE + contact_id = %s + AND + site_id = %s", + $this->_db->makeQueryInteger($contactID), + $this->_siteID + ); + $this->_db->query($sql); + $history = new History($this->_siteID); $history->storeHistoryDeleted(DATA_ITEM_CONTACT, $contactID); } From d63d94a649beb0a259b028be3a0d5c91e9f41d6c Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:53:55 +0100 Subject: [PATCH 4/6] Clean up related job order records on delete --- lib/JobOrders.php | 80 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/lib/JobOrders.php b/lib/JobOrders.php index 43f7b69e3..93ad305ae 100755 --- a/lib/JobOrders.php +++ b/lib/JobOrders.php @@ -271,10 +271,10 @@ public function update($jobOrderID, $title, $companyJobID, $companyID, */ public function delete($jobOrderID) { - /* Delete the job order. */ + /* Delete pipeline entries from candidate_joborder. */ $sql = sprintf( "DELETE FROM - joborder + candidate_joborder WHERE joborder_id = %s AND @@ -284,14 +284,10 @@ public function delete($jobOrderID) ); $this->_db->query($sql); - /* Store history. */ - $history = new History($this->_siteID); - $history->storeHistoryDeleted(DATA_ITEM_JOBORDER, $jobOrderID); - - /* Delete pipeline entries from candidate_joborder. */ + /* Delete pipeline history from candidate_joborder_status_history. */ $sql = sprintf( "DELETE FROM - candidate_joborder + candidate_joborder_status_history WHERE joborder_id = %s AND @@ -301,10 +297,57 @@ public function delete($jobOrderID) ); $this->_db->query($sql); - /* Delete pipeline history from candidate_joborder_status_history. */ + /* Delete job order activity entries. */ $sql = sprintf( "DELETE FROM - candidate_joborder_status_history + activity + WHERE + data_item_type = %s + AND + data_item_id = %s + AND + site_id = %s", + DATA_ITEM_JOBORDER, + $this->_db->makeQueryInteger($jobOrderID), + $this->_siteID + ); + $this->_db->query($sql); + + /* Delete job order calendar events. */ + $sql = sprintf( + "DELETE FROM + calendar_event + WHERE + data_item_type = %s + AND + data_item_id = %s + AND + site_id = %s", + DATA_ITEM_JOBORDER, + $this->_db->makeQueryInteger($jobOrderID), + $this->_siteID + ); + $this->_db->query($sql); + + $sql = sprintf( + "UPDATE + activity + SET + joborder_id = NULL + WHERE + joborder_id = %s + AND + site_id = %s", + $this->_db->makeQueryInteger($jobOrderID), + $this->_siteID + ); + $this->_db->query($sql); + + $sql = sprintf( + "UPDATE + calendar_event + SET + joborder_id = -1 WHERE joborder_id = %s AND @@ -343,6 +386,23 @@ public function delete($jobOrderID) /* Delete extra fields. */ $this->extraFields->deleteValueByDataItemID($jobOrderID); + + /* Delete the job order. */ + $sql = sprintf( + "DELETE FROM + joborder + WHERE + joborder_id = %s + AND + site_id = %s", + $this->_db->makeQueryInteger($jobOrderID), + $this->_siteID + ); + $this->_db->query($sql); + + /* Store history. */ + $history = new History($this->_siteID); + $history->storeHistoryDeleted(DATA_ITEM_JOBORDER, $jobOrderID); } /** From b8550bc99030f43966987f70f4d47f034716ce63 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:17:23 +0100 Subject: [PATCH 5/6] Add schema cleanup for orphaned entity records --- modules/install/Schema.php | 4 + modules/install/scripts/377.php | 409 ++++++++++++++++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 modules/install/scripts/377.php diff --git a/modules/install/Schema.php b/modules/install/Schema.php index 5562d4ccf..d719d6db2 100755 --- a/modules/install/Schema.php +++ b/modules/install/Schema.php @@ -1447,6 +1447,10 @@ public static function get() SET short_description = \'Not reached\' WHERE activity_type_id = 100; ', + '377' => 'PHP: + include_once(\'modules/install/scripts/377.php\'); + update_377($db); + ', ); } diff --git a/modules/install/scripts/377.php b/modules/install/scripts/377.php new file mode 100644 index 000000000..07d058ef2 --- /dev/null +++ b/modules/install/scripts/377.php @@ -0,0 +1,409 @@ +query( + "SELECT DISTINCT + site_id, + directory_name + FROM attachment + WHERE ( + data_item_type = 100 + AND NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = attachment.data_item_id + AND candidate.site_id = attachment.site_id + ) + ) OR ( + data_item_type = 200 + AND NOT EXISTS ( + SELECT 1 + FROM company + WHERE company.company_id = attachment.data_item_id + AND company.site_id = attachment.site_id + ) + ) OR ( + data_item_type = 300 + AND NOT EXISTS ( + SELECT 1 + FROM contact + WHERE contact.contact_id = attachment.data_item_id + AND contact.site_id = attachment.site_id + ) + ) OR ( + data_item_type = 400 + AND NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = attachment.data_item_id + AND joborder.site_id = attachment.site_id + ) + )" + ); + + while ($directoryData = mysqli_fetch_assoc($orphanAttachmentDirectoriesRS)) + { + $directoryName = trim($directoryData['directory_name']); + if ($directoryName == '') + { + continue; + } + + $siteID = (int) $directoryData['site_id']; + $orphanAttachmentDirectories[$siteID . ':' . $directoryName] = array( + 'siteID' => $siteID, + 'directoryName' => $directoryName + ); + } + + $queries = array( + /* Remove orphaned entity-owned activity rows. */ + "DELETE FROM activity + WHERE data_item_type = 100 + AND NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = activity.data_item_id + AND candidate.site_id = activity.site_id + )", + "DELETE FROM activity + WHERE data_item_type = 200 + AND NOT EXISTS ( + SELECT 1 + FROM company + WHERE company.company_id = activity.data_item_id + AND company.site_id = activity.site_id + )", + "DELETE FROM activity + WHERE data_item_type = 300 + AND NOT EXISTS ( + SELECT 1 + FROM contact + WHERE contact.contact_id = activity.data_item_id + AND contact.site_id = activity.site_id + )", + "DELETE FROM activity + WHERE data_item_type = 400 + AND NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = activity.data_item_id + AND joborder.site_id = activity.site_id + )", + + /* Remove orphaned entity-owned calendar rows. */ + "DELETE FROM calendar_event + WHERE data_item_type = 100 + AND NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = calendar_event.data_item_id + AND candidate.site_id = calendar_event.site_id + )", + "DELETE FROM calendar_event + WHERE data_item_type = 200 + AND NOT EXISTS ( + SELECT 1 + FROM company + WHERE company.company_id = calendar_event.data_item_id + AND company.site_id = calendar_event.site_id + )", + "DELETE FROM calendar_event + WHERE data_item_type = 300 + AND NOT EXISTS ( + SELECT 1 + FROM contact + WHERE contact.contact_id = calendar_event.data_item_id + AND contact.site_id = calendar_event.site_id + )", + "DELETE FROM calendar_event + WHERE data_item_type = 400 + AND NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = calendar_event.data_item_id + AND joborder.site_id = calendar_event.site_id + )", + + /* Remove orphaned entity-owned attachment rows. */ + "DELETE FROM attachment + WHERE data_item_type = 100 + AND NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = attachment.data_item_id + AND candidate.site_id = attachment.site_id + )", + "DELETE FROM attachment + WHERE data_item_type = 200 + AND NOT EXISTS ( + SELECT 1 + FROM company + WHERE company.company_id = attachment.data_item_id + AND company.site_id = attachment.site_id + )", + "DELETE FROM attachment + WHERE data_item_type = 300 + AND NOT EXISTS ( + SELECT 1 + FROM contact + WHERE contact.contact_id = attachment.data_item_id + AND contact.site_id = attachment.site_id + )", + "DELETE FROM attachment + WHERE data_item_type = 400 + AND NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = attachment.data_item_id + AND joborder.site_id = attachment.site_id + )", + + /* Remove orphaned candidate-related rows. */ + "DELETE FROM candidate_tag + WHERE NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = candidate_tag.candidate_id + AND candidate.site_id = candidate_tag.site_id + )", + "DELETE FROM career_portal_questionnaire_history + WHERE NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = career_portal_questionnaire_history.candidate_id + AND candidate.site_id = career_portal_questionnaire_history.site_id + )", + "DELETE FROM candidate_duplicates + WHERE NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = candidate_duplicates.old_candidate_id + AND candidate.site_id = candidate_duplicates.site_id + ) + OR NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = candidate_duplicates.new_candidate_id + AND candidate.site_id = candidate_duplicates.site_id + )", + + /* Remove orphaned company-related rows. */ + "DELETE FROM company_department + WHERE NOT EXISTS ( + SELECT 1 + FROM company + WHERE company.company_id = company_department.company_id + AND company.site_id = company_department.site_id + )", + + /* Remove orphaned pipeline rows. */ + "DELETE FROM candidate_joborder + WHERE NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = candidate_joborder.candidate_id + AND candidate.site_id = candidate_joborder.site_id + ) + OR NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = candidate_joborder.joborder_id + AND joborder.site_id = candidate_joborder.site_id + )", + "DELETE FROM candidate_joborder_status_history + WHERE NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = candidate_joborder_status_history.candidate_id + AND candidate.site_id = candidate_joborder_status_history.site_id + ) + OR NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = candidate_joborder_status_history.joborder_id + AND joborder.site_id = candidate_joborder_status_history.site_id + )", + + /* Remove orphaned saved list entries by site-scoped data item. */ + "DELETE FROM saved_list_entry + WHERE data_item_type = 100 + AND NOT EXISTS ( + SELECT 1 + FROM candidate + WHERE candidate.candidate_id = saved_list_entry.data_item_id + AND candidate.site_id = saved_list_entry.site_id + )", + "DELETE FROM saved_list_entry + WHERE data_item_type = 200 + AND NOT EXISTS ( + SELECT 1 + FROM company + WHERE company.company_id = saved_list_entry.data_item_id + AND company.site_id = saved_list_entry.site_id + )", + "DELETE FROM saved_list_entry + WHERE data_item_type = 300 + AND NOT EXISTS ( + SELECT 1 + FROM contact + WHERE contact.contact_id = saved_list_entry.data_item_id + AND contact.site_id = saved_list_entry.site_id + )", + "DELETE FROM saved_list_entry + WHERE data_item_type = 400 + AND NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = saved_list_entry.data_item_id + AND joborder.site_id = saved_list_entry.site_id + )", + + /* Reset orphaned references to the current no-link value. */ + "UPDATE activity + SET joborder_id = NULL + WHERE joborder_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = activity.joborder_id + AND joborder.site_id = activity.site_id + )", + "UPDATE calendar_event + SET joborder_id = -1 + WHERE joborder_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM joborder + WHERE joborder.joborder_id = calendar_event.joborder_id + AND joborder.site_id = calendar_event.site_id + )", + "UPDATE joborder + SET contact_id = -1 + WHERE NOT EXISTS ( + SELECT 1 + FROM contact + WHERE contact.contact_id = joborder.contact_id + AND contact.site_id = joborder.site_id + )", + "UPDATE company + SET billing_contact = -1 + WHERE NOT EXISTS ( + SELECT 1 + FROM contact + WHERE contact.contact_id = company.billing_contact + AND contact.site_id = company.site_id + )", + "UPDATE contact + SET reports_to = -1 + WHERE NOT EXISTS ( + SELECT 1 + FROM contact AS reports_to_contact + WHERE reports_to_contact.contact_id = contact.reports_to + AND reports_to_contact.site_id = contact.site_id + )" + ); + + foreach ($queries as $query) + { + $db->query($query); + } + + /* Remove orphaned attachment directories if no metadata still references them. */ + foreach ($orphanAttachmentDirectories as $directoryData) + { + $sql = sprintf( + "SELECT + attachment_id + FROM + attachment + WHERE + site_id = %s + AND + directory_name = %s + LIMIT 1", + $db->makeQueryInteger($directoryData['siteID']), + $db->makeQueryString($directoryData['directoryName']) + ); + $stillReferenced = $db->getAssoc($sql); + if (!empty($stillReferenced)) + { + continue; + } + + $directoryName = trim($directoryData['directoryName']); + if ($directoryName == '' || $directoryName == '.') + { + continue; + } + + $attachmentsRoot = @realpath('attachments'); + if ($attachmentsRoot === false) + { + continue; + } + + $directory = @realpath('attachments/' . $directoryName); + if ($directory === false || $directory == $attachmentsRoot) + { + continue; + } + + if (strpos($directory, $attachmentsRoot . DIRECTORY_SEPARATOR) !== 0) + { + continue; + } + + $stack = array(array($directory, false)); + while (!empty($stack)) + { + $item = array_pop($stack); + $path = $item[0]; + $visited = $item[1]; + + if ($visited) + { + @rmdir($path); + continue; + } + + $stack[] = array($path, true); + + $handle = @opendir($path); + if (!$handle) + { + continue; + } + + while (($file = readdir($handle)) !== false) + { + if ($file == '.' || $file == '..') + { + continue; + } + + $childPath = $path . '/' . $file; + if (is_dir($childPath)) + { + $stack[] = array($childPath, false); + } + else + { + @unlink($childPath); + } + } + + closedir($handle); + } + } +} + +?> From b1c23cc112ad77b55218a8b67d144b3d8ebbcadd Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Mon, 4 May 2026 13:32:50 +0200 Subject: [PATCH 6/6] Add entity delete cleanup regression tests --- .../EntityDeleteCleanupTest.php | 340 ++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 src/OpenCATS/Tests/IntegrationTests/EntityDeleteCleanupTest.php diff --git a/src/OpenCATS/Tests/IntegrationTests/EntityDeleteCleanupTest.php b/src/OpenCATS/Tests/IntegrationTests/EntityDeleteCleanupTest.php new file mode 100644 index 000000000..2904405f3 --- /dev/null +++ b/src/OpenCATS/Tests/IntegrationTests/EntityDeleteCleanupTest.php @@ -0,0 +1,340 @@ +getColumn($sql, 0, 0); + + return (int) $row[0]; + } + + protected function setUp(): void + { + parent::setUp(); + + if (session_status() !== PHP_SESSION_ACTIVE) + { + @session_start(); + } + + $_SESSION['CATS'] = new class { + public function getUserID() + { + return 1; + } + + public function isLoggedIn() + { + return false; + } + + public function getTimeZoneOffset() + { + return 0; + } + + public function isDateDMY() + { + return false; + } + }; + + include_once(LEGACY_ROOT . '/lib/CATSUtility.php'); + include_once(LEGACY_ROOT . '/lib/Candidates.php'); + include_once(LEGACY_ROOT . '/lib/Companies.php'); + include_once(LEGACY_ROOT . '/lib/Contacts.php'); + include_once(LEGACY_ROOT . '/lib/JobOrders.php'); + } + + public function testJobOrderDeleteResetsGeneralAndCalendarReferences() + { + $db = DatabaseConnection::getInstance(); + + $db->query("INSERT INTO joborder (site_id, title, entered_by) VALUES (1, 'Job Delete Cleanup', 1)"); + $jobOrderID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO activity (data_item_id, data_item_type, joborder_id, site_id, entered_by, type, notes) + VALUES (1, %d, %d, 1, 1, 100, 'activity reference')", + DATA_ITEM_BULKRESUME, + $jobOrderID + )); + $activityID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO calendar_event (type, date, title, data_item_id, data_item_type, entered_by, site_id, joborder_id) + VALUES (100, NOW(), 'calendar reference', 1, %d, 1, 1, %d)", + DATA_ITEM_BULKRESUME, + $jobOrderID + )); + $calendarEventID = (int) $db->getLastInsertID(); + + $jobOrders = new \JobOrders(1); + $jobOrders->delete($jobOrderID); + + $jobOrderCount = $this->countRows( + $db, + sprintf('SELECT COUNT(*) FROM joborder WHERE joborder_id = %d', $jobOrderID) + ); + $this->assertSame(0, $jobOrderCount, 'Job order row should be deleted.'); + + $activityRow = $db->getAssoc(sprintf( + 'SELECT activity_id, joborder_id FROM activity WHERE activity_id = %d', + $activityID + )); + $this->assertNotEmpty($activityRow, 'Activity row should still exist.'); + $this->assertNull($activityRow['joborder_id'], 'activity.joborder_id should be set to NULL.'); + + $calendarRow = $db->getAssoc(sprintf( + 'SELECT calendar_event_id, joborder_id FROM calendar_event WHERE calendar_event_id = %d', + $calendarEventID + )); + $this->assertNotEmpty($calendarRow, 'Calendar event row should still exist.'); + $this->assertSame('-1', (string) $calendarRow['joborder_id'], 'calendar_event.joborder_id should be set to -1.'); + } + + public function testCandidateDeleteRemovesAssociatedRows() + { + $db = DatabaseConnection::getInstance(); + + $db->query("INSERT INTO candidate (site_id, first_name, last_name, entered_by, owner) VALUES (1, 'Delete', 'Candidate', 1, 1)"); + $candidateID = (int) $db->getLastInsertID(); + + $db->query("INSERT INTO joborder (site_id, title, entered_by) VALUES (1, 'Candidate Cleanup Job', 1)"); + $jobOrderID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO candidate_joborder (candidate_id, joborder_id, site_id, status, date_created, date_modified) + VALUES (%d, %d, 1, 100, NOW(), NOW())", + $candidateID, + $jobOrderID + )); + $candidateJoborderID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO candidate_joborder_status_history (candidate_id, joborder_id, date, status_from, status_to, site_id) + VALUES (%d, %d, NOW(), 0, 100, 1)", + $candidateID, + $jobOrderID + )); + $candidateHistoryID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO activity (data_item_id, data_item_type, site_id, entered_by, type, notes) + VALUES (%d, %d, 1, 1, 100, 'candidate cleanup activity')", + $candidateID, + DATA_ITEM_CANDIDATE + )); + $activityID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO calendar_event (type, date, title, data_item_id, data_item_type, entered_by, site_id) + VALUES (100, NOW(), 'candidate cleanup event', %d, %d, 1, 1)", + $candidateID, + DATA_ITEM_CANDIDATE + )); + $calendarEventID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO saved_list_entry (saved_list_id, data_item_type, data_item_id, site_id, date_created) + VALUES (1, %d, %d, 1, NOW())", + DATA_ITEM_CANDIDATE, + $candidateID + )); + $savedListEntryID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO extra_field (data_item_id, field_name, value, site_id, data_item_type) + VALUES (%d, 'candidate_cleanup_field', 'candidate cleanup value', 1, %d)", + $candidateID, + DATA_ITEM_CANDIDATE + )); + $extraFieldID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO candidate_tag (site_id, candidate_id, tag_id) + VALUES (1, %d, 1001)", + $candidateID + )); + $candidateTagID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO career_portal_questionnaire_history (site_id, candidate_id, question, answer, questionnaire_title, questionnaire_description, date) + VALUES (1, %d, 'candidate cleanup question', 'candidate cleanup answer', 'candidate cleanup title', 'candidate cleanup description', NOW())", + $candidateID + )); + $questionnaireHistoryID = (int) $db->getLastInsertID(); + + $candidates = new \Candidates(1); + $candidates->delete($candidateID); + + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM candidate WHERE candidate_id = %d', $candidateID)), 'Candidate row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM candidate_joborder WHERE candidate_joborder_id = %d', $candidateJoborderID)), 'candidate_joborder row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM candidate_joborder_status_history WHERE candidate_joborder_status_history_id = %d', $candidateHistoryID)), 'candidate_joborder_status_history row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM activity WHERE activity_id = %d', $activityID)), 'Candidate activity row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM calendar_event WHERE calendar_event_id = %d', $calendarEventID)), 'Candidate calendar event row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM saved_list_entry WHERE saved_list_entry_id = %d', $savedListEntryID)), 'Candidate saved list row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM extra_field WHERE extra_field_id = %d', $extraFieldID)), 'Candidate extra field row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM candidate_tag WHERE id = %d', $candidateTagID)), 'candidate_tag row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM career_portal_questionnaire_history WHERE career_portal_questionnaire_history_id = %d', $questionnaireHistoryID)), 'career_portal_questionnaire_history row should be deleted.'); + } + + public function testCompanyDeleteRemovesAssociatedRows() + { + $db = DatabaseConnection::getInstance(); + + $db->query("INSERT INTO company (site_id, name, entered_by, owner) VALUES (1, 'Delete Cleanup Company', 1, 1)"); + $companyID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO activity (data_item_id, data_item_type, site_id, entered_by, type, notes) + VALUES (%d, %d, 1, 1, 100, 'company cleanup activity')", + $companyID, + DATA_ITEM_COMPANY + )); + $activityID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO calendar_event (type, date, title, data_item_id, data_item_type, entered_by, site_id) + VALUES (100, NOW(), 'company cleanup event', %d, %d, 1, 1)", + $companyID, + DATA_ITEM_COMPANY + )); + $calendarEventID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO saved_list_entry (saved_list_id, data_item_type, data_item_id, site_id, date_created) + VALUES (1, %d, %d, 1, NOW())", + DATA_ITEM_COMPANY, + $companyID + )); + $savedListEntryID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO extra_field (data_item_id, field_name, value, site_id, data_item_type) + VALUES (%d, 'company_cleanup_field', 'company cleanup value', 1, %d)", + $companyID, + DATA_ITEM_COMPANY + )); + $extraFieldID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO company_department (name, company_id, site_id) + VALUES ('Delete Cleanup Department', %d, 1)", + $companyID + )); + $companyDepartmentID = (int) $db->getLastInsertID(); + + $companies = new \Companies(1); + $companies->delete($companyID); + + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM company WHERE company_id = %d', $companyID)), 'Company row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM activity WHERE activity_id = %d', $activityID)), 'Company activity row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM calendar_event WHERE calendar_event_id = %d', $calendarEventID)), 'Company calendar event row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM saved_list_entry WHERE saved_list_entry_id = %d', $savedListEntryID)), 'Company saved list row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM extra_field WHERE extra_field_id = %d', $extraFieldID)), 'Company extra field row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM company_department WHERE company_department_id = %d', $companyDepartmentID)), 'company_department row should be deleted.'); + } + + public function testContactDeleteRemovesAssociatedRowsAndResetsReferences() + { + $db = DatabaseConnection::getInstance(); + + $db->query("INSERT INTO company (site_id, name, entered_by, owner) VALUES (1, 'Delete Cleanup Contact Company', 1, 1)"); + $companyID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO contact (company_id, site_id, last_name, first_name, entered_by, owner, company_department_id) + VALUES (%d, 1, 'Parent', 'Delete', 1, 1, 1)", + $companyID + )); + $contactID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "UPDATE company SET billing_contact = %d WHERE company_id = %d AND site_id = 1", + $contactID, + $companyID + )); + + $db->query(sprintf( + "INSERT INTO contact (company_id, site_id, last_name, first_name, entered_by, owner, company_department_id, reports_to) + VALUES (%d, 1, 'Child', 'Delete', 1, 1, 1, %d)", + $companyID, + $contactID + )); + $childContactID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO joborder (site_id, title, entered_by, contact_id, company_id) + VALUES (1, 'Delete Cleanup Contact Job', 1, %d, %d)", + $contactID, + $companyID + )); + $jobOrderID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO activity (data_item_id, data_item_type, site_id, entered_by, type, notes) + VALUES (%d, %d, 1, 1, 100, 'contact cleanup activity')", + $contactID, + DATA_ITEM_CONTACT + )); + $activityID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO calendar_event (type, date, title, data_item_id, data_item_type, entered_by, site_id) + VALUES (100, NOW(), 'contact cleanup event', %d, %d, 1, 1)", + $contactID, + DATA_ITEM_CONTACT + )); + $calendarEventID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO saved_list_entry (saved_list_id, data_item_type, data_item_id, site_id, date_created) + VALUES (1, %d, %d, 1, NOW())", + DATA_ITEM_CONTACT, + $contactID + )); + $savedListEntryID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO extra_field (data_item_id, field_name, value, site_id, data_item_type) + VALUES (%d, 'contact_cleanup_field', 'contact cleanup value', 1, %d)", + $contactID, + DATA_ITEM_CONTACT + )); + $extraFieldID = (int) $db->getLastInsertID(); + + $db->query(sprintf( + "INSERT INTO attachment (data_item_id, data_item_type, site_id, title) + VALUES (%d, %d, 1, 'contact cleanup attachment')", + $contactID, + DATA_ITEM_CONTACT + )); + $attachmentID = (int) $db->getLastInsertID(); + + $contacts = new \Contacts(1); + $contacts->delete($contactID); + + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM contact WHERE contact_id = %d', $contactID)), 'Contact row should be deleted.'); + + $jobOrderRow = $db->getAssoc(sprintf('SELECT joborder_id, contact_id FROM joborder WHERE joborder_id = %d', $jobOrderID)); + $this->assertSame('-1', (string) $jobOrderRow['contact_id'], 'joborder.contact_id should be set to -1.'); + + $companyRow = $db->getAssoc(sprintf('SELECT company_id, billing_contact FROM company WHERE company_id = %d', $companyID)); + $this->assertSame('-1', (string) $companyRow['billing_contact'], 'company.billing_contact should be set to -1.'); + + $childContactRow = $db->getAssoc(sprintf('SELECT contact_id, reports_to FROM contact WHERE contact_id = %d', $childContactID)); + $this->assertSame('-1', (string) $childContactRow['reports_to'], 'contact.reports_to should be set to -1.'); + + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM activity WHERE activity_id = %d', $activityID)), 'Contact activity row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM calendar_event WHERE calendar_event_id = %d', $calendarEventID)), 'Contact calendar event row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM saved_list_entry WHERE saved_list_entry_id = %d', $savedListEntryID)), 'Contact saved list row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM extra_field WHERE extra_field_id = %d', $extraFieldID)), 'Contact extra field row should be deleted.'); + $this->assertSame(0, $this->countRows($db, sprintf('SELECT COUNT(*) FROM attachment WHERE attachment_id = %d', $attachmentID)), 'Contact attachment row should be deleted.'); + } +}