Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support financial ACLs #731

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2f87cc7
Refactor mandate contact tab queries to use APIv4 and check permissio…
jensschuppe Aug 20, 2024
c06afc0
Validate financial types when creating mandates
jensschuppe Aug 20, 2024
165af16
Add API4 Get action for SEPA mandates checking Financial ACLs
jensschuppe Aug 20, 2024
0d1de98
Do not evaluate $_REQUEST superglobal directly in CiviSEPA dashboard
jensschuppe Aug 22, 2024
d67ce17
Add and use API4 Get action for SEPA transaction groups checking Fina…
jensschuppe Aug 22, 2024
d694df2
Dashboard: filter for SEPA transaction groups the user has permission…
jensschuppe Aug 23, 2024
4a875cf
Use API4 for retrieving contributions of SEPA transaction groups, fil…
jensschuppe Aug 26, 2024
d4d95ed
Add `financial_type_id` field to SepaTransactionGroup entity
jensschuppe Sep 13, 2024
7407b42
Add a setting for grouping by financial types
jensschuppe Sep 13, 2024
dc6a302
Update OOFF transaction groups with financial type grouping
jensschuppe Sep 13, 2024
4ca9b65
Update RCUR transaction groups with financial type grouping
jensschuppe Sep 17, 2024
e80ed08
Use API4 SepaMandate.get for edit mandate page
jensschuppe Sep 18, 2024
c5b95c4
Use API4 SepaMandate.get for create mandate form
jensschuppe Sep 18, 2024
bef328d
Use API4 SepaMandate.get for Action Provider "FindMandate" action
jensschuppe Sep 18, 2024
5bfdfbd
Fix error in updating RCUR transaction groups
jensschuppe Sep 25, 2024
1ac233f
Replace various ocurrences of APIv3 calls to CiviSEPA entities with A…
jensschuppe Oct 1, 2024
053073c
Catch CRM_Core_Exception only when expected to be thrown by API4 sing…
jensschuppe Oct 9, 2024
ac24272
Code style issues
jensschuppe Oct 10, 2024
6378ab0
Do not index by mandate ID as this causes errors with offsets higher …
jensschuppe Nov 4, 2024
6f18272
Do not index by mandate ID as this causes errors with offsets higher …
jensschuppe Nov 8, 2024
601786f
Use permissions for SepaTransactionGroup API4 actions
jensschuppe Nov 8, 2024
6f5154d
Increase minimal required CiviCRM version due to use of the API4 aggr…
jensschuppe Nov 11, 2024
cb2dd7b
Fix synchronizing of transaction groups
jensschuppe Nov 13, 2024
04fd797
Define permissions to use for SepaContributionGroup API
jensschuppe Nov 13, 2024
5c83275
Fix error message to display exception message after changing to API4
jensschuppe Nov 14, 2024
c954635
Define permissions to use for SepaSddFile API
jensschuppe Nov 14, 2024
cdcace1
Clarify use of financial type in batching
jensschuppe Nov 15, 2024
b1f7a67
minor fix
bjendres Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 116 additions & 92 deletions CRM/Sepa/Logic/Batching.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim
->setLimit($limit)
->setOffset($offset)
->execute()
->indexBy('id')
->getArrayCopy();

