/** 
  * Generate a set of suggestions for the given bank transaction
  * 
  * @return array(match structures)
  */
 public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context)
 {
     $config = $this->_plugin_config;
     $threshold = $this->getThreshold();
     $data_parsed = $btx->getDataParsed();
     // find potential contacts
     $contacts_found = $context->findContacts($threshold, $data_parsed['name'], $config->lookup_contact_by_name);
     // with the identified contacts, look up matching memberships
     $memberships = $this->findMemberships($contacts_found, $btx, $context);
     // transform all memberships into suggestions
     foreach ($memberships as $membership) {
         $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx);
         if (isset($contact->general_options->suggestion_title)) {
             $suggestion->setTitle($contact->general_options->suggestion_title);
         } else {
             $suggestion->setTitle(ts("Record as Membership Fee"));
         }
         $suggestion->setId("existing-{$contribution_id}");
         $suggestion->setParameter('membership_id', $membership['id']);
         $suggestion->setParameter('last_fee_id', $membership['last_fee_id']);
         $suggestion->setProbability($membership['probability']);
         $btx->addSuggestion($suggestion);
     }
     // that's it...
     return empty($this->_suggestions) ? null : $this->_suggestions;
 }
 /**
  * Delete function addendum: update statement's count
  *
  * @see https://github.com/Project60/CiviBanking/issues/59
  */
 static function del($ba_id)
 {
     // get batch (statement) id
     $ba_bao = new CRM_Banking_BAO_BankTransaction();
     $ba_bao->get('id', $ba_id);
     $batch_id = $ba_bao->tx_batch_id;
     // delete the transaction / payments
     $ba_bao->delete();
     // if $batch exists, update count
     if (!empty($batch_id)) {
         $new_count_query = "SELECT COUNT(`id`) FROM `civicrm_bank_tx` WHERE `tx_batch_id`='{$batch_id}'";
         CRM_Core_DAO::executeQuery("UPDATE `civicrm_bank_tx_batch` SET `tx_count` = ({$new_count_query}) WHERE `id`='{$batch_id}';");
     }
 }
 public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context)
 {
     // get list of existing batches (cache in context)
     $existing_batches = $context->getCachedEntry('banking.pluginimpl.matcher.batch');
     if ($existing_batches == NULL) {
         $existing_batches = $this->generateBatchList();
         $context->setCachedEntry('banking.pluginimpl.matcher.batch', $existing_batches);
     }
     // look for a matching batch
     $config = $this->_plugin_config;
     $booking_date = strtotime($btx->booking_date);
     $matching_batches = array();
     foreach ($existing_batches as $batch) {
         $total_amount = $batch['total'];
         if (!empty($batch['export_date'])) {
             $submission_date = strtotime($batch['export_date']);
         } elseif ($batch['modified_date']) {
             $submission_date = strtotime($batch['modified_date']);
         } else {
             $submission_date = strtotime($batch['created_date']);
         }
         // check amount
         if (abs(1 - $total_amount / $btx->amount) > $config->total_amount_tolerance) {
             continue;
         }
         // check export_date_to_payment_min / max
         if ($booking_date < strtotime($config->export_date_to_payment_min, $submission_date)) {
             continue;
         }
         if ($booking_date > strtotime($config->export_date_to_payment_max, $submission_date)) {
             continue;
         }
         // batch is accepted -> calculate probability:
         // first factor: expected income time
         $time_penalty_total = strtotime('-' . $config->export_date_to_payment_tolerance, abs($booking_date - $submission_date));
         $time_penalty = min(1.0, 1 - $time_penalty_total / (strtotime($config->export_date_to_payment_max) - strtotime($config->export_date_to_payment_min)));
         // second factor: equal amount
         $amount_penalty = 1.0 - abs(1 - $total_amount / $btx->amount) / $config->total_amount_tolerance;
         // third factor: statmentes pending
         $status_penalty = 1.0 - count($this->getNonPendingContributionIDs($batch['id'])) / $batch['item_count'];
         $matching_batches[$batch['id']] = $time_penalty * $amount_penalty * $status_penalty;
     }
     // for each matched batch, create a suggestion
     foreach ($matching_batches as $batch_id => $batch_probability) {
         $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx);
         $suggestion->setTitle(ts("Settles a contribution batch"));
         $suggestion->setParameter('batch_id', $batch_id);
         $suggestion->setId("batch-" . $batch_id);
         $suggestion->setProbability($batch_probability);
         $btx->addSuggestion($suggestion);
     }
     // that's it...
     return empty($this->_suggestions) ? null : $this->_suggestions;
 }
 /** 
  * this matcher does not really create suggestions, but rather enriches the parsed data
  */
 public function analyse(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context)
 {
     $config = $this->_plugin_config;
     $data_parsed = $btx->getDataParsed();
     // itreate trough all rules
     foreach ($this->_plugin_config->rules as $rule) {
         if (empty($rule->fields)) {
             $fields = array('purpose');
         } else {
             $fields = $rule->fields;
         }
         // replace [[...]] style variables in the pattern
         $pattern = $rule->pattern;
         $variables = $this->getVariableList();
         foreach ($variables as $variable) {
             if (preg_match("#\\[\\[{$variable}\\]\\]#", $pattern)) {
                 $value = $this->getVariable($variable);
                 $pattern = preg_replace("#\\[\\[{$variable}\\]\\]#", print_r($value, 1), $pattern);
             }
         }
         // appy rule to all the fields listed...
         foreach ($fields as $field) {
             if (isset($data_parsed[$field])) {
                 $field_data = $data_parsed[$field];
                 $matches = array();
                 // match the pattern on the given field data
                 $match_count = preg_match_all($pattern, $field_data, $matches);
                 // and execute the actions for each match...
                 for ($i = 0; $i < $match_count; $i++) {
                     $this->processMatch($matches, $i, $data_parsed, $rule);
                 }
             }
         }
     }
     // save changes and that's it
     $btx->setDataParsed($data_parsed);
 }
 /**
  * Gather all information on the transaction / payment
  * 
  * @return an array containing all values, keys prefixed with 'tx_'
  */
 public function getTxData($tx_id)
 {
     $result = array();
     $tx = array();
     $tx_bao = new CRM_Banking_BAO_BankTransaction();
     $tx_bao->get('id', $tx_id);
     CRM_Core_DAO::storeValues($tx_bao, $tx);
     // add all basic fields
     foreach ($tx as $key => $value) {
         $result['tx_' . $key] = $value;
     }
     // resolve status IDs
     $result['tx_status'] = CRM_Core_OptionGroup::getValue('civicrm_banking.bank_tx_status', $result['tx_status_id'], 'id', 'String', 'name');
     $result['tx_status_name'] = CRM_Core_OptionGroup::getValue('civicrm_banking.bank_tx_status', $result['tx_status_id'], 'id', 'String', 'label');
     // add all data_parsed
     $data_parsed = $tx_bao->getDataParsed();
     foreach ($data_parsed as $key => $value) {
         $result['data_' . $key] = $value;
     }
     unset($result['tx_data_parsed']);
     unset($result['tx_suggestions']);
     // add execution info
     $suggestion_objects = $tx_bao->getSuggestionList();
     foreach ($suggestion_objects as $suggestion) {
         if ($suggestion->isExecuted()) {
             $result['exec_date'] = $suggestion->isExecuted();
             $result['exec_executed_by'] = $suggestion->getParameter('executed_by');
             $result['exec_automatically'] = $suggestion->getParameter('executed_automatically');
             // find contribtion IDs
             $contribution_ids = array();
             $suggestion_contribution_id = $suggestion->getParameter('contribution_id');
             if (!empty($suggestion_contribution_id)) {
                 if ((int) $suggestion_contribution_id) {
                     $contribution_ids[] = (int) $suggestion_contribution_id;
                 }
             }
             $suggestion_contribution_ids = $suggestion->getParameter('contribution_ids');
             if (!empty($suggestion_contribution_ids)) {
                 foreach ($suggestion_contribution_ids as $id) {
                     $id = (int) $id;
                     if ($id) {
                         $contribution_ids[] = $id;
                     }
                 }
             }
             $result['exec_contribution_count'] = count($contribution_ids);
             $result['exec_contribution_list'] = implode(',', $contribution_ids);
             // also, add individual contribution data
             $counter = 1;
             $total_sum = 0.0;
             $total_currency = '';
             $total_non_deductible = 0.0;
             foreach ($contribution_ids as $contribution_id) {
                 $contribution = civicrm_api('Contribution', 'getsingle', array('id' => $contribution_id, 'version' => 3));
                 if (!empty($contribtion['is_error'])) {
                     error_log("org.project60.banking.exporter.csv: error while reading contribution [{$contribution_id}]: " . $contribution['error_message']);
                 } else {
                     $prefix = 'exec_contribution' . ($counter > 1 ? "_{$counter}_" : '_');
                     foreach ($contribution as $key => $value) {
                         $result[$prefix . $key] = $value;
                     }
                     if (!empty($contribution['total_amount'])) {
                         $total_sum += $contribution['total_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++;
             }
             $result['exec_total_amount'] = $total_sum;
             $result['exec_total_currency'] = $total_currency;
             $result['exec_total_non_deductible'] = $total_non_deductible;
             break;
         }
     }
     return $result;
 }
 /**
  * check if this ignore pattern applies to this btx
  */
 private function matches_pattern($ignore_record, CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context)
 {
     // collect all the fields
     $fields = array();
     if (isset($ignore_record->field)) {
         array_push($fields, $ignore_record->field);
     }
     if (isset($ignore_record->fields)) {
         $fields = array_merge($fields, $ignore_record->fields);
     }
     // extract the values
     $values = array();
     foreach ($fields as $field) {
         if (isset($btx->{$field})) {
             array_push($values, $btx->{$field});
         } else {
             $data = $btx->getDataParsed();
             if (isset($data[$field])) {
                 array_push($values, $data[$field]);
             }
         }
     }
     if (isset($ignore_record->regex)) {
         foreach ($values as $value) {
             if (preg_match($ignore_record->regex, $value)) {
                 return true;
             }
         }
     }
     return false;
 }
 /**
  * calculate the absolute probability based on the (possibly) relative value in the config
  */
 private function get_probability($string_value, CRM_Banking_BAO_BankTransaction $btx)
 {
     if (substr($string_value, -1) === "%") {
         // if the value ends in '%' it's meant to be relative to the least probable suggestion
         $suggestion_list = $btx->getSuggestionList();
         $least_probable = end($suggestion_list);
         if ($least_probable) {
             $least_probable_value = $least_probable->getProbability();
         } else {
             $least_probable_value = 1;
         }
         return $least_probable_value * substr($string_value, 0, strlen($string_value) - 1) / 100.0;
     } else {
         // in the default case, we just assume it's an absolute value anyways...
         return $string_value;
     }
 }
 /** 
  * Generate a set of suggestions for the given bank transaction
  * 
  * @return array(match structures)
  */
 public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context)
 {
     $config = $this->_plugin_config;
     $threshold = $this->getThreshold();
     $data_parsed = $btx->getDataParsed();
     $penalty = $this->getPenalty();
     // find potential contacts
     $contactID2probability = array();
     if (empty($config->contact_id_list)) {
         $nameSearch = $context->findContacts($threshold, $data_parsed['name'], $config->lookup_contact_by_name);
         foreach ($nameSearch as $contact_id => $probability) {
             if ($probability - $penalty < $threshold) {
                 continue;
             }
             $contactID2probability[$contact_id] = $probability;
         }
     } else {
         if (!empty($data_parsed[$config->contact_id_list])) {
             $id_list = explode(',', $data_parsed[$config->contact_id_list]);
             foreach ($id_list as $contact_id) {
                 $contact_id = (int) $contact_id;
                 if ($contact_id > 0) {
                     $contactID2probability[$contact_id] = 1.0;
                 }
             }
         }
     }
     // create suggestions
     if (!empty($config->search_terms)) {
         $query = $this->getPropagationSet($btx, $suggestion, '', $config->search_terms);
     } else {
         $query = array();
     }
     if ($config->search_wo_contacts) {
         $suggestions = $this->createRecurringContributionSuggestions($query, 1.0, $btx, $context);
     } else {
         $suggestions = array();
         foreach ($contactID2probability as $contact_id => $probability) {
             $query['contact_id'] = $contact_id;
             $new_suggestions = $this->createRecurringContributionSuggestions($query, $probability, $btx, $context);
             $suggestions = array_merge($suggestions, $new_suggestions);
         }
     }
     // apply penalties and threshold
     foreach ($suggestions as $suggestion) {
         $probability = $suggestion->getProbability();
         $probability -= $penalty;
         if ($probability >= $threshold) {
             if ($penalty) {
                 $suggestion->addEvidence($penalty, ts("A general penalty was applied."));
             }
             $suggestion->setProbability($probability);
             $btx->addSuggestion($suggestion);
         }
     }
     // that's it...
     return empty($this->_suggestions) ? null : $this->_suggestions;
 }
 public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context)
 {
     // this section will be refactored to use different conditions, but for now, this is hardcoded
     $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx);
     $config = $this->_plugin_config;
     // amount range
     if (isset($config->amount)) {
         $camount = $config->amount;
         $low = $camount->low;
         $high = $camount->high;
         $factor = $camount->prob or 1;
         $amount = $btx->amount;
         if ($low == null || $amount >= $low) {
             if ($high == null || $amount <= $high) {
                 $message = ts('the transaction amount is in the range [ ');
                 if ($low) {
                     $message .= number_format($low, 2);
                 }
                 $message .= ' - ';
                 if ($high) {
                     $message .= number_format($high, 2);
                 }
                 $message .= ' ]';
                 $suggestion->addEvidence($factor, $message);
             }
         }
     }
     // date range
     if (isset($config->value_date)) {
         $cvdate = $config->value_date;
         $early = $cvdate->early;
         $late = $cvdate->late;
         $factor = $cvdate->prob or 1;
         $value_date = strtotime($btx->value_date);
         if ($early != '' && $value_date >= strtotime($early)) {
             if ($late != '' && $value_date <= strtotime($late)) {
                 $message = ts('the transaction value date is in the range [ ');
                 if ($early) {
                     $message .= $early;
                 }
                 $message .= ' - ';
                 if ($late) {
                     $message .= $late;
                 }
                 $message .= ' ]';
                 $suggestion->addEvidence($factor, $message);
             }
         }
     }
     // regex
     if (isset($config->purpose)) {
         $cpurp = $config->purpose;
         $regex = $cpurp->regex;
         $factor = $cpurp->prob or 1;
         $parsed = json_decode($btx->data_parsed, true);
         $purpose = $parsed['purpose'];
         if ($regex != '' && preg_match("/{$regex}/", $purpose)) {
             $message = sprintf(ts('the transaction purpose matches the expression "%s"'), htmlentities($regex));
             $suggestion->addEvidence($factor, $message);
         }
     }
     if ($suggestion->getProbability() > 0) {
         $btx->addSuggestion($suggestion);
     }
     // close up
     return empty($this->_suggestions) ? null : $this->_suggestions;
 }
 function _findBTX($status_id, $batch_id)
 {
     $btxs = array();
     $btx_search = new CRM_Banking_BAO_BankTransaction();
     $btx_search->limit(1999);
     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);
     }
     if (count($btxs) >= 1999) {
         CRM_Core_Session::setStatus(sprintf(ts('Internal limit of 2000 transactions hit. Please use smaller statements.')), ts('List incomplete'), 'alert');
     }
     return $btxs;
 }
 /**
  * Bulk-run a set of <n> unprocessed items
  *
  * @param $max_count       the maximal amount of bank transactions to process
  *
  * @return the actual amount of bank transactions prcoessed
  */
 public function bulkRun($max_count)
 {
     $unprocessed_ids = CRM_Banking_BAO_BankTransaction::findUnprocessedIDs($max_count);
     foreach ($unprocessed_ids as $unprocessed_id) {
         $this->match($unprocessed_id);
     }
     return count($unprocessed_ids);
 }
