diff --git a/CRM/Admin/Form/Setting/BankingSettings.php b/CRM/Admin/Form/Setting/BankingSettings.php index 67f0d023..96af9a72 100644 --- a/CRM/Admin/Form/Setting/BankingSettings.php +++ b/CRM/Admin/Form/Setting/BankingSettings.php @@ -148,6 +148,8 @@ function buildQuickForm() { 'positiveInteger' ); + $this->addTransactionDomainElements(); + $this->addButtons(array( array( 'type' => 'submit', @@ -179,6 +181,8 @@ function setDefaultValues() { $defaults['lenient_dedupe'] = Civi::settings()->get('lenient_dedupe'); $defaults[CRM_Banking_Config::SETTING_TRANSACTION_LIST_CUTOFF] = Civi::settings()->get(CRM_Banking_Config::SETTING_TRANSACTION_LIST_CUTOFF); + $defaults[CRM_Banking_Config::SETTING_FORCE_TRANSACTION_DOMAIN] + = Civi::settings()->get(CRM_Banking_Config::SETTING_FORCE_TRANSACTION_DOMAIN); if ($defaults['reference_matching_probability'] === null) { $defaults['reference_matching_probability'] = '1.0'; @@ -226,10 +230,24 @@ function postProcess() { Civi::settings()->set(CRM_Banking_Config::SETTING_TRANSACTION_LIST_CUTOFF, $values[CRM_Banking_Config::SETTING_TRANSACTION_LIST_CUTOFF]); + Civi::settings()->set( + CRM_Banking_Config::SETTING_FORCE_TRANSACTION_DOMAIN, + (bool) $values[CRM_Banking_Config::SETTING_FORCE_TRANSACTION_DOMAIN] + ); + // log results $logger = CRM_Banking_Helpers_Logger::getLogger(); $logger->logDebug("Log level changed to '{$values['banking_log_level']}', file is: {$values['banking_log_file']}"); parent::postProcess(); } + + private function addTransactionDomainElements(): void { + $this->add( + 'checkbox', + CRM_Banking_Config::SETTING_FORCE_TRANSACTION_DOMAIN, + E::ts('Force transaction domain on import') + ); + } + } diff --git a/CRM/Banking/BAO/BankTransaction.php b/CRM/Banking/BAO/BankTransaction.php index ffff2d97..4f497135 100755 --- a/CRM/Banking/BAO/BankTransaction.php +++ b/CRM/Banking/BAO/BankTransaction.php @@ -14,6 +14,7 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Banking\Permissions\AllowedDomainsSqlGenerator; /** * Class contains functions for CiviBanking bank transactions @@ -272,7 +273,10 @@ public static function findUnprocessedIDs($max_count) { $results = array(); $maxcount = (int) $max_count; $status_id_new = (int) banking_helper_optionvalueid_by_groupname_and_name('civicrm_banking.bank_tx_status', 'new'); - $sql_query = "SELECT `id` AS txid FROM `civicrm_bank_tx` WHERE `status_id` = '$status_id_new' ORDER BY `value_date` ASC, `id` ASC LIMIT $maxcount"; + /** @var \Civi\Banking\Permissions\AllowedDomainsSqlGenerator $allowedDomainsSqlGenerator */ + $allowedDomainsSqlGenerator = \Civi::service(AllowedDomainsSqlGenerator::class); + $allowedDomainsClause = $allowedDomainsSqlGenerator->generateWhereClause(); + $sql_query = "SELECT `id` AS txid FROM `civicrm_bank_tx` WHERE `status_id` = '$status_id_new' AND $allowedDomainsClause ORDER BY `value_date` ASC, `id` ASC LIMIT $maxcount"; $query_results = CRM_Core_DAO::executeQuery($sql_query); while ($query_results->fetch()) { $results[] = $query_results->txid; @@ -280,4 +284,3 @@ public static function findUnprocessedIDs($max_count) { return $results; } } - diff --git a/CRM/Banking/BAO/BankTransactionBatch.php b/CRM/Banking/BAO/BankTransactionBatch.php index 3fddc9c1..a2467806 100755 --- a/CRM/Banking/BAO/BankTransactionBatch.php +++ b/CRM/Banking/BAO/BankTransactionBatch.php @@ -14,6 +14,8 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Api4\BankTransaction; + /** * Class contains functions for CiviBanking bank transactions */ @@ -52,14 +54,13 @@ static function add(&$params) { /** * Get the list of transactions * - * @return array of CRM_Banking_BAO_BankTransaction + * @return list> */ - public function getTransactions() - { - $search = new CRM_Banking_BAO_BankTransaction(); - $search->tx_batch_id = $this->id; - $search->find(); - return $search->fetchAll(); + public function getTransactions(): array { + return BankTransaction::get() + ->addWhere('tx_batch_id', '=', $this->id) + ->execute() + ->getArrayCopy(); } } diff --git a/CRM/Banking/BAO/PluginInstance.php b/CRM/Banking/BAO/PluginInstance.php index d5acf010..a59ea1c7 100755 --- a/CRM/Banking/BAO/PluginInstance.php +++ b/CRM/Banking/BAO/PluginInstance.php @@ -44,9 +44,9 @@ static function add(&$params) { * * If $enabled_only is set to true (default), only enabled plugins will be delivered. * - * @return array CRM_Banking_BAO_PluginInstances + * @phpstan-return list<\CRM_Banking_BAO_PluginInstance> */ - static function listInstances($type_name, $enabled_only=TRUE) { + static function listInstances($type_name, $enabled_only = TRUE): array { // find the correct plugin type $import_plugin_type = civicrm_api3('OptionValue', 'get', array( 'name' => $type_name, @@ -100,7 +100,7 @@ function getClass() { /** * getInstance returns an instance of the class implementing this plugin's functionality */ - function getInstance() { + public function getInstance(): \CRM_Banking_PluginModel_Base { $class = $this->getClass(); return new $class( $this ); } @@ -179,4 +179,3 @@ public function updateWithSerialisedData($serialised_data, $skip_fields = [], $v } } } - diff --git a/CRM/Banking/Config.php b/CRM/Banking/Config.php index 96e1940e..4e4e7302 100644 --- a/CRM/Banking/Config.php +++ b/CRM/Banking/Config.php @@ -22,7 +22,9 @@ class CRM_Banking_Config { /** * Setting for the transaction list cutoff */ - const SETTING_TRANSACTION_LIST_CUTOFF = 'transaction_list_cutoff'; + public const SETTING_TRANSACTION_LIST_CUTOFF = 'transaction_list_cutoff'; + + public const SETTING_FORCE_TRANSACTION_DOMAIN = 'force_transaction_domain'; /** * Should the bank account dedupe be done in a lenient way? diff --git a/CRM/Banking/DAO/BankTransaction.php b/CRM/Banking/DAO/BankTransaction.php index f467737a..70a3e734 100755 --- a/CRM/Banking/DAO/BankTransaction.php +++ b/CRM/Banking/DAO/BankTransaction.php @@ -6,7 +6,7 @@ * * Generated from org.project60.banking/xml/schema/CRM/Banking/BankTransaction.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:2ec2bcde06e6f6d87f384622825cf4ce) + * (GenCodeChecksum:ff5b0671cddb9376c39713b0af213118) */ use CRM_Banking_ExtensionUtil as E; @@ -67,6 +67,13 @@ class CRM_Banking_DAO_BankTransaction extends CRM_Core_DAO { */ public $booking_date; + /** + * @var string|null + * (SQL type: varchar(255)) + * Note that values will be retrieved from the database as a string. + */ + public $domain; + /** * Transaction amount (positive or negative) * @@ -290,6 +297,30 @@ public static function &fields() { 'localizable' => 0, 'add' => '4.3', ], + 'domain' => [ + 'name' => 'domain', + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('Domain'), + 'maxlength' => 255, + 'size' => CRM_Utils_Type::HUGE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_bank_tx.domain', + 'default' => 'null', + 'table_name' => 'civicrm_bank_tx', + 'entity' => 'BankTransaction', + 'bao' => 'CRM_Banking_DAO_BankTransaction', + 'localizable' => 0, + 'pseudoconstant' => [ + 'optionGroupName' => 'banking_transaction_domain', + 'optionEditPath' => 'civicrm/admin/options/banking_transaction_domain', + ], + 'add' => NULL, + ], 'amount' => [ 'name' => 'amount', 'type' => CRM_Utils_Type::T_MONEY, @@ -585,6 +616,14 @@ public static function indices($localize = TRUE) { 'unique' => TRUE, 'sig' => 'civicrm_bank_tx::1::bank_reference', ], + 'index_domain' => [ + 'name' => 'index_domain', + 'field' => [ + 0 => 'domain', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_bank_tx::0::domain', + ], ]; return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } diff --git a/CRM/Banking/DAO/BankTransactionBatch.php b/CRM/Banking/DAO/BankTransactionBatch.php index 2b336979..66f95713 100755 --- a/CRM/Banking/DAO/BankTransactionBatch.php +++ b/CRM/Banking/DAO/BankTransactionBatch.php @@ -6,7 +6,7 @@ * * Generated from org.project60.banking/xml/schema/CRM/Banking/BankTransactionBatch.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:0bbb3196810c4399aaec4a31ab6c3b0d) + * (GenCodeChecksum:0bc441ded5ae936cfda329c566363802) */ use CRM_Banking_ExtensionUtil as E; @@ -67,6 +67,13 @@ class CRM_Banking_DAO_BankTransactionBatch extends CRM_Core_DAO { */ public $sequence; + /** + * @var string|null + * (SQL type: varchar(255)) + * Note that values will be retrieved from the database as a string. + */ + public $domain; + /** * @var float|string|null * (SQL type: decimal(20,2)) @@ -222,6 +229,30 @@ public static function &fields() { 'localizable' => 0, 'add' => '4.3', ], + 'domain' => [ + 'name' => 'domain', + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('Domain'), + 'maxlength' => 255, + 'size' => CRM_Utils_Type::HUGE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_bank_tx_batch.domain', + 'default' => 'null', + 'table_name' => 'civicrm_bank_tx_batch', + 'entity' => 'BankTransactionBatch', + 'bao' => 'CRM_Banking_DAO_BankTransactionBatch', + 'localizable' => 0, + 'pseudoconstant' => [ + 'optionGroupName' => 'banking_transaction_domain', + 'optionEditPath' => 'civicrm/admin/options/banking_transaction_domain', + ], + 'add' => NULL, + ], 'starting_balance' => [ 'name' => 'starting_balance', 'type' => CRM_Utils_Type::T_MONEY, @@ -418,6 +449,14 @@ public static function indices($localize = TRUE) { 'unique' => TRUE, 'sig' => 'civicrm_bank_tx_batch::1::reference', ], + 'index_domain' => [ + 'name' => 'index_domain', + 'field' => [ + 0 => 'domain', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_bank_tx_batch::0::domain', + ], ]; return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } diff --git a/CRM/Banking/Form/Report/BankingTransactions.php b/CRM/Banking/Form/Report/BankingTransactions.php index ed635327..9c941ae7 100644 --- a/CRM/Banking/Form/Report/BankingTransactions.php +++ b/CRM/Banking/Form/Report/BankingTransactions.php @@ -1,4 +1,6 @@ bankingStatuses[$status['id']] = $status['label']; } + /** @var \Civi\Banking\Permissions\AllowedDomainsSqlGenerator $allowedDomainsSqlGenerator */ + $allowedDomainsSqlGenerator = \Civi::service(AllowedDomainsSqlGenerator::class); + $this->_whereClauses[] = $allowedDomainsSqlGenerator->generateWhereClause(); + $this->_columns = array( 'civicrm_bank_tx' => array( 'fields' => array(), diff --git a/CRM/Banking/Form/StatementSearch.php b/CRM/Banking/Form/StatementSearch.php index 89ce743a..4b4ce620 100644 --- a/CRM/Banking/Form/StatementSearch.php +++ b/CRM/Banking/Form/StatementSearch.php @@ -14,6 +14,7 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Banking\Permissions\AllowedDomainsSqlGenerator; use CRM_Banking_ExtensionUtil as E; /** @@ -286,6 +287,10 @@ public static function getTransactionsAjax() ]; $whereClauses = []; + /** @var \Civi\Banking\Permissions\AllowedDomainsSqlGenerator $allowedDomainsSqlGenerator */ + $allowedDomainsSqlGenerator = \Civi::service(AllowedDomainsSqlGenerator::class); + $whereClauses[] = $allowedDomainsSqlGenerator->generateWhereClause('tx'); + if (!empty($ajaxParameters[self::VALUE_DATE_START_ELEMENT])) { $parameterCount = count($queryParameters) + 1; diff --git a/CRM/Banking/Helpers/ContributionLinkMigration.php b/CRM/Banking/Helpers/ContributionLinkMigration.php index e0ece89b..6ea0ce6f 100644 --- a/CRM/Banking/Helpers/ContributionLinkMigration.php +++ b/CRM/Banking/Helpers/ContributionLinkMigration.php @@ -14,6 +14,7 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Api4\BankTransaction; use CRM_Banking_ExtensionUtil as E; @@ -51,28 +52,18 @@ public function run($context) } // run a query to the the suggestion strings - $batch = CRM_Core_DAO::executeQuery( - " - SELECT - id AS tx_id, - suggestions AS suggestions - FROM civicrm_bank_tx bank_tx - WHERE bank_tx.id >= %1 - AND bank_tx.id <= %2 - AND bank_tx.status_id IN (%3) - ", - [ - 1 => [(int) $this->from_tx_id, 'Integer'], - 2 => [(int) $this->to_tx_id, 'Integer'], - 3 => [$this->status_ids, 'CommaSeparatedIntegers'], - ] - ); + $transactions = BankTransaction::get() + ->addSelect('id', 'suggestions') + ->addWhere('id', '>=', (int) $this->from_tx_id) + ->addWhere('id', '<=', (int) $this->to_tx_id) + ->addWhere('status_id', 'IN', explode(',', $this->status_ids)) + ->execute(); // migrate all of them. We have to use a heuristic to extract the linked // contributions, because each matcher could do their own thing... $contribution_id_parameters = ['contribution_id', 'contribution_ids']; - while ($batch->fetch()) { - $suggestions = json_decode($batch->suggestions, true); + foreach ($transactions as $transaction) { + $suggestions = json_decode($transaction['suggestions'], true); foreach ($suggestions as $suggestion) { if (!empty($suggestion['executed'])) { // this suggestion has been executed -> find contribution_ids @@ -85,7 +76,7 @@ public function run($context) foreach ($contribution_ids as $contribution_id) { $contribution_id = (int) $contribution_id; if ($contribution_id) { - CRM_Banking_BAO_BankTransactionContribution::linkContribution($batch->tx_id, $contribution_id); + CRM_Banking_BAO_BankTransactionContribution::linkContribution($transaction['id'], $contribution_id); } } } diff --git a/CRM/Banking/Page/AccountDedupe.php b/CRM/Banking/Page/AccountDedupe.php index f7942b1a..1c54fa20 100644 --- a/CRM/Banking/Page/AccountDedupe.php +++ b/CRM/Banking/Page/AccountDedupe.php @@ -14,7 +14,7 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ - +use Civi\Api4\BankTransaction; use CRM_Banking_ExtensionUtil as E; require_once 'CRM/Core/Page.php'; @@ -314,8 +314,14 @@ function executeRequests($duplicates) { unset($bank_account_ids[0]); $delete_ids = implode(',', $bank_account_ids); CRM_Core_DAO::singleValueQuery("UPDATE civicrm_bank_account_reference SET ba_id=$target_id WHERE ba_id IN ($delete_ids);"); - CRM_Core_DAO::singleValueQuery("UPDATE civicrm_bank_tx SET ba_id=$target_id WHERE ba_id IN ($delete_ids);"); - CRM_Core_DAO::singleValueQuery("UPDATE civicrm_bank_tx SET party_ba_id=$target_id WHERE party_ba_id IN ($delete_ids);"); + BankTransaction::update() + ->addValue('ba_id', $target_id) + ->addWhere('ba_id', 'IN', $bank_account_ids) + ->execute(); + BankTransaction::update() + ->addValue('party_ba_id', $target_id) + ->addWhere('party_ba_id', 'IN', $bank_account_ids) + ->execute(); CRM_Core_DAO::singleValueQuery("DELETE FROM civicrm_bank_account WHERE id IN ($delete_ids);"); $accounts_fixed += 1; diff --git a/CRM/Banking/Page/Dashboard.php b/CRM/Banking/Page/Dashboard.php index a7bcf73f..0c7e5ac7 100644 --- a/CRM/Banking/Page/Dashboard.php +++ b/CRM/Banking/Page/Dashboard.php @@ -14,6 +14,8 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Api4\BankTransaction; +use Civi\Banking\Permissions\AllowedDomainsSqlGenerator; use CRM_Banking_ExtensionUtil as E; require_once 'CRM/Core/Page.php'; @@ -30,10 +32,16 @@ function run() { $payment_states = banking_helper_optiongroup_id_name_mapping('civicrm_banking.bank_tx_status'); $account_names = array(); + /** @var \Civi\Banking\Permissions\AllowedDomainsSqlGenerator $allowedDomainsSqlGenerator */ + $allowedDomainsSqlGenerator = \Civi::service(AllowedDomainsSqlGenerator::class); + $allowedDomainsClause = $allowedDomainsSqlGenerator->generateWhereClause(); + // get the week based data $account_week_data = array(); for ($i=$week_count; $i>=0; $i--) $weeks[] = date('YW', strtotime("now -$i weeks")); + + // Migration to APIv4 would require YEARWEEK as \Civi\Api4\Query\SqlFunction. $account_based_data_sql = " SELECT COUNT(*) AS count, @@ -42,6 +50,7 @@ function run() { status_id AS status_id FROM civicrm_bank_tx + WHERE $allowedDomainsClause GROUP BY ba_id, year_week, status_id; "; @@ -74,7 +83,7 @@ function run() { // fill empty weeks foreach ($account_week_data as $account_id => $account_data) { - for ($i=$week_count; $i>=0; $i--) { + for ($i=$week_count; $i>=0; $i--) { $week = date('YW', strtotime("now -$i weeks")); if (!isset($account_data[$week])) { $account_week_data[$account_id][$week] = array('sum' => 0); @@ -106,52 +115,64 @@ function run() { // get statistics data $statistics = array(); $statistics[] = $this->calculateStats( - E::ts("Payments")." (".E::ts("current year").")", - "YEAR(value_date) = '".date('Y')."'", - $payment_states); + E::ts('Payments') . ' (' . E::ts('current year') . ')', + [ + ['value_date', '>=', date('Y-01-01 00:00:00')], + ['value_date', '<', date('Y-01-0100:00:00', strtotime('+1 year'))], + ], + $payment_states + ); $statistics[] = $this->calculateStats( - E::ts("Payments")." (".E::ts("last year").")", - "YEAR(value_date) = '".date('Y', strtotime("-1 year"))."'", - $payment_states); + E::ts('Payments') . ' (' . E::ts('last year') . ')', + [ + ['value_date', '>=', date('Y-01-01 00:00:00', strtotime('-1 year'))], + ['value_date', '<', date('Y-01-01 00:00:00')], + ], + $payment_states + ); $statistics[] = $this->calculateStats( - E::ts("Payments")." (".E::ts("all times").")", - "1", - $payment_states); + E::ts('Payments') . ' (' . E::ts('all times') . ')', + [], + $payment_states + ); $this->assign('statistics', $statistics); - - parent::run(); + + parent::run(); } - function calculateStats($name, $where_clause, $payment_states) { + function calculateStats($name, array $where, $payment_states) { $data = array('title' => $name, 'count' => 0, 'stats' => array()); foreach ($payment_states as $state) { $data['stats'][$state['label']] = 0; } - $stats_sql = " - SELECT - COUNT(id) AS count, - MIN(value_date) AS first_payment, - MAX(value_date) AS last_payment, - status_id AS status_id - FROM - civicrm_bank_tx - WHERE - $where_clause - GROUP BY - status_id; - "; - $stats = CRM_Core_DAO::executeQuery($stats_sql); - while ($stats->fetch()) { - $data['count'] += $stats->count; - $data['stats'][$payment_states[$stats->status_id]['label']] = $stats->count; - if (!isset($data['from'])) { - $data['from'] = $stats->first_payment; - $data['to'] = $stats->last_payment; - } else { - if ($data['from'] > $stats->first_payment) $data['from'] = $stats->first_payment; - if ($data['to'] < $stats->first_payment) $data['to'] = $stats->first_payment; - } - } + + $statsList = BankTransaction::get() + ->addSelect( + 'COUNT(id) AS count', + 'MIN(value_data) AS first_payment', + 'MAX(value_data) AS last_payment', + 'status_id' + ) + ->setWhere($where) + ->addGroupBy('status_id') + ->execute(); + + foreach ($statsList as $stats) { + $data['count'] += $stats['count']; + $data['stats'][$payment_states[$stats['status_id']]['label']] = $stats['count']; + if (!isset($data['from'])) { + $data['from'] = $stats['first_payment']; + $data['to'] = $stats['last_payment']; + } else { + if ($data['from'] > $stats['first_payment']) { + $data['from'] = $stats['first_payment']; + } + if ($data['to'] < $stats['first_payment']) { + $data['to'] = $stats['first_payment']; + } + } + } + return $data; } -} \ No newline at end of file +} diff --git a/CRM/Banking/Page/Import.php b/CRM/Banking/Page/Import.php index 95bdd86b..3d2662d5 100755 --- a/CRM/Banking/Page/Import.php +++ b/CRM/Banking/Page/Import.php @@ -14,46 +14,65 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Banking\Permissions\AssignedTransactionDomainsLoader; use CRM_Banking_ExtensionUtil as E; require_once 'CRM/Core/Page.php'; class CRM_Banking_Page_Import extends CRM_Core_Page { - function run() - { + + private const DOMAIN_NONE = '@none@'; + + /** + * @throws \CRM_Core_Exception + */ + public function run(): void { // Example: Set the page-title dynamically; alternatively, declare a static title in xml/Menu/*.xml CRM_Utils_System::setTitle(E::ts('Bank Transaction Importer')); // get the plugins $plugin_list = CRM_Banking_BAO_PluginInstance::listInstances('import'); + $this->assign('plugin_list', $plugin_list); - // check for the page mode - if (isset($_REQUEST['importer-plugin'])) { - // RUN MODE - $this->assign('page_mode', 'run'); - $plugin_id = $_REQUEST['importer-plugin']; - $this->assign('plugin_id', $plugin_id); + $domains = $this->getDomains(); + $this->assign('domains', $domains); - // assign values - $this->assign('dry_run', $_REQUEST['dry_run'] ?? "off"); - $this->assign('process', $_REQUEST['process'] ?? "off"); - $plugin = reset($plugin_list); // should be overwritten in the next lines + $plugin_id = CRM_Utils_Request::retrieve('importer-plugin', 'Positive'); + if (NULL !== $plugin_id) { foreach ($plugin_list as $plugin) { if ($plugin->id == $plugin_id) { - $this->assign('plugin_list', array($plugin)); + $this->assign('plugin_id', $plugin_id); break; } } + } + + $domain = CRM_Utils_Request::retrieve('domain', 'String'); + if (isset($domains[$domain])) { + $this->assign('domain', $domain); + } + + // check for the page mode + if (isset($plugin) && isset($domains[$domain])) { + // RUN MODE + $this->assign('page_mode', 'run'); + + // assign values + $dry_run = CRM_Utils_Request::retrieve('dry_run', 'String') ?? 'off'; + $this->assign('dry_run', $dry_run); + $this->assign('process', CRM_Utils_Request::retrieve('process', 'String') ?? 'off'); // RUN the importer $file_info = $_FILES['uploadFile'] ?? null; $this->assign('file_info', $file_info); + /** @var \CRM_Banking_PluginModel_Importer $plugin_instance */ $plugin_instance = $plugin->getInstance(); + $plugin_instance->setDomain(self::DOMAIN_NONE === $domain ? NULL : $domain); $import_parameters = [ - 'dry_run' => ($_REQUEST['dry_run'] ?? "off"), - 'source' => ($file_info['name'] ?? 'stream'), + 'dry_run' => $dry_run, + 'source' => ($file_info['name'] ?? 'stream'), ]; if ($file_info != null && $plugin_instance::does_import_files()) { // extract files @@ -86,7 +105,7 @@ function run() } // TODO: RUN the processor - if (isset($_REQUEST['process']) && $_REQUEST['process'] == "on") { + if (CRM_Utils_Request::retrieve('process', 'String') === 'on') { CRM_Core_Session::setStatus(E::ts('Automated running not yet implemented'), E::ts('Not implemented'), 'alert'); } @@ -147,8 +166,7 @@ function run() * @return array * list of file infos */ - public function getFiles(array $file_info): array - { + public function getFiles(array $file_info): array { $uploaded_file = $file_info['tmp_name']; // try ZIP files @@ -187,4 +205,25 @@ public function getFiles(array $file_info): array // no archive: return the file itself return [$uploaded_file]; } + + /** + * @phpstan-return array + * Mapping of domain to label. + * + * @throws \CRM_Core_Exception + */ + private function getDomains(): array { + if (TRUE === Civi::settings()->get(CRM_Banking_Config::SETTING_FORCE_TRANSACTION_DOMAIN)) { + $domains = []; + } + else { + $domains = [self::DOMAIN_NONE => E::ts('None')]; + } + + /** @var \Civi\Banking\Permissions\AssignedTransactionDomainsLoader $domainsLoader */ + $domainsLoader = \Civi::service(AssignedTransactionDomainsLoader::class); + + return $domains + $domainsLoader->getAssignedTransactionDomainsWithLabel(); + } + } diff --git a/CRM/Banking/Page/Payments.php b/CRM/Banking/Page/Payments.php index 2679cd6b..a40bed29 100755 --- a/CRM/Banking/Page/Payments.php +++ b/CRM/Banking/Page/Payments.php @@ -14,6 +14,9 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Api4\BankTransaction; +use Civi\Api4\BankTransactionBatch; +use Civi\Banking\Permissions\AllowedDomainsSqlGenerator; use CRM_Banking_ExtensionUtil as E; require_once 'CRM/Core/Page.php'; @@ -96,12 +99,17 @@ function build_statementPage($payment_states) { $this->assign('url_show_payments_recently_completed', banking_helper_buildURL('civicrm/banking/payments', $this->_pageParameters(array('recent' => 1, 'status_ids'=>$payment_states['processed']['id'].",".$payment_states['ignored']['id'])))); } + /** @var \Civi\Banking\Permissions\AllowedDomainsSqlGenerator $allowedDomainsSqlGenerator */ + $allowedDomainsSqlGenerator = \Civi::service(AllowedDomainsSqlGenerator::class); + $allowedDomainsClause = $allowedDomainsSqlGenerator->generateWhereClause(); + // FIRST: CALCULATE COUNTS // calculate statement counts: NEW (at least one transaction in status new) $new_statement_id_list = CRM_Core_DAO::singleValueQuery(" SELECT GROUP_CONCAT(DISTINCT(tx_batch_id)) FROM civicrm_bank_tx WHERE status_id IN ({$payment_states['new']['id']}) + AND $allowedDomainsClause AND id NOT IN ( SELECT tx_batch_id FROM civicrm_bank_tx @@ -120,7 +128,8 @@ function build_statementPage($payment_states) { $open_statement_id_list = CRM_Core_DAO::singleValueQuery(" SELECT GROUP_CONCAT(DISTINCT(tx_batch_id)) FROM civicrm_bank_tx - WHERE status_id IN ({$payment_states['suggestions']['id']})"); + WHERE status_id IN ({$payment_states['suggestions']['id']}) + AND $allowedDomainsClause"); if (empty($open_statement_id_list)) { $open_statement_ids = []; $open_statement_id_list = ''; @@ -135,7 +144,7 @@ function build_statementPage($payment_states) { $non_closed_statement_ids = array_unique(array_merge($open_statement_ids, $new_statement_ids)); // closed count is merely the total count without the former two - $total_statement_count = CRM_Core_DAO::singleValueQuery("SELECT COUNT(id) FROM civicrm_bank_tx_batch;"); + $total_statement_count = BankTransactionBatch::get()->selectRowCount()->execute()->countMatched(); $closed_statement_count = $total_statement_count - count($non_closed_statement_ids); $this->assign('count_completed', $closed_statement_count); @@ -144,6 +153,9 @@ function build_statementPage($payment_states) { $where_clause .= " AND (btxb.starting_date >= DATE(NOW() - {$recently_closed_cutoff})) "; } + $allowedBatchDomainsClause = $allowedDomainsSqlGenerator->generateWhereClause('btxb'); + $where_clause .= ' AND ' . $allowedBatchDomainsClause; + // add the 'recently closed' count if ($recently_closed_cutoff) { if ($non_closed_statement_ids) { @@ -155,7 +167,8 @@ function build_statementPage($payment_states) { SELECT COUNT(DISTINCT(id)) FROM civicrm_bank_tx_batch btxb WHERE starting_date >= (NOW() - {$recently_closed_cutoff}) - AND btxb.id NOT IN ({$non_closed_statement_id_list});"); + AND btxb.id NOT IN ({$non_closed_statement_id_list}); + AND $allowedBatchDomainsClause"); $this->assign('count_recently_completed', $recently_closed_statement_count); } @@ -371,25 +384,18 @@ function build_paymentPage($payment_states) { * will take a comma separated list of statement IDs and create a list of the related payment ids in the same format */ public static function getPaymentsForStatements($raw_statement_list) { - $payments = array(); - $raw_statements = explode(",", $raw_statement_list); - if (count($raw_statements)==0) { + $raw_statements = explode(',', $raw_statement_list); + if ($raw_statements === []) { return ''; } - $statements = array(); - # make sure, that the statments are all integers (SQL injection) - foreach ($raw_statements as $stmt_id) { - array_push($statements, intval($stmt_id)); - } - $statement_list = implode(",", $statements); + $payments = BankTransaction::get() + ->addSelect('id') + ->addWhere('tx_batch_id', 'IN', $raw_statements) + ->execute() + ->column('id'); - $sql_query = "SELECT id FROM civicrm_bank_tx WHERE tx_batch_id IN ($statement_list);"; - $stmt_ids = CRM_Core_DAO::executeQuery($sql_query); - while($stmt_ids->fetch()) { - array_push($payments, $stmt_ids->id); - } - return implode(",", $payments); + return implode(',', $payments); } /** @@ -404,14 +410,16 @@ function investigate($stmt_id, $payment_states) { $stmt_id = intval($stmt_id); $count = 0; - $sql_query = "SELECT status_id, COUNT(status_id) AS count FROM civicrm_bank_tx WHERE tx_batch_id=$stmt_id GROUP BY status_id;"; - $stats = CRM_Core_DAO::executeQuery($sql_query); - // this creates a table: | status_id | count | + $statsList = BankTransaction::get() + ->addSelect('status_id', 'COUNT(status_id) AS count') + ->addWhere('tx_batch_id', '=', $stmt_id) + ->addGroupBy('status_id') + ->execute(); - $status2count = array(); - while ($stats->fetch()) { - $status2count[$stats->status_id] = $stats->count; - $count += $stats->count; + $status2count = []; + foreach ($statsList as $stats) { + $status2count[$stats['status_id']] = $stats['count']; + $count += $stats['count']; } if ($count) { @@ -468,22 +476,22 @@ function assignTransactionCountStats($payment_states) { } // execute SQL - if (count($clean_batch_ids)) { - $batch_id_list = implode(',', $clean_batch_ids); - $sql = "SELECT status_id, COUNT(id) AS count FROM civicrm_bank_tx WHERE tx_batch_id IN ($batch_id_list) GROUP BY status_id;"; - } else { - $sql = "SELECT status_id, COUNT(id) AS count FROM civicrm_bank_tx GROUP BY status_id;"; + $getAction = BankTransaction::get() + ->addSelect('status_id', 'COUNT(id) AS count') + ->addGroupBy('status_id'); + if ($clean_batch_ids !== []) { + $getAction->addWhere('tx_batch_id', 'IN', $clean_batch_ids); } - $query = CRM_Core_DAO::executeQuery($sql); - while ($query->fetch()) { - if ($query->status_id == $payment_states['new']['id']) { - $count_new += $query->count; - } elseif ($query->status_id == $payment_states['processed']['id']) { - $count_completed += $query->count; - } elseif ($query->status_id == $payment_states['ignored']['id']) { - $count_completed += $query->count; - } elseif ($query->status_id == $payment_states['suggestions']['id']) { - $count_analysed += $query->count; + + foreach ($getAction->execute() as $stats) { + if ($stats['status_id'] == $payment_states['new']['id']) { + $count_new += $stats['count']; + } elseif ($stats['status_id'] == $payment_states['processed']['id']) { + $count_completed += $stats['count']; + } elseif ($stats['status_id'] == $payment_states['ignored']['id']) { + $count_completed += $stats['count']; + } elseif ($stats['status_id'] == $payment_states['suggestions']['id']) { + $count_analysed += $stats['count']; } } @@ -493,7 +501,6 @@ function assignTransactionCountStats($payment_states) { $this->assign('count_completed', $count_completed); } - /** * load BTXs according to the 'status_ids' and 'batch_ids' values in $_REQUEST * @@ -523,27 +530,15 @@ function load_btx($payment_states) { // TODO: later add: $page_nr=0, $page_size function _findBTX($status_id, $batch_id) { $transaction_display_cutoff = CRM_Banking_Config::transactionViewCutOff(); - $btxs = []; - $btx_search = new CRM_Banking_BAO_BankTransaction(); - $btx_search->limit($transaction_display_cutoff); - if (!empty($status_id)) $btx_search->status_id = (int) $status_id; - if (!empty($batch_id)) $btx_search->tx_batch_id = (int) $batch_id; - $btx_search->find(); - while ($btx_search->fetch()) { - $btxs[] = array( - 'id' => $btx_search->id, - 'value_date' => $btx_search->value_date, - 'sequence' => $btx_search->sequence, - 'currency' => $btx_search->currency, - 'amount' => $btx_search->amount, - 'status_id' => $btx_search->status_id, - 'data_parsed' => $btx_search->data_parsed, - 'suggestions' => $btx_search->suggestions, - 'ba_id' => $btx_search->ba_id, - 'party_ba_id' => $btx_search->party_ba_id, - 'tx_batch_id' => $btx_search->tx_batch_id, - ); + $getAction = BankTransaction::get() + ->setLimit($transaction_display_cutoff); + if (!empty($status_id)) { + $getAction->addWhere('status_id', '=', $status_id); + } + if (!empty($batch_id)) { + $getAction->addWhere('tx_bach_id', '=', $batch_id); } + $btxs = $getAction->execute()->getArrayCopy(); if (count($btxs) >= $transaction_display_cutoff) { CRM_Core_Session::setStatus( diff --git a/CRM/Banking/Page/Review.php b/CRM/Banking/Page/Review.php index dc5fff50..866becc1 100755 --- a/CRM/Banking/Page/Review.php +++ b/CRM/Banking/Page/Review.php @@ -14,6 +14,8 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Api4\BankTransaction; +use Civi\Banking\Permissions\TransactionAccessChecker; use CRM_Banking_ExtensionUtil as E; require_once 'CRM/Core/Page.php'; @@ -412,19 +414,23 @@ function _pageParameters($override=array()) { function getUnprocessedInfo($pid_list, $next_pid, $choices) { // first, only query the remaining items $index = array_search($next_pid, $pid_list); - $remaining_list = implode(',', array_slice($pid_list, $index)); - - $unprocessed_states = $choices['ignored']['id'].','.$choices['processed']['id']; - $unprocessed_sql = "SELECT id FROM civicrm_bank_tx WHERE `status_id` NOT IN ($unprocessed_states) AND `id` IN ($remaining_list)"; - $unprocessed_query = CRM_Core_DAO::executeQuery($unprocessed_sql); + $remaining_ids = array_slice($pid_list, $index); + + $unprocessed_ids = BankTransaction::get() + ->addSelect('id') + ->addWhere('status_id', '!=', $choices['ignored']['id']) + ->addWhere('status_id', '!=', $choices['processed']['id']) + ->addWhere('id', 'IN', $remaining_ids) + ->execute() + ->column('id'); $next_unprocessed_pid = count($pid_list) + 1; $unprocessed_count = 0; - while ($unprocessed_query->fetch()) { + foreach ($unprocessed_ids as $unprocessed_id) { $unprocessed_count++; - $unprocessed_id = $unprocessed_query->id; - $new_index = array_search($unprocessed_query->id, $pid_list); - if ($new_index < $next_unprocessed_pid) + $new_index = array_search($unprocessed_id, $pid_list); + if ($new_index < $next_unprocessed_pid) { $next_unprocessed_pid = $new_index; + } } if ($next_unprocessed_pid < count($pid_list)) { @@ -448,6 +454,16 @@ function execute_suggestion($suggestion_hash, $parameters, $btx_bao, $choices) { $btx_bao = new CRM_Banking_BAO_BankTransaction(); $btx_bao->get('id', $parameters['execute']); } + + if (!TransactionAccessChecker::isAccessibleById((int) $btx_bao->id)) { + CRM_Core_Session::setStatus( + E::ts('Invalid transaction ID %1.', [1 => $btx_bao->id]), + E::ts('Error') + ); + + return NULL; + } + $suggestion = $btx_bao->getSuggestionByHash($suggestion_hash); if ($suggestion) { // update the parameters @@ -460,7 +476,7 @@ function execute_suggestion($suggestion_hash, $parameters, $btx_bao, $choices) { if ($result === 're-run') { // re-analyse + reload the page $engine = CRM_Banking_Matcher_Engine::getInstance(); - $engine->match($parameters['execute']); + $engine->match($btx_bao->id); CRM_Core_Session::setStatus(E::ts("The transaction has been analysed again."), E::ts("Transaction analysed"), 'info'); $transaction->commit(); return NULL; // NO SUCCESSFUL EXECUTION (because it's a re-run) @@ -488,4 +504,5 @@ function execute_suggestion($suggestion_hash, $parameters, $btx_bao, $choices) { } return NULL; // NO SUCCESSFUL EXECUTION } + } diff --git a/CRM/Banking/Page/StatementLine.php b/CRM/Banking/Page/StatementLine.php index e939dc50..c6e96c1e 100644 --- a/CRM/Banking/Page/StatementLine.php +++ b/CRM/Banking/Page/StatementLine.php @@ -1,4 +1,6 @@ assign('can_delete', CRM_Core_Permission::check('administer CiviCRM')); - + $statusApi = civicrm_api3('OptionValue', 'get', array('option_group_id' => 'civicrm_banking.bank_tx_status', 'options' => array('limit' => 0))); $statuses = array(); $statusCount = array(); @@ -17,18 +19,18 @@ public function run() { $statusCount[$status['name']] = 0; } $this->assign('statuses', $statuses); - + $selectedStatuses = $session->get('org.project60.banking.statementline.statusfilter'); if ($selectedStatuses) { $selectedStatuses = json_decode($selectedStatuses); } else { - $selectedStatuses = array_keys($statuses); + $selectedStatuses = array_keys($statuses); } - + if (isset($_REQUEST['reset']) && $_REQUEST['reset'] === '1') { $selectedStatuses = array_keys($statuses); } - + if (isset($_REQUEST['status']) && is_array($_REQUEST['status'])) { $selectedStatuses = CRM_Utils_Type::escapeAll($_REQUEST['status'], 'Integer', true); $session->set('org.project60.banking.statementline.statusfilter', json_encode($selectedStatuses)); @@ -40,11 +42,16 @@ public function run() { $this->assign('selectedStatuses', $selectedStatuses); $statement_id = CRM_Utils_Request::retrieve('s_id', 'Positive', CRM_Core_DAO::$_nullObject, TRUE); - - $sql = "SELECT tx.*, DATE(value_date) AS date, status.name as status_name, status.label as status_label - FROM civicrm_bank_tx tx LEFT JOIN civicrm_option_value status ON status.id = tx.status_id + + /** @var \Civi\Banking\Permissions\AllowedDomainsSqlGenerator $allowedDomainsSqlGenerator */ + $allowedDomainsSqlGenerator = \Civi::service(AllowedDomainsSqlGenerator::class); + $allowedDomainsClause = $allowedDomainsSqlGenerator->generateWhereClause('tx'); + + $sql = "SELECT tx.*, DATE(value_date) AS date, status.name as status_name, status.label as status_label + FROM civicrm_bank_tx tx LEFT JOIN civicrm_option_value status ON status.id = tx.status_id WHERE tx.tx_batch_id = %1 {$selectedStatusesWhereClause} + AND $allowedDomainsClause ORDER BY status.weight, value_date"; $queryParams[1] = array($statement_id, 'Integer'); $lines = array(); @@ -61,15 +68,15 @@ public function run() { $line['status'] = $lineDao->status_label; $line['status_name'] = $lineDao->status_name; $lines[$lineDao->id] = $line; - + $statusCount[$lineDao->status_name] ++; } - + $this->assign('lines', $lines); $this->assign('statement_id', $statement_id); $this->assign('status_count', $statusCount); $this->assign('list', implode(",", array_keys($lines))); - + parent::run(); } diff --git a/CRM/Banking/Page/Statements.php b/CRM/Banking/Page/Statements.php index 1eb01164..40907bb9 100644 --- a/CRM/Banking/Page/Statements.php +++ b/CRM/Banking/Page/Statements.php @@ -1,4 +1,6 @@ assign('can_delete', CRM_Core_Permission::check('administer CiviCRM')); $this->assign('url_export_selected_payments', banking_helper_buildURL('civicrm/banking/export', array('s_list'=>"__selected__"))); - $statements = array(); + $statements = []; // collect an array of target accounts, serving to limit the display - $target_accounts = array(); + $target_accounts = []; $statementSelect = "SELECT btxb.id AS id, @@ -36,10 +38,11 @@ public function run() { $statementOrderBy = "ORDER BY starting_date DESC"; $statementLimit = "LIMIT %1, %2"; $paramCount = 2; - $queryParams = array(); + $queryParams = []; - $statementsWhereClauses = array(); - $statementsWhereClauses[] = "1"; + /** @var \Civi\Banking\Permissions\AllowedDomainsSqlGenerator $allowedDomainsSqlGenerator */ + $allowedDomainsSqlGenerator = \Civi::service(AllowedDomainsSqlGenerator::class); + $statementsWhereClauses = [$allowedDomainsSqlGenerator->generateWhereClause('btxb')]; $target_ba_id = CRM_Utils_Request::retrieve('target_ba_id', 'Integer', CRM_Core_DAO::$_nullObject, false, -1); if ($target_ba_id > 0) { @@ -75,7 +78,7 @@ public function run() { $this->_pager = new CRM_Utils_Pager($params); $sql = "{$statementSelect} {$statementFrom} {$statementWhere} {$statementGroupBy} {$statementOrderBy} {$statementLimit}"; - list($offset, $limit) = $this->_pager->getOffsetAndRowCount(); + [$offset, $limit] = $this->_pager->getOffsetAndRowCount(); $queryParams[1] = array($offset, 'Integer'); $queryParams[2] = array($limit, 'Integer'); @@ -113,10 +116,11 @@ public function run() { // Build the status count for each statement. $batchIds = implode(',', array_keys($statements)); if (count($statements)) { + $allowedDomainsClause = $allowedDomainsSqlGenerator->generateWhereClause('tx'); $statusCountSql = "SELECT COUNT(*) as count, status.name as status, tx.tx_batch_id as batch_id FROM civicrm_bank_tx tx LEFT JOIN civicrm_option_value status ON status.id = tx.status_id - WHERE tx.tx_batch_id IN ({$batchIds}) + WHERE tx.tx_batch_id IN ({$batchIds}) AND $allowedDomainsClause GROUP BY tx.tx_batch_id, tx.status_id"; $statusDao = CRM_Core_DAO::executeQuery($statusCountSql); diff --git a/CRM/Banking/PluginModel/Exporter.php b/CRM/Banking/PluginModel/Exporter.php index 82247e3b..49ced337 100755 --- a/CRM/Banking/PluginModel/Exporter.php +++ b/CRM/Banking/PluginModel/Exporter.php @@ -14,6 +14,9 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use Civi\Api4\BankTransaction; +use Civi\Api4\BankTransactionBatch; + /** * * @package org.project60.banking @@ -26,32 +29,32 @@ abstract class CRM_Banking_PluginModel_Exporter extends CRM_Banking_PluginModel_ // ------------------------------------------------------ // Functions to be provided by the plugin implementations // ------------------------------------------------------ - /** + /** * Report if the plugin is capable of exporting files - * + * * @return bool */ abstract function does_export_files(); - /** + /** * Report if the plugin is capable of exporting streams, i.e. data from a non-file source, e.g. the web - * + * * @return bool */ abstract function does_export_stream(); - /** + /** * Export the given btxs - * + * * $txbatch2ids array( => array()) * * @return URL of the resulting file */ abstract function export_file( $txbatch2ids, $parameters ); - /** + /** * Export the given btxs - * + * * $txbatch2ids array( => array()) * * @return bool TRUE if successful @@ -61,39 +64,43 @@ abstract function export_stream( $txbatch2ids, $parameters ); /** - * will evaluate the 'list' (comma separated list of tx IDs) and + * will evaluate the 'list' (comma separated list of tx IDs) and * 's_list' (comma separated list of tx_batch IDs), if given. * - * @return an array('tx_batch_id' => array('tx_id')) + * @phpstan-return array> + * Mapping of 'tx_batch_id' to list of 'tx_id'. */ - public static function getIdLists($params) { + public static function getIdLists($params): array { // first: extract all the IDs if (!empty($params['list'])) { - $ids = explode(",", $params['list']); + $ids = explode(',', $params['list']); } else { - $ids = array(); + $ids = []; } if (!empty($params['s_list'])) { $list = CRM_Banking_Page_Payments::getPaymentsForStatements($params['s_list']); - $ids = array_merge(explode(",", $list), $ids); + $ids = array_merge(explode(',', $list), $ids); } // now create a (sane) SQL query - $sane_ids = array(); + $sane_ids = []; foreach ($ids as $tx_id) { if (is_numeric($tx_id)) { - $sane_ids[]= (int) $tx_id; + $sane_ids[] = (int) $tx_id; } } - if (count($sane_ids) == 0) return array(); - $sane_ids_list = implode(',', $sane_ids); + if ($sane_ids === []) { + return []; + } // query the DB - $query_sql = "SELECT id, tx_batch_id FROM civicrm_bank_tx WHERE id IN ($sane_ids_list);"; - $result = array(); - $query = CRM_Core_DAO::executeQuery($query_sql); - while ($query->fetch()) { - $result[$query->tx_batch_id][] = $query->id; + $transactions = BankTransaction::get() + ->addSelect('id', 'tx_batch_id') + ->addWhere('id', 'IN', $sane_ids) + ->execute(); + $result = []; + foreach ($transactions as $transaction) { + $result[$transaction['tx_batch_id']][] = $transaction['id']; } return $result; @@ -113,11 +120,12 @@ public function getTempFile($params = array()) { /** * Gather all information on the batch - * - * @return an array containing all values, keys prefixed with 'txbatch_' + * + * @phpstan-return array + * All values, keys prefixed with 'txbatch_' */ public function getBatchData($tx_batch_id) { - $result = array(); + $result = []; // add default values $config = $this->_plugin_config; @@ -127,20 +135,25 @@ public function getBatchData($tx_batch_id) { } } - $txbatch = civicrm_api3('BankingTransactionBatch', 'getsingle', array('id' => $tx_batch_id)); - if (empty($txbatch['is_error'])) { + try { + $txbatch = BankTransactionBatch::get() + ->addWhere('id', '=', $tx_batch_id) + ->execute() + ->single(); foreach ($txbatch as $key => $value) { - $result['txbatch_'.$key] = $value; + $result['txbatch_' . $key] = $value; } - } else { - error_log("org.project60.banking.exporter.csv: error while reading tx_batch [$tx_batch_id]: " . $txbatch['error_message']); } + catch (\CRM_Core_Exception $e) { + error_log("org.project60.banking.exporter.csv: error while reading tx_batch [$tx_batch_id]: " . $e->getMessage()); + } + return $result; } /** * Gather all information on the transaction / payment - * + * * @return an array containing all values, keys prefixed with 'tx_' */ public function getTxData($tx_id) { @@ -228,16 +241,16 @@ public function getTxData($tx_id) { foreach ($contribution as $key => $value) { $result[$prefix . $key] = $value; } - if (!empty($contribution['total_amount'])) + if (!empty($contribution['total_amount'])) $total_sum += $contribution['total_amount']; - if (!empty($contribution['non_deductible_amount'])) + if (!empty($contribution['non_deductible_amount'])) $total_non_deductible += $contribution['non_deductible_amount']; if (!empty($contribution['currency'])) { if (empty($total_currency)) { $total_currency = $contribution['currency']; } elseif ($total_currency != $contribution['currency']) { $total_currency = 'MIX'; - } + } } } $counter++; @@ -257,7 +270,7 @@ public function getTxData($tx_id) { /** * standard-method to compile the data blob for the individual line - * + * * exporters may override this method to add more information * * @return the data blob to be used for the next line @@ -266,4 +279,3 @@ protected function compileDataBlob($tx_batch_data, $tx_data) { return array_merge($tx_batch_data, $tx_data); } } - diff --git a/CRM/Banking/PluginModel/Importer.php b/CRM/Banking/PluginModel/Importer.php index df86c035..f61f35a2 100755 --- a/CRM/Banking/PluginModel/Importer.php +++ b/CRM/Banking/PluginModel/Importer.php @@ -16,6 +16,8 @@ require_once 'CRM/Banking/Helpers/OptionValue.php'; +use Civi\Api4\BankTransaction; +use Civi\Api4\BankTransactionBatch; use CRM_Banking_ExtensionUtil as E; /** @@ -30,7 +32,7 @@ abstract class CRM_Banking_PluginModel_Importer extends CRM_Banking_PluginModel_ // these are the fields valid for a BTX record. protected $_primary_btx_fields = array('version', 'debug', 'amount', 'bank_reference', 'value_date', 'booking_date', 'currency', 'type_id', 'status_id', 'data_raw', 'data_parsed', 'ba_id', 'party_ba_id', 'tx_batch_id', 'sequence'); // these fields will be used to determine, if this is a duplicate record... the primary keys if you want - protected $_compare_btx_fields = array('bank_reference' => TRUE, 'amount' => TRUE, 'value_date' => TRUE, 'booking_date' => TRUE, 'currency' => TRUE, 'version' => 3); + protected $_compare_btx_fields = array('bank_reference' => TRUE, 'amount' => TRUE, 'value_date' => TRUE, 'booking_date' => TRUE, 'currency' => TRUE); // if this is set, all checkAndStoreBTX() methods will be added to it protected $_current_transaction_batch = NULL; protected $_current_transaction_batch_attributes = []; @@ -39,6 +41,8 @@ abstract class CRM_Banking_PluginModel_Importer extends CRM_Banking_PluginModel_ // this will be used to avoid multiple account lookups protected $account_cache = array(); + protected ?string $domain = NULL; + // ------------------------------------------------------ // Functions to be provided by the plugin implementations // ------------------------------------------------------ @@ -114,6 +118,16 @@ function __construct($plugin_dao) { if (!isset($config->organisation_ba_ids)) $config->organisation_ba_ids = ''; } + public function getDomain(): ?string { + return $this->domain; + } + + public function setDomain(?string $domain): self { + $this->domain = $domain; + + return $this; + } + // ------------------------------------------------------ // utility functions // ------------------------------------------------------ @@ -192,7 +206,8 @@ protected function lookupBankAccounts(&$data) { * * You can also re-use and extend a given btx batch by providing a batch ID */ - function openTransactionBatch($batch_id = 0) { + protected function openTransactionBatch($batch_id = 0) { + // @fixme Apply permissions if ($this->_current_transaction_batch == NULL) { $this->_current_transaction_batch = new CRM_Banking_BAO_BankTransactionBatch(); $this->_current_transaction_batch_attributes = array(); @@ -203,6 +218,8 @@ function openTransactionBatch($batch_id = 0) { $this->_current_transaction_batch_attributes['isnew'] = FALSE; $this->_current_transaction_batch_attributes['sum'] = ($this->_current_transaction_batch->ending_balance - $this->_current_transaction_batch->starting_balance); } else { + $this->_current_transaction_batch->domain = $this->domain; + // TODO: \/ why are the defaults not generated by CRM_Banking_BAO_BankTransactionBatch::add() ??? $this->_current_transaction_batch->issue_date = date('YmdHis'); $this->_current_transaction_batch->reference = ''; @@ -279,14 +296,19 @@ function closeTransactionBatch($store = TRUE) { // make sure, this reference doesn't exist yet $final_reference = $reference; $counter = 0; - $query_params = array( - 1 => array($final_reference, 'String'), - 2 => array($this->_current_transaction_batch->id, 'Integer')); - $query_sql = "SELECT COUNT(id) FROM civicrm_bank_tx_batch WHERE reference = %1 AND id != %2;"; - while (CRM_Core_DAO::singleValueQuery($query_sql, $query_params)) { + $countAction = BankTransactionBatch::get() + ->selectRowCount() + ->setWhere([ + ['reference', '=', $final_reference], + ['id', '!=', $this->_current_transaction_batch->id], + ]); + while ($countAction->execute()->countMatched() > 0) { $counter += 1; $final_reference = "DUPLICATE-{$counter}-{$reference}"; - $query_params[1] = array($final_reference, 'String'); + $countAction->setWhere([ + ['reference', '=', $final_reference], + ['id', '!=', $this->_current_transaction_batch->id], + ]); } $this->_current_transaction_batch->reference = substr($final_reference, 0, 64); @@ -350,25 +372,31 @@ function _updateTransactionBatchInfo($btx) { * In case the object exists, the existing entry is returned. * If the client wants to merge the data, this has to be done by the client. * - * @return TRUE, if successful, FALSE if not, or a duplicate existing BTX as property array + * @phpstan-return array|bool + * TRUE, if successful, FALSE if not, or a duplicate existing BTX as + * property array */ function checkAndStoreBTX($btx, $progress, $params = array()) { - // make sure the version is set - $btx['version'] = 3; - // first, test for duplicates: $duplicate_test = array_intersect_key($btx, $this->_compare_btx_fields); - $result = civicrm_api('BankingTransaction', 'get', $duplicate_test); - if (isset($result['is_error']) && $result['is_error']) { - $this->reportProgress($progress, E::ts("Failed to query BTX."), CRM_Banking_PluginModel_Base::REPORT_LEVEL_ERROR); + $getAction = BankTransaction::get()->setLimit(1); + foreach ($duplicate_test as $fieldName => $value) { + $getAction->addWhere($fieldName, '=', $value); + } + try { + $result = $getAction->execute(); + } + catch (\CRM_Core_Exception $e) { + $this->reportProgress($progress, E::ts('Failed to query BTX.'), CRM_Banking_PluginModel_Base::REPORT_LEVEL_ERROR); + return FALSE; } - if ($result['count'] > 0) { + if ($result->countFetched() > 0) { // there might be another BTX...check the accounts - $duplicates = $result['values']; $this->reportProgress($progress, E::ts("Duplicate BTX entry detected. Not imported!"), CRM_Banking_PluginModel_Base::REPORT_LEVEL_WARN); - return reset($duplicates); // RETURN FIRST ENTRY + + return $result->first(); // RETURN FIRST ENTRY } // set default state @@ -403,26 +431,31 @@ function checkAndStoreBTX($btx, $progress, $params = array()) { $btx['tx_batch_id'] = $this->_current_transaction_batch->id; } - $result = civicrm_api('BankingTransaction', 'create', $btx); - if ($result['is_error']) { + $btx['domain'] = $this->domain; + try { + $transaction = BankTransaction::create()->setValues($btx)->execute()->single(); + } + catch (\CRM_Core_Exception $e) { $this->reportProgress( - $progress, - sprintf(E::ts("Error while storing BTX: %s"),implode("
", $result)), - CRM_Banking_PluginModel_Base::REPORT_LEVEL_ERROR); + $progress, + sprintf(E::ts('Error while storing BTX: %s'), $e->getUserMessage()), + CRM_Banking_PluginModel_Base::REPORT_LEVEL_ERROR + ); + return FALSE; - } else { - $log_entry = E::ts("Created BTX %1 for %2 on %3", - array( 1 => $result['id'], - 2 => CRM_Utils_Money::format($btx['amount'], $btx['currency']), - 3 => CRM_Utils_Date::customFormat($btx['booking_date'], CRM_Core_Config::singleton()->dateformatFull))); - $this->reportProgress($progress, $log_entry); - $this->_updateTransactionBatchInfo($btx); - return TRUE; } + + $log_entry = E::ts("Created BTX %1 for %2 on %3", [ + 1 => $transaction['id'], + 2 => CRM_Utils_Money::format($transaction['amount'], $transaction['currency']), + 3 => CRM_Utils_Date::customFormat($transaction['booking_date'], CRM_Core_Config::singleton()->dateformatFull), + ]); + $this->reportProgress($progress, $log_entry); + $this->_updateTransactionBatchInfo($transaction); + return TRUE; } } - /** * helper function for prefix testing */ @@ -431,4 +464,3 @@ protected function startsWith($string, $prefix) { } } - diff --git a/CRM/Banking/Upgrader.php b/CRM/Banking/Upgrader.php index 50e44bbd..72d4ecf1 100644 --- a/CRM/Banking/Upgrader.php +++ b/CRM/Banking/Upgrader.php @@ -265,5 +265,13 @@ public function upgrade_0805() { return true; } + public function upgrade_0806(): bool { + $this->executeSql('ALTER TABLE civicrm_bank_tx ADD COLUMN domain VARCHAR(255) DEFAULT NULL AFTER sequence'); + $this->executeSql('ALTER TABLE civicrm_bank_tx ADD INDEX index_domain(domain)'); + $this->executeSql('ALTER TABLE civicrm_bank_tx_batch ADD COLUMN domain VARCHAR(255) DEFAULT NULL AFTER sequence'); + $this->executeSql('ALTER TABLE civicrm_bank_tx_batch ADD INDEX index_domain(domain)'); + + return TRUE; + } } diff --git a/Civi/Api4/BankTransaction.php b/Civi/Api4/BankTransaction.php index 2e3f9860..47caafb4 100644 --- a/Civi/Api4/BankTransaction.php +++ b/Civi/Api4/BankTransaction.php @@ -1,13 +1,18 @@ setCheckPermissions($checkPermissions); + } } diff --git a/Civi/Api4/BankTransactionBatch.php b/Civi/Api4/BankTransactionBatch.php index 14b1ff2c..bec41f64 100644 --- a/Civi/Api4/BankTransactionBatch.php +++ b/Civi/Api4/BankTransactionBatch.php @@ -1,13 +1,18 @@ setCheckPermissions($checkPermissions); + } } diff --git a/Civi/Banking/Api4/Action/BankTransaction/GetAction.php b/Civi/Banking/Api4/Action/BankTransaction/GetAction.php new file mode 100644 index 00000000..ca53e898 --- /dev/null +++ b/Civi/Banking/Api4/Action/BankTransaction/GetAction.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Api4\Action\BankTransaction; + +use Civi\Api4\Generic\DAOGetAction; +use Civi\Api4\Generic\Result; +use Civi\Banking\Permissions\AssignedTransactionDomainsLoader; +use Civi\Banking\Permissions\Permissions; + +/** + * Used for BankTransaction and BankTransactionBatch. + */ +final class GetAction extends DAOGetAction { + + private ?AssignedTransactionDomainsLoader $assignedTransactionDomainsLoader; + + public function __construct( + $entityName, + $actionName, + ?AssignedTransactionDomainsLoader $assignedTransactionDomainsLoader = NULL + ) { + parent::__construct($entityName, $actionName); + $this->assignedTransactionDomainsLoader = $assignedTransactionDomainsLoader; + } + + public function _run(Result $result): void { + $this->addPermissionConditions(); + + parent::_run($result); + } + + private function addPermissionConditions(): void { + if (!$this->getCheckPermissions() || \CRM_Core_Permission::check(Permissions::ACCESS_TRANSACTIONS_ALL)) { + return; + } + + $assignedDomains = $this->getAssignedTransactionDomainsLoader()->getAssignedTransactionDomains(); + if ([] === $assignedDomains) { + $this->addWhere('domain', 'IS NULL'); + } + else { + $this->addClause( + 'OR', + ['domain', 'IN', $assignedDomains], + ['domain', 'IS NULL'], + ); + } + } + + private function getAssignedTransactionDomainsLoader(): AssignedTransactionDomainsLoader { + // @phpstan-ignore-next-line + return $this->assignedTransactionDomainsLoader ??= \Civi::service(AssignedTransactionDomainsLoader::class); + } + +} diff --git a/Civi/Banking/Api4/Api3To4Util.php b/Civi/Banking/Api4/Api3To4Util.php new file mode 100644 index 00000000..752c9a52 --- /dev/null +++ b/Civi/Banking/Api4/Api3To4Util.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Api4; + +final class Api3To4Util { + + /** + * @phpstan-param array $params + * @phpstan-return list + * + * @throws \CRM_Core_Exception + */ + public static function createWhere(string $entityName, array $params): array { + $where = []; + + $fieldNames = civicrm_api4($entityName, 'getFields', [ + 'checkPermissions' => FALSE, + 'select' => ['name'], + ])->column('name'); + + foreach ($params as $fieldName => $value) { + if (!in_array($fieldName, $fieldNames, TRUE)) { + continue; + } + + if (is_array($value)) { + $where[] = [$fieldName, 'IN', $value]; + } + elseif (NULL === $value) { + $where[] = [$fieldName, 'IS NULL']; + } + else { + $where[] = [$fieldName, '=', $value]; + } + } + + return $where; + } + + /** + * @param array $params + * + * @phpstan-return array + * + * @throws \CRM_Core_Exception + */ + public static function createValues(string $entityName, array $params): array { + $fieldNames = civicrm_api4($entityName, 'getFields', [ + 'checkPermissions' => FALSE, + 'select' => ['name'], + ])->column('name'); + + return array_filter($params, fn ($key) => in_array($key, $fieldNames, TRUE), ARRAY_FILTER_USE_KEY); + } + +} diff --git a/Civi/Banking/Api4/Traits/TransactionPermissionsTrait.php b/Civi/Banking/Api4/Traits/TransactionPermissionsTrait.php new file mode 100644 index 00000000..ef4a4838 --- /dev/null +++ b/Civi/Banking/Api4/Traits/TransactionPermissionsTrait.php @@ -0,0 +1,52 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Api4\Traits; + +use Civi\Banking\Permissions\Permissions; + +/** + * Permissions for funding entities related to administration. + */ +trait TransactionPermissionsTrait { + + /** + * @phpstan-return array> + */ + public static function permissions(): array { + return [ + 'meta' => [ + 'access CiviCRM', + [ + Permissions::ACCESS_TRANSACTIONS, + 'access CiviContribute', + ], + ], + 'default' => [ + 'access CiviCRM', + [ + Permissions::ACCESS_TRANSACTIONS, + 'access CiviContribute', + ], + ], + 'delete' => ['administer CiviCRM'], + ]; + } + +} diff --git a/Civi/Banking/EventSubscriber/TransactionAuthorizeRecordSubscriber.php b/Civi/Banking/EventSubscriber/TransactionAuthorizeRecordSubscriber.php new file mode 100644 index 00000000..ba11a43b --- /dev/null +++ b/Civi/Banking/EventSubscriber/TransactionAuthorizeRecordSubscriber.php @@ -0,0 +1,88 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\EventSubscriber; + +use Civi\Api4\BankTransaction; +use Civi\Api4\BankTransactionBatch; +use Civi\Api4\Event\AuthorizeRecordEvent; +use Civi\Banking\Permissions\AssignedTransactionDomainsLoader; +use Civi\Banking\Permissions\Permissions; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +final class TransactionAuthorizeRecordSubscriber implements EventSubscriberInterface { + + /** + * @inheritDoc + */ + public static function getSubscribedEvents(): array { + return [ + 'civi.api4.authorizeRecord' => 'onAuthorizeRecord', + ]; + } + + /** + * @throws \CRM_Core_Exception + */ + public function onAuthorizeRecord(AuthorizeRecordEvent $event): void { + if (BankTransaction::getEntityName() !== $event->getEntityName() + && BankTransactionBatch::getEntityName() !== $event->getEntityName() + ) { + return; + } + + if (\CRM_Core_Permission::check(Permissions::ACCESS_TRANSACTIONS_ALL)) { + return; + } + + if (!$this->isDomainAllowed($event->getRecord()['domain'] ?? NULL) + // Actually this check is only necessary if the id was given in the initial request. + // Otherwise, the get action was already called to fetch the id. + || (isset($event->getRecord()['id']) + && !$this->isCurrentRecordAccessible($event->getEntityName(), $event->getRecord()['id'])) + ) { + $event->setAuthorized(FALSE); + $event->stopPropagation(); + } + } + + private function isDomainAllowed(?string $domain): bool { + if (NULL === $domain) { + return TRUE; + } + + /** @var \Civi\Banking\Permissions\AssignedTransactionDomainsLoader $assignedTransactionDomainsLoader */ + $assignedTransactionDomainsLoader = \Civi::service(AssignedTransactionDomainsLoader::class); + $assignedDomains = $assignedTransactionDomainsLoader->getAssignedTransactionDomains(); + + return !in_array($domain, $assignedDomains, TRUE); + } + + /** + * @throws \CRM_Core_Exception + */ + private function isCurrentRecordAccessible(string $entityName, int $id): bool { + return NULL !== civicrm_api4($entityName, 'get', [ + 'select' => ['id'], + 'where' => [['id', '=', $id]], + ])->first(); + } + +} diff --git a/Civi/Banking/Permissions/AllowedDomainsSqlGenerator.php b/Civi/Banking/Permissions/AllowedDomainsSqlGenerator.php new file mode 100644 index 00000000..0742ea99 --- /dev/null +++ b/Civi/Banking/Permissions/AllowedDomainsSqlGenerator.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Permissions; + +final class AllowedDomainsSqlGenerator { + + private AssignedTransactionDomainsLoader $assignedTransactionDomainsLoader; + + public function __construct(AssignedTransactionDomainsLoader $assignedTransactionDomainsLoader) { + $this->assignedTransactionDomainsLoader = $assignedTransactionDomainsLoader; + } + + public function generateWhereClause(string $tableAlias = ''): string { + $fieldName = 'domain'; + if ('' !== $tableAlias) { + $fieldName = $tableAlias . '.' . $fieldName; + } + + $assignedDomains = $this->assignedTransactionDomainsLoader->getAssignedTransactionDomains(); + if ([] === $assignedDomains) { + return $fieldName . ' IS NULL'; + } + else { + $quotedDomains = array_map([\CRM_Core_DAO::class, 'escapeString'], $assignedDomains); + + return "($fieldName is NULL OR $fieldName IN ('" . implode("','", $quotedDomains) . "'))"; + } + } + +} diff --git a/Civi/Banking/Permissions/AssignedTransactionDomainsLoader.php b/Civi/Banking/Permissions/AssignedTransactionDomainsLoader.php new file mode 100644 index 00000000..b4e6d907 --- /dev/null +++ b/Civi/Banking/Permissions/AssignedTransactionDomainsLoader.php @@ -0,0 +1,86 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Permissions; + +use Psr\SimpleCache\CacheInterface; + +class AssignedTransactionDomainsLoader { + + private CacheInterface $cache; + + private TransactionDomainPermissionsGenerator $permissionsGenerator; + + public function __construct(CacheInterface $cache, TransactionDomainPermissionsGenerator $permissionsGenerator) { + $this->cache = $cache; + $this->permissionsGenerator = $permissionsGenerator; + } + + /** + * @param int|null $contactId + * NULL for the current user's contact ID. + * + * @phpstan-return list + * + * @throws \CRM_Core_Exception + */ + public function getAssignedTransactionDomains(?int $contactId = NULL): array { + return array_keys($this->getAssignedTransactionDomainsWithLabel($contactId)); + } + + /** + * @param int|null $contactId + * NULL for the current user's contact ID. + * + * @phpstan-return array + * Mapping of domain to label. + * + * @throws \CRM_Core_Exception + */ + public function getAssignedTransactionDomainsWithLabel(?int $contactId = NULL): array { + $contactId ??= \CRM_Core_Session::getLoggedInContactID(); + $cacheKey = 'banking.transaction.assignedDomains:' . $contactId; + if (!$this->cache->has($cacheKey)) { + $this->cache->set($cacheKey, $this->doGetAssignedTransactionDomainsWithLabel($contactId)); + } + + // @phpstan-ignore return.type + return $this->cache->get($cacheKey); + } + + /** + * @phpstan-return array + * Mapping of domain to label. + * + * @throws \CRM_Core_Exception + */ + private function doGetAssignedTransactionDomainsWithLabel(?int $contactId): array { + $domains = []; + + foreach ($this->permissionsGenerator->generatePermissions() as $permission => $details) { + if (\CRM_Core_Permission::check($permission, $contactId)) { + $domains[$details['_domain']] = $details['_domain_label']; + } + } + + return $domains; + } + +} diff --git a/Civi/Banking/Permissions/Permissions.php b/Civi/Banking/Permissions/Permissions.php new file mode 100644 index 00000000..88f80bc0 --- /dev/null +++ b/Civi/Banking/Permissions/Permissions.php @@ -0,0 +1,29 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Permissions; + +final class Permissions { + + public const ACCESS_TRANSACTIONS = 'access banking transactions'; + + public const ACCESS_TRANSACTIONS_ALL = 'access banking transactions all'; + +} diff --git a/Civi/Banking/Permissions/TransactionAccessChecker.php b/Civi/Banking/Permissions/TransactionAccessChecker.php new file mode 100644 index 00000000..132a6f4b --- /dev/null +++ b/Civi/Banking/Permissions/TransactionAccessChecker.php @@ -0,0 +1,44 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Permissions; + +use Civi\Api4\BankTransaction; + +final class TransactionAccessChecker { + + /** + * This method should only be used in cases where direct usage of the APIv4 + * actions is not possible. + * + * @return bool + * True if the transaction is accessible, or false if it doesn't exist or + * permission is not granted. + * + * @throws \CRM_Core_Exception + */ + public static function isAccessibleById(int $transactionId): bool { + return 1 === BankTransaction::get() + ->addSelect('id') + ->addWhere('id', '=', $transactionId) + ->execute() + ->countMatched(); + } + +} diff --git a/Civi/Banking/Permissions/TransactionDomainPermissionsGenerator.php b/Civi/Banking/Permissions/TransactionDomainPermissionsGenerator.php new file mode 100644 index 00000000..a9f16143 --- /dev/null +++ b/Civi/Banking/Permissions/TransactionDomainPermissionsGenerator.php @@ -0,0 +1,89 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Permissions; + +use Civi\Api4\OptionValue; +use CRM_Banking_ExtensionUtil as E; +use Psr\SimpleCache\CacheInterface; + +class TransactionDomainPermissionsGenerator { + + private CacheInterface $cache; + + public function __construct(CacheInterface $cache) { + $this->cache = $cache; + } + + /** + * @phpstan-return array + * + * @throws \CRM_Core_Exception + */ + public function generatePermissions(): array { + if (!$this->cache->has('banking.transaction.permissions')) { + $this->cache->set('banking.transaction.permissions', iterator_to_array($this->doGeneratePermissions())); + } + + // @phpstan-ignore return.type + return $this->cache->get('banking.transaction.permissions'); + } + + /** + * @phpstan-return \Traversable + * + * @throws \CRM_Core_Exception + */ + private function doGeneratePermissions(): \Traversable { + $domains = civicrm_api4(OptionValue::getEntityName(), 'get', [ + 'select' => ['value', 'label'], + 'where' => [ + ['option_group_id:name', '=', 'banking_transaction_domain'], + ['is_active', '=', TRUE], + ], + 'orderBy' => ['weight' => 'ASC'], + 'checkPermissions' => FALSE, + ]); + + /** @phpstan-var array{value: string, label: string} $domain */ + foreach ($domains as $domain) { + yield 'access banking transactions for ' . $domain['value'] => [ + 'label' => E::ts('CiviBanking: Access transactions for %1', [1 => $domain['label']]), + 'description' => E::ts('Access CiviBanking transactions with domain %1.', [1 => $domain['label']]), + 'implies' => [Permissions::ACCESS_TRANSACTIONS], + '_domain' => $domain['value'], + '_domain_label' => $domain['label'], + ]; + } + } + +} diff --git a/api/v3/BankingRule/Match.php b/api/v3/BankingRule/Match.php index e339fe37..a68f0062 100644 --- a/api/v3/BankingRule/Match.php +++ b/api/v3/BankingRule/Match.php @@ -1,5 +1,7 @@ setValues($values) + ->execute() + ->single(); + + return civicrm_api3_create_success($resultValues, $params, 'BankTransaction', 'create'); } /** @@ -56,16 +67,27 @@ function _civicrm_api3_banking_transaction_create_spec(&$params) { /** * Deletes an existing BankingTransaction * - * @param array $params + * @param array $params * * @example BankingTransaction.php Standard Delete Example * - * @return boolean | error true if successfull, error otherwise - * {@getfields banking_transaction_delete} + * @return array * @access public */ -function civicrm_api3_banking_transaction_delete($params) { - return _civicrm_api3_basic_delete('CRM_Banking_BAO_BankTransaction', $params); +function civicrm_api3_banking_transaction_delete($params): array { + if (isset($params['id'])) { + throw new CRM_Core_Exception( + 'Mandatory key(s) missing from params array: ' . implode(', ', ['id']), + 'mandatory_missing', + ['fields' => ['id']] + ); + } + + BankTransaction::delete($params['check_permissions'] ?? FALSE) + ->addWhere('id', '=', $params['id']) + ->execute(); + + return civicrm_api3_create_success(); } /** @@ -78,14 +100,48 @@ function civicrm_api3_banking_transaction_delete($params) { * * @param array $params an associative array of name/value pairs. * - * @return array api result array + * @return array api result array * {@getfields banking_transaction_get} * @access public */ function civicrm_api3_banking_transaction_get($params) { - return _civicrm_api3_basic_get('CRM_Banking_BAO_BankTransaction', $params); -} + $options = _civicrm_api3_get_options_from_params($params); + $where = Api3To4Util::createWhere(BankTransaction::getEntityName(), $params); + $action = BankTransaction::get($params['check_permissions'] ?? FALSE) + ->setWhere($where) + ->setLimit($options['limit']) + ->setOffset($options['offset']); + + if ($options['is_count']) { + $action->selectRowCount(); + } + else { + $action->setSelect(array_keys(array_filter($options['return']))); + if (isset($options['sort'])) { + [$sortFieldName, $sortDirection] = explode(' ', $options['sort']); + $action->addOrderBy($sortFieldName, $sortDirection); + } + } + + $result = $action->execute(); + + if ($options['is_count']) { + return civicrm_api3_create_success( + $result->countMatched(), + $params, + 'BankTransaction', + 'get' + ); + } + + return civicrm_api3_create_success( + $result->indexBy('id')->getArrayCopy(), + $params, + 'BankTransaction', + 'get' + ); +} /** * Deletes a given list of bank statments and transactions @@ -103,7 +159,9 @@ function civicrm_api3_banking_transaction_deletelist($params) { // first, delete the indivdual transactions $tx_ids = _civicrm_api3_banking_transaction_getTxIDs($params); foreach ($tx_ids as $tx_id) { - civicrm_api3('BankingTransaction', 'delete', array('id' => $tx_id)); + BankTransaction::delete($params['check_permissions'] ?? FALSE) + ->addWhere('id', '=', $tx_id) + ->execute(); $result['tx_count'] += 1; } @@ -113,7 +171,9 @@ function civicrm_api3_banking_transaction_deletelist($params) { foreach ($tx_batch_ids as $tx_batch_id) { $tx_batch_id = (int) $tx_batch_id; if (!empty($tx_batch_id)) { - civicrm_api3('BankingTransactionBatch', 'delete', array('id' => $tx_batch_id)); + BankTransactionBatch::delete($params['check_permissions'] ?? FALSE) + ->addWhere('id', '=', $tx_batch_id) + ->execute(); $result['tx_batch_count'] += 1; } } @@ -143,16 +203,12 @@ function civicrm_api3_banking_transaction_analyselist($params) { $payment_states = banking_helper_optiongroup_id_name_mapping('civicrm_banking.bank_tx_status'); $state_ignored = (int) $payment_states['ignored']['id']; $state_processed = (int) $payment_states['processed']['id']; - $list_string = implode(',', $tx_ids); - $filter_query = "SELECT `id` - FROM `civicrm_bank_tx` - WHERE `status_id` NOT IN ({$state_ignored},{$state_processed}) - AND `id` IN ($list_string);"; - $filter_result = CRM_Core_DAO::executeQuery($filter_query); - $filtered_list = array(); - while ($filter_result->fetch()) { - $filtered_list[] = $filter_result->id; - } + $filtered_list = BankTransaction::get($params['check_permissions'] ?? FALSE) + ->addSelect('id') + ->addWhere('status_id', 'NOT IN', [$state_ignored, $state_processed]) + ->addWhere('id', 'IN', $tx_ids) + ->execute() + ->column('id'); // check if we should use a runner if (!empty($params['use_runner']) && count($filtered_list) >= $params['use_runner']) { @@ -193,7 +249,7 @@ function civicrm_api3_banking_transaction_analyselist($params) { // done. create a result. $after_exec = strtotime('now'); - $payment_count = count(explode(',', $list_string)); + $payment_count = count($tx_ids); $result = array( 'payment_count' => $payment_count, 'processed_count' => $processed_count, diff --git a/api/v3/BankingTransactionBatch.php b/api/v3/BankingTransactionBatch.php index 04f1bfcd..2719801b 100755 --- a/api/v3/BankingTransactionBatch.php +++ b/api/v3/BankingTransactionBatch.php @@ -20,6 +20,8 @@ * */ +use Civi\Api4\BankTransactionBatch; +use Civi\Banking\Api4\Api3To4Util; /** * Add an BankingTransactionBatch @@ -33,12 +35,18 @@ * @access public */ function civicrm_api3_banking_transaction_batch_create($params) { - return _civicrm_api3_basic_create('CRM_Banking_BAO_BankTransactionBatch', $params); + $values = Api3To4Util::createValues(BankTransactionBatch::getEntityName(), $params); + $resultValues = BankTransactionBatch::create($params['check_permissions'] ?? FALSE) + ->setValues($values) + ->execute() + ->single(); + + return civicrm_api3_create_success($resultValues, $params, 'BankTransactionBatch', 'create'); } /** * Adjust Metadata for Create action - * + * * The metadata is used for setting defaults, documentation & validation * @param array $params array or parameters determined by getfields */ @@ -80,8 +88,40 @@ function civicrm_api3_banking_transaction_batch_delete($params) { * @access public */ function civicrm_api3_banking_transaction_batch_get($params) { - return _civicrm_api3_basic_get('CRM_Banking_BAO_BankTransactionBatch', $params); -} + $options = _civicrm_api3_get_options_from_params($params); + $where = Api3To4Util::createWhere(BankTransactionBatch::getEntityName(), $params); + $action = BankTransactionBatch::get($params['check_permissions'] ?? FALSE) + ->setWhere($where) + ->setLimit($options['limit']) + ->setOffset($options['offset']); + if ($options['is_count']) { + $action->selectRowCount(); + } + else { + $action->setSelect(array_keys(array_filter($options['return']))); + if (isset($options['sort'])) { + [$sortFieldName, $sortDirection] = explode(' ', $options['sort']); + $action->addOrderBy($sortFieldName, $sortDirection); + } + } + $result = $action->execute(); + + if ($options['is_count']) { + return civicrm_api3_create_success( + $result->countMatched(), + $params, + 'BankTransactionBatch', + 'get' + ); + } + + return civicrm_api3_create_success( + $result->indexBy('id')->getArrayCopy(), + $params, + 'BankTransactionBatch', + 'get' + ); +} diff --git a/banking.php b/banking.php index f04c3733..86e50f1b 100755 --- a/banking.php +++ b/banking.php @@ -17,19 +17,50 @@ require_once 'banking.civix.php'; require_once 'banking_options.php'; -use Symfony\Component\DependencyInjection\ContainerBuilder; +use Civi\Banking\EventSubscriber\TransactionAuthorizeRecordSubscriber; +use Civi\Banking\Permissions\AssignedTransactionDomainsLoader; +use Civi\Banking\Permissions\AllowedDomainsSqlGenerator; +use Civi\Banking\Permissions\Permissions; +use Civi\Banking\Permissions\TransactionDomainPermissionsGenerator; use CRM_Banking_ExtensionUtil as E; - +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; /** * Implements hook_civicrm_container(). * * @param ContainerBuilder $container */ -function banking_civicrm_container(ContainerBuilder $container) { - if (class_exists('Civi\Banking\CompilerPass')) { +function banking_civicrm_container(ContainerBuilder $container): void { + $container->addResource(new FileResource(__FILE__)); + + if (class_exists(\Civi\ActionProvider\Action\AbstractAction::class)) { $container->addCompilerPass(new Civi\Banking\CompilerPass()); } + + $container->register('cache.banking.array', CacheInterface::class) + ->setFactory([\CRM_Utils_Cache::class, 'create']) + ->addArgument([ + 'type' => ['ArrayCache'], + 'name' => 'banking.array', + ]) + ->setPublic(TRUE); + + $container->autowire(TransactionDomainPermissionsGenerator::class) + ->setArgument('$cache', new Reference('cache.banking.array')) + ->setPublic(TRUE); + + $container->autowire(AssignedTransactionDomainsLoader::class) + ->setArgument('$cache', new Reference('cache.banking.array')) + ->setPublic(TRUE); + + $container->autowire(AllowedDomainsSqlGenerator::class) + ->setPublic(TRUE); + + $container->autowire(TransactionAuthorizeRecordSubscriber::class) + ->addTag('kernel.event_subscriber'); } /** @@ -277,3 +308,28 @@ function banking_civicrm_navigationMenu(&$menu) { _banking_civix_navigationMenu($menu); } + +/** + * Implements hook_civicrm_permission(). + * + * @phpstan-param array}> $permissions + */ +function banking_civicrm_permission(array &$permissions): void { + /** @var \Civi\Banking\Permissions\TransactionDomainPermissionsGenerator $domainPermissionsGenerator */ + $domainPermissionsGenerator = \Civi::service(TransactionDomainPermissionsGenerator::class); + + $permissions[Permissions::ACCESS_TRANSACTIONS] = [ + 'label' => E::ts('CiviBanking: Access transactions without domain'), + 'description' => E::ts( + 'Access CiviBanking transactions without domain. The domain-specific permissions imply this one.' + ), + ]; + + $permissions[Permissions::ACCESS_TRANSACTIONS_ALL] = [ + 'label' => E::ts('CiviBanking: Access transactions for all domains'), + 'description' => E::ts('Access CiviBanking transactions with any domain.'), + 'implies' => [Permissions::ACCESS_TRANSACTIONS], + ]; + + $permissions += $domainPermissionsGenerator->generatePermissions(); +} diff --git a/managed/OptionGroup_banking_transaction_domain.mgd.php b/managed/OptionGroup_banking_transaction_domain.mgd.php new file mode 100644 index 00000000..615ef59d --- /dev/null +++ b/managed/OptionGroup_banking_transaction_domain.mgd.php @@ -0,0 +1,33 @@ + 'OptionGroup_banking_transaction_domain', + 'entity' => 'OptionGroup', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'banking_transaction_domain', + 'title' => E::ts('CiviBanking Transaction Domain'), + 'description' => E::ts('Domains for CiviBanking transactions.'), + 'data_type' => 'String', + 'is_reserved' => FALSE, + 'is_active' => TRUE, + 'is_locked' => FALSE, + 'option_value_fields' => [ + 'name', + 'label', + 'description', + ], + ], + ], + 'match' => [ + 'name', + ], + ], +]; diff --git a/sql/auto_install.sql b/sql/auto_install.sql new file mode 100644 index 00000000..db2f1663 --- /dev/null +++ b/sql/auto_install.sql @@ -0,0 +1,155 @@ +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC. All rights reserved. | +-- | | +-- | This work is published under the GNU AGPLv3 license with some | +-- | permitted exceptions and without any warranty. For full license | +-- | and copyright information, see https://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from schema.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +-- +-- /******************************************************* +-- * +-- * Clean up the existing tables - this section generated from drop.tpl +-- * +-- *******************************************************/ + +SET FOREIGN_KEY_CHECKS=0; + +DROP TABLE IF EXISTS `civicrm_bank_tx_contribution`; +DROP TABLE IF EXISTS `civicrm_bank_tx`; +DROP TABLE IF EXISTS `civicrm_bank_plugin_instance`; +DROP TABLE IF EXISTS `civicrm_bank_tx_batch`; +DROP TABLE IF EXISTS `civicrm_bank_account_reference`; +DROP TABLE IF EXISTS `civicrm_bank_account`; + +SET FOREIGN_KEY_CHECKS=1; +-- /******************************************************* +-- * +-- * Create new tables +-- * +-- *******************************************************/ + +-- /******************************************************* +-- * +-- * civicrm_bank_account +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_bank_account` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `description` varchar(255) COMMENT 'Purpose or use of the bank account', + `created_date` datetime NOT NULL, + `modified_date` datetime NOT NULL, + `data_raw` text COMMENT 'The complete information received for this bank account', + `data_parsed` text COMMENT 'A JSON-formatted array containing decoded fields', + `contact_id` int unsigned COMMENT 'FK to contact owning this account', + PRIMARY KEY (`id`), + CONSTRAINT FK_civicrm_bank_account_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE SET NULL +) +ENGINE=InnoDB; + +-- /******************************************************* +-- * +-- * civicrm_bank_account_reference +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_bank_account_reference` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `reference` varchar(255) COMMENT 'The value for this account', + `reference_type_id` int unsigned NOT NULL COMMENT 'Link to an option list', + `ba_id` int unsigned COMMENT 'FK to bank_account of target account', + PRIMARY KEY (`id`), + INDEX `reference`(reference), + INDEX `reftype`(ba_id, reference_type_id), + CONSTRAINT FK_civicrm_bank_account_reference_ba_id FOREIGN KEY (`ba_id`) REFERENCES `civicrm_bank_account`(`id`) ON DELETE SET NULL +) +ENGINE=InnoDB; + +-- /******************************************************* +-- * +-- * civicrm_bank_tx_batch +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_bank_tx_batch` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `issue_date` datetime NOT NULL COMMENT 'When the statement was issued', + `reference` varchar(64) NOT NULL COMMENT 'The unique reference for this statement', + `sequence` int NOT NULL COMMENT 'Used to maintain ordering and consistency', + `domain` varchar(255) DEFAULT null, + `starting_balance` decimal(20,2), + `ending_balance` decimal(20,2), + `currency` varchar(3) COMMENT 'Currency', + `tx_count` int NOT NULL, + `starting_date` datetime COMMENT 'Start date of the statement period', + `ending_date` datetime COMMENT 'End date of the statement period', + PRIMARY KEY (`id`), + UNIQUE INDEX `reference`(reference), + INDEX `index_domain`(domain) +) +ENGINE=InnoDB; + +-- /******************************************************* +-- * +-- * civicrm_bank_plugin_instance +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_bank_plugin_instance` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `plugin_type_id` int unsigned NOT NULL COMMENT 'Link to an option list of plugin types', + `plugin_class_id` int unsigned NOT NULL COMMENT 'Link to an option list of plugin class names', + `name` varchar(255) COMMENT 'Name of the plugin', + `description` text COMMENT 'Short description of what the plugin does', + `enabled` tinyint NOT NULL DEFAULT 1 COMMENT 'If this plugin is enabled', + `weight` double NOT NULL DEFAULT 100.0 COMMENT 'Relative weight of this plugin', + `config` text COMMENT 'Configuration JSON', + `state` text COMMENT 'State JSON', + PRIMARY KEY (`id`) +) +ENGINE=InnoDB; + +-- /******************************************************* +-- * +-- * civicrm_bank_tx +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_bank_tx` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `bank_reference` varchar(64) NOT NULL COMMENT 'The unique reference for this transaction', + `value_date` datetime NOT NULL COMMENT 'Value date for this bank transaction', + `booking_date` datetime NOT NULL COMMENT 'Booking date for this bank transaction', + `domain` varchar(255) DEFAULT null, + `amount` decimal(20,2) NOT NULL COMMENT 'Transaction amount (positive or negative)', + `currency` varchar(3) COMMENT 'Currency for the amount of the transaction', + `type_id` int unsigned NOT NULL COMMENT 'Link to an option list', + `status_id` int unsigned NOT NULL COMMENT 'Link to an option list', + `data_raw` text COMMENT 'The complete information received for this transaction', + `data_parsed` text COMMENT 'A JSON-formatted array containing decoded fields', + `ba_id` int unsigned COMMENT 'FK to bank_account of target account', + `party_ba_id` int unsigned COMMENT 'FK to bank_account of party account', + `tx_batch_id` int unsigned COMMENT 'FK to parent bank_tx_batch', + `sequence` int unsigned COMMENT 'Numbering local to the tx_batch_id', + `suggestions` text COMMENT 'A JSON-formatted array containing suggestions', + PRIMARY KEY (`id`), + UNIQUE INDEX `bank_reference`(bank_reference), + INDEX `index_domain`(domain), + CONSTRAINT FK_civicrm_bank_tx_ba_id FOREIGN KEY (`ba_id`) REFERENCES `civicrm_bank_account`(`id`) ON DELETE SET NULL, + CONSTRAINT FK_civicrm_bank_tx_party_ba_id FOREIGN KEY (`party_ba_id`) REFERENCES `civicrm_bank_account`(`id`) ON DELETE SET NULL, + CONSTRAINT FK_civicrm_bank_tx_tx_batch_id FOREIGN KEY (`tx_batch_id`) REFERENCES `civicrm_bank_tx_batch`(`id`) ON DELETE SET NULL +) +ENGINE=InnoDB; + +-- /******************************************************* +-- * +-- * civicrm_bank_tx_contribution +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_bank_tx_contribution` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `bank_tx_id` int unsigned COMMENT 'FK to bank transaction', + `contribution_id` int unsigned COMMENT 'FK to contribution', + PRIMARY KEY (`id`), + CONSTRAINT FK_civicrm_bank_tx_contribution_bank_tx_id FOREIGN KEY (`bank_tx_id`) REFERENCES `civicrm_bank_tx`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicrm_bank_tx_contribution_contribution_id FOREIGN KEY (`contribution_id`) REFERENCES `civicrm_contribution`(`id`) ON DELETE CASCADE +) +ENGINE=InnoDB; diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql new file mode 100644 index 00000000..c4e899c7 --- /dev/null +++ b/sql/auto_uninstall.sql @@ -0,0 +1,25 @@ +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC. All rights reserved. | +-- | | +-- | This work is published under the GNU AGPLv3 license with some | +-- | permitted exceptions and without any warranty. For full license | +-- | and copyright information, see https://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from drop.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +---- /******************************************************* +-- * +-- * Clean up the existing tables-- * +-- *******************************************************/ + +SET FOREIGN_KEY_CHECKS=0; + +DROP TABLE IF EXISTS `civicrm_bank_tx_contribution`; +DROP TABLE IF EXISTS `civicrm_bank_tx`; +DROP TABLE IF EXISTS `civicrm_bank_plugin_instance`; +DROP TABLE IF EXISTS `civicrm_bank_tx_batch`; +DROP TABLE IF EXISTS `civicrm_bank_account_reference`; +DROP TABLE IF EXISTS `civicrm_bank_account`; + +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/templates/CRM/Admin/Form/Setting/BankingSettings.tpl b/templates/CRM/Admin/Form/Setting/BankingSettings.tpl index 03e27379..3305dbc8 100644 --- a/templates/CRM/Admin/Form/Setting/BankingSettings.tpl +++ b/templates/CRM/Admin/Form/Setting/BankingSettings.tpl @@ -92,6 +92,27 @@
+

{ts domain='org.project60.banking'}Transaction Domains{/ts}

+ +
+
{$form.force_transaction_domain.label}
+
{$form.force_transaction_domain.html}
+
+
+ +
+
+ {ts domain='org.project60.banking'}Edit Transaction Domains{/ts} +
+ +
+
+
{* FOOTER *}
diff --git a/templates/CRM/Banking/Page/Import.tpl b/templates/CRM/Banking/Page/Import.tpl index f34d3440..5feec419 100755 --- a/templates/CRM/Banking/Page/Import.tpl +++ b/templates/CRM/Banking/Page/Import.tpl @@ -16,7 +16,7 @@ {literal} {/literal} - +
@@ -37,7 +37,7 @@ - {foreach from=$plugin_list item=field key=fieldName} @@ -49,15 +49,36 @@ - - - -
+ + +
+ +
+

{ts domain='org.project60.banking'}Select Domain{/ts}

+ + + + + + + +
+ + + +
+