foreach ($relevant_mandates as &$mandate) {
Expand Down Expand Up @@ -334,15 +333,14 @@ static function updateOOFF($creditor_id, $now = 'now', $offset = NULL, $limit =
->setLimit($limit ?? 0)
->setOffset($offset ?? 0)
->execute()
->indexBy('id')
->getArrayCopy();

// step 2: group mandates in collection dates
$calculated_groups = [];
$earliest_collection_date = date('Y-m-d', strtotime("$now +$ooff_notice days"));
$latest_collection_date = '';

foreach ($relevant_mandates as $mandate_id => $mandate) {
foreach ($relevant_mandates as $mandate) {
$mandate['mandate_id'] = $mandate['id'];
$mandate['mandate_contact_id'] = $mandate['contact_id'];
$mandate['mandate_entity_id'] = $mandate['entity_id'];
Expand Down Expand Up @@ -482,6 +480,57 @@ static function closeEnded() {
** **
****************************************************************************/

public static function getOrCreateTransactionGroup(
int $creditor_id,
string $mode,
string $collection_date,
?int $financial_type_id,
int $notice,
array &$existing_groups
): int {
$group_status_id_open = (int) CRM_Core_PseudoConstant::getKey('CRM_Batch_BAO_Batch', 'status_id', 'Open');

if (!isset($existing_groups[$collection_date][$financial_type_id ?? 0])) {
// this group does not yet exist -> create

// find unused reference
$reference = self::getTransactionGroupReference($creditor_id, $mode, $collection_date, $financial_type_id);

$group = civicrm_api('SepaTransactionGroup', 'create', array(
jensschuppe marked this conversation as resolved.
Show resolved Hide resolved
'version' => 3,
'reference' => $reference,
'type' => $mode,
'collection_date' => $collection_date,
// Financial type may be NULL if not grouping by financial type.
'financial_type_id' => $financial_type_id,
'latest_submission_date' => date('Y-m-d', strtotime("-$notice days", strtotime($collection_date))),
'created_date' => date('Y-m-d'),
'status_id' => $group_status_id_open,
'sdd_creditor_id' => $creditor_id,
));
if (!empty($group['is_error'])) {
// TODO: Error handling
Civi::log()->debug("org.project60.sepa: batching:syncGroups/createGroup ".$group['error_message']);
jensschuppe marked this conversation as resolved.
Show resolved Hide resolved
}
}
else {
try {
$group = \Civi\Api4\SepaTransactionGroup::get(TRUE)
->addWhere('id', '=', $existing_groups[$collection_date][$financial_type_id ?? 0])
->addWhere('status_id', '=', $group_status_id_open)
->execute()
->single();
}
catch (\CRM_Core_Exception $exception) {
// TODO: Error handling
Civi::log()->debug('org.project60.sepa: batching:syncGroups/getGroup ' . $exception->getMessage());
jensschuppe marked this conversation as resolved.
Show resolved Hide resolved
}
unset($existing_groups[$collection_date][$financial_type_id ?? 0]);
}

return (int) $group['id'];
}

/**
* subroutine to create the group/contribution structure as calculated
* @param $calculated_groups array [collection_date] -> array(contributions) as calculated
Expand Down Expand Up @@ -515,124 +564,99 @@ protected static function syncGroups(
}

foreach ($financial_type_groups as $financial_type_id => $mandates) {
if (0 === $financial_type_id) {
$financial_type_id = NULL;
}
if (!isset($existing_groups[$collection_date][$financial_type_id ?? 0])) {
// this group does not yet exist -> create

// find unused reference
$reference = self::getTransactionGroupReference($creditor_id, $mode, $collection_date, $financial_type_id);

$group = civicrm_api('SepaTransactionGroup', 'create', array(
'version' => 3,
'reference' => $reference,
'type' => $mode,
'collection_date' => $collection_date,
'financial_type_id' => $financial_type_id,
'latest_submission_date' => date('Y-m-d', strtotime("-$notice days", strtotime($collection_date))),
'created_date' => date('Y-m-d'),
'status_id' => $group_status_id_open,
'sdd_creditor_id' => $creditor_id,
));
if (!empty($group['is_error'])) {
// TODO: Error handling
Civi::log()->debug("org.project60.sepa: batching:syncGroups/createGroup ".$group['error_message']);
$group_id = self::getOrCreateTransactionGroup(
(int) $creditor_id,
$mode,
$collection_date,
0 === $financial_type_id ? NULL : $financial_type_id,
(int) $notice,
$existing_groups
);

// now we have the right group. Prepare some parameters...
$entity_ids = [];
foreach ($mandates as $mandate) {
// remark: "mandate_entity_id" in this case means the contribution ID
if (empty($mandate['mandate_entity_id'])) {
// this shouldn't happen
Civi::log()
->debug("org.project60.sepa: batching:syncGroups mandate with bad mandate_entity_id ignored:" . $mandate['mandate_id']);
jensschuppe marked this conversation as resolved.
Show resolved Hide resolved
}
else {
array_push($entity_ids, $mandate['mandate_entity_id']);
}
}
else {
try {
$group = \Civi\Api4\SepaTransactionGroup::get(TRUE)
->addWhere('id', '=', $existing_groups[$collection_date][$financial_type_id ?? 0])
->addWhere('status_id', '=', $group_status_id_open)
->execute()
->single();

// now we have the right group. Prepare some parameters...
$group_id = $group['id'];
$entity_ids = [];
foreach ($mandates as $mandate) {
// remark: "mandate_entity_id" in this case means the contribution ID
if (empty($mandate['mandate_entity_id'])) {
// this shouldn't happen
Civi::log()->debug("org.project60.sepa: batching:syncGroups mandate with bad mandate_entity_id ignored:" . $mandate['mandate_id']);
}
else {
array_push($entity_ids, $mandate['mandate_entity_id']);
}
}
if (count($entity_ids)<=0) continue;
if (count($entity_ids) <= 0) {
continue;
}

// now, filter out the entity_ids that are are already in a non-open group
// (DO NOT CHANGE CLOSED GROUPS!)
$entity_ids_list = implode(',', $entity_ids);
$already_sent_contributions = CRM_Core_DAO::executeQuery(
<<<SQL
// now, filter out the entity_ids that are are already in a non-open group
// (DO NOT CHANGE CLOSED GROUPS!)
$entity_ids_list = implode(',', $entity_ids);
$already_sent_contributions = CRM_Core_DAO::executeQuery(
<<<SQL
SELECT contribution_id
FROM civicrm_sdd_contribution_txgroup
LEFT JOIN civicrm_sdd_txgroup ON civicrm_sdd_contribution_txgroup.txgroup_id = civicrm_sdd_txgroup.id
WHERE contribution_id IN ($entity_ids_list)
AND civicrm_sdd_txgroup.status_id <> $group_status_id_open;
SQL
);
while ($already_sent_contributions->fetch()) {
$index = array_search($already_sent_contributions->contribution_id, $entity_ids);
if ($index !== false) unset($entity_ids[$index]);
}
if (count($entity_ids)<=0) continue;
);
while ($already_sent_contributions->fetch()) {
$index = array_search($already_sent_contributions->contribution_id, $entity_ids);
if ($index !== FALSE) {
unset($entity_ids[$index]);
}
}
if (count($entity_ids) <= 0) {
continue;
}

// remove all the unwanted entries from our group
$entity_ids_list = implode(',', $entity_ids);
if (!$partial_groups || $partial_first) {
CRM_Core_DAO::executeQuery(
<<<SQL
// remove all the unwanted entries from our group
$entity_ids_list = implode(',', $entity_ids);
if (!$partial_groups || $partial_first) {
CRM_Core_DAO::executeQuery(
<<<SQL
DELETE FROM civicrm_sdd_contribution_txgroup
WHERE
txgroup_id=$group_id
AND contribution_id NOT IN ($entity_ids_list);
SQL
);
}
);
}

// remove all our entries from other groups, if necessary
CRM_Core_DAO::executeQuery(
<<<SQL
// remove all our entries from other groups, if necessary
CRM_Core_DAO::executeQuery(
<<<SQL
DELETE FROM civicrm_sdd_contribution_txgroup
WHERE txgroup_id!=$group_id
AND contribution_id IN ($entity_ids_list);
SQL
);
);

// now check which ones are already in our group...
$existing = CRM_Core_DAO::executeQuery(
<<<SQL
// now check which ones are already in our group...
$existing = CRM_Core_DAO::executeQuery(
<<<SQL
SELECT *
FROM civicrm_sdd_contribution_txgroup
WHERE txgroup_id=$group_id
AND contribution_id IN ($entity_ids_list);
SQL
);
while ($existing->fetch()) {
// remove from entity ids, if in there:
if(($key = array_search($existing->contribution_id, $entity_ids)) !== false) {
unset($entity_ids[$key]);
}
}
);
while ($existing->fetch()) {
// remove from entity ids, if in there:
if (($key = array_search($existing->contribution_id, $entity_ids)) !== FALSE) {
unset($entity_ids[$key]);
}
}

// the remaining must be added
foreach ($entity_ids as $entity_id) {
CRM_Core_DAO::executeQuery(
<<<SQL
// the remaining must be added
foreach ($entity_ids as $entity_id) {
CRM_Core_DAO::executeQuery(
<<<SQL
INSERT INTO civicrm_sdd_contribution_txgroup (txgroup_id, contribution_id) VALUES ($group_id, $entity_id);
SQL
);
}
}
catch (\CRM_Core_Exception $exception) {
// TODO: Error handling
Civi::log()->debug('org.project60.sepa: batching:syncGroups/getGroup ' . $exception->getMessage());
}
unset($existing_groups[$collection_date][$financial_type_id ?? 0]);
);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion CRM/Sepa/Page/DashBoard.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function run() {
CRM_Core_Session::setStatus(
E::ts(
"Couldn't read transaction groups. Error was: %1",
[1 => $result['error_message']]
[1 => $exception->getMessage()]
),
E::ts('Error'),
'error'
Expand Down
12 changes: 12 additions & 0 deletions Civi/Api4/SepaContributionGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,16 @@
*/
class SepaContributionGroup extends Generic\DAOEntity {
use Generic\Traits\EntityBridge;

public static function permissions(): array {
return [
'get' => ['view sepa groups'],
'create' => ['batch sepa groups'],
'update' => ['batch sepa groups'],
'delete' => [
['batch sepa groups', 'delete sepa groups'],
],
];
}

}
11 changes: 11 additions & 0 deletions Civi/Api4/SepaSddFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,15 @@
*/
class SepaSddFile extends Generic\DAOEntity {

public static function permissions(): array {
return [
'get' => ['view sepa groups'],
'create' => ['batch sepa groups'],
'update' => ['batch sepa groups'],
'delete' => [
['batch sepa groups', 'delete sepa groups'],
],
];
}

}
9 changes: 9 additions & 0 deletions Civi/Api4/SepaTransactionGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,13 @@ public static function get($checkPermissions = TRUE) {
->setCheckPermissions($checkPermissions);
}

public static function permissions(): array {
return [
'get' => ['view sepa groups'],
'create' => ['batch sepa groups'],
'update' => ['batch sepa groups'],
'delete' => ['delete sepa groups'],
jensschuppe marked this conversation as resolved.
Show resolved Hide resolved
];
}

}
2 changes: 1 addition & 1 deletion api/v3/SepaAlternativeBatching.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function civicrm_api3_sepa_alternative_batching_update($params) {
} else {
$creditors = array();
foreach ($creditor_query['values'] as $creditor) {
$creditors[] = $creditor['id'];
$creditors[] = (int) $creditor['id'];
}
}

Expand Down
2 changes: 1 addition & 1 deletion info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<develStage>dev</develStage>
<compatibility>
<ver>5.75</ver>
<ver>5.65</ver>
<ver>5.69</ver>
</compatibility>
<urls>
<url desc="Main Extension Page">https://github.com/Project60/org.project60.sepa</url>
Expand Down