Skip to content
Open
18 changes: 17 additions & 1 deletion cypress/e2e/1_feature_tests/5_3_Manage_Beneficiaries.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,17 @@ describe('Manage beneficiaries', () => {
clickMergeButton();
verifyBeneficiaryRowLevel(TEST_LASTNAME1,0);
verifyBeneficiaryRowLevel(TEST_LASTNAME2,1);


// Verify history logs for merge operation.
// TEST_LASTNAME1 is the family head (level 0) - its parent_id does not change, so it must NOT be logged.
cy.getBeneficiaryIdFromRow(TEST_LASTNAME1).then(id1 => {
cy.checkHistoryLogAbsent('people', id1, 'parent_id; merged to family');
});
// TEST_LASTNAME2 is a member (level 1) - it was merged into the family, so it must be logged.
cy.getBeneficiaryIdFromRow(TEST_LASTNAME2).then(id2 => {
cy.checkHistoryLog('people', id2, 'parent_id; merged to family');
});

//cleanup
fullDeleteOfMergedUsers();
});
Expand All @@ -266,6 +276,12 @@ describe('Manage beneficiaries', () => {
clickDetachButton();
verifyBeneficiaryRowLevel(TEST_LASTNAME1,0);
verifyBeneficiaryRowLevel(TEST_LASTNAME2,0);

// Verify history log for detach operation
cy.getBeneficiaryIdFromRow(TEST_LASTNAME2).then(id2 => {
cy.checkHistoryLog('people', id2, 'parent_id; detached from family');
});

//cleanup
fullDeleteTestedBeneficiaries([TEST_FIRSTNAME1,TEST_FIRSTNAME2]);
});
Expand Down
43 changes: 43 additions & 0 deletions cypress/support/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,46 @@ Cypress.Commands.add("testauth0user", (email) => {
expect(response.body).to.contain("true");
});
});

Cypress.Commands.add("checkHistoryLog", (tablename, recordId, expectedChange) => {
cy.request({
method: "POST",
url: "/ajax.php?file=testhistorycheck",
body: {
tablename: tablename,
record_id: recordId,
expected_change: expectedChange
},
form: true
}).then(response => {
expect(response.status).to.eq(200);
const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
expect(body.found).to.eq(true);
expect(body.count).to.be.greaterThan(0);
});
});

Cypress.Commands.add("checkHistoryLogAbsent", (tablename, recordId, expectedChange) => {
cy.request({
method: "POST",
url: "/ajax.php?file=testhistorycheck",
body: {
tablename: tablename,
record_id: recordId,
expected_change: expectedChange
},
form: true
}).then(response => {
expect(response.status).to.eq(200);
const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
expect(body.found).to.eq(false);
});
});