Example #12
0
 /**
  * Will trigger the execution of the given suggestion (identified by its hash)
  */
 function execute_suggestion($suggestion_hash, $parameters, $btx_bao, $choices)
 {
     // load BTX object if not provided
     if (!$btx_bao) {
         $btx_bao = new CRM_Banking_BAO_BankTransaction();
         $btx_bao->get('id', $parameters['execute']);
     }
     $suggestion = $btx_bao->getSuggestionByHash($suggestion_hash);
     if ($suggestion) {
         // update the parameters
         $suggestion->update_parameters($parameters);
         $btx_bao->saveSuggestions();
         $suggestion->execute($btx_bao);
         // create a notification bubble for the user
         $text = $suggestion->visualize_execution($btx_bao);
         if ($btx_bao->status_id == $choices['processed']['id']) {
             CRM_Core_Session::setStatus(ts("The transaction was booked.") . "<br/>" . $text, ts("Transaction closed"), 'info');
         } elseif ($btx_bao->status_id == $choices['ignored']['id']) {
             CRM_Core_Session::setStatus(ts("The transaction was ignored.") . "<br/>" . $text, ts("Transaction closed"), 'info');
         } else {
             CRM_Core_Session::setStatus(ts("The transaction could not be closed."), ts("Error"), 'alert');
         }
     } else {
         CRM_Core_Session::setStatus(ts("Selected suggestions disappeared. Suggestion NOT executed!"), ts("Internal Error"), 'error');
     }
 }
 /** 
  * Generate a set of suggestions for the given bank transaction
  * 
  * @return array(match structures)
  */
 public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context)
 {
     $config = $this->_plugin_config;
     $threshold = $this->getThreshold();
     $data_parsed = $btx->getDataParsed();
     $probability = 1.0 - $this->getPenalty($btx);
     $cancellation_mode = (bool) $config->cancellation_enabled && $btx->amount < 0;
     // look for the 'sepa_mandate' key
     if (empty($data_parsed['sepa_mandate'])) {
         return null;
     }
     // now load the mandate
     $mandate_reference = $data_parsed['sepa_mandate'];
     $mandate = civicrm_api('SepaMandate', 'getsingle', array('version' => 3, 'reference' => $mandate_reference));
     if (!empty($mandate['is_error'])) {
         CRM_Core_Session::setStatus(sprintf(ts("Couldn't load SEPA mandate for reference %s"), $mandate_reference), ts('Error'), 'error');
         return null;
     }
     // find the contribution
     if ($mandate['type'] == 'OOFF' && $mandate['entity_table'] == 'civicrm_contribution') {
         $contribution_id = $mandate['entity_id'];
     } elseif ($mandate['entity_table'] == 'civicrm_contribution_recur') {
         $contribution_recur_id = $mandate['entity_id'];
         $value_date = strtotime($btx->value_date);
         if ($cancellation_mode) {
             $earliest_date = date('Ymdhis', strtotime($config->cancellation_date_minimum, $value_date));
             $latest_date = date('Ymdhis', strtotime($config->cancellation_date_maximum, $value_date));
         } else {
             $earliest_date = date('Ymdhis', strtotime($config->received_date_minimum, $value_date));
             $latest_date = date('Ymdhis', strtotime($config->received_date_maximum, $value_date));
         }
         $contribution_id = 0;
         $find_contribution_query = "\n      SELECT  id\n      FROM    civicrm_contribution\n      WHERE   contribution_recur_id={$contribution_recur_id}\n      AND     receive_date <= DATE('{$latest_date}')\n      AND     receive_date >= DATE('{$earliest_date}');";
         $found_contribution = CRM_Core_DAO::executeQuery($find_contribution_query);
         while ($found_contribution->fetch()) {
             if (!$contribution_id) {
                 $contribution_id = $found_contribution->id;
             } else {
                 // this is the second contribution found!
                 CRM_Core_Session::setStatus(ts("There was more than one matching contribution found! Try to configure the plugin with a smaller search time span."), ts('Error'), 'error');
                 return null;
             }
         }
         if (!$contribution_id) {
             // no contribution found
             CRM_Core_Session::setStatus(ts("There was no matching contribution! Try to configure the plugin with a larger search time span."), ts('Error'), 'error');
             return null;
         }
     } else {
         error_log("org.project60.sepa: matcher_sepa: Bad mandate type.");
         return null;
     }
     // now, let's have a look at this contribution and its contact...
     $contribution = civicrm_api('Contribution', 'getsingle', array('id' => $contribution_id, 'version' => 3));
     if (!empty($contribution['is_error'])) {
         CRM_Core_Session::setStatus(ts("The contribution connected to this mandate could not be read."), ts('Error'), 'error');
         return null;
     }
     $contact = civicrm_api('Contact', 'getsingle', array('id' => $contribution['contact_id'], 'version' => 3));
     if (!empty($contact['is_error'])) {
         CRM_Core_Session::setStatus(ts("The contact connected to this mandate could not be read."), ts('Error'), 'error');
         return null;
     }
     // now: create a suggestion
     $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx);
     $suggestion->setParameter('contribution_id', $contribution_id);
     $suggestion->setParameter('contact_id', $contribution['contact_id']);
     $suggestion->setParameter('mandate_id', $mandate['id']);
     $suggestion->setParameter('mandate_reference', $mandate_reference);
     if (!$cancellation_mode) {
         // STANDARD SUGGESTION:
         $suggestion->setTitle(ts("SEPA SDD Transaction"));
         // add penalties for deviations in amount,status,deleted contact
         if ($btx->amount != $contribution['total_amount']) {
             $suggestion->addEvidence($config->deviation_penalty, ts("The contribution does not feature the expected amount."));
             $probability -= $config->deviation_penalty;
         }
         $status_inprogress = banking_helper_optionvalue_by_groupname_and_name('contribution_status', 'In Progress');
         if ($contribution['contribution_status_id'] != $status_inprogress) {
             $suggestion->addEvidence($config->deviation_penalty, ts("The contribution does not have the expected status 'in Progress'."));
             $probability -= $config->deviation_penalty;
         }
         if (!empty($contact['contact_is_deleted'])) {
             $suggestion->addEvidence($config->deviation_penalty, ts("The contact this mandate belongs to has been deleted."));
             $probability -= $config->deviation_penalty;
         }
     } else {
         // CANCELLATION SUGGESTION:
         $suggestion->setTitle(ts("Cancel SEPA SDD Transaction"));
         $suggestion->setParameter('cancellation_mode', $cancellation_mode);
         // calculate penalties (based on CRM_Banking_PluginImpl_Matcher_ExistingContribution::rateContribution)
         $contribution_amount = $contribution['total_amount'];
         $target_amount = -$context->btx->amount;
         $amount_range_rel = $contribution_amount * ($config->cancellation_amount_relative_maximum - $config->cancellation_amount_relative_minimum);
         $amount_range_abs = $config->cancellation_amount_absolute_maximum - $config->cancellation_amount_absolute_minimum;
         $amount_range = max($amount_range_rel, $amount_range_abs);
         $amount_delta = $contribution_amount - $target_amount;
         // check for amount limits
         if ($amount_range) {
             $penalty = $config->cancellation_amount_penalty * (abs($amount_delta) / $amount_range);
             if ($penalty > $config->cancellation_penalty_threshold) {
                 $suggestion->addEvidence($config->cancellation_amount_penalty, ts("The cancellation fee, i.e. the deviation from the original amount, is not in the specified range."));
                 $probability -= $penalty;
             }
         }
         // add general cancellation penalty, if set
         $probability -= (double) $config->cancellation_general_penalty;
         // generate cancellation extra parameters
         if ($config->cancellation_cancel_reason) {
             // determine the cancel reason
             if (empty($data_parsed[$config->cancellation_cancel_reason_source])) {
                 $suggestion->setParameter('cancel_reason', $config->cancellation_cancel_reason_default);
             } else {
                 $suggestion->setParameter('cancel_reason', $data_parsed[$config->cancellation_cancel_reason_source]);
             }
         }
         if ($config->cancellation_cancel_fee) {
             // calculate / determine the cancellation fee
             try {
                 $meval = new EvalMath();
                 // first initialise variables 'difference' and 'source'
                 $meval->evaluate("difference = -{$btx->amount} - {$contribution_amount}");
                 if (empty($config->cancellation_cancel_fee_source) || empty($data_parsed[$config->cancellation_cancel_fee_source])) {
                     $meval->evaluate("source = 0.0");
                 } else {
                     $meval->evaluate("source = {$data_parsed[$config->cancellation_cancel_fee_source]}");
                 }
                 $suggestion->setParameter('cancel_fee', number_format($meval->evaluate($config->cancellation_cancel_fee_default), 2));
             } catch (Exception $e) {
                 error_log("org.project60.banking.matcher.existing: Couldn't calculate cancellation_fee. Error was: {$e}");
             }
         }
     }
     // store it
     $suggestion->setProbability($probability);
     $btx->addSuggestion($suggestion);
     return $this->_suggestions;
 }