Cypress.Commands.add("getBeneficiaryIdFromRow", (lastname) => {
return cy.getRowWithText(lastname).then($row => {
const id = $row.closest('tr').attr('data-id');
expect(id).to.not.be.undefined;
return parseInt(id);
});
});
42 changes: 40 additions & 2 deletions include/people.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,9 @@ function () use ($cmsmain, $data) {
}
}
});
// Only log the members whose parent_id actually changed, not the family head itself
$mergedIds = array_values(array_filter($ids, fn ($id) => $id != $oldest));
simpleBulkSaveChangeHistory('people', $mergedIds, 'parent_id; merged to family', null, [], ['int' => $oldest]);
$success = true;
$message = 'The merge has be successfully applied';
$redirect = true;
Expand All @@ -450,9 +453,13 @@ function () use ($cmsmain, $data) {

case 'detach':
$ids = explode(',', (string) $_POST['ids']);
$parentIdsByBeneficiary = [];
foreach ($ids as $key => $value) {
if (!db_value('SELECT parent_id FROM people WHERE id = :id', ['id' => $value])) {
$parentId = db_value('SELECT parent_id FROM people WHERE id = :id', ['id' => $value]);
if (!$parentId) {
$containsmembers = true;
} else {
$parentIdsByBeneficiary[$value] = $parentId;
}
}
if ($containsmembers) {
Expand All @@ -465,6 +472,14 @@ function () use ($cmsmain, $data) {
db_query('UPDATE people SET parent_id = NULL WHERE id = :id', ['id' => $id]);
}
});
// Group beneficiaries by their old parent_id for history logging
$beneficiariesByParent = [];
foreach ($parentIdsByBeneficiary as $beneficiaryId => $parentId) {
$beneficiariesByParent[$parentId][] = $beneficiaryId;
}
foreach ($beneficiariesByParent as $parentId => $beneficiaryIds) {
simpleBulkSaveChangeHistory('people', $beneficiaryIds, 'parent_id; detached from family', null, ['int' => $parentId], []);
}
$redirect = true;
$success = true;
$message = ($success) ? 'Selected people have been detached' : 'Something went wrong';
Expand All @@ -483,7 +498,30 @@ function () use ($cmsmain, $data) {
$ids = json_decode((string) $_POST['ids']);
// list($success, $message, $redirect, $aftermove) = listMove($table, $ids, true, 'correctdrops');
// Refactored list move method to use a transaction block and bulk insert for the correctdrops method
[$success, $message, $redirect, $aftermove] = listBulkMove($table, $ids, true, 'bulkcorrectdrops', true);
[$success, $message, $redirect, $aftermove, $parentChanges] = listBulkMove($table, $ids, true, 'bulkcorrectdrops', true);

// Log history for drag & drop family operations
if (!empty($parentChanges)) {
$addedToFamilyByParent = [];
$removedFromFamilyByParent = [];

foreach ($parentChanges as $change) {
if (is_null($change['old_parent_id']) && !is_null($change['new_parent_id'])) {
// Added to family - group by new parent
$addedToFamilyByParent[$change['new_parent_id']][] = $change['id'];
} elseif (!is_null($change['old_parent_id']) && is_null($change['new_parent_id'])) {
// Removed from family - group by old parent
$removedFromFamilyByParent[$change['old_parent_id']][] = $change['id'];
}
}

foreach ($addedToFamilyByParent as $parentId => $beneficiaryIds) {
simpleBulkSaveChangeHistory('people', $beneficiaryIds, 'parent_id; added to family via drag & drop', null, [], ['int' => $parentId]);
}
foreach ($removedFromFamilyByParent as $parentId => $beneficiaryIds) {
simpleBulkSaveChangeHistory('people', $beneficiaryIds, 'parent_id; removed from family via drag & drop', null, ['int' => $parentId], []);
}
}

break;

Expand Down
36 changes: 36 additions & 0 deletions library/ajax/testhistorycheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

// allowed databases
$devdbs = ['dropapp_dev', 'dropapp_staging'];
// confirmed testusers
$testusers = ['admin@admin.co', 'madmin@admin.co', 'coordinator@coordinator.co', 'user@user.co'];

$return = [];

// Test if user is a testusers and the database is a dev database
if (!(in_array($settings['db_database'], $devdbs) && in_array($_SESSION['user']['email'], $testusers))) {
$msg = 'You do not have access to check test data!';
trigger_error($msg, E_USER_ERROR);

echo json_encode(['error' => 'No permission']);
} else {
$recordId = $_POST['record_id'];
$tablename = $_POST['tablename'];
$expectedChange = $_POST['expected_change'];

// Query history table for matching record
$historyEntries = db_array(
'SELECT * FROM history WHERE tablename = :tablename AND record_id = :record_id AND changes LIKE :changes ORDER BY changedate DESC',
[
'tablename' => $tablename,
'record_id' => $recordId,
'changes' => '%'.$expectedChange.'%',
]
);

if (count($historyEntries) > 0) {
echo json_encode(['found' => true, 'count' => count($historyEntries), 'entries' => $historyEntries]);
} else {
echo json_encode(['found' => false, 'count' => 0]);
}
}
7 changes: 5 additions & 2 deletions library/lib/list.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ function listBulkMove($table, $ids, $regardparent = true, $hook = '', $updatetra

$i = 1;
$return = '';
$hookIds = db_transaction(function () use ($ids, $hasParent, $table, $hook, $i, $updatetransactions) {
$parentChanges = [];
$hookIds = db_transaction(function () use ($ids, $hasParent, $table, $hook, $i, $updatetransactions, &$parentChanges) {
$hookIds = [];
$seq = [];
foreach ($ids as $line) {
Expand All @@ -72,6 +73,8 @@ function listBulkMove($table, $ids, $regardparent = true, $hook = '', $updatetra
if ($updatetransactions && null != $new_parent_id) {
db_query('UPDATE transactions SET people_id = :parent_id WHERE people_id = :id', ['parent_id' => $new_parent_id, 'id' => $id]);
}
// Track parent_id changes for history logging
$parentChanges[] = ['id' => $id, 'old_parent_id' => $old_parent_id, 'new_parent_id' => $new_parent_id];
}
}

Expand All @@ -88,7 +91,7 @@ function listBulkMove($table, $ids, $regardparent = true, $hook = '', $updatetra
$aftermove = $hook($hookIds);
}

return [true, $return, false, $aftermove];
return [true, $return, false, $aftermove, $parentChanges];
}

function listBulkRealDelete($table, $ids, $now = null)
Expand Down
19 changes: 15 additions & 4 deletions library/lib/tools.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,9 @@ function simpleSaveChangeHistory($table, $record, $changes, $now = null, $from =
db_query('INSERT INTO history (tablename, record_id, changes, user_id, ip, changedate, from_int, from_float, to_int, to_float) VALUES (:table,:id,:change,:user_id,:ip,:now, :from_int, :from_float, :to_int, :to_float)', ['table' => $table, 'id' => $record, 'change' => $changes, 'user_id' => $_SESSION['user']['id'], 'ip' => $_SERVER['REMOTE_ADDR'], 'now' => $now, 'from_int' => $from['int'], 'from_float' => $from['float'], 'to_int' => $to['int'], 'to_float' => $to['float']]);
}

function simpleBulkSaveChangeHistory($table, $records, $changes, $now = null)
function simpleBulkSaveChangeHistory($table, $records, $changes, $now = null, $from = [], $to = [])
{
// from and to variable must be arrays with entry 'int' or 'float'
if (!db_tableexists('history')) {
return;
}
Expand All @@ -310,14 +311,24 @@ function simpleBulkSaveChangeHistory($table, $records, $changes, $now = null)
$params = ['now' => $now];
if (is_iterable($records)) {
for ($i = 0; $i < sizeof($records); ++$i) {
$query .= "(:table{$i},:id{$i},:change{$i},:user_id{$i},:ip{$i},:now)";
$params = array_merge($params, ['table'.$i => $table, 'id'.$i => $records[$i], 'change'.$i => $changes, 'user_id'.$i => $_SESSION['user']['id'], 'ip'.$i => $_SERVER['REMOTE_ADDR']]);
$query .= "(:table{$i},:id{$i},:change{$i},:user_id{$i},:ip{$i},:now, :from_int{$i}, :from_float{$i}, :to_int{$i}, :to_float{$i})";
$params = array_merge($params, ['table'.$i => $table, 'id'.$i => $records[$i], 'change'.$i => $changes, 'user_id'.$i => $_SESSION['user']['id'], 'ip'.$i => $_SERVER['REMOTE_ADDR'], 'from_int'.$i => $from['int'] ?? null, 'from_float'.$i => $from['float'] ?? null, 'to_int'.$i => $to['int'] ?? null, 'to_float'.$i => $to['float'] ?? null]);
if ($i !== sizeof($records) - 1) {
$query .= ',';
}
}
}
if (strlen($query) > 0) {
db_query("INSERT INTO history (tablename, record_id, changes, user_id, ip, changedate) VALUES {$query}", $params);
db_query("INSERT INTO history (tablename, record_id, changes, user_id, ip, changedate, from_int, from_float, to_int, to_float) VALUES {$query}", $params);

// Update modified timestamp for all affected records
if (db_fieldexists($table, 'modified')) {
$idPlaceholders = implode(',', array_map(fn ($i) => ":id{$i}", array_keys($records)));
$updateParams = ['user' => $_SESSION['user']['id'], 'now' => $now];
foreach ($records as $i => $id) {
$updateParams["id{$i}"] = $id;
}
db_query("UPDATE {$table} SET modified = :now, modified_by = :user WHERE id IN ({$idPlaceholders})", $updateParams);
}
}
}
Loading